diff --git a/.ci/ci.json b/.ci/ci.json index 69c10aff983..e8fa80e412b 100644 --- a/.ci/ci.json +++ b/.ci/ci.json @@ -32,4 +32,4 @@ } ] } -] +] \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index f8623698ae3..7a61cbb89b0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,22 +2,24 @@ version: 2 jobs: go-version-latest: docker: - - image: cimg/go:1.18-node + - image: cimg/go:1.25-node + resource_class: large steps: - - checkout - - run: make - - run: make alltest - go-version-last: - docker: - - image: cimg/go:1.17-node - steps: - - checkout - - run: make - - run: make alltest + - checkout + - run: + name: Test and build web assets + command: make web-ci + - run: + name: Check Go formatting and build binaries + command: | + set -e + make env fmt + git diff --exit-code + make build + - run: make alltest workflows: version: 2 build_and_test: jobs: - - go-version-latest - - go-version-last + - go-version-latest diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e137534b724..e87e0d49f2b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ # These are supported funding model platforms github: [fatedier] +custom: ["https://afdian.com/a/fatedier"] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..7724848141f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,3 @@ +### WHY + + diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml index b7f7318b62f..aaa97705caf 100644 --- a/.github/workflows/build-and-push-image.yml +++ b/.github/workflows/build-and-push-image.yml @@ -2,13 +2,16 @@ name: Build Image and Publish to Dockerhub & GPR on: release: - types: [ created ] + types: [ published ] workflow_dispatch: inputs: tag: description: 'Image tag' required: true default: 'test' +permissions: + contents: read + jobs: image: name: Build Image from Dockerfile and binaries @@ -16,15 +19,15 @@ jobs: steps: # environment - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 with: fetch-depth: '0' - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v4 # get image tag name - name: Get Image Tag Name @@ -35,13 +38,13 @@ jobs: echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV fi - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Login to the GPR - uses: docker/login-action@v1 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -58,22 +61,22 @@ jobs: echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV - name: Build and push frpc - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v7 with: context: . file: ./dockerfiles/Dockerfile-for-frpc - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le push: true tags: | ${{ env.TAG_FRPC }} ${{ env.TAG_FRPC_GPR }} - name: Build and push frps - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v7 with: context: . file: ./dockerfiles/Dockerfile-for-frps - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le push: true tags: | ${{ env.TAG_FRPS }} diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 00000000000..36e8d50b24f --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,31 @@ +name: golangci-lint +on: + push: + branches: + - master + - dev + pull_request: +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.25' + cache: false + - uses: actions/setup-node@v6 + with: + node-version: '22' + - name: Test and build web assets + run: make web-ci + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: v2.11 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 40914a46725..5f3f156fb09 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -8,27 +8,31 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: - go-version: 1.18 - - - run: | - # https://github.com/actions/setup-go/issues/107 - cp -f `which go` /usr/bin/go - + go-version: '1.25' + - 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: Make All run: | ./package.sh - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v7 with: version: latest - args: release --rm-dist --release-notes=./Release.md + args: release --clean --release-notes=./Release.md env: GITHUB_TOKEN: ${{ secrets.GPR_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1a44ddf67c4..8960807be6a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ -name: "Close stale issues" +name: "Close stale issues and PRs" on: schedule: - cron: "20 0 * * *" @@ -8,21 +8,28 @@ on: description: 'In debug mod' required: false default: 'false' +permissions: + contents: read + jobs: stale: + permissions: + issues: write # for actions/stale to close stale issues + pull-requests: write # for actions/stale to close stale PRs + actions: write runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v10 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.' - stale-pr-message: "PRs go stale after 30d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close." + stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.' + stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close." stale-issue-label: 'lifecycle/stale' exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned' stale-pr-label: 'lifecycle/stale' exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned' - days-before-stale: 30 - days-before-close: 7 + days-before-stale: 14 + days-before-close: 3 debug-only: ${{ github.event.inputs.debug-only }} exempt-all-pr-milestones: true exempt-all-pr-assignees: true + operations-per-run: 200 diff --git a/.gitignore b/.gitignore index eeccf24af01..38917e90d6e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,18 +7,6 @@ _obj _test -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - *.exe *.test *.prof @@ -29,8 +17,21 @@ packages/ release/ test/bin/ vendor/ +lastversion/ +.cache/ dist/ .idea/ +.vscode/ +.autogen_ssh_key +client.crt +client.key + +node_modules/ # Cache *.swp + +# AI +.claude/ +.sisyphus/ +.superpowers/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000000..886b55fb5b7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,119 @@ +version: "2" +run: + concurrency: 4 + timeout: 20m + build-tags: + - integ + - integfuzz +linters: + default: none + enable: + - asciicheck + - copyloopvar + - errcheck + - gocritic + - gosec + - govet + - ineffassign + - lll + - makezero + - misspell + - modernize + - prealloc + - predeclared + - revive + - staticcheck + - unconvert + - unparam + - unused + settings: + errcheck: + check-type-assertions: false + check-blank: false + gocritic: + disabled-checks: + - exitAfterDefer + gosec: + excludes: ["G115", "G117", "G118", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"] + severity: low + confidence: low + govet: + disable: + - shadow + lll: + line-length: 160 + tab-width: 1 + misspell: + locale: US + ignore-rules: + - cancelled + - marshalled + modernize: + disable: + - omitzero + unparam: + check-exported: false + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - errcheck + - maligned + path: _test\.go$|^tests/|^samples/ + - linters: + - revive + - staticcheck + text: use underscores in Go names + - linters: + - revive + text: unused-parameter + - linters: + - revive + text: "avoid meaningless package names" + - linters: + - revive + text: "Go standard library package names" + - linters: + - unparam + text: is always false + paths: + - .*\.pb\.go + - .*\.gen\.go + - genfiles$ + - vendor$ + - bin$ + - third_party$ + - builtin$ + - examples$ + - node_modules +formatters: + enable: + - gci + - gofumpt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/fatedier/frp/) + exclusions: + generated: lax + paths: + - .*\.pb\.go + - .*\.gen\.go + - genfiles$ + - vendor$ + - bin$ + - third_party$ + - builtin$ + - examples$ + - node_modules +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..23f0e734a3a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS.md + +## Development Commands + +### Build +- `make build` - Build both frps and frpc binaries +- `make frps` - Build server binary only +- `make frpc` - Build client binary only +- `make all` - Build everything with formatting + +### Testing +- `make test` - Run unit tests +- `make e2e` - Run end-to-end tests +- `make e2e-trace` - Run e2e tests with trace logging +- `make alltest` - Run all tests including vet, unit tests, and e2e + +### Code Quality +- `make fmt` - Run go fmt +- `make fmt-more` - Run gofumpt for more strict formatting +- `make gci` - Run gci import organizer +- `make vet` - Run go vet +- `golangci-lint run` - Run comprehensive linting (configured in .golangci.yml) + +### Assets +- `make web` - Build web dashboards (frps and frpc) + +### Cleanup +- `make clean` - Remove built binaries and temporary files + +## Testing + +- E2E tests using Ginkgo/Gomega framework +- Mock servers in `/test/e2e/mock/` +- Run: `make e2e` or `make alltest` + +## Agent Runbooks + +Operational procedures for agents are in `doc/agents/`: +- `doc/agents/release.md` - Release process diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000000..47dc3e3d863 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile index 0cc24c86fcf..1b8d8a08a5d 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,56 @@ -export PATH := $(GOPATH)/bin:$(PATH) +export PATH := $(PATH):`go env GOPATH`/bin export GO111MODULE=on LDFLAGS := -s -w +NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb') +FRP_COMPAT_BASELINE_COUNT ?= 8 +FRP_COMPAT_FLOOR_VERSION ?= 0.61.0 -all: fmt build +.PHONY: web web-ci frps-web frpc-web frps frpc e2e-compatibility-smoke e2e-compatibility e2e-compatibility-floor + +all: env fmt web build build: frps frpc -# compile assets into binary file -file: - rm -rf ./assets/frps/static/* - rm -rf ./assets/frpc/static/* - cp -rf ./web/frps/dist/* ./assets/frps/static - cp -rf ./web/frpc/dist/* ./assets/frpc/static +env: + @go version + +web: frps-web frpc-web + +web-ci: + 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 + +frpc-web: + $(MAKE) -C web/frpc build fmt: go fmt ./... +fmt-more: + gofumpt -l -w . + +gci: + gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./ + vet: - go vet ./... + go vet -tags "$(NOWEB_TAG)" ./... frps: - env CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps + env CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps frpc: - env CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc + env CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc test: gotest gotest: - go test -v --cover ./assets/... - go test -v --cover ./cmd/... - go test -v --cover ./client/... - go test -v --cover ./server/... - go test -v --cover ./pkg/... + go test -tags "$(NOWEB_TAG)" -v --cover ./assets/... + go test -tags "$(NOWEB_TAG)" -v --cover ./cmd/... + go test -tags "$(NOWEB_TAG)" -v --cover ./client/... + go test -tags "$(NOWEB_TAG)" -v --cover ./server/... + go test -tags "$(NOWEB_TAG)" -v --cover ./pkg/... e2e: ./hack/run-e2e.sh @@ -40,8 +58,34 @@ e2e: e2e-trace: DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh +e2e-compatibility-smoke: build + FRP_COMPAT_BASELINE_COUNT=1 ./hack/run-e2e-compatibility.sh + +e2e-compatibility: build + FRP_COMPAT_BASELINE_COUNT="$(FRP_COMPAT_BASELINE_COUNT)" ./hack/run-e2e-compatibility.sh + +e2e-compatibility-floor: build + FRP_COMPAT_BASELINE_VERSIONS="$(FRP_COMPAT_FLOOR_VERSION)" ./hack/run-e2e-compatibility.sh + +e2e-compatibility-last-frpc: + if [ ! -d "./lastversion" ]; then \ + TARGET_DIRNAME=lastversion ./hack/download.sh; \ + fi + FRPC_PATH="`pwd`/lastversion/frpc" ./hack/run-e2e.sh + rm -r ./lastversion + +e2e-compatibility-last-frps: + if [ ! -d "./lastversion" ]; then \ + TARGET_DIRNAME=lastversion ./hack/download.sh; \ + fi + FRPS_PATH="`pwd`/lastversion/frps" ./hack/run-e2e.sh + rm -r ./lastversion + alltest: vet gotest e2e clean: rm -f ./bin/frpc rm -f ./bin/frps + rm -rf ./lastversion + rm -rf ./.cache + rm -rf ./.compat diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index 9351924a922..eb74d6479e2 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -1,25 +1,37 @@ -export PATH := $(GOPATH)/bin:$(PATH) +export PATH := $(PATH):`go env GOPATH`/bin export GO111MODULE=on LDFLAGS := -s -w -os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat +os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 openbsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 linux:loong64 android:arm64 all: build build: app app: - @$(foreach n, $(os-archs),\ - os=$(shell echo "$(n)" | cut -d : -f 1);\ - arch=$(shell echo "$(n)" | cut -d : -f 2);\ - gomips=$(shell echo "$(n)" | cut -d : -f 3);\ - target_suffix=$${os}_$${arch};\ - echo "Build $${os}-$${arch}...";\ - env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_$${target_suffix} ./cmd/frpc;\ - env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\ - echo "Build $${os}-$${arch} done";\ + @set -e; $(foreach n, $(os-archs), \ + os=$(shell echo "$(n)" | cut -d : -f 1); \ + arch=$(shell echo "$(n)" | cut -d : -f 2); \ + extra=$(shell echo "$(n)" | cut -d : -f 3); \ + flags=''; \ + target_suffix=$${os}_$${arch}; \ + if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \ + if [ "$${extra}" = "7" ]; then \ + flags=GOARM=7; \ + target_suffix=$${os}_arm_hf; \ + elif [ "$${extra}" = "5" ]; then \ + flags=GOARM=5; \ + target_suffix=$${os}_arm; \ + fi; \ + elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \ + flags=GOMIPS=$${extra}; \ + fi; \ + echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \ + env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \ + env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \ + echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \ ) - @mv ./release/frpc_windows_386 ./release/frpc_windows_386.exe - @mv ./release/frps_windows_386 ./release/frps_windows_386.exe @mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe @mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe + @mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe + @mv ./release/frps_windows_arm64 ./release/frps_windows_arm64.exe diff --git a/README.md b/README.md index 92c14933c24..c3bc2f603f3 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,76 @@ - # frp [![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) +[![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) -

Platinum Sponsors

- +## Sponsors + +frp is an open source project with its ongoing development made possible entirely by the support of our awesome sponsors. If you'd like to join them, please consider [sponsoring frp's development](https://github.com/sponsors/fatedier). +

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

- +
-

Gold Sponsors

- +## Recall.ai - API for meeting recordings + +If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), + +an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. + +

- - + + +
+ The complete IDE crafted for professional Go developers

- -

Silver Sponsors

- -* Sakura Frp - 欢迎点击 "加入我们" - ## What is frp? -frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the Internet. As of now, it supports **TCP** and **UDP**, as well as **HTTP** and **HTTPS** protocols, where requests can be forwarded to internal services by domain name. +frp is a fast reverse proxy that allows you to expose a local server located behind a NAT or firewall to the Internet. It currently supports **TCP** and **UDP**, as well as **HTTP** and **HTTPS** protocols, enabling requests to be forwarded to internal services via domain name. -frp also has a P2P connect mode. +frp also offers a P2P connect mode. ## Table of Contents * [Development Status](#development-status) + * [About V2](#about-v2) * [Architecture](#architecture) * [Example Usage](#example-usage) - * [Access your computer in LAN by SSH](#access-your-computer-in-lan-by-ssh) - * [Visit your web service in LAN by custom domains](#visit-your-web-service-in-lan-by-custom-domains) - * [Forward DNS query request](#forward-dns-query-request) - * [Forward Unix domain socket](#forward-unix-domain-socket) + * [Access your computer in a LAN network via SSH](#access-your-computer-in-a-lan-network-via-ssh) + * [Multiple SSH services sharing the same port](#multiple-ssh-services-sharing-the-same-port) + * [Accessing Internal Web Services with Custom Domains in LAN](#accessing-internal-web-services-with-custom-domains-in-lan) + * [Forward DNS query requests](#forward-dns-query-requests) + * [Forward Unix Domain Socket](#forward-unix-domain-socket) * [Expose a simple HTTP file server](#expose-a-simple-http-file-server) - * [Enable HTTPS for local HTTP(S) service](#enable-https-for-local-https-service) + * [Enable HTTPS for a local HTTP(S) service](#enable-https-for-a-local-https-service) * [Expose your service privately](#expose-your-service-privately) * [P2P Mode](#p2p-mode) * [Features](#features) * [Configuration Files](#configuration-files) * [Using Environment Variables](#using-environment-variables) * [Split Configures Into Different Files](#split-configures-into-different-files) - * [Dashboard](#dashboard) - * [Admin UI](#admin-ui) + * [Server Dashboard](#server-dashboard) + * [Client Admin UI](#client-admin-ui) + * [Dynamic Proxy Management (Store)](#dynamic-proxy-management-store) * [Monitor](#monitor) * [Prometheus](#prometheus) * [Authenticating the Client](#authenticating-the-client) @@ -74,6 +86,7 @@ frp also has a P2P connect mode. * [For Each Proxy](#for-each-proxy) * [TCP Stream Multiplexing](#tcp-stream-multiplexing) * [Support KCP Protocol](#support-kcp-protocol) + * [Support QUIC Protocol](#support-quic-protocol) * [Connection Pooling](#connection-pooling) * [Load balancing](#load-balancing) * [Service Health Check](#service-health-check) @@ -86,11 +99,17 @@ frp also has a P2P connect mode. * [Custom Subdomain Names](#custom-subdomain-names) * [URL Routing](#url-routing) * [TCP Port Multiplexing](#tcp-port-multiplexing) - * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy) - * [Range ports mapping](#range-ports-mapping) + * [Connecting to frps via PROXY](#connecting-to-frps-via-proxy) + * [Port range mapping](#port-range-mapping) * [Client Plugins](#client-plugins) * [Server Manage Plugins](#server-manage-plugins) -* [Development Plan](#development-plan) + * [SSH Tunnel Gateway](#ssh-tunnel-gateway) + * [Virtual Network (VirtualNet)](#virtual-network-virtualnet) +* [Feature Gates](#feature-gates) + * [Available Feature Gates](#available-feature-gates) + * [Enabling Feature Gates](#enabling-feature-gates) + * [Feature Lifecycle](#feature-lifecycle) +* [Related Projects](#related-projects) * [Contributing](#contributing) * [Donation](#donation) * [GitHub Sponsors](#github-sponsors) @@ -100,254 +119,320 @@ frp also has a P2P connect mode. ## Development Status -frp is under development. Try the latest release version in the `master` branch, or use the `dev` branch for the version in development. +frp is currently under development. You can try the latest release version in the `master` branch, or use the `dev` branch to access the version currently in development. + +We are currently working on version 2 and attempting to perform some code refactoring and improvements. However, please note that it will not be compatible with version 1. + +We will transition from version 0 to version 1 at the appropriate time and will only accept bug fixes and improvements, rather than big feature requests. + +### About V2 + +The complexity and difficulty of the v2 version are much higher than anticipated. I can only work on its development during fragmented time periods, and the constant interruptions disrupt productivity significantly. Given this situation, we will continue to optimize and iterate on the current version until we have more free time to proceed with the major version overhaul. -We are working on v2 version and trying to do some code refactor and improvements. It won't be compatible with v1. +The concept behind v2 is based on my years of experience and reflection in the cloud-native domain, particularly in K8s and ServiceMesh. Its core is a modernized four-layer and seven-layer proxy, similar to envoy. This proxy itself is highly scalable, not only capable of implementing the functionality of intranet penetration but also applicable to various other domains. Building upon this highly scalable core, we aim to implement all the capabilities of frp v1 while also addressing the functionalities that were previously unachievable or difficult to implement in an elegant manner. Furthermore, we will maintain efficient development and iteration capabilities. -We will switch v0 to v1 at the right time and only accept bug fixes and improvements instead of big feature requirements. +In addition, I envision frp itself becoming a highly extensible system and platform, similar to how we can provide a range of extension capabilities based on K8s. In K8s, we can customize development according to enterprise needs, utilizing features such as CRD, controller mode, webhook, CSI, and CNI. In frp v1, we introduced the concept of server plugins, which implemented some basic extensibility. However, it relies on a simple HTTP protocol and requires users to start independent processes and manage them on their own. This approach is far from flexible and convenient, and real-world demands vary greatly. It is unrealistic to expect a non-profit open-source project maintained by a few individuals to meet everyone's needs. + +Finally, we acknowledge that the current design of modules such as configuration management, permission verification, certificate management, and API management is not modern enough. While we may carry out some optimizations in the v1 version, ensuring compatibility remains a challenging issue that requires a considerable amount of effort to address. + +We sincerely appreciate your support for frp. ## Architecture -![architecture](/doc/pic/architecture.png) +

+ architecture +

## Example Usage -Firstly, download the latest programs from [Release](https://github.com/fatedier/frp/releases) page according to your operating system and architecture. +To begin, download the latest program for your operating system and architecture from the [Release](https://github.com/fatedier/frp/releases) page. -Put `frps` and `frps.ini` onto your server A with public IP. +Next, place the `frps` binary and server configuration file on Server A, which has a public IP address. -Put `frpc` and `frpc.ini` onto your server B in LAN (that can't be connected from public Internet). +Finally, place the `frpc` binary and client configuration file on Server B, which is located on a LAN that cannot be directly accessed from the public internet. -### Access your computer in LAN by SSH +Some antiviruses improperly mark frpc as malware and delete it. This is due to frp being a networking tool capable of creating reverse proxies. Antiviruses sometimes flag reverse proxies due to their ability to bypass firewall port restrictions. If you are using antivirus, then you may need to whitelist/exclude frpc in your antivirus settings to avoid accidental quarantine/deletion. See [issue 3637](https://github.com/fatedier/frp/issues/3637) for more details. -1. Modify `frps.ini` on server A and set the `bind_port` to be connected to frp clients: +### Access your computer in a LAN network via SSH - ```ini - # frps.ini - [common] - bind_port = 7000 +1. Modify `frps.toml` on server A by setting the `bindPort` for frp clients to connect to: + + ```toml + # frps.toml + bindPort = 7000 ``` 2. Start `frps` on server A: - `./frps -c ./frps.ini` + `./frps -c ./frps.toml` -3. On server B, modify `frpc.ini` to put in your `frps` server public IP as `server_addr` field: +3. Modify `frpc.toml` on server B and set the `serverAddr` field to the public IP address of your frps server: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [ssh] - type = tcp - local_ip = 127.0.0.1 - local_port = 22 - remote_port = 6000 + [[proxies]] + name = "ssh" + type = "tcp" + localIP = "127.0.0.1" + localPort = 22 + remotePort = 6000 ``` -Note that `local_port` (listened on client) and `remote_port` (exposed on server) are for traffic goes in/out the frp system, whereas `server_port` is used between frps. +Note that the `localPort` (listened on the client) and `remotePort` (exposed on the server) are used for traffic going in and out of the frp system, while the `serverPort` is used for communication between frps and frpc. 4. Start `frpc` on server B: - `./frpc -c ./frpc.ini` + `./frpc -c ./frpc.toml` -5. From another machine, SSH to server B like this (assuming that username is `test`): +5. To access server B from another machine through server A via SSH (assuming the username is `test`), use the following command: `ssh -oPort=6000 test@x.x.x.x` -### Visit your web service in LAN by custom domains +### Multiple SSH services sharing the same port + +This example implements multiple SSH services exposed through the same port using a proxy of type tcpmux. Similarly, as long as the client supports the HTTP Connect proxy connection method, port reuse can be achieved in this way. + +1. Deploy frps on a machine with a public IP and modify the frps.toml file. Here is a simplified configuration: + + ```toml + bindPort = 7000 + tcpmuxHTTPConnectPort = 5002 + ``` + +2. Deploy frpc on the internal machine A with the following configuration: + + ```toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "ssh1" + type = "tcpmux" + multiplexer = "httpconnect" + customDomains = ["machine-a.example.com"] + localIP = "127.0.0.1" + localPort = 22 + ``` + +3. Deploy another frpc on the internal machine B with the following configuration: + + ```toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "ssh2" + type = "tcpmux" + multiplexer = "httpconnect" + customDomains = ["machine-b.example.com"] + localIP = "127.0.0.1" + localPort = 22 + ``` + +4. To access internal machine A using SSH ProxyCommand, assuming the username is "test": + + `ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-a.example.com` + +5. To access internal machine B, the only difference is the domain name, assuming the username is "test": -Sometimes we want to expose a local web service behind a NAT network to others for testing with your own domain name and unfortunately we can't resolve a domain name to a local IP. + `ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-b.example.com` -However, we can expose an HTTP(S) service using frp. +### Accessing Internal Web Services with Custom Domains in LAN -1. Modify `frps.ini`, set the vhost HTTP port to 8080: +Sometimes we need to expose a local web service behind a NAT network to others for testing purposes with our own domain name. - ```ini - # frps.ini - [common] - bind_port = 7000 - vhost_http_port = 8080 +Unfortunately, we cannot resolve a domain name to a local IP. However, we can use frp to expose an HTTP(S) service. + +1. Modify `frps.toml` and set the HTTP port for vhost to 8080: + + ```toml + # frps.toml + bindPort = 7000 + vhostHTTPPort = 8080 ``` + If you want to configure an https proxy, you need to set up the `vhostHTTPSPort`. + 2. Start `frps`: - `./frps -c ./frps.ini` + `./frps -c ./frps.toml` -3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server. The `local_port` is the port of your web service: +3. Modify `frpc.toml` and set `serverAddr` to the IP address of the remote frps server. Specify the `localPort` of your web service: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [web] - type = http - local_port = 80 - custom_domains = www.example.com + [[proxies]] + name = "web" + type = "http" + localPort = 80 + customDomains = ["www.example.com"] ``` 4. Start `frpc`: - `./frpc -c ./frpc.ini` + `./frpc -c ./frpc.toml` -5. Resolve A record of `www.example.com` to the public IP of the remote frps server or CNAME record to your origin domain. +5. Map the A record of `www.example.com` to either the public IP of the remote frps server or a CNAME record pointing to your original domain. -6. Now visit your local web service using url `http://www.example.com:8080`. +6. Visit your local web service using url `http://www.example.com:8080`. -### Forward DNS query request +### Forward DNS query requests -1. Modify `frps.ini`: +1. Modify `frps.toml`: - ```ini - # frps.ini - [common] - bind_port = 7000 + ```toml + # frps.toml + bindPort = 7000 ``` 2. Start `frps`: - `./frps -c ./frps.ini` + `./frps -c ./frps.toml` -3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server, forward DNS query request to Google Public DNS server `8.8.8.8:53`: +3. Modify `frpc.toml` and set `serverAddr` to the IP address of the remote frps server. Forward DNS query requests to the Google Public DNS server `8.8.8.8:53`: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [dns] - type = udp - local_ip = 8.8.8.8 - local_port = 53 - remote_port = 6000 + [[proxies]] + name = "dns" + type = "udp" + localIP = "8.8.8.8" + localPort = 53 + remotePort = 6000 ``` 4. Start frpc: - `./frpc -c ./frpc.ini` + `./frpc -c ./frpc.toml` -5. Test DNS resolution using `dig` command: +5. Test DNS resolution using the `dig` command: `dig @x.x.x.x -p 6000 www.google.com` -### Forward Unix domain socket +### Forward Unix Domain Socket Expose a Unix domain socket (e.g. the Docker daemon socket) as TCP. -Configure `frps` same as above. +Configure `frps` as above. -1. Start `frpc` with configuration: +1. Start `frpc` with the following configuration: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [unix_domain_socket] - type = tcp - remote_port = 6000 - plugin = unix_domain_socket - plugin_unix_path = /var/run/docker.sock + [[proxies]] + name = "unix_domain_socket" + type = "tcp" + remotePort = 6000 + [proxies.plugin] + type = "unix_domain_socket" + unixPath = "/var/run/docker.sock" ``` -2. Test: Get Docker version using `curl`: +2. Test the configuration by getting the docker version using `curl`: `curl http://x.x.x.x:6000/version` ### Expose a simple HTTP file server -Browser your files stored in the LAN, from public Internet. +Expose a simple HTTP file server to access files stored in the LAN from the public Internet. -Configure `frps` same as above. +Configure `frps` as described above, then: + +1. Start `frpc` with the following configuration: -1. Start `frpc` with configuration: - - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [test_static_file] - type = tcp - remote_port = 6000 - plugin = static_file - plugin_local_path = /tmp/files - plugin_strip_prefix = static - plugin_http_user = abc - plugin_http_passwd = abc + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "test_static_file" + type = "tcp" + remotePort = 6000 + [proxies.plugin] + type = "static_file" + localPath = "/tmp/files" + stripPrefix = "static" + httpUser = "abc" + httpPassword = "abc" ``` -2. Visit `http://x.x.x.x:6000/static/` from your browser and specify correct user and password to view files in `/tmp/files` on the `frpc` machine. +2. Visit `http://x.x.x.x:6000/static/` from your browser and specify correct username and password to view files in `/tmp/files` on the `frpc` machine. -### Enable HTTPS for local HTTP(S) service +### Enable HTTPS for a local HTTP(S) service -You may substitute `https2https` for the plugin, and point the `plugin_local_addr` to a HTTPS endpoint. +You may substitute `https2https` for the plugin, and point the `localAddr` to a HTTPS endpoint. -1. Start `frpc` with configuration: +1. Start `frpc` with the following configuration: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [test_https2http] - type = https - custom_domains = test.example.com + [[proxies]] + name = "test_https2http" + type = "https" + customDomains = ["test.example.com"] - plugin = https2http - plugin_local_addr = 127.0.0.1:80 - plugin_crt_path = ./server.crt - plugin_key_path = ./server.key - plugin_host_header_rewrite = 127.0.0.1 - plugin_header_X-From-Where = frp + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:80" + crtPath = "./server.crt" + keyPath = "./server.key" + hostHeaderRewrite = "127.0.0.1" + requestHeaders.set.x-from-where = "frp" ``` 2. Visit `https://test.example.com`. ### Expose your service privately -Some services will be at risk if exposed directly to the public network. With **STCP** (secret TCP) mode, a preshared key is needed to access the service from another client. +To mitigate risks associated with exposing certain services directly to the public network, STCP (Secret TCP) mode requires a preshared key to be used for access to the service from other clients. Configure `frps` same as above. -1. Start `frpc` on machine B with the following config. This example is for exposing the SSH service (port 22), and note the `sk` field for the preshared key, and that the `remote_port` field is removed here: +1. Start `frpc` on machine B with the following config. This example is for exposing the SSH service (port 22), and note the `secretKey` field for the preshared key, and that the `remotePort` field is removed here: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [secret_ssh] - type = stcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 + [[proxies]] + name = "secret_ssh" + type = "stcp" + secretKey = "abcdefg" + localIP = "127.0.0.1" + localPort = 22 ``` -2. Start another `frpc` (typically on another machine C) with the following config to access the SSH service with a security key (`sk` field): - - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [secret_ssh_visitor] - type = stcp - role = visitor - server_name = secret_ssh - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 6000 +2. Start another `frpc` (typically on another machine C) with the following config to access the SSH service with a security key (`secretKey` field): + + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[visitors]] + name = "secret_ssh_visitor" + type = "stcp" + serverName = "secret_ssh" + secretKey = "abcdefg" + bindAddr = "127.0.0.1" + bindPort = 6000 ``` 3. On machine C, connect to SSH on machine B, using this command: @@ -356,50 +441,48 @@ Configure `frps` same as above. ### P2P Mode -**xtcp** is designed for transmitting large amounts of data directly between clients. A frps server is still needed, as P2P here only refers the actual data transmission. - -Note it can't penetrate all types of NAT devices. You might want to fallback to **stcp** if **xtcp** doesn't work. +**xtcp** is designed to transmit large amounts of data directly between clients. A frps server is still needed, as P2P here only refers to the actual data transmission. -1. In `frps.ini` configure a UDP port for xtcp: - - ```ini - # frps.ini - bind_udp_port = 7001 - ``` +Note that it may not work with all types of NAT devices. You might want to fallback to stcp if xtcp doesn't work. -2. Start `frpc` on machine B, expose the SSH port. Note that `remote_port` field is removed: +1. Start `frpc` on machine B, and expose the SSH port. Note that the `remotePort` field is removed: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + # set up a new stun server if the default one is not available. + # natHoleStunServer = "xxx" - [p2p_ssh] - type = xtcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 + [[proxies]] + name = "p2p_ssh" + type = "xtcp" + secretKey = "abcdefg" + localIP = "127.0.0.1" + localPort = 22 ``` -3. Start another `frpc` (typically on another machine C) with the config to connect to SSH using P2P mode: - - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [p2p_ssh_visitor] - type = xtcp - role = visitor - server_name = p2p_ssh - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 6000 +2. Start another `frpc` (typically on another machine C) with the configuration to connect to SSH using P2P mode: + + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + # set up a new stun server if the default one is not available. + # natHoleStunServer = "xxx" + + [[visitors]] + name = "p2p_ssh_visitor" + type = "xtcp" + serverName = "p2p_ssh" + secretKey = "abcdefg" + bindAddr = "127.0.0.1" + bindPort = 6000 + # when automatic tunnel persistence is required, set it to true + keepTunnelOpen = false ``` -4. On machine C, connect to SSH on machine B, using this command: +3. On machine C, connect to SSH on machine B, using this command: `ssh -oPort=6000 127.0.0.1` @@ -407,35 +490,41 @@ Note it can't penetrate all types of NAT devices. You might want to fallback to ### Configuration Files +Since v0.52.0, we support TOML, YAML, and JSON for configuration. Please note that INI is deprecated and will be removed in future releases. New features will only be available in TOML, YAML, or JSON. Users wanting these new features should switch their configuration format accordingly. + Read the full example configuration files to find out even more features not described here. -[Full configuration file for frps (Server)](./conf/frps_full.ini) +Examples use TOML format, but you can still use YAML or JSON. + +These configuration files is for reference only. Please do not use this configuration directly to run the program as it may have various issues. + +[Full configuration file for frps (Server)](./conf/frps_full_example.toml) -[Full configuration file for frpc (Client)](./conf/frpc_full.ini) +[Full configuration file for frpc (Client)](./conf/frpc_full_example.toml) ### Using Environment Variables Environment variables can be referenced in the configuration file, using Go's standard format: -```ini -# frpc.ini -[common] -server_addr = {{ .Envs.FRP_SERVER_ADDR }} -server_port = 7000 - -[ssh] -type = tcp -local_ip = 127.0.0.1 -local_port = 22 -remote_port = {{ .Envs.FRP_SSH_REMOTE_PORT }} +```toml +# frpc.toml +serverAddr = "{{ .Envs.FRP_SERVER_ADDR }}" +serverPort = 7000 + +[[proxies]] +name = "ssh" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +remotePort = {{ .Envs.FRP_SSH_REMOTE_PORT }} ``` With the config above, variables can be passed into `frpc` program like this: ``` -export FRP_SERVER_ADDR="x.x.x.x" -export FRP_SSH_REMOTE_PORT="6000" -./frpc -c ./frpc.ini +export FRP_SERVER_ADDR=x.x.x.x +export FRP_SSH_REMOTE_PORT=6000 +./frpc -c ./frpc.toml ``` `frpc` will render configuration file template using OS environment variables. Remember to prefix your reference with `.Envs`. @@ -444,81 +533,93 @@ export FRP_SSH_REMOTE_PORT="6000" You can split multiple proxy configs into different files and include them in the main file. -```ini -# frpc.ini -[common] -server_addr = x.x.x.x -server_port = 7000 -includes=./confd/*.ini +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 +includes = ["./confd/*.toml"] ``` -```ini -# ./confd/test.ini -[ssh] -type = tcp -local_ip = 127.0.0.1 -local_port = 22 -remote_port = 6000 +```toml +# ./confd/test.toml + +[[proxies]] +name = "ssh" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +remotePort = 6000 ``` -### Dashboard +### Server Dashboard Check frp's status and proxies' statistics information by Dashboard. Configure a port for dashboard to enable this feature: -```ini -[common] -dashboard_port = 7500 +```toml +# The default value is 127.0.0.1. Change it to 0.0.0.0 when you want to access it from a public network. +webServer.addr = "0.0.0.0" +webServer.port = 7500 # dashboard's username and password are both optional -dashboard_user = admin -dashboard_pwd = admin +webServer.user = "admin" +webServer.password = "admin" ``` -Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin`. +Then visit `http://[serverAddr]:7500` to see the dashboard, with username and password both being `admin`. Additionally, you can use HTTPS port by using your domains wildcard or normal SSL certificate: -```ini -[common] -dashboard_port = 7500 +```toml +webServer.port = 7500 # dashboard's username and password are both optional -dashboard_user = admin -dashboard_pwd = admin -dashboard_tls_mode = true -dashboard_tls_cert_file = server.crt -dashboard_tls_key_file = server.key +webServer.user = "admin" +webServer.password = "admin" +webServer.tls.certFile = "server.crt" +webServer.tls.keyFile = "server.key" ``` -Then visit `https://[server_addr]:7500` to see the dashboard in secure HTTPS connection, with username and password both being `admin`. +Then visit `https://[serverAddr]:7500` to see the dashboard in secure HTTPS connection, with username and password both being `admin`. ![dashboard](/doc/pic/dashboard.png) -### Admin UI +### Client Admin UI -The Admin UI helps you check and manage frpc's configuration. +The Client Admin UI helps you check and manage frpc's configuration and proxies. Configure an address for admin UI to enable this feature: -```ini -[common] -admin_addr = 127.0.0.1 -admin_port = 7400 -admin_user = admin -admin_pwd = admin +```toml +webServer.addr = "127.0.0.1" +webServer.port = 7400 +webServer.user = "admin" +webServer.password = "admin" ``` Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`. +#### Dynamic Proxy Management (Store) + +You can dynamically create, update, and delete proxies and visitors at runtime through the Web UI or API, without restarting frpc. + +To enable this feature, configure `store.path` to specify a file for persisting the configurations: + +```toml +[store] +path = "./db.json" +``` + +Proxies and visitors managed through the Store are saved to disk and automatically restored on frpc restart. They work alongside proxies defined in the configuration file — Store entries take precedence when names conflict. + ### Monitor -When dashboard is enabled, frps will save monitor data in cache. It will be cleared after process restart. +When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart. Prometheus is also supported. #### Prometheus -Enable dashboard first, then configure `enable_prometheus = true` in `frps.ini`. +Enable dashboard first, then configure `enablePrometheus = true` in `frps.toml`. `http://{dashboard_addr}/metrics` will provide prometheus monitor data. @@ -526,83 +627,97 @@ Enable dashboard first, then configure `enable_prometheus = true` in `frps.ini`. There are 2 authentication methods to authenticate frpc with frps. -You can decide which one to use by configuring `authentication_method` under `[common]` in `frpc.ini` and `frps.ini`. +You can decide which one to use by configuring `auth.method` in `frpc.toml` and `frps.toml`, the default one is token. -Configuring `authenticate_heartbeats = true` under `[common]` will use the configured authentication method to add and validate authentication on every heartbeat between frpc and frps. +Configuring `auth.additionalScopes = ["HeartBeats"]` will use the configured authentication method to add and validate authentication on every heartbeat between frpc and frps. -Configuring `authenticate_new_work_conns = true` under `[common]` will do the same for every new work connection between frpc and frps. +Configuring `auth.additionalScopes = ["NewWorkConns"]` will do the same for every new work connection between frpc and frps. #### Token Authentication -When specifying `authentication_method = token` under `[common]` in `frpc.ini` and `frps.ini` - token based authentication will be used. +When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token based authentication will be used. + +Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation -Make sure to specify the same `token` in the `[common]` section in `frps.ini` and `frpc.ini` for frpc to pass frps validation +##### Token Source + +frp supports reading authentication tokens from external sources using the `tokenSource` configuration. Currently, file-based token source is supported. + +**File-based token source:** + +```toml +# frpc.toml +auth.method = "token" +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "/path/to/token/file" +``` + +The token will be read from the specified file at startup. This is useful for scenarios where tokens are managed by external systems or need to be kept separate from configuration files for security reasons. #### OIDC Authentication -When specifying `authentication_method = oidc` under `[common]` in `frpc.ini` and `frps.ini` - OIDC based authentication will be used. +When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used. OIDC stands for OpenID Connect, and the flow used is called [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). -To use this authentication type - configure `frpc.ini` and `frps.ini` as follows: +To use this authentication type - configure `frpc.toml` and `frps.toml` as follows: -```ini -# frps.ini -[common] -authentication_method = oidc -oidc_issuer = https://example-oidc-issuer.com/ -oidc_audience = https://oidc-audience.com/.default +```toml +# frps.toml +auth.method = "oidc" +auth.oidc.issuer = "https://example-oidc-issuer.com/" +auth.oidc.audience = "https://oidc-audience.com/.default" ``` -```ini -# frpc.ini -[common] -authentication_method = oidc -oidc_client_id = 98692467-37de-409a-9fac-bb2585826f18 # Replace with OIDC client ID -oidc_client_secret = oidc_secret -oidc_audience = https://oidc-audience.com/.default -oidc_token_endpoint_url = https://example-oidc-endpoint.com/oauth2/v2.0/token +```toml +# frpc.toml +auth.method = "oidc" +auth.oidc.clientID = "98692467-37de-409a-9fac-bb2585826f18" # Replace with OIDC client ID +auth.oidc.clientSecret = "oidc_secret" +auth.oidc.audience = "https://oidc-audience.com/.default" +auth.oidc.tokenEndpointURL = "https://example-oidc-endpoint.com/oauth2/v2.0/token" ``` ### Encryption and Compression The features are off by default. You can turn on encryption and/or compression: -```ini -# frpc.ini -[ssh] -type = tcp -local_port = 22 -remote_port = 6000 -use_encryption = true -use_compression = true +```toml +# frpc.toml + +[[proxies]] +name = "ssh" +type = "tcp" +localPort = 22 +remotePort = 6000 +transport.useEncryption = true +transport.useCompression = true ``` #### TLS -frp supports the TLS protocol between `frpc` and `frps` since v0.25.0. +Since v0.50.0, the default value of `transport.tls.enable` and `transport.tls.disableCustomTLSFirstByte` has been changed to true, and tls is enabled by default. -For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. +For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. This only takes effect when you set `transport.tls.disableCustomTLSFirstByte` to false. -Configure `tls_enable = true` in the `[common]` section to `frpc.ini` to enable this feature. +To **enforce** `frps` to only accept TLS connections - configure `transport.tls.force = true` in `frps.toml`. **This is optional.** -To **enforce** `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. **This is optional.** +**`frpc` TLS settings:** -**`frpc` TLS settings (under the `[common]` section):** -```ini -tls_enable = true -tls_cert_file = certificate.crt -tls_key_file = certificate.key -tls_trusted_ca_file = ca.crt +```toml +transport.tls.enable = true +transport.tls.certFile = "certificate.crt" +transport.tls.keyFile = "certificate.key" +transport.tls.trustedCaFile = "ca.crt" ``` -**`frps` TLS settings (under the `[common]` section):** -```ini -tls_only = true -tls_enable = true -tls_cert_file = certificate.crt -tls_key_file = certificate.key -tls_trusted_ca_file = ca.crt +**`frps` TLS settings:** + +```toml +transport.tls.force = true +transport.tls.certFile = "certificate.crt" +transport.tls.keyFile = "certificate.key" +transport.tls.trustedCaFile = "ca.crt" ``` You will need **a root CA cert** and **at least one SSL/TLS certificate**. It **can** be self-signed or regular (such as Let's Encrypt or another SSL/TLS certificate provider). @@ -656,7 +771,7 @@ openssl req -new -sha256 -key server.key \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com")) \ -out server.csr -openssl x509 -req -days 365 \ +openssl x509 -req -days 365 -sha256 \ -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com") \ -out server.crt @@ -671,7 +786,7 @@ openssl req -new -sha256 -key client.key \ -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:client.com,DNS:example.client.com")) \ -out client.csr -openssl x509 -req -days 365 \ +openssl x509 -req -days 365 -sha256 \ -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -extfile <(printf "subjectAltName=DNS:client.com,DNS:example.client.com") \ -out client.crt @@ -679,40 +794,51 @@ openssl x509 -req -days 365 \ ### Hot-Reloading frpc configuration -The `admin_addr` and `admin_port` fields are required for enabling HTTP API: +The `webServer` fields are required for enabling HTTP API: -```ini -# frpc.ini -[common] -admin_addr = 127.0.0.1 -admin_port = 7400 +```toml +# frpc.toml +webServer.addr = "127.0.0.1" +webServer.port = 7400 ``` -Then run command `frpc reload -c ./frpc.ini` and wait for about 10 seconds to let `frpc` create or update or remove proxies. +Then run command `frpc reload -c ./frpc.toml` and wait for about 10 seconds to let `frpc` create or update or remove proxies. + +**Note that global client parameters won't be modified except 'start'.** -**Note that parameters in [common] section won't be modified except 'start'.** +`start` is a global allowlist evaluated after all sources are merged (config file/include/store). +If `start` is non-empty, any proxy or visitor not listed there will not be started, including +entries created via Store API. -You can run command `frpc verify -c ./frpc.ini` before reloading to check if there are config errors. +`start` is kept mainly for compatibility and is generally not recommended for new configurations. +Prefer per-proxy/per-visitor `enabled`, and keep `start` empty unless you explicitly want this +global allowlist behavior. + +You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors. ### Get proxy status from client -Use `frpc status -c ./frpc.ini` to get status of all proxies. The `admin_addr` and `admin_port` fields are required for enabling HTTP API. +Use `frpc status -c ./frpc.toml` to get status of all proxies. The `webServer` fields are required for enabling HTTP API. ### Only allowing certain ports on the server -`allow_ports` in `frps.ini` is used to avoid abuse of ports: +`allowPorts` in `frps.toml` is used to avoid abuse of ports: -```ini -# frps.ini -[common] -allow_ports = 2000-3000,3001,3003,4000-50000 +```toml +# frps.toml +allowPorts = [ + { start = 2000, end = 3000 }, + { single = 3001 }, + { single = 3003 }, + { start = 4000, end = 50000 } +] ``` -`allow_ports` consists of specific ports or port ranges (lowest port number, dash `-`, highest port number), separated by comma `,`. - ### Port Reuse -`vhost_http_port` and `vhost_https_port` in frps can use same port with `bind_port`. frps will detect the connection's protocol and handle it correspondingly. +`vhostHTTPPort` and `vhostHTTPSPort` in frps can use same port with `bindPort`. frps will detect the connection's protocol and handle it correspondingly. + +What you need to pay attention to is that if you want to configure `vhostHTTPSPort` and `bindPort` to the same port, you need to first set `transport.tls.disableCustomTLSFirstByte` to false. We would like to try to allow multiple proxies bind a same remote port with different protocols in the future. @@ -720,27 +846,30 @@ We would like to try to allow multiple proxies bind a same remote port with diff #### For Each Proxy -```ini -# frpc.ini -[ssh] -type = tcp -local_port = 22 -remote_port = 6000 -bandwidth_limit = 1MB +```toml +# frpc.toml + +[[proxies]] +name = "ssh" +type = "tcp" +localPort = 22 +remotePort = 6000 +transport.bandwidthLimit = "1MB" ``` -Set `bandwidth_limit` in each proxy's configure to enable this feature. Supported units are `MB` and `KB`. +Set `transport.bandwidthLimit` in each proxy's configure to enable this feature. Supported units are `MB` and `KB`. + +Set `transport.bandwidthLimitMode` to `client` or `server` to limit bandwidth on the client or server side. Default is `client`. ### TCP Stream Multiplexing frp supports tcp stream multiplexing since v0.10.0 like HTTP2 Multiplexing, in which case all logic connections to the same frpc are multiplexed into the same TCP connection. -You can disable this feature by modify `frps.ini` and `frpc.ini`: +You can disable this feature by modify `frps.toml` and `frpc.toml`: -```ini -# frps.ini and frpc.ini, must be same -[common] -tcp_mux = false +```toml +# frps.toml and frpc.toml, must be same +transport.tcpMux = false ``` ### Support KCP Protocol @@ -751,25 +880,50 @@ KCP mode uses UDP as the underlying transport. Using KCP in frp: 1. Enable KCP in frps: - ```ini - # frps.ini - [common] - bind_port = 7000 + ```toml + # frps.toml + bindPort = 7000 # Specify a UDP port for KCP. - kcp_bind_port = 7000 + kcpBindPort = 7000 + ``` + + The `kcpBindPort` number can be the same number as `bindPort`, since `bindPort` field specifies a TCP port. + +2. Configure `frpc.toml` to use KCP to connect to frps: + + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + # Same as the 'kcpBindPort' in frps.toml + serverPort = 7000 + transport.protocol = "kcp" + ``` + +### Support QUIC Protocol + +QUIC is a new multiplexed transport built on top of UDP. + +Using QUIC in frp: + +1. Enable QUIC in frps: + + ```toml + # frps.toml + bindPort = 7000 + # Specify a UDP port for QUIC. + quicBindPort = 7000 ``` - The `kcp_bind_port` number can be the same number as `bind_port`, since `bind_port` field specifies a TCP port. + The `quicBindPort` number can be the same number as `bindPort`, since `bindPort` field specifies a TCP port. -2. Configure `frpc.ini` to use KCP to connect to frps: +2. Configure `frpc.toml` to use QUIC to connect to frps: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - # Same as the 'kcp_bind_port' in frps.ini - server_port = 7000 - protocol = kcp + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + # Same as the 'quicBindPort' in frps.toml + serverPort = 7000 + transport.protocol = "quic" ``` ### Connection Pooling @@ -778,20 +932,18 @@ By default, frps creates a new frpc connection to the backend service upon a use This feature is suitable for a large number of short connections. -1. Configure the limit of pool count each proxy can use in `frps.ini`: +1. Configure the limit of pool count each proxy can use in `frps.toml`: - ```ini - # frps.ini - [common] - max_pool_count = 5 + ```toml + # frps.toml + transport.maxPoolCount = 5 ``` 2. Enable and specify the number of connection pool: - ```ini - # frpc.ini - [common] - pool_count = 1 + ```toml + # frpc.toml + transport.poolCount = 1 ``` ### Load balancing @@ -800,132 +952,144 @@ Load balancing is supported by `group`. This feature is only available for types `tcp`, `http`, `tcpmux` now. -```ini -# frpc.ini -[test1] -type = tcp -local_port = 8080 -remote_port = 80 -group = web -group_key = 123 - -[test2] -type = tcp -local_port = 8081 -remote_port = 80 -group = web -group_key = 123 +```toml +# frpc.toml + +[[proxies]] +name = "test1" +type = "tcp" +localPort = 8080 +remotePort = 80 +loadBalancer.group = "web" +loadBalancer.groupKey = "123" + +[[proxies]] +name = "test2" +type = "tcp" +localPort = 8081 +remotePort = 80 +loadBalancer.group = "web" +loadBalancer.groupKey = "123" ``` -`group_key` is used for authentication. +`loadBalancer.groupKey` is used for authentication. Connections to port 80 will be dispatched to proxies in the same group randomly. -For type `tcp`, `remote_port` in the same group should be the same. +For type `tcp`, `remotePort` in the same group should be the same. -For type `http`, `custom_domains`, `subdomain`, `locations` should be the same. +For type `http`, `customDomains`, `subdomain`, `locations` should be the same. ### Service Health Check Health check feature can help you achieve high availability with load balancing. -Add `health_check_type = tcp` or `health_check_type = http` to enable health check. +Add `healthCheck.type = "tcp"` or `healthCheck.type = "http"` to enable health check. With health check type **tcp**, the service port will be pinged (TCPing): -```ini -# frpc.ini -[test1] -type = tcp -local_port = 22 -remote_port = 6000 +```toml +# frpc.toml + +[[proxies]] +name = "test1" +type = "tcp" +localPort = 22 +remotePort = 6000 # Enable TCP health check -health_check_type = tcp +healthCheck.type = "tcp" # TCPing timeout seconds -health_check_timeout_s = 3 +healthCheck.timeoutSeconds = 3 # If health check failed 3 times in a row, the proxy will be removed from frps -health_check_max_failed = 3 +healthCheck.maxFailed = 3 # A health check every 10 seconds -health_check_interval_s = 10 +healthCheck.intervalSeconds = 10 ``` With health check type **http**, an HTTP request will be sent to the service and an HTTP 2xx OK response is expected: -```ini -# frpc.ini -[web] -type = http -local_ip = 127.0.0.1 -local_port = 80 -custom_domains = test.example.com +```toml +# frpc.toml + +[[proxies]] +name = "web" +type = "http" +localIP = "127.0.0.1" +localPort = 80 +customDomains = ["test.example.com"] # Enable HTTP health check -health_check_type = http +healthCheck.type = "http" # frpc will send a GET request to '/status' # and expect an HTTP 2xx OK response -health_check_url = /status -health_check_timeout_s = 3 -health_check_max_failed = 3 -health_check_interval_s = 10 +healthCheck.path = "/status" +healthCheck.timeoutSeconds = 3 +healthCheck.maxFailed = 3 +healthCheck.intervalSeconds = 10 ``` ### Rewriting the HTTP Host Header By default frp does not modify the tunneled HTTP requests at all as it's a byte-for-byte copy. -However, speaking of web servers and HTTP requests, your web server might rely on the `Host` HTTP header to determine the website to be accessed. frp can rewrite the `Host` header when forwarding the HTTP requests, with the `host_header_rewrite` field: +However, speaking of web servers and HTTP requests, your web server might rely on the `Host` HTTP header to determine the website to be accessed. frp can rewrite the `Host` header when forwarding the HTTP requests, with the `hostHeaderRewrite` field: + +```toml +# frpc.toml -```ini -# frpc.ini -[web] -type = http -local_port = 80 -custom_domains = test.example.com -host_header_rewrite = dev.example.com +[[proxies]] +name = "web" +type = "http" +localPort = 80 +customDomains = ["test.example.com"] +hostHeaderRewrite = "dev.example.com" ``` -The HTTP request will have the the `Host` header rewritten to `Host: dev.example.com` when it reaches the actual web server, although the request from the browser probably has `Host: test.example.com`. +The HTTP request will have the `Host` header rewritten to `Host: dev.example.com` when it reaches the actual web server, although the request from the browser probably has `Host: test.example.com`. ### Setting other HTTP Headers -Similar to `Host`, You can override other HTTP request headers with proxy type `http`. +Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`. -```ini -# frpc.ini -[web] -type = http -local_port = 80 -custom_domains = test.example.com -host_header_rewrite = dev.example.com -header_X-From-Where = frp -``` +```toml +# frpc.toml -Note that parameter(s) prefixed with `header_` will be added to HTTP request headers. +[[proxies]] +name = "web" +type = "http" +localPort = 80 +customDomains = ["test.example.com"] +hostHeaderRewrite = "dev.example.com" +requestHeaders.set.x-from-where = "frp" +responseHeaders.set.foo = "bar" +``` -In this example, it will set header `X-From-Where: frp` in the HTTP request. +In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response. ### Get Real IP #### HTTP X-Forwarded-For -This feature is for http proxy only. +This feature is for `http` proxies or proxies with the `https2http` and `https2https` plugins enabled. You can get user's real IP from HTTP request headers `X-Forwarded-For`. #### Proxy Protocol -frp supports Proxy Protocol to send user's real IP to local services. It support all types except UDP. +frp supports Proxy Protocol to send user's real IP to local services. Here is an example for https service: -```ini -# frpc.ini -[web] -type = https -local_port = 443 -custom_domains = test.example.com +```toml +# frpc.toml + +[[proxies]] +name = "web" +type = "https" +localPort = 443 +customDomains = ["test.example.com"] # now v1 and v2 are supported -proxy_protocol_version = v2 +transport.proxyProtocolVersion = "v2" ``` You can enable Proxy Protocol support in nginx to expose user's real IP in HTTP header `X-Real-IP`, and then read `X-Real-IP` header in your web service for the real IP. @@ -938,14 +1102,16 @@ This enforces HTTP Basic Auth on all requests with the username and password spe It can only be enabled when proxy type is http. -```ini -# frpc.ini -[web] -type = http -local_port = 80 -custom_domains = test.example.com -http_user = abc -http_pwd = abc +```toml +# frpc.toml + +[[proxies]] +name = "web" +type = "http" +localPort = 80 +customDomains = ["test.example.com"] +httpUser = "abc" +httpPassword = "abc" ``` Visit `http://test.example.com` in the browser and now you are prompted to enter the username and password. @@ -954,24 +1120,26 @@ Visit `http://test.example.com` in the browser and now you are prompted to enter It is convenient to use `subdomain` configure for http and https types when many people share one frps server. -```ini -# frps.ini -subdomain_host = frps.com +```toml +# frps.toml +subDomainHost = "frps.com" ``` Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard DNS record. -```ini -# frpc.ini -[web] -type = http -local_port = 80 -subdomain = test +```toml +# frpc.toml + +[[proxies]] +name = "web" +type = "http" +localPort = 80 +subdomain = "test" ``` Now you can visit your web service on `test.frps.com`. -Note that if `subdomain_host` is not empty, `custom_domains` should not be the subdomain of `subdomain_host`. +Note that if `subdomainHost` is not empty, `customDomains` should not be the subdomain of `subdomainHost`. ### URL Routing @@ -979,59 +1147,62 @@ frp supports forwarding HTTP requests to different backend web services by url r `locations` specifies the prefix of URL used for routing. frps first searches for the most specific prefix location given by literal strings regardless of the listed order. -```ini -# frpc.ini -[web01] -type = http -local_port = 80 -custom_domains = web.example.com -locations = / - -[web02] -type = http -local_port = 81 -custom_domains = web.example.com -locations = /news,/about +```toml +# frpc.toml + +[[proxies]] +name = "web01" +type = "http" +localPort = 80 +customDomains = ["web.example.com"] +locations = ["/"] + +[[proxies]] +name = "web02" +type = "http" +localPort = 81 +customDomains = ["web.example.com"] +locations = ["/news", "/about"] ``` HTTP requests with URL prefix `/news` or `/about` will be forwarded to **web02** and other requests to **web01**. ### TCP Port Multiplexing -frp supports receiving TCP sockets directed to different proxies on a single port on frps, similar to `vhost_http_port` and `vhost_https_port`. +frp supports receiving TCP sockets directed to different proxies on a single port on frps, similar to `vhostHTTPPort` and `vhostHTTPSPort`. The only supported TCP port multiplexing method available at the moment is `httpconnect` - HTTP CONNECT tunnel. -When setting `tcpmux_httpconnect_port` to anything other than 0 in frps under `[common]`, frps will listen on this port for HTTP CONNECT requests. +When setting `tcpmuxHTTPConnectPort` to anything other than 0 in frps, frps will listen on this port for HTTP CONNECT requests. -The host of the HTTP CONNECT request will be used to match the proxy in frps. Proxy hosts can be configured in frpc by configuring `custom_domain` and / or `subdomain` under `type = tcpmux` proxies, when `multiplexer = httpconnect`. +The host of the HTTP CONNECT request will be used to match the proxy in frps. Proxy hosts can be configured in frpc by configuring `customDomains` and / or `subdomain` under `tcpmux` proxies, when `multiplexer = "httpconnect"`. For example: -```ini -# frps.ini -[common] -bind_port = 7000 -tcpmux_httpconnect_port = 1337 +```toml +# frps.toml +bindPort = 7000 +tcpmuxHTTPConnectPort = 1337 ``` -```ini -# frpc.ini -[common] -server_addr = x.x.x.x -server_port = 7000 - -[proxy1] -type = tcpmux -multiplexer = httpconnect -custom_domains = test1 -local_port = 80 - -[proxy2] -type = tcpmux -multiplexer = httpconnect -custom_domains = test2 -local_port = 8080 +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 + +[[proxies]] +name = "proxy1" +type = "tcpmux" +multiplexer = "httpconnect" +customDomains = ["test1"] +localPort = 80 + +[[proxies]] +name = "proxy2" +type = "tcpmux" +multiplexer = "httpconnect" +customDomains = ["test2"] +localPort = 8080 ``` In the above configuration - frps can be contacted on port 1337 with a HTTP CONNECT header such as: @@ -1041,34 +1212,36 @@ CONNECT test1 HTTP/1.1\r\n\r\n ``` and the connection will be routed to `proxy1`. -### Connecting to frps via HTTP PROXY +### Connecting to frps via PROXY -frpc can connect to frps using HTTP proxy if you set OS environment variable `HTTP_PROXY`, or if `http_proxy` is set in frpc.ini file. +frpc can connect to frps through proxy if you set OS environment variable `HTTP_PROXY`, or if `transport.proxyURL` is set in frpc.toml file. It only works when protocol is tcp. -```ini -# frpc.ini -[common] -server_addr = x.x.x.x -server_port = 7000 -http_proxy = http://user:pwd@192.168.1.128:8080 +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 +transport.proxyURL = "http://user:pwd@192.168.1.128:8080" ``` -### Range ports mapping +### Port range mapping -Proxy with names that start with `range:` will support mapping range ports. +*Added in v0.56.0* -```ini -# frpc.ini -[range:test_tcp] -type = tcp -local_ip = 127.0.0.1 -local_port = 6000-6006,6007 -remote_port = 6000-6006,6007 -``` +We can use the range syntax of Go template combined with the built-in `parseNumberRangePair` function to achieve port range mapping. -frpc will generate 8 proxies like `test_tcp_0`, `test_tcp_1`, ..., `test_tcp_7`. +The following example, when run, will create 8 proxies named `test-6000, test-6001 ... test-6007`, each mapping the remote port to the local port. + +``` +{{- range $_, $v := parseNumberRangePair "6000-6006,6007" "6000-6006,6007" }} +[[proxies]] +name = "tcp-{{ $v.First }}" +type = "tcp" +localPort = {{ $v.First }} +remotePort = {{ $v.Second }} +{{- end }} +``` ### Client Plugins @@ -1076,21 +1249,22 @@ frpc only forwards requests to local TCP or UDP ports by default. Plugins are used for providing rich features. There are built-in plugins such as `unix_domain_socket`, `http_proxy`, `socks5`, `static_file`, `http2https`, `https2http`, `https2https` and you can see [example usage](#example-usage). -Specify which plugin to use with the `plugin` parameter. Configuration parameters of plugin should be started with `plugin_`. `local_ip` and `local_port` are not used for plugin. - Using plugin **http_proxy**: -```ini -# frpc.ini -[http_proxy] -type = tcp -remote_port = 6000 -plugin = http_proxy -plugin_http_user = abc -plugin_http_passwd = abc +```toml +# frpc.toml + +[[proxies]] +name = "http_proxy" +type = "tcp" +remotePort = 6000 +[proxies.plugin] +type = "http_proxy" +httpUser = "abc" +httpPassword = "abc" ``` -`plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin. +`httpUser` and `httpPassword` are configuration parameters used in `http_proxy` plugin. ### Server Manage Plugins @@ -1098,9 +1272,81 @@ Read the [document](/doc/server_plugin.md). Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin). -## Development Plan +### SSH Tunnel Gateway + +*added in v0.53.0* + +frp supports listening to an SSH port on the frps side and achieves TCP protocol proxying through the SSH -R protocol, without relying on frpc. + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +``` + +When running `./frps -c frps.toml`, a private key file named `.autogen_ssh_key` will be automatically created in the current working directory. This generated private key file will be used by the SSH server in frps. + +Executing the command + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090 +``` + +sets up a proxy on frps that forwards the local 8080 service to the port 9090. + +```bash +frp (via SSH) (Ctrl+C to quit) + +User: +ProxyName: test-tcp +Type: tcp +RemoteAddress: :9090 +``` + +This is equivalent to: + +```bash +frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090 +``` + +Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information. + +### Virtual Network (VirtualNet) + +*Alpha feature added in v0.62.0* + +The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity. + +For detailed information about configuration and usage, please refer to the [VirtualNet documentation](/doc/virtual_net.md). + +## Feature Gates + +frp supports feature gates to enable or disable experimental features. This allows users to try out new features before they're considered stable. + +### Available Feature Gates + +| Name | Stage | Default | Description | +|------|-------|---------|-------------| +| VirtualNet | ALPHA | false | Virtual network capabilities for frp | + +### Enabling Feature Gates + +To enable an experimental feature, add the feature gate to your configuration: + +```toml +featureGates = { VirtualNet = true } +``` + +### Feature Lifecycle + +Features typically go through three stages: +1. **ALPHA**: Disabled by default, may be unstable +2. **BETA**: May be enabled by default, more stable but still evolving +3. **GA (Generally Available)**: Enabled by default, ready for production use + +## Related Projects -* Log HTTP request information in frps. +* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios. +* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources. ## Contributing diff --git a/README_zh.md b/README_zh.md index 854ae966a46..600cd4fd39f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,48 +1,59 @@ # frp -[![Build Status](https://travis-ci.org/fatedier/frp.svg?branch=master)](https://travis-ci.org/fatedier/frp) +[![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) +[![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) -frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。 +frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。 -

Platinum Sponsors

- +## Sponsors +frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者们的支持。如果你愿意加入他们的行列,请考虑 [赞助 frp 的开发](https://github.com/sponsors/fatedier)。 + +

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

- +
-

Gold Sponsors

- +## Recall.ai - API for meeting recordings + +If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), + +an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. + +

- - + + +
+ The complete IDE crafted for professional Go developers

- -

Silver Sponsors

- -* Sakura Frp - 欢迎点击 "加入我们" - ## 为什么使用 frp ? 通过在具有公网 IP 的节点上部署 frp 服务端,可以轻松地将内网服务穿透到公网,同时提供诸多专业的功能特性,这包括: -* 客户端服务端通信支持 TCP、KCP 以及 Websocket 等多种协议。 -* 采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间。 +* 客户端服务端通信支持 TCP、QUIC、KCP 以及 Websocket 等多种协议。 +* 采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间,降低请求延迟。 * 代理组间的负载均衡。 * 端口复用,多个服务通过同一个服务端端口暴露。 -* 多个原生支持的客户端插件(静态文件查看,HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作。 -* 高度扩展性的服务端插件系统,方便结合自身需求进行功能扩展。 +* 支持 P2P 通信,流量不经过服务器中转,充分利用带宽资源。 +* 多个原生支持的客户端插件(静态文件查看,HTTPS/HTTP 协议转换,HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作。 +* 高度扩展性的服务端插件系统,易于结合自身需求进行功能扩展。 * 服务端和客户端 UI 页面。 ## 开发状态 @@ -51,13 +62,25 @@ frp 目前已被很多公司广泛用于测试、生产环境。 master 分支用于发布稳定版本,dev 分支用于开发,您可以尝试下载最新的 release 版本进行测试。 -我们正在进行 v2 大版本的开发,将会尝试在各个方面进行重构和升级,且不会与 v1 版本进行兼容,预计会持续一段时间。 +我们正在进行 v2 大版本的开发,将会尝试在各个方面进行重构和升级,且不会与 v1 版本进行兼容,预计会持续较长的一段时间。 现在的 v0 版本将会在合适的时间切换为 v1 版本并且保证兼容性,后续只做 bug 修复和优化,不再进行大的功能性更新。 +### 关于 v2 的一些说明 + +v2 版本的复杂度和难度比我们预期的要高得多。我只能利用零散的时间进行开发,而且由于上下文经常被打断,效率极低。由于这种情况可能会持续一段时间,我们仍然会在当前版本上进行一些优化和迭代,直到我们有更多空闲时间来推进大版本的重构,或者也有可能放弃一次性的重构,而是采用渐进的方式在当前版本上逐步做一些可能会导致不兼容的修改。 + +v2 的构想是基于我多年在云原生领域,特别是在 K8s 和 ServiceMesh 方面的工作经验和思考。它的核心是一个现代化的四层和七层代理,类似于 envoy。这个代理本身高度可扩展,不仅可以用于实现内网穿透的功能,还可以应用于更多领域。在这个高度可扩展的内核基础上,我们将实现 frp v1 中的所有功能,并且能够以一种更加优雅的方式实现原先架构中无法实现或不易实现的功能。同时,我们将保持高效的开发和迭代能力。 + +除此之外,我希望 frp 本身也成为一个高度可扩展的系统和平台,就像我们可以基于 K8s 提供一系列扩展能力一样。在 K8s 上,我们可以根据企业需求进行定制化开发,例如使用 CRD、controller 模式、webhook、CSI 和 CNI 等。在 frp v1 中,我们引入了服务端插件的概念,实现了一些简单的扩展性。但是,它实际上依赖于简单的 HTTP 协议,并且需要用户自己启动独立的进程和管理。这种方式远远不够灵活和方便,而且现实世界的需求千差万别,我们不能期望一个由少数人维护的非营利性开源项目能够满足所有人的需求。 + +最后,我们意识到像配置管理、权限验证、证书管理和管理 API 等模块的当前设计并不够现代化。尽管我们可能在 v1 版本中进行一些优化,但确保兼容性是一个令人头疼的问题,需要投入大量精力来解决。 + +非常感谢您对 frp 的支持。 + ## 文档 -完整文档已经迁移至 [https://gofrp.org](https://gofrp.org/docs)。 +完整文档已经迁移至 [https://gofrp.org](https://gofrp.org)。 ## 为 frp 做贡献 @@ -70,28 +93,23 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 * 贡献代码请提交 PR 至 dev 分支,master 分支仅用于发布稳定可用版本。 * 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。 -**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。** +**提醒:和项目相关的问题请在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。** -## 捐助 +## 关联项目 -如果您觉得 frp 对你有帮助,欢迎给予我们一定的捐助来维持项目的长期发展。 - -### GitHub Sponsors +* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。 +* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。 -您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。 +## 赞助 -企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 - -### 知识星球 - -如果您想学习 frp 相关的知识和技术,或者寻求任何帮助及咨询,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群: +如果您觉得 frp 对你有帮助,欢迎给予我们一定的捐助来维持项目的长期发展。 -![zsxq](/doc/pic/zsxq.jpg) +### Sponsors -### 支付宝扫码捐赠 +长期赞助可以帮助我们保持项目的持续发展。 -![donate-alipay](/doc/pic/donate-alipay.png) +您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。 -### 微信支付捐赠 +国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。 -![donate-wechatpay](/doc/pic/donate-wechatpay.png) +企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 diff --git a/Release.md b/Release.md index 35c8b12ea31..33fc0aea201 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,9 @@ -### New +## Features -* Use auto generated certificates if `plugin_key_path` and `plugin_crt_path` are empty for plugin `https2https` and `https2http`. -* Server dashboard supports TLS configs. +* 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. -### Fix +## Fixes -* xtcp error with IPv6 address. +* 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. diff --git a/assets/assets.go b/assets/assets.go index 721c2f6cdda..713087adf6b 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -29,19 +29,28 @@ var ( prefixPath string ) +type emptyFS struct{} + +func (emptyFS) Open(name string) (http.File, error) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} + // if path is empty, load assets in memory // or set FileSystem using disk files func Load(path string) { prefixPath = path - if prefixPath != "" { + switch { + case prefixPath != "": FileSystem = http.Dir(prefixPath) - } else { + case content != nil: FileSystem = http.FS(content) + default: + FileSystem = emptyFS{} } } func Register(fileSystem fs.FS) { - subFs, err := fs.Sub(fileSystem, "static") + subFs, err := fs.Sub(fileSystem, "dist") if err == nil { content = subFs } diff --git a/assets/frpc/static/535877f50039c0cb49a6196a5b7517cd.woff b/assets/frpc/static/535877f50039c0cb49a6196a5b7517cd.woff deleted file mode 100644 index 02b9a2539e4..00000000000 Binary files a/assets/frpc/static/535877f50039c0cb49a6196a5b7517cd.woff and /dev/null differ diff --git a/assets/frpc/static/732389ded34cb9c52dd88271f1345af9.ttf b/assets/frpc/static/732389ded34cb9c52dd88271f1345af9.ttf deleted file mode 100644 index 91b74de3677..00000000000 Binary files a/assets/frpc/static/732389ded34cb9c52dd88271f1345af9.ttf and /dev/null differ diff --git a/assets/frpc/static/index.html b/assets/frpc/static/index.html deleted file mode 100644 index 0eb31dcd1ce..00000000000 --- a/assets/frpc/static/index.html +++ /dev/null @@ -1 +0,0 @@ - frp client admin UI
\ No newline at end of file diff --git a/assets/frpc/static/manifest.js b/assets/frpc/static/manifest.js deleted file mode 100644 index 7a6e69a30b9..00000000000 --- a/assets/frpc/static/manifest.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l=i)return e;switch(e){case"%s":return String(t[r++]);case"%d":return Number(t[r++]);case"%j":try{return JSON.stringify(t[r++])}catch(e){return"[Circular]"}break;default:return e}}),a=t[r];r=0&&y.splice(t,1)}function a(e){var t=document.createElement("style");if(void 0===e.attrs.type&&(e.attrs.type="text/css"),void 0===e.attrs.nonce){var o=d();o&&(e.attrs.nonce=o)}return c(t,e.attrs),i(e,t),t}function s(e){var t=document.createElement("link");return void 0===e.attrs.type&&(e.attrs.type="text/css"),e.attrs.rel="stylesheet",c(t,e.attrs),i(e,t),t}function c(e,t){Object.keys(t).forEach(function(o){e.setAttribute(o,t[o])})}function d(){return o.nc}function u(e,t){var o,r,n,i;if(t.transform&&e.css){if(!(i="function"==typeof t.transform?t.transform(e.css):t.transform.default(e.css)))return function(){};e.css=i}if(t.singleton){var c=x++;o=v||(v=a(t)),r=p.bind(null,o,c,!1),n=p.bind(null,o,c,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(o=s(t),r=h.bind(null,o,t),n=function(){l(o),o.href&&URL.revokeObjectURL(o.href)}):(o=a(t),r=f.bind(null,o),n=function(){l(o)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else n()}}function p(e,t,o,r){var n=o?"":r.css;if(e.styleSheet)e.styleSheet.cssText=k(t,n);else{var i=document.createTextNode(n),l=e.childNodes;l[t]&&e.removeChild(l[t]),l.length?e.insertBefore(i,l[t]):e.appendChild(i)}}function f(e,t){var o=t.css,r=t.media;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=o;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(o))}}function h(e,t,o){var r=o.css,n=o.sourceMap,i=void 0===t.convertToAbsoluteUrls&&n;(t.convertToAbsoluteUrls||i)&&(r=w(r)),n&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(n))))+" */");var l=new Blob([r],{type:"text/css"}),a=e.href;e.href=URL.createObjectURL(l),a&&URL.revokeObjectURL(a)}var b={},g=function(e){var t;return function(){return void 0===t&&(t=e.apply(this,arguments)),t}}(function(){return window&&document&&document.all&&!window.atob}),m=function(e,t){return t?t.querySelector(e):document.querySelector(e)},_=function(e){var t={};return function(e,o){if("function"==typeof e)return e();if(void 0===t[e]){var r=m.call(this,e,o);if(window.HTMLIFrameElement&&r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(e){r=null}t[e]=r}return t[e]}}(),v=null,x=0,y=[],w=o(197);e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");t=t||{},t.attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||"boolean"==typeof t.singleton||(t.singleton=g()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var o=n(e,t);return r(o,t),function(e){for(var i=[],l=0;l=0&&Math.floor(t)===t&&isFinite(e)}function p(e){return n(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function f(e){return null==e?"":Array.isArray(e)||c(e)&&e.toString===Ci?JSON.stringify(e,null,2):String(e)}function h(e){var t=parseFloat(e);return isNaN(t)?e:t}function b(e,t){for(var o=Object.create(null),r=e.split(","),n=0;n-1)return e.splice(o,1)}}function m(e,t){return zi.call(e,t)}function _(e){var t=Object.create(null);return function(o){return t[o]||(t[o]=e(o))}}function v(e,t){function o(o){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,o):e.call(t)}return o._length=e.length,o}function x(e,t){return e.bind(t)}function y(e,t){t=t||0;for(var o=e.length-t,r=new Array(o);o--;)r[o]=e[o+t];return r}function w(e,t){for(var o in t)e[o]=t[o];return e}function k(e){for(var t={},o=0;o-1)if(i&&!m(n,"default"))l=!1;else if(""===l||l===ji(e)){var s=ne(String,n.type);(s<0||a0&&(l=ye(l,(t||"")+"_"+o),xe(l[0])&&xe(c)&&(d[s]=M(c.text+l[0].text),l.shift()),d.push.apply(d,l)):a(l)?xe(c)?d[s]=M(c.text+l):""!==l&&d.push(M(l)):xe(l)&&xe(c)?d[s]=M(c.text+l.text):(i(e._isVList)&&n(l.tag)&&r(l.key)&&n(t)&&(l.key="__vlist"+t+"_"+o+"__"),d.push(l)));return d}function we(e){var t=e.$options.provide;t&&(e._provided="function"==typeof t?t.call(e):t)}function ke(e){var t=Fe(e.$options.inject,e);t&&(I(!1),Object.keys(t).forEach(function(o){R(e,o,t[o])}),I(!0))}function Fe(e,t){if(e){for(var o=Object.create(null),r=ll?Reflect.ownKeys(e):Object.keys(e),n=0;n0,i=e?!!e.$stable:!n,l=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(i&&o&&o!==Fi&&l===o.$key&&!n&&!o.$hasNormal)return o;r={};for(var a in e)e[a]&&"$"!==a[0]&&(r[a]=ze(t,a,e[a]))}else r={};for(var s in t)s in r||(r[s]=Oe(t,s));return e&&Object.isExtensible(e)&&(e._normalized=r),O(r,"$stable",i),O(r,"$key",l),O(r,"$hasNormal",n),r}function ze(e,t,o){var r=function(){var e=arguments.length?o.apply(null,arguments):o({});return e=e&&"object"==typeof e&&!Array.isArray(e)?[e]:ve(e),e&&(0===e.length||1===e.length&&e[0].isComment)?void 0:e};return o.proxy&&Object.defineProperty(e,t,{get:r,enumerable:!0,configurable:!0}),r}function Oe(e,t){return function(){return e[t]}}function Ae(e,t){var o,r,i,l,a;if(Array.isArray(e)||"string"==typeof e)for(o=new Array(e.length),r=0,i=e.length;rWl&&Nl[o].id>e.id;)o--;Nl.splice(o+1,0,e)}else Nl.push(e);Bl||(Bl=!0,de(Ft))}}function Ot(e,t,o){Xl.get=function(){return this[t][o]},Xl.set=function(e){this[t][o]=e},Object.defineProperty(e,o,Xl)}function At(e){e._watchers=[];var t=e.$options;t.props&&$t(e,t.props),t.methods&&Nt(e,t.methods),t.data?Tt(e):D(e._data={},!0),t.computed&&Mt(e,t.computed),t.watch&&t.watch!==Qi&&Dt(e,t.watch)}function $t(e,t){var o=e.$options.propsData||{},r=e._props={},n=e.$options._propKeys=[];!e.$parent||I(!1);for(var i in t)!function(i){n.push(i);var l=ee(i,t,o,e);R(r,i,l),i in e||Ot(e,"_props",i)}(i);I(!0)}function Tt(e){var t=e.$options.data;t=e._data="function"==typeof t?jt(t,e):t||{},c(t)||(t={});for(var o=Object.keys(t),r=e.$options.props,n=(e.$options.methods,o.length);n--;){var i=o[n];r&&m(r,i)||z(i)||Ot(e,"_data",i)}D(t,!0)}function jt(e,t){T();try{return e.call(t,t)}catch(e){return ie(e,t,"data()"),{}}finally{j()}}function Mt(e,t){var o=e._computedWatchers=Object.create(null),r=nl();for(var n in t){var i=t[n],l="function"==typeof i?i:i.get;r||(o[n]=new Kl(e,l||F,F,Gl)),n in e||Lt(e,n,i)}}function Lt(e,t,o){var r=!nl();"function"==typeof o?(Xl.get=r?It(t):Pt(o),Xl.set=F):(Xl.get=o.get?r&&!1!==o.cache?It(t):Pt(o.get):F,Xl.set=o.set||F),Object.defineProperty(e,t,Xl)}function It(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),cl.target&&t.depend(),t.value}}function Pt(e){return function(){return e.call(this,this)}}function Nt(e,t){e.$options.props;for(var o in t)e[o]="function"!=typeof t[o]?F:Mi(t[o],e)}function Dt(e,t){for(var o in t){var r=t[o];if(Array.isArray(r))for(var n=0;n-1)return this;var o=y(arguments,1);return o.unshift(this),"function"==typeof e.install?e.install.apply(e,o):"function"==typeof e&&e.apply(null,o),t.push(e),this}}function Vt(e){e.mixin=function(e){return this.options=Z(this.options,e),this}}function Yt(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var o=this,r=o.cid,n=e._Ctor||(e._Ctor={});if(n[r])return n[r];var i=e.name||o.options.name,l=function(e){this._init(e)};return l.prototype=Object.create(o.prototype),l.prototype.constructor=l,l.cid=t++,l.options=Z(o.options,e),l.super=o,l.options.props&&Kt(l),l.options.computed&&Xt(l),l.extend=o.extend,l.mixin=o.mixin,l.use=o.use,Ni.forEach(function(e){l[e]=o[e]}),i&&(l.options.components[i]=l),l.superOptions=o.options,l.extendOptions=e,l.sealedOptions=w({},l.options),n[r]=l,l}}function Kt(e){var t=e.options.props;for(var o in t)Ot(e.prototype,"_props",o)}function Xt(e){var t=e.options.computed;for(var o in t)Lt(e.prototype,o,t[o])}function Gt(e){Ni.forEach(function(t){e[t]=function(e,o){return o?("component"===t&&c(o)&&(o.name=o.name||e,o=this.options._base.extend(o)),"directive"===t&&"function"==typeof o&&(o={bind:o,update:o}),this.options[t+"s"][e]=o,o):this.options[t+"s"][e]}})}function Jt(e){return e&&(e.Ctor.options.name||e.tag)}function Zt(e,t){return Array.isArray(e)?e.indexOf(t)>-1:"string"==typeof e?e.split(",").indexOf(t)>-1:!!d(e)&&e.test(t)}function Qt(e,t){var o=e.cache,r=e.keys,n=e._vnode;for(var i in o){var l=o[i];if(l){var a=Jt(l.componentOptions);a&&!t(a)&&eo(o,i,r,n)}}}function eo(e,t,o,r){var n=e[t];!n||r&&n.tag===r.tag||n.componentInstance.$destroy(),e[t]=null,g(o,t)}function to(e){for(var t=e.data,o=e,r=e;n(r.componentInstance);)(r=r.componentInstance._vnode)&&r.data&&(t=oo(r.data,t));for(;n(o=o.parent);)o&&o.data&&(t=oo(t,o.data));return ro(t.staticClass,t.class)}function oo(e,t){return{staticClass:no(e.staticClass,t.staticClass),class:n(e.class)?[e.class,t.class]:t.class}}function ro(e,t){return n(e)||n(t)?no(e,io(t)):""}function no(e,t){return e?t?e+" "+t:e:t||""}function io(e){return Array.isArray(e)?lo(e):s(e)?ao(e):"string"==typeof e?e:""}function lo(e){for(var t,o="",r=0,i=e.length;r-1?Sa[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Sa[e]=/HTMLUnknownElement/.test(t.toString())}function uo(e){if("string"==typeof e){return document.querySelector(e)||document.createElement("div")}return e}function po(e,t){var o=document.createElement(e);return"select"!==e?o:(t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&o.setAttribute("multiple","multiple"),o)}function fo(e,t){return document.createElementNS(wa[e],t)}function ho(e){return document.createTextNode(e)}function bo(e){return document.createComment(e)}function go(e,t,o){e.insertBefore(t,o)}function mo(e,t){e.removeChild(t)}function _o(e,t){e.appendChild(t)}function vo(e){return e.parentNode}function xo(e){return e.nextSibling}function yo(e){return e.tagName}function wo(e,t){e.textContent=t}function ko(e,t){e.setAttribute(t,"")}function Fo(e,t){var o=e.data.ref;if(n(o)){var r=e.context,i=e.componentInstance||e.elm,l=r.$refs;t?Array.isArray(l[o])?g(l[o],i):l[o]===i&&(l[o]=void 0):e.data.refInFor?Array.isArray(l[o])?l[o].indexOf(i)<0&&l[o].push(i):l[o]=[i]:l[o]=i}}function Co(e,t){return e.key===t.key&&(e.tag===t.tag&&e.isComment===t.isComment&&n(e.data)===n(t.data)&&Eo(e,t)||i(e.isAsyncPlaceholder)&&e.asyncFactory===t.asyncFactory&&r(t.asyncFactory.error))}function Eo(e,t){if("input"!==e.tag)return!0;var o,r=n(o=e.data)&&n(o=o.attrs)&&o.type,i=n(o=t.data)&&n(o=o.attrs)&&o.type;return r===i||za(r)&&za(i)}function So(e,t,o){var r,i,l={};for(r=t;r<=o;++r)i=e[r].key,n(i)&&(l[i]=r);return l}function zo(e,t){(e.data.directives||t.data.directives)&&Oo(e,t)}function Oo(e,t){var o,r,n,i=e===$a,l=t===$a,a=Ao(e.data.directives,e.context),s=Ao(t.data.directives,t.context),c=[],d=[];for(o in s)r=a[o],n=s[o],r?(n.oldValue=r.value,n.oldArg=r.arg,To(n,"update",t,e),n.def&&n.def.componentUpdated&&d.push(n)):(To(n,"bind",t,e),n.def&&n.def.inserted&&c.push(n));if(c.length){var u=function(){for(var o=0;o-1?Lo(e,t,o):ma(t)?ya(o)?e.removeAttribute(t):(o="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,o)):ha(t)?e.setAttribute(t,ga(t,o)):va(t)?ya(o)?e.removeAttributeNS(_a,xa(t)):e.setAttributeNS(_a,t,o):Lo(e,t,o)}function Lo(e,t,o){if(ya(o))e.removeAttribute(t);else{if(Ki&&!Xi&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==o&&!e.__ieph){var r=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",r)};e.addEventListener("input",r),e.__ieph=!0}e.setAttribute(t,o)}}function Io(e,t){var o=t.elm,i=t.data,l=e.data;if(!(r(i.staticClass)&&r(i.class)&&(r(l)||r(l.staticClass)&&r(l.class)))){var a=to(t),s=o._transitionClasses;n(s)&&(a=no(a,io(s))),a!==o._prevClass&&(o.setAttribute("class",a),o._prevClass=a)}}function Po(e){function t(){(l||(l=[])).push(e.slice(h,n).trim()),h=n+1}var o,r,n,i,l,a=!1,s=!1,c=!1,d=!1,u=0,p=0,f=0,h=0;for(n=0;n=0&&" "===(g=e.charAt(b));b--);g&&Na.test(g)||(d=!0)}}else void 0===i?(h=n+1,i=e.slice(0,n).trim()):t();if(void 0===i?i=e.slice(0,n).trim():0!==h&&t(),l)for(n=0;n-1?{exp:e.slice(0,na),key:'"'+e.slice(na+1)+'"'}:{exp:e,key:null};for(oa=e,na=ia=la=0;!or();)ra=tr(),rr(ra)?ir(ra):91===ra&&nr(ra);return{exp:e.slice(0,ia),key:e.slice(ia+1,la)}}function tr(){return oa.charCodeAt(++na)}function or(){return na>=ta}function rr(e){return 34===e||39===e}function nr(e){var t=1;for(ia=na;!or();)if(e=tr(),rr(e))ir(e);else if(91===e&&t++,93===e&&t--,0===t){la=na;break}}function ir(e){for(var t=e;!or()&&(e=tr())!==t;);}function lr(e,t,o){aa=o;var r=t.value,n=t.modifiers,i=e.tag,l=e.attrsMap.type;if(e.component)return Zo(e,r,n),!1;if("select"===i)cr(e,r,n);else if("input"===i&&"checkbox"===l)ar(e,r,n);else if("input"===i&&"radio"===l)sr(e,r,n);else if("input"===i||"textarea"===i)dr(e,r,n);else if(!Ri.isReservedTag(i))return Zo(e,r,n),!1;return!0}function ar(e,t,o){var r=o&&o.number,n=Ko(e,"value")||"null",i=Ko(e,"true-value")||"true",l=Ko(e,"false-value")||"false";Bo(e,"checked","Array.isArray("+t+")?_i("+t+","+n+")>-1"+("true"===i?":("+t+")":":_q("+t+","+i+")")),Vo(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+i+"):("+l+");if(Array.isArray($$a)){var $$v="+(r?"_n("+n+")":n)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Qo(t,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Qo(t,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Qo(t,"$$c")+"}",null,!0)}function sr(e,t,o){var r=o&&o.number,n=Ko(e,"value")||"null";n=r?"_n("+n+")":n,Bo(e,"checked","_q("+t+","+n+")"),Vo(e,"change",Qo(t,n),null,!0)}function cr(e,t,o){var r=o&&o.number,n='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",i="var $$selectedVal = "+n+";";i=i+" "+Qo(t,"$event.target.multiple ? $$selectedVal : $$selectedVal[0]"),Vo(e,"change",i,null,!0)}function dr(e,t,o){var r=e.attrsMap.type,n=o||{},i=n.lazy,l=n.number,a=n.trim,s=!i&&"range"!==r,c=i?"change":"range"===r?Da:"input",d="$event.target.value";a&&(d="$event.target.value.trim()"),l&&(d="_n("+d+")");var u=Qo(t,d);s&&(u="if($event.target.composing)return;"+u),Bo(e,"value","("+t+")"),Vo(e,c,u,null,!0),(a||l)&&Vo(e,"blur","$forceUpdate()")}function ur(e){if(n(e[Da])){var t=Ki?"change":"input";e[t]=[].concat(e[Da],e[t]||[]),delete e[Da]}n(e[Ra])&&(e.change=[].concat(e[Ra],e.change||[]),delete e[Ra])}function pr(e,t,o){var r=sa;return function n(){null!==t.apply(null,arguments)&&hr(e,n,o,r)}}function fr(e,t,o,r){if(Ba){var n=ql,i=t;t=i._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=n||e.timeStamp<=0||e.target.ownerDocument!==document)return i.apply(this,arguments)}}sa.addEventListener(e,t,el?{capture:o,passive:r}:o)}function hr(e,t,o,r){(r||sa).removeEventListener(e,t._wrapper||t,o)}function br(e,t){if(!r(e.data.on)||!r(t.data.on)){var o=t.data.on||{},n=e.data.on||{};sa=t.elm,ur(o),he(o,n,fr,hr,pr,t.context),sa=void 0}}function gr(e,t){if(!r(e.data.domProps)||!r(t.data.domProps)){var o,i,l=t.elm,a=e.data.domProps||{},s=t.data.domProps||{};n(s.__ob__)&&(s=t.data.domProps=w({},s));for(o in a)o in s||(l[o]="");for(o in s){if(i=s[o],"textContent"===o||"innerHTML"===o){if(t.children&&(t.children.length=0),i===a[o])continue;1===l.childNodes.length&&l.removeChild(l.childNodes[0])}if("value"===o&&"PROGRESS"!==l.tagName){l._value=i;var c=r(i)?"":String(i);mr(l,c)&&(l.value=c)}else if("innerHTML"===o&&Fa(l.tagName)&&r(l.innerHTML)){ca=ca||document.createElement("div"),ca.innerHTML=""+i+"";for(var d=ca.firstChild;l.firstChild;)l.removeChild(l.firstChild);for(;d.firstChild;)l.appendChild(d.firstChild)}else if(i!==a[o])try{l[o]=i}catch(e){}}}}function mr(e,t){return!e.composing&&("OPTION"===e.tagName||_r(e,t)||vr(e,t))}function _r(e,t){var o=!0;try{o=document.activeElement!==e}catch(e){}return o&&e.value!==t}function vr(e,t){var o=e.value,r=e._vModifiers;if(n(r)){if(r.number)return h(o)!==h(t);if(r.trim)return o.trim()!==t.trim()}return o!==t}function xr(e){var t=yr(e.style);return e.staticStyle?w(e.staticStyle,t):t}function yr(e){return Array.isArray(e)?k(e):"string"==typeof e?qa(e):e}function wr(e,t){var o,r={};if(t)for(var n=e;n.componentInstance;)(n=n.componentInstance._vnode)&&n.data&&(o=xr(n.data))&&w(r,o);(o=xr(e.data))&&w(r,o);for(var i=e;i=i.parent;)i.data&&(o=xr(i.data))&&w(r,o);return r}function kr(e,t){var o=t.data,i=e.data;if(!(r(o.staticStyle)&&r(o.style)&&r(i.staticStyle)&&r(i.style))){var l,a,s=t.elm,c=i.staticStyle,d=i.normalizedStyle||i.style||{},u=c||d,p=yr(t.data.style)||{};t.data.normalizedStyle=n(p.__ob__)?w({},p):p;var f=wr(t,!0);for(a in u)r(f[a])&&Ya(s,a,"");for(a in f)(l=f[a])!==u[a]&&Ya(s,a,null==l?"":l)}}function Fr(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(Ja).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var o=" "+(e.getAttribute("class")||"")+" ";o.indexOf(" "+t+" ")<0&&e.setAttribute("class",(o+t).trim())}}function Cr(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(Ja).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{for(var o=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";o.indexOf(r)>=0;)o=o.replace(r," ");o=o.trim(),o?e.setAttribute("class",o):e.removeAttribute("class")}}function Er(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&w(t,Za(e.name||"v")),w(t,e),t}return"string"==typeof e?Za(e):void 0}}function Sr(e){ls(function(){ls(e)})}function zr(e,t){var o=e._transitionClasses||(e._transitionClasses=[]);o.indexOf(t)<0&&(o.push(t),Fr(e,t))}function Or(e,t){e._transitionClasses&&g(e._transitionClasses,t),Cr(e,t)}function Ar(e,t,o){var r=$r(e,t),n=r.type,i=r.timeout,l=r.propCount;if(!n)return o();var a=n===es?rs:is,s=0,c=function(){e.removeEventListener(a,d),o()},d=function(t){t.target===e&&++s>=l&&c()};setTimeout(function(){s0&&(o=es,d=l,u=i.length):t===ts?c>0&&(o=ts,d=c,u=s.length):(d=Math.max(l,c),o=d>0?l>c?es:ts:null,u=o?o===es?i.length:s.length:0),{type:o,timeout:d,propCount:u,hasTransform:o===es&&as.test(r[os+"Property"])}}function Tr(e,t){for(;e.length1}function Nr(e,t){!0!==t.data.show&&Mr(t)}function Dr(e,t,o){Rr(e,t,o),(Ki||Gi)&&setTimeout(function(){Rr(e,t,o)},0)}function Rr(e,t,o){var r=t.value,n=e.multiple;if(!n||Array.isArray(r)){for(var i,l,a=0,s=e.options.length;a-1,l.selected!==i&&(l.selected=i);else if(C(Hr(l),r))return void(e.selectedIndex!==a&&(e.selectedIndex=a));n||(e.selectedIndex=-1)}}function Br(e,t){return t.every(function(t){return!C(t,e)})}function Hr(e){return"_value"in e?e._value:e.value}function Wr(e){e.target.composing=!0}function qr(e){e.target.composing&&(e.target.composing=!1,Ur(e.target,"input"))}function Ur(e,t){var o=document.createEvent("HTMLEvents");o.initEvent(t,!0,!0),e.dispatchEvent(o)}function Vr(e){return!e.componentInstance||e.data&&e.data.transition?e:Vr(e.componentInstance._vnode)}function Yr(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?Yr(ct(t.children)):e}function Kr(e){var t={},o=e.$options;for(var r in o.propsData)t[r]=e[r];var n=o._parentListeners;for(var i in n)t[Ai(i)]=n[i];return t}function Xr(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}function Gr(e){for(;e=e.parent;)if(e.data.transition)return!0}function Jr(e,t){return t.key===e.key&&t.tag===e.tag}function Zr(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb()}function Qr(e){e.data.newPos=e.elm.getBoundingClientRect()}function en(e){var t=e.data.pos,o=e.data.newPos,r=t.left-o.left,n=t.top-o.top;if(r||n){e.data.moved=!0;var i=e.elm.style;i.transform=i.WebkitTransform="translate("+r+"px,"+n+"px)",i.transitionDuration="0s"}}function tn(e,t){var o=t?Ps(t):Ls;if(o.test(e)){for(var r,n,i,l=[],a=[],s=o.lastIndex=0;r=o.exec(e);){(n=r.index)>s&&(a.push(i=e.slice(s,n)),l.push(JSON.stringify(i)));var c=Po(r[1].trim());l.push("_s("+c+")"),a.push({"@binding":c}),s=n+r[0].length}return s=0&&l[n].lowerCasedTag!==a;n--);else n=0;if(n>=0){for(var s=l.length-1;s>=n;s--)t.end&&t.end(l[s].tag,o,r);l.length=n,i=n&&l[n-1].tag}else"br"===a?t.start&&t.start(e,[],!0,o,r):"p"===a&&(t.start&&t.start(e,[],!1,o,r),t.end&&t.end(e,o,r))}for(var n,i,l=[],a=t.expectHTML,s=t.isUnaryTag||Li,c=t.canBeLeftOpenTag||Li,d=0;e;){if(n=e,i&&ec(i)){var u=0,p=i.toLowerCase(),f=tc[p]||(tc[p]=new RegExp("([\\s\\S]*?)(]*>)","i")),h=e.replace(f,function(e,o,r){return u=r.length,ec(p)||"noscript"===p||(o=o.replace(//g,"$1").replace(//g,"$1")),lc(p,o)&&(o=o.slice(1)),t.chars&&t.chars(o),""});d+=e.length-h.length,e=h,r(p,d-u,d)}else{var b=e.indexOf("<");if(0===b){if(Zs.test(e)){var g=e.indexOf("--\x3e");if(g>=0){t.shouldKeepComment&&t.comment(e.substring(4,g),d,d+g+3),o(g+3);continue}}if(Qs.test(e)){var m=e.indexOf("]>");if(m>=0){o(m+2);continue}}var _=e.match(Js);if(_){o(_[0].length);continue}var v=e.match(Gs);if(v){var x=d;o(v[0].length),r(v[1],x,d);continue}var y=function(){var t=e.match(Ks);if(t){var r={tagName:t[1],attrs:[],start:d};o(t[0].length);for(var n,i;!(n=e.match(Xs))&&(i=e.match(Us)||e.match(qs));)i.start=d,o(i[0].length),i.end=d,r.attrs.push(i);if(n)return r.unarySlash=n[1],o(n[0].length),r.end=d,r}}();if(y){!function(e){var o=e.tagName,n=e.unarySlash;a&&("p"===i&&Ws(o)&&r(i),c(o)&&i===o&&r(o));for(var d=s(o)||!!n,u=e.attrs.length,p=new Array(u),f=0;f=0){for(k=e.slice(b);!(Gs.test(k)||Ks.test(k)||Zs.test(k)||Qs.test(k)||(F=k.indexOf("<",1))<0);)b+=F,k=e.slice(b);w=e.substring(0,b)}b<0&&(w=e),w&&o(w.length),t.chars&&w&&t.chars(w,d-w.length,d)}if(e===n){t.chars&&t.chars(e);break}}r()}function cn(e,t,o){return{type:1,tag:e,attrsList:t,attrsMap:An(t),rawAttrsMap:{},parent:o,children:[]}}function dn(e,t){function o(e){if(r(e),d||e.processed||(e=fn(e,t)),a.length||e===i||i.if&&(e.elseif||e.else)&&yn(i,{exp:e.elseif,block:e}),l&&!e.forbidden)if(e.elseif||e.else)vn(e,l);else{if(e.slotScope){var o=e.slotTarget||'"default"';(l.scopedSlots||(l.scopedSlots={}))[o]=e}l.children.push(e),e.parent=l}e.children=e.children.filter(function(e){return!e.slotScope}),r(e),e.pre&&(d=!1),zs(e.tag)&&(u=!1);for(var n=0;n>>0}function si(e){return 1===e.type&&("slot"===e.tag||e.children.some(si))}function ci(e,t){var o=e.attrsMap["slot-scope"];if(e.if&&!e.ifProcessed&&!o)return ei(e,t,ci,"null");if(e.for&&!e.forProcessed)return oi(e,t,ci);var r=e.slotScope===xc?"":String(e.slotScope),n="function("+r+"){return "+("template"===e.tag?e.if&&o?"("+e.if+")?"+(di(e,t)||"undefined")+":undefined":di(e,t)||"undefined":Jn(e,t))+"}",i=r?"":",proxy:true";return"{key:"+(e.slotTarget||'"default"')+",fn:"+n+i+"}"}function di(e,t,o,r,n){var i=e.children;if(i.length){var l=i[0];if(1===i.length&&l.for&&"template"!==l.tag&&"slot"!==l.tag){var a=o?t.maybeComponent(l)?",1":",0":"";return""+(r||Jn)(l,t)+a}var s=o?ui(i,t.maybeComponent):0,c=n||fi;return"["+i.map(function(e){return c(e,t)}).join(",")+"]"+(s?","+s:"")}}function ui(e,t){for(var o=0,r=0;r':'
',Ms.innerHTML.indexOf(" ")>0}function ki(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}var Fi=Object.freeze({}),Ci=Object.prototype.toString,Ei=b("slot,component",!0),Si=b("key,ref,slot,slot-scope,is"),zi=Object.prototype.hasOwnProperty,Oi=/-(\w)/g,Ai=_(function(e){return e.replace(Oi,function(e,t){return t?t.toUpperCase():""})}),$i=_(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),Ti=/\B([A-Z])/g,ji=_(function(e){return e.replace(Ti,"-$1").toLowerCase()}),Mi=Function.prototype.bind?x:v,Li=function(e,t,o){return!1},Ii=function(e){return e},Pi="data-server-rendered",Ni=["component","directive","filter"],Di=["beforeCreate","created","beforeMount","mounted","beforeUpdate","updated","beforeDestroy","destroyed","activated","deactivated","errorCaptured","serverPrefetch"],Ri={optionMergeStrategies:Object.create(null),silent:!1,productionTip:!1,devtools:!1,performance:!1,errorHandler:null,warnHandler:null,ignoredElements:[],keyCodes:Object.create(null),isReservedTag:Li,isReservedAttr:Li,isUnknownElement:Li,getTagNamespace:F,parsePlatformTagName:Ii,mustUseProp:Li,async:!0,_lifecycleHooks:Di},Bi=/a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/,Hi=new RegExp("[^"+Bi.source+".$_\\d]"),Wi="__proto__"in{},qi="undefined"!=typeof window,Ui="undefined"!=typeof WXEnvironment&&!!WXEnvironment.platform,Vi=Ui&&WXEnvironment.platform.toLowerCase(),Yi=qi&&window.navigator.userAgent.toLowerCase(),Ki=Yi&&/msie|trident/.test(Yi),Xi=Yi&&Yi.indexOf("msie 9.0")>0,Gi=Yi&&Yi.indexOf("edge/")>0,Ji=(Yi&&Yi.indexOf("android"),Yi&&/iphone|ipad|ipod|ios/.test(Yi)||"ios"===Vi),Zi=(Yi&&/chrome\/\d+/.test(Yi),Yi&&/phantomjs/.test(Yi),Yi&&Yi.match(/firefox\/(\d+)/)),Qi={}.watch,el=!1;if(qi)try{var tl={};Object.defineProperty(tl,"passive",{get:function(){el=!0}}),window.addEventListener("test-passive",null,tl)}catch(e){}var ol,rl,nl=function(){return void 0===ol&&(ol=!qi&&!Ui&&void 0!==e&&e.process&&"server"===e.process.env.VUE_ENV),ol},il=qi&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,ll="undefined"!=typeof Symbol&&$(Symbol)&&"undefined"!=typeof Reflect&&$(Reflect.ownKeys);rl="undefined"!=typeof Set&&$(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var al=F,sl=0,cl=function(){this.id=sl++,this.subs=[]};cl.prototype.addSub=function(e){this.subs.push(e)},cl.prototype.removeSub=function(e){g(this.subs,e)},cl.prototype.depend=function(){cl.target&&cl.target.addDep(this)},cl.prototype.notify=function(){for(var e=this.subs.slice(),t=0,o=e.length;tdocument.createEvent("Event").timeStamp&&(Ul=function(){return Vl.now()})}var Yl=0,Kl=function(e,t,o,r,n){this.vm=e,n&&(e._watcher=this),e._watchers.push(this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=o,this.id=++Yl,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new rl,this.newDepIds=new rl,this.expression="","function"==typeof t?this.getter=t:(this.getter=A(t),this.getter||(this.getter=F)),this.value=this.lazy?void 0:this.get()};Kl.prototype.get=function(){T(this);var e,t=this.vm;try{e=this.getter.call(t,t)}catch(e){if(!this.user)throw e;ie(e,t,'getter for watcher "'+this.expression+'"')}finally{this.deep&&ue(e),j(),this.cleanupDeps()}return e},Kl.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},Kl.prototype.cleanupDeps=function(){for(var e=this.deps.length;e--;){var t=this.deps[e];this.newDepIds.has(t.id)||t.removeSub(this)}var o=this.depIds;this.depIds=this.newDepIds,this.newDepIds=o,this.newDepIds.clear(),o=this.deps,this.deps=this.newDeps,this.newDeps=o,this.newDeps.length=0},Kl.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():zt(this)},Kl.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||s(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){ie(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},Kl.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},Kl.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},Kl.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||g(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var Xl={enumerable:!0,configurable:!0,get:F,set:F},Gl={lazy:!0},Jl=0;!function(e){e.prototype._init=function(e){var t=this;t._uid=Jl++,t._isVue=!0,e&&e._isComponent?Bt(t,e):t.$options=Z(Ht(t.constructor),e||{},t),t._renderProxy=t,t._self=t,gt(t),dt(t),nt(t),wt(t,"beforeCreate"),ke(t),At(t),we(t),wt(t,"created"),t.$options.el&&t.$mount(t.$options.el)}}(qt),function(e){var t={};t.get=function(){return this._data};var o={};o.get=function(){return this._props},Object.defineProperty(e.prototype,"$data",t),Object.defineProperty(e.prototype,"$props",o),e.prototype.$set=B,e.prototype.$delete=H,e.prototype.$watch=function(e,t,o){var r=this;if(c(t))return Rt(r,e,t,o);o=o||{},o.user=!0;var n=new Kl(r,e,t,o);if(o.immediate)try{t.call(r,n.value)}catch(e){ie(e,r,'callback for immediate watcher "'+n.expression+'"')}return function(){n.teardown()}}}(qt),function(e){var t=/^hook:/;e.prototype.$on=function(e,o){var r=this;if(Array.isArray(e))for(var n=0,i=e.length;n1?y(o):o;for(var r=y(arguments,1),n='event handler for "'+e+'"',i=0,l=o.length;iparseInt(this.max)&&eo(s,c[0],c,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}},ea={KeepAlive:Ql};!function(e){var t={};t.get=function(){return Ri},Object.defineProperty(e,"config",t),e.util={warn:al,extend:w,mergeOptions:Z,defineReactive:R},e.set=B,e.delete=H,e.nextTick=de,e.observable=function(e){return D(e),e},e.options=Object.create(null),Ni.forEach(function(t){e.options[t+"s"]=Object.create(null)}),e.options._base=e,w(e.options.components,ea),Ut(e),Vt(e),Yt(e),Gt(e)}(qt),Object.defineProperty(qt.prototype,"$isServer",{get:nl}),Object.defineProperty(qt.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(qt,"FunctionalRenderContext",{value:Ue}),qt.version="2.6.12";var ta,oa,ra,na,ia,la,aa,sa,ca,da,ua=b("style,class"),pa=b("input,textarea,option,select,progress"),fa=function(e,t,o){return"value"===o&&pa(e)&&"button"!==t||"selected"===o&&"option"===e||"checked"===o&&"input"===e||"muted"===o&&"video"===e},ha=b("contenteditable,draggable,spellcheck"),ba=b("events,caret,typing,plaintext-only"),ga=function(e,t){return ya(t)||"false"===t?"false":"contenteditable"===e&&ba(t)?t:"true"},ma=b("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),_a="http://www.w3.org/1999/xlink",va=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},xa=function(e){return va(e)?e.slice(6,e.length):""},ya=function(e){return null==e||!1===e},wa={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},ka=b("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template,blockquote,iframe,tfoot"),Fa=b("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view",!0),Ca=function(e){return"pre"===e},Ea=function(e){return ka(e)||Fa(e)},Sa=Object.create(null),za=b("text,number,password,search,email,tel,url"),Oa=Object.freeze({createElement:po,createElementNS:fo,createTextNode:ho,createComment:bo,insertBefore:go,removeChild:mo,appendChild:_o,parentNode:vo,nextSibling:xo,tagName:yo,setTextContent:wo,setStyleScope:ko}),Aa={create:function(e,t){Fo(t)},update:function(e,t){e.data.ref!==t.data.ref&&(Fo(e,!0),Fo(t))},destroy:function(e){Fo(e,!0)}},$a=new ul("",{},[]),Ta=["create","activate","update","remove","destroy"],ja={create:zo,update:zo,destroy:function(e){zo(e,$a)}},Ma=Object.create(null),La=[Aa,ja],Ia={create:jo,update:jo},Pa={create:Io,update:Io},Na=/[\w).+\-_$\]]/,Da="__r",Ra="__c",Ba=wl&&!(Zi&&Number(Zi[1])<=53),Ha={create:br,update:br},Wa={create:gr,update:gr},qa=_(function(e){var t={},o=/;(?![^(]*\))/g,r=/:(.+)/;return e.split(o).forEach(function(e){if(e){var o=e.split(r);o.length>1&&(t[o[0].trim()]=o[1].trim())}}),t}),Ua=/^--/,Va=/\s*!important$/,Ya=function(e,t,o){if(Ua.test(t))e.style.setProperty(t,o);else if(Va.test(o))e.style.setProperty(ji(t),o.replace(Va,""),"important");else{var r=Xa(t);if(Array.isArray(o))for(var n=0,i=o.length;nh?(u=r(o[m+1])?null:o[m+1].elm,_(e,u,o,f,m,i)):f>m&&x(t,p,h)}function k(e,t,o,r){for(var i=o;i\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Us=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Vs="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+Bi.source+"]*",Ys="((?:"+Vs+"\\:)?"+Vs+")",Ks=new RegExp("^<"+Ys),Xs=/^\s*(\/?)>/,Gs=new RegExp("^<\\/"+Ys+"[^>]*>"),Js=/^]+>/i,Zs=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},rc=/&(?:lt|gt|quot|amp|#39);/g,nc=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,ic=b("pre,textarea",!0),lc=function(e,t){return e&&ic(e)&&"\n"===t[0]},ac=/^@|^v-on:/,sc=/^v-|^@|^:|^#/,cc=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,dc=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,uc=/^\(|\)$/g,pc=/^\[.*\]$/,fc=/:(.*)$/,hc=/^:|^\.|^v-bind:/,bc=/\.[^.\]]+(?=[^\]]*$)/g,gc=/^v-slot(:|$)|^#/,mc=/[\r\n]/,_c=/\s+/g,vc=_(Rs.decode),xc="_empty_",yc=/^xmlns:NS\d+/,wc=/^NS\d+:/,kc={preTransformNode:Mn},Fc=[Ns,Ds,kc],Cc={model:lr,text:In,html:Pn},Ec={expectHTML:!0,modules:Fc,directives:Cc,isPreTag:Ca,isUnaryTag:Bs,mustUseProp:fa,canBeLeftOpenTag:Hs,isReservedTag:Ea,getTagNamespace:so,staticKeys:function(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}(Fc)},Sc=_(Dn),zc=/^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/,Oc=/\([^)]*?\);*$/,Ac=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,$c={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Tc={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},jc=function(e){return"if("+e+")return null;"},Mc={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:jc("$event.target !== $event.currentTarget"),ctrl:jc("!$event.ctrlKey"),shift:jc("!$event.shiftKey"),alt:jc("!$event.altKey"),meta:jc("!$event.metaKey"),left:jc("'button' in $event && $event.button !== 0"),middle:jc("'button' in $event && $event.button !== 1"),right:jc("'button' in $event && $event.button !== 2")},Lc={on:Kn,bind:Xn,cloak:F},Ic=function(e){this.options=e,this.warn=e.warn||Do,this.transforms=Ro(e.modules,"transformCode"),this.dataGenFns=Ro(e.modules,"genData"),this.directives=w(w({},Lc),e.directives);var t=e.isReservedTag||Li;this.maybeComponent=function(e){return!!e.component||!t(e.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1},Pc=(new RegExp("\\b"+"do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,super,throw,while,yield,delete,export,import,return,switch,default,extends,finally,continue,debugger,function,arguments".split(",").join("\\b|\\b")+"\\b"),new RegExp("\\b"+"delete,typeof,void".split(",").join("\\s*\\([^\\)]*\\)|\\b")+"\\s*\\([^\\)]*\\)"),function(e){return function(t){function o(o,r){var n=Object.create(t),i=[],l=[],a=function(e,t,o){(o?l:i).push(e)};if(r){r.modules&&(n.modules=(t.modules||[]).concat(r.modules)),r.directives&&(n.directives=w(Object.create(t.directives||null),r.directives));for(var s in r)"modules"!==s&&"directives"!==s&&(n[s]=r[s])}n.warn=a;var c=e(o.trim(),n);return c.errors=i,c.tips=l,c}return{compile:o,compileToFunctions:yi(o)}}}(function(e,t){var o=dn(e.trim(),t);!1!==t.optimize&&Nn(o,t);var r=Gn(o,t);return{ast:o,render:r.render,staticRenderFns:r.staticRenderFns}})),Nc=Pc(Ec),Dc=(Nc.compile,Nc.compileToFunctions),Rc=!!qi&&wi(!1),Bc=!!qi&&wi(!0),Hc=_(function(e){var t=uo(e);return t&&t.innerHTML}),Wc=qt.prototype.$mount;qt.prototype.$mount=function(e,t){if((e=e&&uo(e))===document.body||e===document.documentElement)return this;var o=this.$options;if(!o.render){var r=o.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=Hc(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=ki(e));if(r){var n=Dc(r,{outputSourceRange:!1,shouldDecodeNewlines:Rc,shouldDecodeNewlinesForHref:Bc,delimiters:o.delimiters,comments:o.comments},this),i=n.render,l=n.staticRenderFns;o.render=i,o.staticRenderFns=l}}return Wc.call(this,e,t)},qt.compile=Dc,t.default=qt}.call(t,o(28),o(199).setImmediate)},function(e,t,o){"use strict";var r=o(50),n=o(110),i=o(109),l=o(108),a=o(106),s=o(107);t.a={required:r.a,whitespace:n.a,type:i.a,range:l.a,enum:a.a,pattern:s.a}},function(e,t){var o=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=o)},function(e,t,o){"use strict";function r(e,t){if(!e||!t)return!1;if(-1!==t.indexOf(" "))throw new Error("className should not contain space.");return e.classList?e.classList.contains(t):(" "+e.className+" ").indexOf(" "+t+" ")>-1}function n(e,t){if(e){for(var o=e.className,n=(t||"").split(" "),i=0,l=n.length;ir.top&&o.right>r.left&&o.left0&&void 0!==arguments[0]?arguments[0]:"";return String(e).replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")},t.arrayFindIndex=function(e,t){for(var o=0;o!==e.length;++o)if(t(e[o]))return o;return-1}),g=(t.arrayFind=function(e,t){var o=b(e,t);return-1!==o?e[o]:void 0},t.coerceTruthyValueToArray=function(e){return Array.isArray(e)?e:e?[e]:[]},t.isIE=function(){return!p.default.prototype.$isServer&&!isNaN(Number(document.documentMode))},t.isEdge=function(){return!p.default.prototype.$isServer&&navigator.userAgent.indexOf("Edge")>-1},t.isFirefox=function(){return!p.default.prototype.$isServer&&!!window.navigator.userAgent.match(/firefox/i)},t.autoprefixer=function(e){if("object"!==(void 0===e?"undefined":d(e)))return e;var t=["ms-","webkit-"];return["transform","transition","animation"].forEach(function(o){var r=e[o];o&&r&&t.forEach(function(t){e[t+o]=r})}),e},t.kebabCase=function(e){var t=/([^-])([A-Z])/g;return e.replace(t,"$1-$2").replace(t,"$1-$2").toLowerCase()},t.capitalize=function(e){return(0,f.isString)(e)?e.charAt(0).toUpperCase()+e.slice(1):e},t.looseEqual=function(e,t){var o=(0,f.isObject)(e),r=(0,f.isObject)(t);return o&&r?JSON.stringify(e)===JSON.stringify(t):!o&&!r&&String(e)===String(t)}),m=t.arrayEquals=function(e,t){if(e=e||[],t=t||[],e.length!==t.length)return!1;for(var o=0;o0?this._openTimer=setTimeout(function(){t._openTimer=null,t.doOpen(o)},r):this.doOpen(o)},doOpen:function(e){if(!this.$isServer&&(!this.willOpen||this.willOpen())&&!this.opened){this._opening=!0;var t=this.$el,o=e.modal,r=e.zIndex;if(r&&(c.default.zIndex=r),o&&(this._closing&&(c.default.closeModal(this._popupId),this._closing=!1),c.default.openModal(this._popupId,c.default.nextZIndex(),this.modalAppendToBody?void 0:t,e.modalClass,e.modalFade),e.lockScroll)){this.withoutHiddenClass=!(0,p.hasClass)(document.body,"el-popup-parent--hidden"),this.withoutHiddenClass&&(this.bodyPaddingRight=document.body.style.paddingRight,this.computedBodyPaddingRight=parseInt((0,p.getStyle)(document.body,"paddingRight"),10)),h=(0,u.default)();var n=document.documentElement.clientHeight0&&(n||"scroll"===i)&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.computedBodyPaddingRight+h+"px"),(0,p.addClass)(document.body,"el-popup-parent--hidden")}"static"===getComputedStyle(t).position&&(t.style.position="absolute"),t.style.zIndex=c.default.nextZIndex(),this.opened=!0,this.onOpen&&this.onOpen(),this.doAfterOpen()}},doAfterOpen:function(){this._opening=!1},close:function(){var e=this;if(!this.willClose||this.willClose()){null!==this._openTimer&&(clearTimeout(this._openTimer),this._openTimer=null),clearTimeout(this._closeTimer);var t=Number(this.closeDelay);t>0?this._closeTimer=setTimeout(function(){e._closeTimer=null,e.doClose()},t):this.doClose()}},doClose:function(){this._closing=!0,this.onClose&&this.onClose(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose()},doAfterClose:function(){c.default.closeModal(this._popupId),this._closing=!1},restoreBodyStyle:function(){this.modal&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.bodyPaddingRight,(0,p.removeClass)(document.body,"el-popup-parent--hidden")),this.withoutHiddenClass=!0}}},t.PopupManager=c.default},function(e,t){var o;o=function(){return this}();try{o=o||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(o=window)}e.exports=o},function(e,t,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0,t.i18n=t.use=t.t=void 0;var n=o(49),i=r(n),l=o(4),a=r(l),s=o(176),c=r(s),d=o(178),u=r(d),p=(0,u.default)(a.default),f=i.default,h=!1,b=function(){var e=Object.getPrototypeOf(this||a.default).$t;if("function"==typeof e&&a.default.locale)return h||(h=!0,a.default.locale(a.default.config.lang,(0,c.default)(f,a.default.locale(a.default.config.lang)||{},{clone:!0}))),e.apply(this,arguments)},g=t.t=function(e,t){var o=b.apply(this,arguments);if(null!==o&&void 0!==o)return o;for(var r=e.split("."),n=f,i=0,l=r.length;i0?r:o)(e)}},function(e,t,o){var r=o(30);e.exports=function(e){return Object(r(e))}},function(e,t,o){var r=o(19);e.exports=function(e,t){if(!r(e))return e;var o,n;if(t&&"function"==typeof(o=e.toString)&&!r(n=o.call(e)))return n;if("function"==typeof(o=e.valueOf)&&!r(n=o.call(e)))return n;if(!t&&"function"==typeof(o=e.toString)&&!r(n=o.call(e)))return n;throw TypeError("Can't convert object to primitive value")}},function(e,t,o){var r=o(6),n=o(17),i=o(22),l=o(42),a=o(12).f;e.exports=function(e){var t=n.Symbol||(n.Symbol=i?{}:r.Symbol||{});"_"==e.charAt(0)||e in t||a(t,e,{value:l.f(e)})}},function(e,t,o){t.f=o(14)},function(e,t,o){"use strict";t.__esModule=!0,o(8),t.default={mounted:function(){},methods:{getMigratingConfig:function(){return{props:{},events:{}}}}}},function(e,t,o){"use strict";t.__esModule=!0,t.default=function(){if(n.default.prototype.$isServer)return 0;if(void 0!==i)return i;var e=document.createElement("div");e.className="el-scrollbar__wrap",e.style.visibility="hidden",e.style.width="100px",e.style.position="absolute",e.style.top="-9999px",document.body.appendChild(e);var t=e.offsetWidth;e.style.overflow="scroll";var o=document.createElement("div");o.style.width="100%",e.appendChild(o);var r=o.offsetWidth;return e.parentNode.removeChild(e),i=t-r};var r=o(4),n=function(e){return e&&e.__esModule?e:{default:e}}(r),i=void 0},function(e,t,o){var r=o(71);e.exports=function(e,t,o){return void 0===o?r(e,t,!1):r(e,o,!1!==t)}},function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,(s.functional?this.parent:this).$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}t.a=r},function(e,t){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=97)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},97:function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("button",{staticClass:"el-button",class:[e.type?"el-button--"+e.type:"",e.buttonSize?"el-button--"+e.buttonSize:"",{"is-disabled":e.buttonDisabled,"is-loading":e.loading,"is-plain":e.plain,"is-round":e.round,"is-circle":e.circle}],attrs:{disabled:e.buttonDisabled||e.loading,autofocus:e.autofocus,type:e.nativeType},on:{click:e.handleClick}},[e.loading?o("i",{staticClass:"el-icon-loading"}):e._e(),e.icon&&!e.loading?o("i",{class:e.icon}):e._e(),e.$slots.default?o("span",[e._t("default")],2):e._e()])},n=[];r._withStripped=!0;var i={name:"ElButton",inject:{elForm:{default:""},elFormItem:{default:""}},props:{type:{type:String,default:"default"},size:String,icon:{type:String,default:""},nativeType:{type:String,default:"button"},loading:Boolean,disabled:Boolean,plain:Boolean,autofocus:Boolean,round:Boolean,circle:Boolean},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},buttonSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},buttonDisabled:function(){return this.disabled||(this.elForm||{}).disabled}},methods:{handleClick:function(e){this.$emit("click",e)}}},l=i,a=o(0),s=Object(a.a)(l,r,n,!1,null,null,null);s.options.__file="packages/button/src/button.vue";var c=s.exports;c.install=function(e){e.component(c.name,c)},t.default=c}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=76)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},11:function(e,t){e.exports=o(43)},21:function(e,t){e.exports=o(185)},4:function(e,t){e.exports=o(15)},76:function(e,t,o){"use strict";function r(e){var t=window.getComputedStyle(e),o=t.getPropertyValue("box-sizing"),r=parseFloat(t.getPropertyValue("padding-bottom"))+parseFloat(t.getPropertyValue("padding-top")),n=parseFloat(t.getPropertyValue("border-bottom-width"))+parseFloat(t.getPropertyValue("border-top-width"));return{contextStyle:f.map(function(e){return e+":"+t.getPropertyValue(e)}).join(";"),paddingSize:r,borderSize:n,boxSizing:o}}function n(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;u||(u=document.createElement("textarea"),document.body.appendChild(u));var n=r(e),i=n.paddingSize,l=n.borderSize,a=n.boxSizing,s=n.contextStyle;u.setAttribute("style",s+";"+p),u.value=e.value||e.placeholder||"";var c=u.scrollHeight,d={};"border-box"===a?c+=l:"content-box"===a&&(c-=i),u.value="";var f=u.scrollHeight-i;if(null!==t){var h=f*t;"border-box"===a&&(h=h+i+l),c=Math.max(h,c),d.minHeight=h+"px"}if(null!==o){var b=f*o;"border-box"===a&&(b=b+i+l),c=Math.min(b,c)}return d.height=c+"px",u.parentNode&&u.parentNode.removeChild(u),u=null,d}o.r(t);var i=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("div",{class:["textarea"===e.type?"el-textarea":"el-input",e.inputSize?"el-input--"+e.inputSize:"",{"is-disabled":e.inputDisabled,"is-exceed":e.inputExceed,"el-input-group":e.$slots.prepend||e.$slots.append,"el-input-group--append":e.$slots.append,"el-input-group--prepend":e.$slots.prepend,"el-input--prefix":e.$slots.prefix||e.prefixIcon,"el-input--suffix":e.$slots.suffix||e.suffixIcon||e.clearable||e.showPassword}],on:{mouseenter:function(t){e.hovering=!0},mouseleave:function(t){e.hovering=!1}}},["textarea"!==e.type?[e.$slots.prepend?o("div",{staticClass:"el-input-group__prepend"},[e._t("prepend")],2):e._e(),"textarea"!==e.type?o("input",e._b({ref:"input",staticClass:"el-input__inner",attrs:{tabindex:e.tabindex,type:e.showPassword?e.passwordVisible?"text":"password":e.type,disabled:e.inputDisabled,readonly:e.readonly,autocomplete:e.autoComplete||e.autocomplete,"aria-label":e.label},on:{compositionstart:e.handleCompositionStart,compositionupdate:e.handleCompositionUpdate,compositionend:e.handleCompositionEnd,input:e.handleInput,focus:e.handleFocus,blur:e.handleBlur,change:e.handleChange}},"input",e.$attrs,!1)):e._e(),e.$slots.prefix||e.prefixIcon?o("span",{staticClass:"el-input__prefix"},[e._t("prefix"),e.prefixIcon?o("i",{staticClass:"el-input__icon",class:e.prefixIcon}):e._e()],2):e._e(),e.getSuffixVisible()?o("span",{staticClass:"el-input__suffix"},[o("span",{staticClass:"el-input__suffix-inner"},[e.showClear&&e.showPwdVisible&&e.isWordLimitVisible?e._e():[e._t("suffix"),e.suffixIcon?o("i",{staticClass:"el-input__icon",class:e.suffixIcon}):e._e()],e.showClear?o("i",{staticClass:"el-input__icon el-icon-circle-close el-input__clear",on:{mousedown:function(e){e.preventDefault()},click:e.clear}}):e._e(),e.showPwdVisible?o("i",{staticClass:"el-input__icon el-icon-view el-input__clear",on:{click:e.handlePasswordVisible}}):e._e(),e.isWordLimitVisible?o("span",{staticClass:"el-input__count"},[o("span",{staticClass:"el-input__count-inner"},[e._v("\n "+e._s(e.textLength)+"/"+e._s(e.upperLimit)+"\n ")])]):e._e()],2),e.validateState?o("i",{staticClass:"el-input__icon",class:["el-input__validateIcon",e.validateIcon]}):e._e()]):e._e(),e.$slots.append?o("div",{staticClass:"el-input-group__append"},[e._t("append")],2):e._e()]:o("textarea",e._b({ref:"textarea",staticClass:"el-textarea__inner",style:e.textareaStyle,attrs:{tabindex:e.tabindex,disabled:e.inputDisabled,readonly:e.readonly,autocomplete:e.autoComplete||e.autocomplete,"aria-label":e.label},on:{compositionstart:e.handleCompositionStart,compositionupdate:e.handleCompositionUpdate,compositionend:e.handleCompositionEnd,input:e.handleInput,focus:e.handleFocus,blur:e.handleBlur,change:e.handleChange}},"textarea",e.$attrs,!1)),e.isWordLimitVisible&&"textarea"===e.type?o("span",{staticClass:"el-input__count"},[e._v(e._s(e.textLength)+"/"+e._s(e.upperLimit))]):e._e()],2)},l=[];i._withStripped=!0;var a=o(4),s=o.n(a),c=o(11),d=o.n(c),u=void 0,p="\n height:0 !important;\n visibility:hidden !important;\n overflow:hidden !important;\n position:absolute !important;\n z-index:-1000 !important;\n top:0 !important;\n right:0 !important\n",f=["letter-spacing","line-height","padding-top","padding-bottom","font-family","font-weight","font-size","text-rendering","text-transform","width","text-indent","padding-left","padding-right","border-width","box-sizing"],h=o(9),b=o.n(h),g=o(21),m={name:"ElInput",componentName:"ElInput",mixins:[s.a,d.a],inheritAttrs:!1,inject:{elForm:{default:""},elFormItem:{default:""}},data:function(){return{textareaCalcStyle:{},hovering:!1,focused:!1,isComposing:!1,passwordVisible:!1}},props:{value:[String,Number],size:String,resize:String,form:String,disabled:Boolean,readonly:Boolean,type:{type:String,default:"text"},autosize:{type:[Boolean,Object],default:!1},autocomplete:{type:String,default:"off"},autoComplete:{type:String,validator:function(e){return!0}},validateEvent:{type:Boolean,default:!0},suffixIcon:String,prefixIcon:String,label:String,clearable:{type:Boolean,default:!1},showPassword:{type:Boolean,default:!1},showWordLimit:{type:Boolean,default:!1},tabindex:String},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},validateState:function(){return this.elFormItem?this.elFormItem.validateState:""},needStatusIcon:function(){return!!this.elForm&&this.elForm.statusIcon},validateIcon:function(){return{validating:"el-icon-loading",success:"el-icon-circle-check",error:"el-icon-circle-close"}[this.validateState]},textareaStyle:function(){return b()({},this.textareaCalcStyle,{resize:this.resize})},inputSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},inputDisabled:function(){return this.disabled||(this.elForm||{}).disabled},nativeInputValue:function(){return null===this.value||void 0===this.value?"":String(this.value)},showClear:function(){return this.clearable&&!this.inputDisabled&&!this.readonly&&this.nativeInputValue&&(this.focused||this.hovering)},showPwdVisible:function(){return this.showPassword&&!this.inputDisabled&&!this.readonly&&(!!this.nativeInputValue||this.focused)},isWordLimitVisible:function(){return this.showWordLimit&&this.$attrs.maxlength&&("text"===this.type||"textarea"===this.type)&&!this.inputDisabled&&!this.readonly&&!this.showPassword},upperLimit:function(){return this.$attrs.maxlength},textLength:function(){return"number"==typeof this.value?String(this.value).length:(this.value||"").length},inputExceed:function(){return this.isWordLimitVisible&&this.textLength>this.upperLimit}},watch:{value:function(e){this.$nextTick(this.resizeTextarea),this.validateEvent&&this.dispatch("ElFormItem","el.form.change",[e])},nativeInputValue:function(){this.setNativeInputValue()},type:function(){var e=this;this.$nextTick(function(){e.setNativeInputValue(),e.resizeTextarea(),e.updateIconOffset()})}},methods:{focus:function(){this.getInput().focus()},blur:function(){this.getInput().blur()},getMigratingConfig:function(){return{props:{icon:"icon is removed, use suffix-icon / prefix-icon instead.","on-icon-click":"on-icon-click is removed."},events:{click:"click is removed."}}},handleBlur:function(e){this.focused=!1,this.$emit("blur",e),this.validateEvent&&this.dispatch("ElFormItem","el.form.blur",[this.value])},select:function(){this.getInput().select()},resizeTextarea:function(){if(!this.$isServer){var e=this.autosize;if("textarea"===this.type){if(!e)return void(this.textareaCalcStyle={minHeight:n(this.$refs.textarea).minHeight});var t=e.minRows,o=e.maxRows;this.textareaCalcStyle=n(this.$refs.textarea,t,o)}}},setNativeInputValue:function(){var e=this.getInput();e&&e.value!==this.nativeInputValue&&(e.value=this.nativeInputValue)},handleFocus:function(e){this.focused=!0,this.$emit("focus",e)},handleCompositionStart:function(){this.isComposing=!0},handleCompositionUpdate:function(e){var t=e.target.value,o=t[t.length-1]||"";this.isComposing=!Object(g.isKorean)(o)},handleCompositionEnd:function(e){this.isComposing&&(this.isComposing=!1,this.handleInput(e))},handleInput:function(e){this.isComposing||e.target.value!==this.nativeInputValue&&(this.$emit("input",e.target.value),this.$nextTick(this.setNativeInputValue))},handleChange:function(e){this.$emit("change",e.target.value)},calcIconOffset:function(e){var t=[].slice.call(this.$el.querySelectorAll(".el-input__"+e)||[]);if(t.length){for(var o=null,r=0;rdocument.F=Object<\/script>"),e.close(),s=e.F;r--;)delete s.prototype[i[r]];return s()};e.exports=Object.create||function(e,t){var o;return null!==e?(a.prototype=r(e),o=new a,a.prototype=null,o[l]=e):o=s(),void 0===t?o:n(o,t)}},function(e,t,o){var r=o(62),n=o(31).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,n)}},function(e,t,o){var r=o(10),n=o(13),i=o(135)(!1),l=o(36)("IE_PROTO");e.exports=function(e,t){var o,a=n(e),s=0,c=[];for(o in a)o!=l&&r(a,o)&&c.push(o);for(;t.length>s;)r(a,o=t[s++])&&(~i(c,o)||c.push(o));return c}},function(e,t,o){e.exports=o(11)},function(e,t,o){"use strict";e.exports=function(e,t){return"string"!=typeof e?e:(/^['"].*['"]$/.test(e)&&(e=e.slice(1,-1)),/["'() \t\n]/.test(e)||t?'"'+e.replace(/"/g,'\\"').replace(/\n/g,"\\n")+'"':e)}},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=83)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},4:function(e,t){e.exports=o(15)},83:function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("label",{staticClass:"el-checkbox",class:[e.border&&e.checkboxSize?"el-checkbox--"+e.checkboxSize:"",{"is-disabled":e.isDisabled},{"is-bordered":e.border},{"is-checked":e.isChecked}],attrs:{id:e.id}},[o("span",{staticClass:"el-checkbox__input",class:{"is-disabled":e.isDisabled,"is-checked":e.isChecked,"is-indeterminate":e.indeterminate,"is-focus":e.focus},attrs:{tabindex:!!e.indeterminate&&0,role:!!e.indeterminate&&"checkbox","aria-checked":!!e.indeterminate&&"mixed"}},[o("span",{staticClass:"el-checkbox__inner"}),e.trueLabel||e.falseLabel?o("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",name:e.name,disabled:e.isDisabled,"true-value":e.trueLabel,"false-value":e.falseLabel},domProps:{checked:Array.isArray(e.model)?e._i(e.model,null)>-1:e._q(e.model,e.trueLabel)},on:{change:[function(t){var o=e.model,r=t.target,n=r.checked?e.trueLabel:e.falseLabel;if(Array.isArray(o)){var i=e._i(o,null);r.checked?i<0&&(e.model=o.concat([null])):i>-1&&(e.model=o.slice(0,i).concat(o.slice(i+1)))}else e.model=n},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}}):o("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",disabled:e.isDisabled,name:e.name},domProps:{value:e.label,checked:Array.isArray(e.model)?e._i(e.model,e.label)>-1:e.model},on:{change:[function(t){var o=e.model,r=t.target,n=!!r.checked;if(Array.isArray(o)){var i=e.label,l=e._i(o,i);r.checked?l<0&&(e.model=o.concat([i])):l>-1&&(e.model=o.slice(0,l).concat(o.slice(l+1)))}else e.model=n},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}})]),e.$slots.default||e.label?o("span",{staticClass:"el-checkbox__label"},[e._t("default"),e.$slots.default?e._e():[e._v(e._s(e.label))]],2):e._e()])},n=[];r._withStripped=!0;var i=o(4),l=o.n(i),a={name:"ElCheckbox",mixins:[l.a],inject:{elForm:{default:""},elFormItem:{default:""}},componentName:"ElCheckbox",data:function(){return{selfModel:!1,focus:!1,isLimitExceeded:!1}},computed:{model:{get:function(){return this.isGroup?this.store:void 0!==this.value?this.value:this.selfModel},set:function(e){this.isGroup?(this.isLimitExceeded=!1,void 0!==this._checkboxGroup.min&&e.lengththis._checkboxGroup.max&&(this.isLimitExceeded=!0),!1===this.isLimitExceeded&&this.dispatch("ElCheckboxGroup","input",[e])):(this.$emit("input",e),this.selfModel=e)}},isChecked:function(){return"[object Boolean]"==={}.toString.call(this.model)?this.model:Array.isArray(this.model)?this.model.indexOf(this.label)>-1:null!==this.model&&void 0!==this.model?this.model===this.trueLabel:void 0},isGroup:function(){for(var e=this.$parent;e;){if("ElCheckboxGroup"===e.$options.componentName)return this._checkboxGroup=e,!0;e=e.$parent}return!1},store:function(){return this._checkboxGroup?this._checkboxGroup.value:this.value},isLimitDisabled:function(){var e=this._checkboxGroup,t=e.max,o=e.min;return!(!t&&!o)&&this.model.length>=t&&!this.isChecked||this.model.length<=o&&this.isChecked},isDisabled:function(){return this.isGroup?this._checkboxGroup.disabled||this.disabled||(this.elForm||{}).disabled||this.isLimitDisabled:this.disabled||(this.elForm||{}).disabled},_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},checkboxSize:function(){var e=this.size||this._elFormItemSize||(this.$ELEMENT||{}).size;return this.isGroup?this._checkboxGroup.checkboxGroupSize||e:e}},props:{value:{},label:{},indeterminate:Boolean,disabled:Boolean,checked:Boolean,name:String,trueLabel:[String,Number],falseLabel:[String,Number],id:String,controls:String,border:Boolean,size:String},methods:{addToStore:function(){Array.isArray(this.model)&&-1===this.model.indexOf(this.label)?this.model.push(this.label):this.model=this.trueLabel||!0},handleChange:function(e){var t=this;if(!this.isLimitExceeded){var o=void 0;o=e.target.checked?void 0===this.trueLabel||this.trueLabel:void 0!==this.falseLabel&&this.falseLabel,this.$emit("change",o,e),this.$nextTick(function(){t.isGroup&&t.dispatch("ElCheckboxGroup","change",[t._checkboxGroup.value])})}}},created:function(){this.checked&&this.addToStore()},mounted:function(){this.indeterminate&&this.$el.setAttribute("aria-controls",this.controls)},watch:{value:function(e){this.dispatch("ElFormItem","el.form.change",e)}}},s=a,c=o(0),d=Object(c.a)(s,r,n,!1,null,null,null);d.options.__file="packages/checkbox/src/checkbox.vue";var u=d.exports;u.install=function(e){e.component(u.name,u)},t.default=u}})},function(e,t,o){"use strict";t.__esModule=!0;var r=o(29);t.default={methods:{t:function(){for(var e=arguments.length,t=Array(e),o=0;o0&&(this.timeoutPending=setTimeout(function(){e.showPopper=!1},this.hideAfter)))},handleClosePopper:function(){this.enterable&&this.expectedState||this.manual||(clearTimeout(this.timeout),this.timeoutPending&&clearTimeout(this.timeoutPending),this.showPopper=!1,this.disabled&&this.doDestroy())},setExpectedState:function(e){!1===e&&clearTimeout(this.timeoutPending),this.expectedState=e},getFirstElement:function(){var e=this.$slots.default;if(!Array.isArray(e))return null;for(var t=null,o=0;o=t.length)break;n=t[r++]}else{if(r=t.next(),r.done)break;n=r.value}var i=n,l=i.target.__resizeListeners__||[];l.length&&l.forEach(function(e){e()})}};t.addResizeListener=function(e,t){i||(e.__resizeListeners__||(e.__resizeListeners__=[],e.__ro__=new n.default(l),e.__ro__.observe(e)),e.__resizeListeners__.push(t))},t.removeResizeListener=function(e,t){e&&e.__resizeListeners__&&(e.__resizeListeners__.splice(e.__resizeListeners__.indexOf(t),1),e.__resizeListeners__.length||e.__ro__.disconnect())}},function(e,t,o){"use strict";function r(e){return null!==e&&"object"===(void 0===e?"undefined":n(e))&&(0,i.hasOwn)(e,"componentOptions")}t.__esModule=!0;var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.isVNode=r;var i=o(8)},function(e,t,o){"use strict";t.__esModule=!0;var r=o(4),n=function(e){return e&&e.__esModule?e:{default:e}}(r),i=o(27),l=n.default.prototype.$isServer?function(){}:o(183),a=function(e){return e.stopPropagation()};t.default={props:{transformOrigin:{type:[Boolean,String],default:!0},placement:{type:String,default:"bottom"},boundariesPadding:{type:Number,default:5},reference:{},popper:{},offset:{default:0},value:Boolean,visibleArrow:Boolean,arrowOffset:{type:Number,default:35},appendToBody:{type:Boolean,default:!0},popperOptions:{type:Object,default:function(){return{gpuAcceleration:!1}}}},data:function(){return{showPopper:!1,currentPlacement:""}},watch:{value:{immediate:!0,handler:function(e){this.showPopper=e,this.$emit("input",e)}},showPopper:function(e){this.disabled||(e?this.updatePopper():this.destroyPopper(),this.$emit("input",e))}},methods:{createPopper:function(){var e=this;if(!this.$isServer&&(this.currentPlacement=this.currentPlacement||this.placement,/^(top|bottom|left|right)(-start|-end)?$/g.test(this.currentPlacement))){var t=this.popperOptions,o=this.popperElm=this.popperElm||this.popper||this.$refs.popper,r=this.referenceElm=this.referenceElm||this.reference||this.$refs.reference;!r&&this.$slots.reference&&this.$slots.reference[0]&&(r=this.referenceElm=this.$slots.reference[0].elm),o&&r&&(this.visibleArrow&&this.appendArrow(o),this.appendToBody&&document.body.appendChild(this.popperElm),this.popperJS&&this.popperJS.destroy&&this.popperJS.destroy(),t.placement=this.currentPlacement,t.offset=this.offset,t.arrowOffset=this.arrowOffset,this.popperJS=new l(r,o,t),this.popperJS.onCreate(function(t){e.$emit("created",e),e.resetTransformOrigin(),e.$nextTick(e.updatePopper)}),"function"==typeof t.onUpdate&&this.popperJS.onUpdate(t.onUpdate),this.popperJS._popper.style.zIndex=i.PopupManager.nextZIndex(),this.popperElm.addEventListener("click",a))}},updatePopper:function(){var e=this.popperJS;e?(e.update(),e._popper&&(e._popper.style.zIndex=i.PopupManager.nextZIndex())):this.createPopper()},doDestroy:function(e){!this.popperJS||this.showPopper&&!e||(this.popperJS.destroy(),this.popperJS=null)},destroyPopper:function(){this.popperJS&&this.resetTransformOrigin()},resetTransformOrigin:function(){if(this.transformOrigin){var e={top:"bottom",bottom:"top",left:"right",right:"left"},t=this.popperJS._popper.getAttribute("x-placement").split("-")[0],o=e[t];this.popperJS._popper.style.transformOrigin="string"==typeof this.transformOrigin?this.transformOrigin:["top","bottom"].indexOf(t)>-1?"center "+o:o+" center"}},appendArrow:function(e){var t=void 0;if(!this.appended){this.appended=!0;for(var o in e.attributes)if(/^_v-/.test(e.attributes[o].name)){t=e.attributes[o].name;break}var r=document.createElement("div");t&&r.setAttribute(t,""),r.setAttribute("x-arrow",""),r.className="popper__arrow",e.appendChild(r)}}},beforeDestroy:function(){this.doDestroy(!0),this.popperElm&&this.popperElm.parentNode===document.body&&(this.popperElm.removeEventListener("click",a),document.body.removeChild(this.popperElm))},deactivated:function(){this.$options.beforeDestroy[0].call(this)}}},function(e,t){e.exports=function(e,t,o,r){function n(){function n(){l=Number(new Date),o.apply(s,d)}function a(){i=void 0}var s=this,c=Number(new Date)-l,d=arguments;r&&!i&&n(),i&&clearTimeout(i),void 0===r&&c>e?n():!0!==t&&(i=setTimeout(r?a:n,void 0===r?e-c:e))}var i,l=0;return"boolean"!=typeof t&&(r=o,o=t,t=void 0),n}},function(e,t,o){e.exports=o.p+"732389ded34cb9c52dd88271f1345af9.ttf"},function(e,t,o){e.exports=o.p+"535877f50039c0cb49a6196a5b7517cd.woff"},function(e,t,o){"use strict";var r=o(51);t.a=r.a},function(e,t,o){"use strict";var r=o(52);t.a=r.a},function(e,t,o){"use strict";var r=o(53);t.a=r.a},function(e,t,o){"use strict";var r=o(4),n=o(210),i=o(201),l=o(200);r.default.use(n.a),t.a=new n.a({routes:[{path:"/",name:"Overview",component:i.a},{path:"/configure",name:"Configure",component:l.a}]})},function(e,t){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=134)}({134:function(e,t,o){"use strict";o.r(t);var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n={name:"ElCol",props:{span:{type:Number,default:24},tag:{type:String,default:"div"},offset:Number,pull:Number,push:Number,xs:[Number,Object],sm:[Number,Object],md:[Number,Object],lg:[Number,Object],xl:[Number,Object]},computed:{gutter:function(){for(var e=this.$parent;e&&"ElRow"!==e.$options.componentName;)e=e.$parent;return e?e.gutter:0}},render:function(e){var t=this,o=[],n={};return this.gutter&&(n.paddingLeft=this.gutter/2+"px",n.paddingRight=n.paddingLeft),["span","offset","pull","push"].forEach(function(e){(t[e]||0===t[e])&&o.push("span"!==e?"el-col-"+e+"-"+t[e]:"el-col-"+t[e])}),["xs","sm","md","lg","xl"].forEach(function(e){if("number"==typeof t[e])o.push("el-col-"+e+"-"+t[e]);else if("object"===r(t[e])){var n=t[e];Object.keys(n).forEach(function(t){o.push("span"!==t?"el-col-"+e+"-"+t+"-"+n[t]:"el-col-"+e+"-"+n[t])})}}),e(this.tag,{class:["el-col",o],style:n},this.$slots.default)}};n.install=function(e){e.component(n.name,n)},t.default=n}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=67)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},3:function(e,t){e.exports=o(8)},4:function(e,t){e.exports=o(15)},48:function(e,t){e.exports=o(104)},67:function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("div",{staticClass:"el-form-item",class:[{"el-form-item--feedback":e.elForm&&e.elForm.statusIcon,"is-error":"error"===e.validateState,"is-validating":"validating"===e.validateState,"is-success":"success"===e.validateState,"is-required":e.isRequired||e.required,"is-no-asterisk":e.elForm&&e.elForm.hideRequiredAsterisk},e.sizeClass?"el-form-item--"+e.sizeClass:""]},[o("label-wrap",{attrs:{"is-auto-width":e.labelStyle&&"auto"===e.labelStyle.width,"update-all":"auto"===e.form.labelWidth}},[e.label||e.$slots.label?o("label",{staticClass:"el-form-item__label",style:e.labelStyle,attrs:{for:e.labelFor}},[e._t("label",[e._v(e._s(e.label+e.form.labelSuffix))])],2):e._e()]),o("div",{staticClass:"el-form-item__content",style:e.contentStyle},[e._t("default"),o("transition",{attrs:{name:"el-zoom-in-top"}},["error"===e.validateState&&e.showMessage&&e.form.showMessage?e._t("error",[o("div",{staticClass:"el-form-item__error",class:{"el-form-item__error--inline":"boolean"==typeof e.inlineMessage?e.inlineMessage:e.elForm&&e.elForm.inlineMessage||!1}},[e._v("\n "+e._s(e.validateMessage)+"\n ")])],{error:e.validateMessage}):e._e()],2)],2)],1)},n=[];r._withStripped=!0;var i=o(48),l=o.n(i),a=o(4),s=o.n(a),c=o(9),d=o.n(c),u=o(3),p={props:{isAutoWidth:Boolean,updateAll:Boolean},inject:["elForm","elFormItem"],render:function(){var e=arguments[0],t=this.$slots.default;if(!t)return null;if(this.isAutoWidth){var o=this.elForm.autoLabelWidth,r={};if(o&&"auto"!==o){var n=parseInt(o,10)-this.computedWidth;n&&(r.marginLeft=n+"px")}return e("div",{class:"el-form-item__label-wrap",style:r},[t])}return t[0]},methods:{getLabelWidth:function(){if(this.$el&&this.$el.firstElementChild){var e=window.getComputedStyle(this.$el.firstElementChild).width;return Math.ceil(parseFloat(e))}return 0},updateLabelWidth:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"update";this.$slots.default&&this.isAutoWidth&&this.$el.firstElementChild&&("update"===e?this.computedWidth=this.getLabelWidth():"remove"===e&&this.elForm.deregisterLabelWidth(this.computedWidth))}},watch:{computedWidth:function(e,t){this.updateAll&&(this.elForm.registerLabelWidth(e,t),this.elFormItem.updateComputedLabelWidth(e))}},data:function(){return{computedWidth:0}},mounted:function(){this.updateLabelWidth("update")},updated:function(){this.updateLabelWidth("update")},beforeDestroy:function(){this.updateLabelWidth("remove")}},f=p,h=o(0),b=Object(h.a)(f,void 0,void 0,!1,null,null,null);b.options.__file="packages/form/src/label-wrap.vue";var g=b.exports,m={name:"ElFormItem",componentName:"ElFormItem",mixins:[s.a],provide:function(){return{elFormItem:this}},inject:["elForm"],props:{label:String,labelWidth:String,prop:String,required:{type:Boolean,default:void 0},rules:[Object,Array],error:String,validateStatus:String,for:String,inlineMessage:{type:[String,Boolean],default:""},showMessage:{type:Boolean,default:!0},size:String},components:{LabelWrap:g},watch:{error:{immediate:!0,handler:function(e){this.validateMessage=e,this.validateState=e?"error":""}},validateStatus:function(e){this.validateState=e}},computed:{labelFor:function(){return this.for||this.prop},labelStyle:function(){var e={};if("top"===this.form.labelPosition)return e;var t=this.labelWidth||this.form.labelWidth;return t&&(e.width=t),e},contentStyle:function(){var e={},t=this.label;if("top"===this.form.labelPosition||this.form.inline)return e;if(!t&&!this.labelWidth&&this.isNested)return e;var o=this.labelWidth||this.form.labelWidth;return"auto"===o?"auto"===this.labelWidth?e.marginLeft=this.computedLabelWidth:"auto"===this.form.labelWidth&&(e.marginLeft=this.elForm.autoLabelWidth):e.marginLeft=o,e},form:function(){for(var e=this.$parent,t=e.$options.componentName;"ElForm"!==t;)"ElFormItem"===t&&(this.isNested=!0),e=e.$parent,t=e.$options.componentName;return e},fieldValue:function(){var e=this.form.model;if(e&&this.prop){var t=this.prop;return-1!==t.indexOf(":")&&(t=t.replace(/:/,".")),Object(u.getPropByPath)(e,t,!0).v}},isRequired:function(){var e=this.getRules(),t=!1;return e&&e.length&&e.every(function(e){return!e.required||(t=!0,!1)}),t},_formSize:function(){return this.elForm.size},elFormItemSize:function(){return this.size||this._formSize},sizeClass:function(){return this.elFormItemSize||(this.$ELEMENT||{}).size}},data:function(){return{validateState:"",validateMessage:"",validateDisabled:!1,validator:{},isNested:!1,computedLabelWidth:""}},methods:{validate:function(e){var t=this,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:u.noop;this.validateDisabled=!1;var r=this.getFilteredRule(e);if((!r||0===r.length)&&void 0===this.required)return o(),!0;this.validateState="validating";var n={};r&&r.length>0&&r.forEach(function(e){delete e.trigger}),n[this.prop]=r;var i=new l.a(n),a={};a[this.prop]=this.fieldValue,i.validate(a,{firstFields:!0},function(e,r){t.validateState=e?"error":"success",t.validateMessage=e?e[0].message:"",o(t.validateMessage,r),t.elForm&&t.elForm.$emit("validate",t.prop,!e,t.validateMessage||null)})},clearValidate:function(){this.validateState="",this.validateMessage="",this.validateDisabled=!1},resetField:function(){var e=this;this.validateState="",this.validateMessage="";var t=this.form.model,o=this.fieldValue,r=this.prop;-1!==r.indexOf(":")&&(r=r.replace(/:/,"."));var n=Object(u.getPropByPath)(t,r,!0);this.validateDisabled=!0,Array.isArray(o)?n.o[n.k]=[].concat(this.initialValue):n.o[n.k]=this.initialValue,this.$nextTick(function(){e.validateDisabled=!1}),this.broadcast("ElTimeSelect","fieldReset",this.initialValue)},getRules:function(){var e=this.form.rules,t=this.rules,o=void 0!==this.required?{required:!!this.required}:[],r=Object(u.getPropByPath)(e,this.prop||"");return e=e?r.o[this.prop||""]||r.v:[],[].concat(t||e||[]).concat(o)},getFilteredRule:function(e){return this.getRules().filter(function(t){return!t.trigger||""===e||(Array.isArray(t.trigger)?t.trigger.indexOf(e)>-1:t.trigger===e)}).map(function(e){return d()({},e)})},onFieldBlur:function(){this.validate("blur")},onFieldChange:function(){if(this.validateDisabled)return void(this.validateDisabled=!1);this.validate("change")},updateComputedLabelWidth:function(e){this.computedLabelWidth=e?e+"px":""},addValidateEvents:function(){(this.getRules().length||void 0!==this.required)&&(this.$on("el.form.blur",this.onFieldBlur),this.$on("el.form.change",this.onFieldChange))},removeValidateEvents:function(){this.$off()}},mounted:function(){if(this.prop){this.dispatch("ElForm","el.form.addField",[this]);var e=this.fieldValue;Array.isArray(e)&&(e=[].concat(e)),Object.defineProperty(this,"initialValue",{value:e}),this.addValidateEvents()}},beforeDestroy:function(){this.dispatch("ElForm","el.form.removeField",[this])}},_=m,v=Object(h.a)(_,r,n,!1,null,null,null);v.options.__file="packages/form/src/form-item.vue";var x=v.exports;x.install=function(e){e.component(x.name,x)},t.default=x},9:function(e,t){e.exports=o(16)}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=121)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},121:function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e.$createElement;return(e._self._c||t)("form",{staticClass:"el-form",class:[e.labelPosition?"el-form--label-"+e.labelPosition:"",{"el-form--inline":e.inline}]},[e._t("default")],2)},n=[];r._withStripped=!0;var i=o(9),l=o.n(i),a={name:"ElForm",componentName:"ElForm",provide:function(){return{elForm:this}},props:{model:Object,rules:Object,labelPosition:String,labelWidth:String,labelSuffix:{type:String,default:""},inline:Boolean,inlineMessage:Boolean,statusIcon:Boolean,showMessage:{type:Boolean,default:!0},size:String,disabled:Boolean,validateOnRuleChange:{type:Boolean,default:!0},hideRequiredAsterisk:{type:Boolean,default:!1}},watch:{rules:function(){this.fields.forEach(function(e){e.removeValidateEvents(),e.addValidateEvents()}),this.validateOnRuleChange&&this.validate(function(){})}},computed:{autoLabelWidth:function(){if(!this.potentialLabelWidthArr.length)return 0;var e=Math.max.apply(Math,this.potentialLabelWidthArr);return e?e+"px":""}},data:function(){return{fields:[],potentialLabelWidthArr:[]}},created:function(){var e=this;this.$on("el.form.addField",function(t){t&&e.fields.push(t)}),this.$on("el.form.removeField",function(t){t.prop&&e.fields.splice(e.fields.indexOf(t),1)})},methods:{resetFields:function(){if(!this.model)return void console.warn("[Element Warn][Form]model is required for resetFields to work.");this.fields.forEach(function(e){e.resetField()})},clearValidate:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];(e.length?"string"==typeof e?this.fields.filter(function(t){return e===t.prop}):this.fields.filter(function(t){return e.indexOf(t.prop)>-1}):this.fields).forEach(function(e){e.clearValidate()})},validate:function(e){var t=this;if(!this.model)return void console.warn("[Element Warn][Form]model is required for validate to work!");var o=void 0;"function"!=typeof e&&window.Promise&&(o=new window.Promise(function(t,o){e=function(e){e?t(e):o(e)}}));var r=!0,n=0;0===this.fields.length&&e&&e(!0);var i={};return this.fields.forEach(function(o){o.validate("",function(o,a){o&&(r=!1),i=l()({},i,a),"function"==typeof e&&++n===t.fields.length&&e(r,i)})}),o||void 0},validateField:function(e,t){e=[].concat(e);var o=this.fields.filter(function(t){return-1!==e.indexOf(t.prop)});if(!o.length)return void console.warn("[Element Warn]please pass correct props!");o.forEach(function(e){e.validate("",t)})},getLabelWidthIndex:function(e){var t=this.potentialLabelWidthArr.indexOf(e);if(-1===t)throw new Error("[ElementForm]unpected width ",e);return t},registerLabelWidth:function(e,t){if(e&&t){var o=this.getLabelWidthIndex(t);this.potentialLabelWidthArr.splice(o,1,e)}else e&&this.potentialLabelWidthArr.push(e)},deregisterLabelWidth:function(e){var t=this.getLabelWidthIndex(e);this.potentialLabelWidthArr.splice(t,1)}}},s=a,c=o(0),d=Object(c.a)(s,r,n,!1,null,null,null);d.options.__file="packages/form/src/form.vue";var u=d.exports;u.install=function(e){e.component(u.name,u)},t.default=u},9:function(e,t){e.exports=o(16)}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=101)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},101:function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("li",{staticClass:"el-menu-item",class:{"is-active":e.active,"is-disabled":e.disabled},style:[e.paddingStyle,e.itemStyle,{backgroundColor:e.backgroundColor}],attrs:{role:"menuitem",tabindex:"-1"},on:{click:e.handleClick,mouseenter:e.onMouseEnter,focus:e.onMouseEnter,blur:e.onMouseLeave,mouseleave:e.onMouseLeave}},["ElMenu"===e.parentMenu.$options.componentName&&e.rootMenu.collapse&&e.$slots.title?o("el-tooltip",{attrs:{effect:"dark",placement:"right"}},[o("div",{attrs:{slot:"content"},slot:"content"},[e._t("title")],2),o("div",{staticStyle:{position:"absolute",left:"0",top:"0",height:"100%",width:"100%",display:"inline-block","box-sizing":"border-box",padding:"0 20px"}},[e._t("default")],2)]):[e._t("default"),e._t("title")]],2)},n=[];r._withStripped=!0;var i=o(36),l=o(29),a=o.n(l),s=o(4),c=o.n(s),d={name:"ElMenuItem",componentName:"ElMenuItem",mixins:[i.a,c.a],components:{ElTooltip:a.a},props:{index:{default:null,validator:function(e){return"string"==typeof e||null===e}},route:[String,Object],disabled:Boolean},computed:{active:function(){return this.index===this.rootMenu.activeIndex},hoverBackground:function(){return this.rootMenu.hoverBackground},backgroundColor:function(){return this.rootMenu.backgroundColor||""},activeTextColor:function(){return this.rootMenu.activeTextColor||""},textColor:function(){return this.rootMenu.textColor||""},mode:function(){return this.rootMenu.mode},itemStyle:function(){var e={color:this.active?this.activeTextColor:this.textColor};return"horizontal"!==this.mode||this.isNested||(e.borderBottomColor=this.active?this.rootMenu.activeTextColor?this.activeTextColor:"":"transparent"),e},isNested:function(){return this.parentMenu!==this.rootMenu}},methods:{onMouseEnter:function(){("horizontal"!==this.mode||this.rootMenu.backgroundColor)&&(this.$el.style.backgroundColor=this.hoverBackground)},onMouseLeave:function(){("horizontal"!==this.mode||this.rootMenu.backgroundColor)&&(this.$el.style.backgroundColor=this.backgroundColor)},handleClick:function(){this.disabled||(this.dispatch("ElMenu","item-click",this),this.$emit("click",this))}},mounted:function(){this.parentMenu.addItem(this),this.rootMenu.addItem(this)},beforeDestroy:function(){this.parentMenu.removeItem(this),this.rootMenu.removeItem(this)}},u=d,p=o(0),f=Object(p.a)(u,r,n,!1,null,null,null);f.options.__file="packages/menu/src/menu-item.vue";var h=f.exports;h.install=function(e){e.component(h.name,h)},t.default=h},29:function(e,t){e.exports=o(67)},36:function(e,t,o){"use strict";t.a={inject:["rootMenu"],computed:{indexPath:function(){for(var e=[this.index],t=this.$parent;"ElMenu"!==t.$options.componentName;)t.index&&e.unshift(t.index),t=t.$parent;return e},parentMenu:function(){for(var e=this.$parent;e&&-1===["ElMenu","ElSubmenu"].indexOf(e.$options.componentName);)e=e.$parent;return e},paddingStyle:function(){if("vertical"!==this.rootMenu.mode)return{};var e=20,t=this.$parent;if(this.rootMenu.collapse)e=20;else for(;t&&"ElMenu"!==t.$options.componentName;)"ElSubmenu"===t.$options.componentName&&(e+=20),t=t.$parent;return{paddingLeft:e+"px"}}}}},4:function(e,t){e.exports=o(15)}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=69)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},11:function(e,t){e.exports=o(43)},2:function(e,t){e.exports=o(7)},4:function(e,t){e.exports=o(15)},69:function(e,t,o){"use strict";o.r(t);var r=o(4),n=o.n(r),i=o(11),l=o.n(i),a=a||{};a.Utils=a.Utils||{},a.Utils.focusFirstDescendant=function(e){for(var t=0;t=0;t--){var o=e.childNodes[t];if(a.Utils.attemptFocus(o)||a.Utils.focusLastDescendant(o))return!0}return!1},a.Utils.attemptFocus=function(e){if(!a.Utils.isFocusable(e))return!1;a.Utils.IgnoreUtilFocusChanges=!0;try{e.focus()}catch(e){}return a.Utils.IgnoreUtilFocusChanges=!1,document.activeElement===e},a.Utils.isFocusable=function(e){if(e.tabIndex>0||0===e.tabIndex&&null!==e.getAttribute("tabIndex"))return!0;if(e.disabled)return!1;switch(e.nodeName){case"A":return!!e.href&&"ignore"!==e.rel;case"INPUT":return"hidden"!==e.type&&"file"!==e.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},a.Utils.triggerEvent=function(e,t){var o=void 0;o=/^mouse|click/.test(t)?"MouseEvents":/^key/.test(t)?"KeyboardEvent":"HTMLEvents";for(var r=document.createEvent(o),n=arguments.length,i=Array(n>2?n-2:0),l=2;l=0;t--)e.splice(t,0,e[t]);e=e.join("")}return/^[0-9a-fA-F]{6}$/.test(e)?{red:parseInt(e.slice(0,2),16),green:parseInt(e.slice(2,4),16),blue:parseInt(e.slice(4,6),16)}:{red:255,green:255,blue:255}},mixColor:function(e,t){var o=this.getColorChannels(e),r=o.red,n=o.green,i=o.blue;return t>0?(r*=1-t,n*=1-t,i*=1-t):(r+=(255-r)*t,n+=(255-n)*t,i+=(255-i)*t),"rgb("+Math.round(r)+", "+Math.round(n)+", "+Math.round(i)+")"},addItem:function(e){this.$set(this.items,e.index,e)},removeItem:function(e){delete this.items[e.index]},addSubmenu:function(e){this.$set(this.submenus,e.index,e)},removeSubmenu:function(e){delete this.submenus[e.index]},openMenu:function(e,t){var o=this.openedMenus;-1===o.indexOf(e)&&(this.uniqueOpened&&(this.openedMenus=o.filter(function(e){return-1!==t.indexOf(e)})),this.openedMenus.push(e))},closeMenu:function(e){var t=this.openedMenus.indexOf(e);-1!==t&&this.openedMenus.splice(t,1)},handleSubmenuClick:function(e){var t=e.index,o=e.indexPath;-1!==this.openedMenus.indexOf(t)?(this.closeMenu(t),this.$emit("close",t,o)):(this.openMenu(t,o),this.$emit("open",t,o))},handleItemClick:function(e){var t=this,o=e.index,r=e.indexPath,n=this.activeIndex,i=null!==e.index;i&&(this.activeIndex=e.index),this.$emit("select",o,r,e),("horizontal"===this.mode||this.collapse)&&(this.openedMenus=[]),this.router&&i&&this.routeToItem(e,function(e){if(t.activeIndex=n,e){if("NavigationDuplicated"===e.name)return;console.error(e)}})},initOpenedMenu:function(){var e=this,t=this.activeIndex,o=this.items[t];o&&"horizontal"!==this.mode&&!this.collapse&&o.indexPath.forEach(function(t){var o=e.submenus[t];o&&e.openMenu(t,o.indexPath)})},routeToItem:function(e,t){var o=e.route||e.index;try{this.$router.push(o,function(){},t)}catch(e){console.error(e)}},open:function(e){var t=this,o=this.submenus[e.toString()].indexPath;o.forEach(function(e){return t.openMenu(e,o)})},close:function(e){this.closeMenu(e)}},mounted:function(){this.initOpenedMenu(),this.$on("item-click",this.handleItemClick),this.$on("submenu-click",this.handleSubmenuClick),"horizontal"===this.mode&&new h(this.$el),this.$watch("items",this.updateActiveIndex)}},m=g,_=o(0),v=Object(_.a)(m,void 0,void 0,!1,null,null,null);v.options.__file="packages/menu/src/menu.vue";var x=v.exports;x.install=function(e){e.component(x.name,x)},t.default=x}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=77)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},10:function(e,t){e.exports=o(48)},13:function(e,t){e.exports=o(47)},15:function(e,t){e.exports=o(27)},2:function(e,t){e.exports=o(7)},20:function(e,t){e.exports=o(29)},23:function(e,t){e.exports=o(69)},47:function(e,t){e.exports=o(180)},6:function(e,t){e.exports=o(66)},7:function(e,t){e.exports=o(4)},77:function(e,t,o){"use strict";o.r(t);var r=o(7),n=o.n(r),i=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("transition",{attrs:{name:"msgbox-fade"}},[o("div",{directives:[{name:"show",rawName:"v-show",value:e.visible,expression:"visible"}],staticClass:"el-message-box__wrapper",attrs:{tabindex:"-1",role:"dialog","aria-modal":"true","aria-label":e.title||"dialog"},on:{click:function(t){return t.target!==t.currentTarget?null:e.handleWrapperClick(t)}}},[o("div",{staticClass:"el-message-box",class:[e.customClass,e.center&&"el-message-box--center"]},[null!==e.title?o("div",{staticClass:"el-message-box__header"},[o("div",{staticClass:"el-message-box__title"},[e.icon&&e.center?o("div",{class:["el-message-box__status",e.icon]}):e._e(),o("span",[e._v(e._s(e.title))])]),e.showClose?o("button",{staticClass:"el-message-box__headerbtn",attrs:{type:"button","aria-label":"Close"},on:{click:function(t){e.handleAction(e.distinguishCancelAndClose?"close":"cancel")},keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction(e.distinguishCancelAndClose?"close":"cancel")}}},[o("i",{staticClass:"el-message-box__close el-icon-close"})]):e._e()]):e._e(),o("div",{staticClass:"el-message-box__content"},[o("div",{staticClass:"el-message-box__container"},[e.icon&&!e.center&&""!==e.message?o("div",{class:["el-message-box__status",e.icon]}):e._e(),""!==e.message?o("div",{staticClass:"el-message-box__message"},[e._t("default",[e.dangerouslyUseHTMLString?o("p",{domProps:{innerHTML:e._s(e.message)}}):o("p",[e._v(e._s(e.message))])])],2):e._e()]),o("div",{directives:[{name:"show",rawName:"v-show",value:e.showInput,expression:"showInput"}],staticClass:"el-message-box__input"},[o("el-input",{ref:"input",attrs:{type:e.inputType,placeholder:e.inputPlaceholder},nativeOn:{keydown:function(t){return"button"in t||!e._k(t.keyCode,"enter",13,t.key,"Enter")?e.handleInputEnter(t):null}},model:{value:e.inputValue,callback:function(t){e.inputValue=t},expression:"inputValue"}}),o("div",{staticClass:"el-message-box__errormsg",style:{visibility:e.editorErrorMessage?"visible":"hidden"}},[e._v(e._s(e.editorErrorMessage))])],1)]),o("div",{staticClass:"el-message-box__btns"},[e.showCancelButton?o("el-button",{class:[e.cancelButtonClasses],attrs:{loading:e.cancelButtonLoading,round:e.roundButton,size:"small"},on:{keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction("cancel")}},nativeOn:{click:function(t){e.handleAction("cancel")}}},[e._v("\n "+e._s(e.cancelButtonText||e.t("el.messagebox.cancel"))+"\n ")]):e._e(),o("el-button",{directives:[{name:"show",rawName:"v-show",value:e.showConfirmButton,expression:"showConfirmButton"}],ref:"confirm",class:[e.confirmButtonClasses],attrs:{loading:e.confirmButtonLoading,round:e.roundButton,size:"small"},on:{keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction("confirm")}},nativeOn:{click:function(t){e.handleAction("confirm")}}},[e._v("\n "+e._s(e.confirmButtonText||e.t("el.messagebox.confirm"))+"\n ")])],1)])])])},l=[];i._withStripped=!0;var a=o(15),s=o.n(a),c=o(6),d=o.n(c),u=o(10),p=o.n(u),f=o(13),h=o.n(f),b=o(2),g=o(20),m=o(47),_=o.n(m),v=void 0,x={success:"success",info:"info",warning:"warning",error:"error"},y={mixins:[s.a,d.a],props:{modal:{default:!0},lockScroll:{default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{default:!0},closeOnPressEscape:{default:!0},closeOnHashChange:{default:!0},center:{default:!1,type:Boolean},roundButton:{default:!1,type:Boolean}},components:{ElInput:p.a,ElButton:h.a},computed:{icon:function(){var e=this.type;return this.iconClass||(e&&x[e]?"el-icon-"+x[e]:"")},confirmButtonClasses:function(){return"el-button--primary "+this.confirmButtonClass},cancelButtonClasses:function(){return""+this.cancelButtonClass}},methods:{getSafeClose:function(){var e=this,t=this.uid;return function(){e.$nextTick(function(){t===e.uid&&e.doClose()})}},doClose:function(){var e=this;this.visible&&(this.visible=!1,this._closing=!0,this.onClose&&this.onClose(),v.closeDialog(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose(),setTimeout(function(){e.action&&e.callback(e.action,e)}))},handleWrapperClick:function(){this.closeOnClickModal&&this.handleAction(this.distinguishCancelAndClose?"close":"cancel")},handleInputEnter:function(){if("textarea"!==this.inputType)return this.handleAction("confirm")},handleAction:function(e){("prompt"!==this.$type||"confirm"!==e||this.validate())&&(this.action=e,"function"==typeof this.beforeClose?(this.close=this.getSafeClose(),this.beforeClose(e,this,this.close)):this.doClose())},validate:function(){if("prompt"===this.$type){var e=this.inputPattern;if(e&&!e.test(this.inputValue||""))return this.editorErrorMessage=this.inputErrorMessage||Object(g.t)("el.messagebox.error"),Object(b.addClass)(this.getInputElement(),"invalid"),!1;var t=this.inputValidator;if("function"==typeof t){var o=t(this.inputValue);if(!1===o)return this.editorErrorMessage=this.inputErrorMessage||Object(g.t)("el.messagebox.error"),Object(b.addClass)(this.getInputElement(),"invalid"),!1;if("string"==typeof o)return this.editorErrorMessage=o,Object(b.addClass)(this.getInputElement(),"invalid"),!1}}return this.editorErrorMessage="",Object(b.removeClass)(this.getInputElement(),"invalid"),!0},getFirstFocus:function(){var e=this.$el.querySelector(".el-message-box__btns .el-button"),t=this.$el.querySelector(".el-message-box__btns .el-message-box__title");return e||t},getInputElement:function(){var e=this.$refs.input.$refs;return e.input||e.textarea},handleClose:function(){this.handleAction("close")}},watch:{inputValue:{immediate:!0,handler:function(e){var t=this;this.$nextTick(function(o){"prompt"===t.$type&&null!==e&&t.validate()})}},visible:function(e){var t=this;e&&(this.uid++,"alert"!==this.$type&&"confirm"!==this.$type||this.$nextTick(function(){t.$refs.confirm.$el.focus()}),this.focusAfterClosed=document.activeElement,v=new _.a(this.$el,this.focusAfterClosed,this.getFirstFocus())),"prompt"===this.$type&&(e?setTimeout(function(){t.$refs.input&&t.$refs.input.$el&&t.getInputElement().focus()},500):(this.editorErrorMessage="",Object(b.removeClass)(this.getInputElement(),"invalid")))}},mounted:function(){var e=this;this.$nextTick(function(){e.closeOnHashChange&&window.addEventListener("hashchange",e.close)})},beforeDestroy:function(){this.closeOnHashChange&&window.removeEventListener("hashchange",this.close),setTimeout(function(){v.closeDialog()})},data:function(){return{uid:1,title:void 0,message:"",type:"",iconClass:"",customClass:"",showInput:!1,inputValue:null,inputPlaceholder:"",inputType:"text",inputPattern:null,inputValidator:null,inputErrorMessage:"",showConfirmButton:!0,showCancelButton:!1,action:"",confirmButtonText:"",cancelButtonText:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonClass:"",confirmButtonDisabled:!1,cancelButtonClass:"",editorErrorMessage:null,callback:null,dangerouslyUseHTMLString:!1,focusAfterClosed:null,isOnComposition:!1,distinguishCancelAndClose:!1}}},w=y,k=o(0),F=Object(k.a)(w,i,l,!1,null,null,null);F.options.__file="packages/message-box/src/main.vue";var C=F.exports,E=o(9),S=o.n(E),z=o(23),O="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},A={title:null,message:"",type:"",iconClass:"",showInput:!1,showClose:!0,modalFade:!0,lockScroll:!0,closeOnClickModal:!0,closeOnPressEscape:!0,closeOnHashChange:!0,inputValue:null,inputPlaceholder:"",inputType:"text",inputPattern:null,inputValidator:null,inputErrorMessage:"",showConfirmButton:!0,showCancelButton:!1,confirmButtonPosition:"right",confirmButtonHighlight:!1,cancelButtonHighlight:!1,confirmButtonText:"",cancelButtonText:"",confirmButtonClass:"",cancelButtonClass:"",customClass:"",beforeClose:null,dangerouslyUseHTMLString:!1,center:!1,roundButton:!1,distinguishCancelAndClose:!1},$=n.a.extend(C),T=void 0,j=void 0,M=[],L=function(e){if(T){var t=T.callback;"function"==typeof t&&(j.showInput?t(j.inputValue,e):t(e)),T.resolve&&("confirm"===e?j.showInput?T.resolve({value:j.inputValue,action:e}):T.resolve(e):!T.reject||"cancel"!==e&&"close"!==e||T.reject(e))}},I=function(){j=new $({el:document.createElement("div")}),j.callback=L},P=function e(){if(j||I(),j.action="",(!j.visible||j.closeTimer)&&M.length>0){T=M.shift();var t=T.options;for(var o in t)t.hasOwnProperty(o)&&(j[o]=t[o]);void 0===t.callback&&(j.callback=L);var r=j.callback;j.callback=function(t,o){r(t,o),e()},Object(z.isVNode)(j.message)?(j.$slots.default=[j.message],j.message=null):delete j.$slots.default,["modal","showClose","closeOnClickModal","closeOnPressEscape","closeOnHashChange"].forEach(function(e){void 0===j[e]&&(j[e]=!0)}),document.body.appendChild(j.$el),n.a.nextTick(function(){j.visible=!0})}},N=function e(t,o){if(!n.a.prototype.$isServer){if("string"==typeof t||Object(z.isVNode)(t)?(t={message:t},"string"==typeof arguments[1]&&(t.title=arguments[1])):t.callback&&!o&&(o=t.callback),"undefined"!=typeof Promise)return new Promise(function(r,n){M.push({options:S()({},A,e.defaults,t),callback:o,resolve:r,reject:n}),P()});M.push({options:S()({},A,e.defaults,t),callback:o}),P()}};N.setDefaults=function(e){N.defaults=e},N.alert=function(e,t,o){return"object"===(void 0===t?"undefined":O(t))?(o=t,t=""):void 0===t&&(t=""),N(S()({title:t,message:e,$type:"alert",closeOnPressEscape:!1,closeOnClickModal:!1},o))},N.confirm=function(e,t,o){return"object"===(void 0===t?"undefined":O(t))?(o=t,t=""):void 0===t&&(t=""),N(S()({title:t,message:e,$type:"confirm",showCancelButton:!0},o))},N.prompt=function(e,t,o){return"object"===(void 0===t?"undefined":O(t))?(o=t,t=""):void 0===t&&(t=""),N(S()({title:t,message:e,showCancelButton:!0,showInput:!0,$type:"prompt"},o))},N.close=function(){j.doClose(),j.visible=!1,M=[],T=null};var D=N;t.default=D},9:function(e,t){e.exports=o(16)}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=75)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},15:function(e,t){e.exports=o(27)},23:function(e,t){e.exports=o(69)},7:function(e,t){e.exports=o(4)},75:function(e,t,o){"use strict";o.r(t);var r=o(7),n=o.n(r),i=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("transition",{attrs:{name:"el-message-fade"},on:{"after-leave":e.handleAfterLeave}},[o("div",{directives:[{name:"show",rawName:"v-show",value:e.visible,expression:"visible"}],class:["el-message",e.type&&!e.iconClass?"el-message--"+e.type:"",e.center?"is-center":"",e.showClose?"is-closable":"",e.customClass],style:e.positionStyle,attrs:{role:"alert"},on:{mouseenter:e.clearTimer,mouseleave:e.startTimer}},[e.iconClass?o("i",{class:e.iconClass}):o("i",{class:e.typeClass}),e._t("default",[e.dangerouslyUseHTMLString?o("p",{staticClass:"el-message__content",domProps:{innerHTML:e._s(e.message)}}):o("p",{staticClass:"el-message__content"},[e._v(e._s(e.message))])]),e.showClose?o("i",{staticClass:"el-message__closeBtn el-icon-close",on:{click:e.close}}):e._e()],2)])},l=[];i._withStripped=!0;var a={success:"success",info:"info",warning:"warning",error:"error"},s={data:function(){return{visible:!1,message:"",duration:3e3,type:"info",iconClass:"",customClass:"",onClose:null,showClose:!1,closed:!1,verticalOffset:20,timer:null,dangerouslyUseHTMLString:!1,center:!1}},computed:{typeClass:function(){return this.type&&!this.iconClass?"el-message__icon el-icon-"+a[this.type]:""},positionStyle:function(){return{top:this.verticalOffset+"px"}}},watch:{closed:function(e){e&&(this.visible=!1)}},methods:{handleAfterLeave:function(){this.$destroy(!0),this.$el.parentNode.removeChild(this.$el)},close:function(){this.closed=!0,"function"==typeof this.onClose&&this.onClose(this)},clearTimer:function(){clearTimeout(this.timer)},startTimer:function(){var e=this;this.duration>0&&(this.timer=setTimeout(function(){e.closed||e.close()},this.duration))},keydown:function(e){27===e.keyCode&&(this.closed||this.close())}},mounted:function(){this.startTimer(),document.addEventListener("keydown",this.keydown)},beforeDestroy:function(){document.removeEventListener("keydown",this.keydown)}},c=s,d=o(0),u=Object(d.a)(c,i,l,!1,null,null,null);u.options.__file="packages/message/src/main.vue";var p=u.exports,f=o(15),h=o(23),b=n.a.extend(p),g=void 0,m=[],_=1,v=function e(t){if(!n.a.prototype.$isServer){"string"==typeof(t=t||{})&&(t={message:t});var o=t.onClose,r="message_"+_++;t.onClose=function(){e.close(r,o)},g=new b({data:t}),g.id=r,Object(h.isVNode)(g.message)&&(g.$slots.default=[g.message],g.message=null),g.$mount(),document.body.appendChild(g.$el);var i=t.offset||20;return m.forEach(function(e){i+=e.$el.offsetHeight+16}),g.verticalOffset=i,g.visible=!0,g.$el.style.zIndex=f.PopupManager.nextZIndex(),m.push(g),g}};["success","warning","info","error"].forEach(function(e){v[e]=function(t){return"string"==typeof t&&(t={message:t}),t.type=e,v(t)}}),v.close=function(e,t){for(var o=m.length,r=-1,n=void 0,i=0;im.length-1))for(var l=r;l=0;e--)m[e].close()};var x=v;t.default=x}})},function(e,t){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=132)}({132:function(e,t,o){"use strict";o.r(t);var r={name:"ElRow",componentName:"ElRow",props:{tag:{type:String,default:"div"},gutter:Number,type:String,justify:{type:String,default:"start"},align:{type:String,default:"top"}},computed:{style:function(){var e={};return this.gutter&&(e.marginLeft="-"+this.gutter/2+"px",e.marginRight=e.marginLeft),e}},render:function(e){return e(this.tag,{class:["el-row","start"!==this.justify?"is-justify-"+this.justify:"","top"!==this.align?"is-align-"+this.align:"",{"el-row--flex":"flex"===this.type}],style:this.style},this.$slots.default)}};r.install=function(e){e.component(r.name,r)},t.default=r}})},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=130)}({130:function(e,t,o){"use strict";function r(e,t){var o=t.row,r=t.column,n=t.$index,l=r.property,a=l&&Object(i.getPropByPath)(o,l).v;return r&&r.formatter?r.formatter(o,r,a,n):a}function n(e,t){var o=t.row,r=t.treeNode,n=t.store;if(!r)return null;var i=[],l=function(e){e.stopPropagation(),n.loadOrToggle(o)};if(r.indent&&i.push(e("span",{class:"el-table__indent",style:{"padding-left":r.indent+"px"}})),"boolean"!=typeof r.expanded||r.noLazyChildren)i.push(e("span",{class:"el-table__placeholder"}));else{var a=["el-table__expand-icon",r.expanded?"el-table__expand-icon--expanded":""],s=["el-icon-arrow-right"];r.loading&&(s=["el-icon-loading"]),i.push(e("div",{class:a,on:{click:l}},[e("i",{class:s})]))}return i}o.r(t);var i=o(3),l={default:{order:""},selection:{width:48,minWidth:48,realWidth:48,order:"",className:"el-table-column--selection"},expand:{width:48,minWidth:48,realWidth:48,order:""},index:{width:48,minWidth:48,realWidth:48,order:""}},a={selection:{renderHeader:function(e,t){var o=t.store;return e("el-checkbox",{attrs:{disabled:o.states.data&&0===o.states.data.length,indeterminate:o.states.selection.length>0&&!this.isAllSelected,value:this.isAllSelected},nativeOn:{click:this.toggleAllSelection}})},renderCell:function(e,t){var o=t.row,r=t.column,n=t.store,i=t.$index;return e("el-checkbox",{nativeOn:{click:function(e){return e.stopPropagation()}},attrs:{value:n.isSelected(o),disabled:!!r.selectable&&!r.selectable.call(null,o,i)},on:{input:function(){n.commit("rowSelectedChanged",o)}}})},sortable:!1,resizable:!1},index:{renderHeader:function(e,t){return t.column.label||"#"},renderCell:function(e,t){var o=t.$index,r=t.column,n=o+1,i=r.index;return"number"==typeof i?n=o+i:"function"==typeof i&&(n=i(o)),e("div",[n])},sortable:!1},expand:{renderHeader:function(e,t){return t.column.label||""},renderCell:function(e,t){var o=t.row,r=t.store,n=["el-table__expand-icon"];return r.states.expandRows.indexOf(o)>-1&&n.push("el-table__expand-icon--expanded"),e("div",{class:n,on:{click:function(e){e.stopPropagation(),r.toggleRowExpansion(o)}}},[e("i",{class:"el-icon el-icon-arrow-right"})])},sortable:!1,resizable:!1,className:"el-table__expand-column"}},s=o(8),c=o(18),d=o.n(c),u=Object.assign||function(e){for(var t=1;t-1})}}},data:function(){return{isSubColumn:!1,columns:[]}},computed:{owner:function(){for(var e=this.$parent;e&&!e.tableId;)e=e.$parent;return e},columnOrTableParent:function(){for(var e=this.$parent;e&&!e.tableId&&!e.columnId;)e=e.$parent;return e},realWidth:function(){return Object(s.l)(this.width)},realMinWidth:function(){return Object(s.k)(this.minWidth)},realAlign:function(){return this.align?"is-"+this.align:null},realHeaderAlign:function(){return this.headerAlign?"is-"+this.headerAlign:this.realAlign}},methods:{getPropsData:function(){for(var e=this,t=arguments.length,o=Array(t),r=0;r2&&void 0!==arguments[2]?arguments[2]:"children",n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",i=function(e){return!(Array.isArray(e)&&e.length)};e.forEach(function(e){if(e[n])return void t(e,null,0);var l=e[r];i(l)||o(e,l,0)})}o.d(t,"b",function(){return f}),o.d(t,"i",function(){return b}),o.d(t,"d",function(){return g}),o.d(t,"e",function(){return m}),o.d(t,"c",function(){return _}),o.d(t,"g",function(){return v}),o.d(t,"f",function(){return x}),o.d(t,"h",function(){return n}),o.d(t,"l",function(){return i}),o.d(t,"k",function(){return l}),o.d(t,"j",function(){return a}),o.d(t,"a",function(){return s}),o.d(t,"m",function(){return c}),o.d(t,"n",function(){return d});var u=o(3),p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f=function(e){for(var t=e.target;t&&"HTML"!==t.tagName.toUpperCase();){if("TD"===t.tagName.toUpperCase())return t;t=t.parentNode}return null},h=function(e){return null!==e&&"object"===(void 0===e?"undefined":p(e))},b=function(e,t,o,r,n){if(!t&&!r&&(!n||Array.isArray(n)&&!n.length))return e;o="string"==typeof o?"descending"===o?-1:1:o&&o<0?-1:1;var i=r?null:function(o,r){return n?(Array.isArray(n)||(n=[n]),n.map(function(t){return"string"==typeof t?Object(u.getValueByPath)(o,t):t(o,r,e)})):("$key"!==t&&h(o)&&"$value"in o&&(o=o.$value),[h(o)?Object(u.getValueByPath)(o,t):o])},l=function(e,t){if(r)return r(e.value,t.value);for(var o=0,n=e.key.length;ot.key[o])return 1}return 0};return e.map(function(e,t){return{value:e,index:t,key:i?i(e,t):null}}).sort(function(e,t){var r=l(e,t);return r||(r=e.index-t.index),r*o}).map(function(e){return e.value})},g=function(e,t){var o=null;return e.columns.forEach(function(e){e.id===t&&(o=e)}),o},m=function(e,t){for(var o=null,r=0;r2&&void 0!==arguments[2]?arguments[2]:"children",n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",i=function(e){return!(Array.isArray(e)&&e.length)};e.forEach(function(e){if(e[n])return void t(e,null,0);var l=e[r];i(l)||o(e,l,0)})}o.d(t,"b",function(){return f}),o.d(t,"i",function(){return b}),o.d(t,"d",function(){return g}),o.d(t,"e",function(){return m}),o.d(t,"c",function(){return _}),o.d(t,"g",function(){return v}),o.d(t,"f",function(){return x}),o.d(t,"h",function(){return n}),o.d(t,"l",function(){return i}),o.d(t,"k",function(){return l}),o.d(t,"j",function(){return a}),o.d(t,"a",function(){return s}),o.d(t,"m",function(){return c}),o.d(t,"n",function(){return d});var u=o(3),p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f=function(e){for(var t=e.target;t&&"HTML"!==t.tagName.toUpperCase();){if("TD"===t.tagName.toUpperCase())return t;t=t.parentNode}return null},h=function(e){return null!==e&&"object"===(void 0===e?"undefined":p(e))},b=function(e,t,o,r,n){if(!t&&!r&&(!n||Array.isArray(n)&&!n.length))return e;o="string"==typeof o?"descending"===o?-1:1:o&&o<0?-1:1;var i=r?null:function(o,r){return n?(Array.isArray(n)||(n=[n]),n.map(function(t){return"string"==typeof t?Object(u.getValueByPath)(o,t):t(o,r,e)})):("$key"!==t&&h(o)&&"$value"in o&&(o=o.$value),[h(o)?Object(u.getValueByPath)(o,t):o])},l=function(e,t){if(r)return r(e.value,t.value);for(var o=0,n=e.key.length;ot.key[o])return 1}return 0};return e.map(function(e,t){return{value:e,index:t,key:i?i(e,t):null}}).sort(function(e,t){var r=l(e,t);return r||(r=e.index-t.index),r*o}).map(function(e){return e.value})},g=function(e,t){var o=null;return e.columns.forEach(function(e){e.id===t&&(o=e)}),o},m=function(e,t){for(var o=null,r=0;r1&&void 0!==arguments[1]?arguments[1]:{};if(!e)throw new Error("Table is required.");var o=new M;return o.table=e,o.toggleAllSelection=I()(10,o._toggleAllSelection),Object.keys(t).forEach(function(e){o.states[e]=t[e]}),o}function n(e){var t={};return Object.keys(e).forEach(function(o){var r=e[o],n=void 0;"string"==typeof r?n=function(){return this.store.states[r]}:"function"==typeof r?n=function(){return r.call(this,this.store.states)}:console.error("invalid value type"),n&&(t[o]=n)}),t}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}o.r(t);var l=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("div",{staticClass:"el-table",class:[{"el-table--fit":e.fit,"el-table--striped":e.stripe,"el-table--border":e.border||e.isGroup,"el-table--hidden":e.isHidden,"el-table--group":e.isGroup,"el-table--fluid-height":e.maxHeight,"el-table--scrollable-x":e.layout.scrollX,"el-table--scrollable-y":e.layout.scrollY,"el-table--enable-row-hover":!e.store.states.isComplex,"el-table--enable-row-transition":0!==(e.store.states.data||[]).length&&(e.store.states.data||[]).length<100},e.tableSize?"el-table--"+e.tableSize:""],on:{mouseleave:function(t){e.handleMouseLeave(t)}}},[o("div",{ref:"hiddenColumns",staticClass:"hidden-columns"},[e._t("default")],2),e.showHeader?o("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"headerWrapper",staticClass:"el-table__header-wrapper"},[o("table-header",{ref:"tableHeader",style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"default-sort":e.defaultSort}})],1):e._e(),o("div",{ref:"bodyWrapper",staticClass:"el-table__body-wrapper",class:[e.layout.scrollX?"is-scrolling-"+e.scrollPosition:"is-scrolling-none"],style:[e.bodyHeight]},[o("table-body",{style:{width:e.bodyWidth},attrs:{context:e.context,store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.data&&0!==e.data.length?e._e():o("div",{ref:"emptyBlock",staticClass:"el-table__empty-block",style:e.emptyBlockStyle},[o("span",{staticClass:"el-table__empty-text"},[e._t("empty",[e._v(e._s(e.emptyText||e.t("el.table.emptyText")))])],2)]),e.$slots.append?o("div",{ref:"appendWrapper",staticClass:"el-table__append-wrapper"},[e._t("append")],2):e._e()],1),e.showSummary?o("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"},{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"footerWrapper",staticClass:"el-table__footer-wrapper"},[o("table-footer",{style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,"default-sort":e.defaultSort}})],1):e._e(),e.fixedColumns.length>0?o("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"fixedWrapper",staticClass:"el-table__fixed",style:[{width:e.layout.fixedWidth?e.layout.fixedWidth+"px":""},e.fixedHeight]},[e.showHeader?o("div",{ref:"fixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[o("table-header",{ref:"fixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,store:e.store}})],1):e._e(),o("div",{ref:"fixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[o("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"left",store:e.store,stripe:e.stripe,highlight:e.highlightCurrentRow,"row-class-name":e.rowClassName,"row-style":e.rowStyle}}),e.$slots.append?o("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?o("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"fixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[o("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?o("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"rightFixedWrapper",staticClass:"el-table__fixed-right",style:[{width:e.layout.rightFixedWidth?e.layout.rightFixedWidth+"px":"",right:e.layout.scrollY?(e.border?e.layout.gutterWidth:e.layout.gutterWidth||0)+"px":""},e.fixedHeight]},[e.showHeader?o("div",{ref:"rightFixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[o("table-header",{ref:"rightFixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,store:e.store}})],1):e._e(),o("div",{ref:"rightFixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[o("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"right",store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.$slots.append?o("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?o("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"rightFixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[o("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?o("div",{ref:"rightFixedPatch",staticClass:"el-table__fixed-right-patch",style:{width:e.layout.scrollY?e.layout.gutterWidth+"px":"0",height:e.layout.headerHeight+"px"}}):e._e(),o("div",{directives:[{name:"show",rawName:"v-show",value:e.resizeProxyVisible,expression:"resizeProxyVisible"}],ref:"resizeProxy",staticClass:"el-table__column-resize-proxy"})])},a=[];l._withStripped=!0;var s=o(18),c=o.n(s),d=o(43),u=o(16),p=o(46),f=o.n(p),h="undefined"!=typeof navigator&&navigator.userAgent.toLowerCase().indexOf("firefox")>-1,b=function(e,t){e&&e.addEventListener&&e.addEventListener(h?"DOMMouseScroll":"mousewheel",function(e){var o=f()(e);t&&t.apply(this,[e,o])})},g={bind:function(e,t){b(e,t.value)}},m=o(6),_=o.n(m),v=o(11),x=o.n(v),y=o(7),w=o.n(y),k=o(9),F=o.n(k),C=o(8),E={data:function(){return{states:{defaultExpandAll:!1,expandRows:[]}}},methods:{updateExpandRows:function(){var e=this.states,t=e.data,o=void 0===t?[]:t,r=e.rowKey,n=e.defaultExpandAll,i=e.expandRows;if(n)this.states.expandRows=o.slice();else if(r){var l=Object(C.f)(i,r);this.states.expandRows=o.reduce(function(e,t){var o=Object(C.g)(t,r);return l[o]&&e.push(t),e},[])}else this.states.expandRows=[]},toggleRowExpansion:function(e,t){Object(C.m)(this.states.expandRows,e,t)&&(this.table.$emit("expand-change",e,this.states.expandRows.slice()),this.scheduleLayout())},setExpandRowKeys:function(e){this.assertRowKey();var t=this.states,o=t.data,r=t.rowKey,n=Object(C.f)(o,r);this.states.expandRows=e.reduce(function(e,t){var o=n[t];return o&&e.push(o.row),e},[])},isRowExpanded:function(e){var t=this.states,o=t.expandRows,r=void 0===o?[]:o,n=t.rowKey;return n?!!Object(C.f)(r,n)[Object(C.g)(e,n)]:-1!==r.indexOf(e)}}},S=o(3),z={data:function(){return{states:{_currentRowKey:null,currentRow:null}}},methods:{setCurrentRowKey:function(e){this.assertRowKey(),this.states._currentRowKey=e,this.setCurrentRowByKey(e)},restoreCurrentRowKey:function(){this.states._currentRowKey=null},setCurrentRowByKey:function(e){var t=this.states,o=t.data,r=void 0===o?[]:o,n=t.rowKey,i=null;n&&(i=Object(S.arrayFind)(r,function(t){return Object(C.g)(t,n)===e})),t.currentRow=i},updateCurrentRow:function(e){var t=this.states,o=this.table,r=t.currentRow;if(e&&e!==r)return t.currentRow=e,void o.$emit("current-change",e,r);!e&&r&&(t.currentRow=null,o.$emit("current-change",null,r))},updateCurrentRowData:function(){var e=this.states,t=this.table,o=e.rowKey,r=e._currentRowKey,n=e.data||[],i=e.currentRow;if(-1===n.indexOf(i)&&i){if(o){var l=Object(C.g)(i,o);this.setCurrentRowByKey(l)}else e.currentRow=null;null===e.currentRow&&t.$emit("current-change",null,i)}else r&&(this.setCurrentRowByKey(r),this.restoreCurrentRowKey())}}},O=Object.assign||function(e){for(var t=1;t0&&t[0]&&"selection"===t[0].type&&!t[0].fixed&&(t[0].fixed=!0,e.fixedColumns.unshift(t[0]));var o=t.filter(function(e){return!e.fixed});e.originColumns=[].concat(e.fixedColumns).concat(o).concat(e.rightFixedColumns);var r=T(o),n=T(e.fixedColumns),i=T(e.rightFixedColumns);e.leafColumnsLength=r.length,e.fixedLeafColumnsLength=n.length,e.rightFixedLeafColumnsLength=i.length,e.columns=[].concat(n).concat(r).concat(i),e.isComplex=e.fixedColumns.length>0||e.rightFixedColumns.length>0},scheduleLayout:function(e){e&&this.updateColumns(),this.table.debouncedUpdateLayout()},isSelected:function(e){var t=this.states.selection;return(void 0===t?[]:t).indexOf(e)>-1},clearSelection:function(){var e=this.states;e.isAllSelected=!1,e.selection.length&&(e.selection=[],this.table.$emit("selection-change",[]))},cleanSelection:function(){var e=this.states,t=e.data,o=e.rowKey,r=e.selection,n=void 0;if(o){n=[];var i=Object(C.f)(r,o),l=Object(C.f)(t,o);for(var a in i)i.hasOwnProperty(a)&&!l[a]&&n.push(i[a].row)}else n=r.filter(function(e){return-1===t.indexOf(e)});if(n.length){var s=r.filter(function(e){return-1===n.indexOf(e)});e.selection=s,this.table.$emit("selection-change",s.slice())}},toggleRowSelection:function(e,t){var o=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];if(Object(C.m)(this.states.selection,e,t)){var r=(this.states.selection||[]).slice();o&&this.table.$emit("select",r,e),this.table.$emit("selection-change",r)}},_toggleAllSelection:function(){var e=this.states,t=e.data,o=void 0===t?[]:t,r=e.selection,n=e.selectOnIndeterminate?!e.isAllSelected:!(e.isAllSelected||r.length);e.isAllSelected=n;var i=!1;o.forEach(function(t,o){e.selectable?e.selectable.call(null,t,o)&&Object(C.m)(r,t,n)&&(i=!0):Object(C.m)(r,t,n)&&(i=!0)}),i&&this.table.$emit("selection-change",r?r.slice():[]),this.table.$emit("select-all",r)},updateSelectionByRowKey:function(){var e=this.states,t=e.selection,o=e.rowKey,r=e.data,n=Object(C.f)(t,o);r.forEach(function(e){var r=Object(C.g)(e,o),i=n[r];i&&(t[i.index]=e)})},updateAllSelected:function(){var e=this.states,t=e.selection,o=e.rowKey,r=e.selectable,n=e.data||[];if(0===n.length)return void(e.isAllSelected=!1);var i=void 0;o&&(i=Object(C.f)(t,o));for(var l=!0,a=0,s=0,c=n.length;s1?o-1:0),n=1;nthis.bodyHeight;return this.scrollY=r,o!==r}return!1},e.prototype.setHeight=function(e){var t=this,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"height";if(!w.a.prototype.$isServer){var r=this.table.$el;if(e=Object(C.j)(e),this.height=e,!r&&(e||0===e))return w.a.nextTick(function(){return t.setHeight(e,o)});"number"==typeof e?(r.style[o]=e+"px",this.updateElsHeight()):"string"==typeof e&&(r.style[o]=e,this.updateElsHeight())}},e.prototype.setMaxHeight=function(e){this.setHeight(e,"max-height")},e.prototype.getFlattenColumns=function(){var e=[];return this.table.columns.forEach(function(t){t.isColumnGroup?e.push.apply(e,t.columns):e.push(t)}),e},e.prototype.updateElsHeight=function(){var e=this;if(!this.table.$ready)return w.a.nextTick(function(){return e.updateElsHeight()});var t=this.table.$refs,o=t.headerWrapper,r=t.appendWrapper,n=t.footerWrapper;if(this.appendHeight=r?r.offsetHeight:0,!this.showHeader||o){var i=o?o.querySelector(".el-table__header tr"):null,l=this.headerDisplayNone(i),a=this.headerHeight=this.showHeader?o.offsetHeight:0;if(this.showHeader&&!l&&o.offsetWidth>0&&(this.table.columns||[]).length>0&&a<2)return w.a.nextTick(function(){return e.updateElsHeight()});var s=this.tableHeight=this.table.$el.clientHeight,c=this.footerHeight=n?n.offsetHeight:0;null!==this.height&&(this.bodyHeight=s-a-c+(n?1:0)),this.fixedBodyHeight=this.scrollX?this.bodyHeight-this.gutterWidth:this.bodyHeight;var d=!(this.store.states.data&&this.store.states.data.length);this.viewportHeight=this.scrollX?s-(d?0:this.gutterWidth):s,this.updateScrollY(),this.notifyObservers("scrollable")}},e.prototype.headerDisplayNone=function(e){if(!e)return!0;for(var t=e;"DIV"!==t.tagName;){if("none"===getComputedStyle(t).display)return!0;t=t.parentElement}return!1},e.prototype.updateColumnsWidth=function(){if(!w.a.prototype.$isServer){var e=this.fit,t=this.table.$el.clientWidth,o=0,r=this.getFlattenColumns(),n=r.filter(function(e){return"number"!=typeof e.width});if(r.forEach(function(e){"number"==typeof e.width&&e.realWidth&&(e.realWidth=null)}),n.length>0&&e){r.forEach(function(e){o+=e.width||e.minWidth||80});var i=this.scrollY?this.gutterWidth:0;if(o<=t-i){this.scrollX=!1;var l=t-i-o;if(1===n.length)n[0].realWidth=(n[0].minWidth||80)+l;else{var a=n.reduce(function(e,t){return e+(t.minWidth||80)},0),s=l/a,c=0;n.forEach(function(e,t){if(0!==t){var o=Math.floor((e.minWidth||80)*s);c+=o,e.realWidth=(e.minWidth||80)+o}}),n[0].realWidth=(n[0].minWidth||80)+l-c}}else this.scrollX=!0,n.forEach(function(e){e.realWidth=e.minWidth});this.bodyWidth=Math.max(o,t),this.table.resizeState.width=this.bodyWidth}else r.forEach(function(e){e.width||e.minWidth?e.realWidth=e.width||e.minWidth:e.realWidth=80,o+=e.realWidth}),this.scrollX=o>t,this.bodyWidth=o;var d=this.store.states.fixedColumns;if(d.length>0){var u=0;d.forEach(function(e){u+=e.realWidth||e.width}),this.fixedWidth=u}var p=this.store.states.rightFixedColumns;if(p.length>0){var f=0;p.forEach(function(e){f+=e.realWidth||e.width}),this.rightFixedWidth=f}this.notifyObservers("columns")}},e.prototype.addObserver=function(e){this.observers.push(e)},e.prototype.removeObserver=function(e){var t=this.observers.indexOf(e);-1!==t&&this.observers.splice(t,1)},e.prototype.notifyObservers=function(e){var t=this;this.observers.forEach(function(o){switch(e){case"columns":o.onColumnsChange(t);break;case"scrollable":o.onScrollableChange(t);break;default:throw new Error("Table Layout don't have event "+e+".")}})},e}(),R=D,B=o(2),H=o(29),W=o.n(H),q={created:function(){this.tableLayout.addObserver(this)},destroyed:function(){this.tableLayout.removeObserver(this)},computed:{tableLayout:function(){var e=this.layout;if(!e&&this.table&&(e=this.table.layout),!e)throw new Error("Can not find table layout.");return e}},mounted:function(){this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout)},updated:function(){this.__updated__||(this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout),this.__updated__=!0)},methods:{onColumnsChange:function(e){var t=this.$el.querySelectorAll("colgroup > col");if(t.length){var o=e.getFlattenColumns(),r={};o.forEach(function(e){r[e.id]=e});for(var n=0,i=t.length;n col[name=gutter]"),o=0,r=t.length;o=this.leftFixedLeafCount:"right"===this.fixed?e=this.columnsCount-this.rightFixedLeafCount},getSpan:function(e,t,o,r){var n=1,i=1,l=this.table.spanMethod;if("function"==typeof l){var a=l({row:e,column:t,rowIndex:o,columnIndex:r});Array.isArray(a)?(n=a[0],i=a[1]):"object"===(void 0===a?"undefined":U(a))&&(n=a.rowspan,i=a.colspan)}return{rowspan:n,colspan:i}},getRowStyle:function(e,t){var o=this.table.rowStyle;return"function"==typeof o?o.call(null,{row:e,rowIndex:t}):o||null},getRowClass:function(e,t){var o=["el-table__row"];this.table.highlightCurrentRow&&e===this.store.states.currentRow&&o.push("current-row"),this.stripe&&t%2==1&&o.push("el-table__row--striped");var r=this.table.rowClassName;return"string"==typeof r?o.push(r):"function"==typeof r&&o.push(r.call(null,{row:e,rowIndex:t})),this.store.states.expandRows.indexOf(e)>-1&&o.push("expanded"),o},getCellStyle:function(e,t,o,r){var n=this.table.cellStyle;return"function"==typeof n?n.call(null,{rowIndex:e,columnIndex:t,row:o,column:r}):n},getCellClass:function(e,t,o,r){var n=[r.id,r.align,r.className];this.isColumnHidden(t)&&n.push("is-hidden");var i=this.table.cellClassName;return"string"==typeof i?n.push(i):"function"==typeof i&&n.push(i.call(null,{rowIndex:e,columnIndex:t,row:o,column:r})),n.join(" ")},getColspanRealWidth:function(e,t,o){return t<1?e[o].realWidth:e.map(function(e){return e.realWidth}).slice(o,o+t).reduce(function(e,t){return e+t},-1)},handleCellMouseEnter:function(e,t){var o=this.table,r=Object(C.b)(e);if(r){var n=Object(C.c)(o,r),i=o.hoverState={cell:r,column:n,row:t};o.$emit("cell-mouse-enter",i.row,i.column,i.cell,e)}var l=e.target.querySelector(".cell");if(Object(B.hasClass)(l,"el-tooltip")&&l.childNodes.length){var a=document.createRange();if(a.setStart(l,0),a.setEnd(l,l.childNodes.length),(a.getBoundingClientRect().width+((parseInt(Object(B.getStyle)(l,"paddingLeft"),10)||0)+(parseInt(Object(B.getStyle)(l,"paddingRight"),10)||0))>l.offsetWidth||l.scrollWidth>l.offsetWidth)&&this.$refs.tooltip){var s=this.$refs.tooltip;this.tooltipContent=r.innerText||r.textContent,s.referenceElm=r,s.$refs.popper&&(s.$refs.popper.style.display="none"),s.doDestroy(),s.setExpectedState(!0),this.activateTooltip(s)}}},handleCellMouseLeave:function(e){var t=this.$refs.tooltip;if(t&&(t.setExpectedState(!1),t.handleClosePopper()),Object(C.b)(e)){var o=this.table.hoverState||{};this.table.$emit("cell-mouse-leave",o.row,o.column,o.cell,e)}},handleMouseEnter:I()(30,function(e){this.store.commit("setHoverRow",e)}),handleMouseLeave:I()(30,function(){this.store.commit("setHoverRow",null)}),handleContextMenu:function(e,t){this.handleEvent(e,t,"contextmenu")},handleDoubleClick:function(e,t){this.handleEvent(e,t,"dblclick")},handleClick:function(e,t){this.store.commit("setCurrentRow",t),this.handleEvent(e,t,"click")},handleEvent:function(e,t,o){var r=this.table,n=Object(C.b)(e),i=void 0;n&&(i=Object(C.c)(r,n))&&r.$emit("cell-"+o,t,i,n,e),r.$emit("row-"+o,t,i,e)},rowRender:function(e,t,o){var r=this,n=this.$createElement,i=this.treeIndent,l=this.columns,a=this.firstDefaultColumnIndex,s=l.map(function(e,t){return r.isColumnHidden(t)}),c=this.getRowClass(e,t),d=!0;return o&&(c.push("el-table__row--level-"+o.level),d=o.display),n("tr",{style:[d?null:{display:"none"},this.getRowStyle(e,t)],class:c,key:this.getKeyOfRow(e,t),on:{dblclick:function(t){return r.handleDoubleClick(t,e)},click:function(t){return r.handleClick(t,e)},contextmenu:function(t){return r.handleContextMenu(t,e)},mouseenter:function(e){return r.handleMouseEnter(t)},mouseleave:this.handleMouseLeave}},[l.map(function(c,d){var u=r.getSpan(e,c,t,d),p=u.rowspan,f=u.colspan;if(!p||!f)return null;var h=V({},c);h.realWidth=r.getColspanRealWidth(l,f,d);var b={store:r.store,_self:r.context||r.table.$vnode.context,column:h,row:e,$index:t};return d===a&&o&&(b.treeNode={indent:o.level*i,level:o.level},"boolean"==typeof o.expanded&&(b.treeNode.expanded=o.expanded,"loading"in o&&(b.treeNode.loading=o.loading),"noLazyChildren"in o&&(b.treeNode.noLazyChildren=o.noLazyChildren))),n("td",{style:r.getCellStyle(t,d,e,c),class:r.getCellClass(t,d,e,c),attrs:{rowspan:p,colspan:f},on:{mouseenter:function(t){return r.handleCellMouseEnter(t,e)},mouseleave:r.handleCellMouseLeave}},[c.renderCell.call(r._renderProxy,r.$createElement,b,s[d])])})])},wrappedRowRender:function(e,t){var o=this,r=this.$createElement,n=this.store,i=n.isRowExpanded,l=n.assertRowKey,a=n.states,s=a.treeData,c=a.lazyTreeNodeMap,d=a.childrenColumnName,u=a.rowKey;if(this.hasExpandColumn&&i(e)){var p=this.table.renderExpanded,f=this.rowRender(e,t);return p?[[f,r("tr",{key:"expanded-row__"+f.key},[r("td",{attrs:{colspan:this.columnsCount},class:"el-table__expanded-cell"},[p(this.$createElement,{row:e,$index:t,store:this.store})])])]]:(console.error("[Element Error]renderExpanded is required."),f)}if(Object.keys(s).length){l();var h=Object(C.g)(e,u),b=s[h],g=null;b&&(g={expanded:b.expanded,level:b.level,display:!0},"boolean"==typeof b.lazy&&("boolean"==typeof b.loaded&&b.loaded&&(g.noLazyChildren=!(b.children&&b.children.length)),g.loading=b.loading));var m=[this.rowRender(e,t,g)];if(b){var _=0;b.display=!0;var v=c[h]||e[d];!function e(r,n){r&&r.length&&n&&r.forEach(function(r){var i={display:n.display&&n.expanded,level:n.level+1},l=Object(C.g)(r,u);if(void 0===l||null===l)throw new Error("for nested data item, row-key is required.");if(b=V({},s[l]),b&&(i.expanded=b.expanded,b.level=b.level||i.level,b.display=!(!b.expanded||!i.display),"boolean"==typeof b.lazy&&("boolean"==typeof b.loaded&&b.loaded&&(i.noLazyChildren=!(b.children&&b.children.length)),i.loading=b.loading)),_++,m.push(o.rowRender(r,t+_,i)),b){var a=c[l]||r[d];e(a,b)}})}(v,b)}return m}return this.rowRender(e,t)}}},K=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("transition",{attrs:{name:"el-zoom-in-top"}},[e.multiple?o("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[o("div",{staticClass:"el-table-filter__content"},[o("el-scrollbar",{attrs:{"wrap-class":"el-table-filter__wrap"}},[o("el-checkbox-group",{staticClass:"el-table-filter__checkbox-group",model:{value:e.filteredValue,callback:function(t){e.filteredValue=t},expression:"filteredValue"}},e._l(e.filters,function(t){return o("el-checkbox",{key:t.value,attrs:{label:t.value}},[e._v(e._s(t.text))])}),1)],1)],1),o("div",{staticClass:"el-table-filter__bottom"},[o("button",{class:{"is-disabled":0===e.filteredValue.length},attrs:{disabled:0===e.filteredValue.length},on:{click:e.handleConfirm}},[e._v(e._s(e.t("el.table.confirmFilter")))]),o("button",{on:{click:e.handleReset}},[e._v(e._s(e.t("el.table.resetFilter")))])])]):o("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[o("ul",{staticClass:"el-table-filter__list"},[o("li",{staticClass:"el-table-filter__list-item",class:{"is-active":void 0===e.filterValue||null===e.filterValue},on:{click:function(t){e.handleSelect(null)}}},[e._v(e._s(e.t("el.table.clearFilter")))]),e._l(e.filters,function(t){return o("li",{key:t.value,staticClass:"el-table-filter__list-item",class:{"is-active":e.isActive(t)},attrs:{label:t.value},on:{click:function(o){e.handleSelect(t.value)}}},[e._v(e._s(t.text))])})],2)])])},X=[];K._withStripped=!0;var G=o(5),J=o.n(G),Z=o(15),Q=o(12),ee=o.n(Q),te=[];!w.a.prototype.$isServer&&document.addEventListener("click",function(e){te.forEach(function(t){var o=e.target;t&&t.$el&&(o===t.$el||t.$el.contains(o)||t.handleOutsideClick&&t.handleOutsideClick(e))})});var oe={open:function(e){e&&te.push(e)},close:function(e){-1!==te.indexOf(e)&&te.splice(e,1)}},re=o(39),ne=o.n(re),ie=o(14),le=o.n(ie),ae={name:"ElTableFilterPanel",mixins:[J.a,_.a],directives:{Clickoutside:ee.a},components:{ElCheckbox:c.a,ElCheckboxGroup:ne.a,ElScrollbar:le.a},props:{placement:{type:String,default:"bottom-end"}},methods:{isActive:function(e){return e.value===this.filterValue},handleOutsideClick:function(){var e=this;setTimeout(function(){e.showPopper=!1},16)},handleConfirm:function(){this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleReset:function(){this.filteredValue=[],this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleSelect:function(e){this.filterValue=e,void 0!==e&&null!==e?this.confirmFilter(this.filteredValue):this.confirmFilter([]),this.handleOutsideClick()},confirmFilter:function(e){this.table.store.commit("filterChange",{column:this.column,values:e}),this.table.store.updateAllSelected()}},data:function(){return{table:null,cell:null,column:null}},computed:{filters:function(){return this.column&&this.column.filters},filterValue:{get:function(){return(this.column.filteredValue||[])[0]},set:function(e){this.filteredValue&&(void 0!==e&&null!==e?this.filteredValue.splice(0,1,e):this.filteredValue.splice(0,1))}},filteredValue:{get:function(){return this.column?this.column.filteredValue||[]:[]},set:function(e){this.column&&(this.column.filteredValue=e)}},multiple:function(){return!this.column||this.column.filterMultiple}},mounted:function(){var e=this;this.popperElm=this.$el,this.referenceElm=this.cell,this.table.bodyWrapper.addEventListener("scroll",function(){e.updatePopper()}),this.$watch("showPopper",function(t){e.column&&(e.column.filterOpened=t),t?oe.open(e):oe.close(e)})},watch:{showPopper:function(e){!0===e&&parseInt(this.popperJS._popper.style.zIndex,10)1;return n&&(this.$parent.isGroup=!0),e("table",{class:"el-table__header",attrs:{cellspacing:"0",cellpadding:"0",border:"0"}},[e("colgroup",[this.columns.map(function(t){return e("col",{attrs:{name:t.id},key:t.id})}),this.hasGutter?e("col",{attrs:{name:"gutter"}}):""]),e("thead",{class:[{"is-group":n,"has-gutter":this.hasGutter}]},[this._l(r,function(o,r){return e("tr",{style:t.getHeaderRowStyle(r),class:t.getHeaderRowClass(r)},[o.map(function(n,i){return e("th",{attrs:{colspan:n.colSpan,rowspan:n.rowSpan},on:{mousemove:function(e){return t.handleMouseMove(e,n)},mouseout:t.handleMouseOut,mousedown:function(e){return t.handleMouseDown(e,n)},click:function(e){return t.handleHeaderClick(e,n)},contextmenu:function(e){return t.handleHeaderContextMenu(e,n)}},style:t.getHeaderCellStyle(r,i,o,n),class:t.getHeaderCellClass(r,i,o,n),key:n.id},[e("div",{class:["cell",n.filteredValue&&n.filteredValue.length>0?"highlight":"",n.labelClassName]},[n.renderHeader?n.renderHeader.call(t._renderProxy,e,{column:n,$index:i,store:t.store,_self:t.$parent.$vnode.context}):n.label,n.sortable?e("span",{class:"caret-wrapper",on:{click:function(e){return t.handleSortClick(e,n)}}},[e("i",{class:"sort-caret ascending",on:{click:function(e){return t.handleSortClick(e,n,"ascending")}}}),e("i",{class:"sort-caret descending",on:{click:function(e){return t.handleSortClick(e,n,"descending")}}})]):"",n.filterable?e("span",{class:"el-table__column-filter-trigger",on:{click:function(e){return t.handleFilterClick(e,n)}}},[e("i",{class:["el-icon-arrow-down",n.filterOpened?"el-icon-arrow-up":""]})]):""])])}),t.hasGutter?e("th",{class:"gutter"}):""])})])])},props:{fixed:String,store:{required:!0},border:Boolean,defaultSort:{type:Object,default:function(){return{prop:"",order:""}}}},components:{ElCheckbox:c.a},computed:pe({table:function(){return this.$parent},hasGutter:function(){return!this.fixed&&this.tableLayout.gutterWidth}},n({columns:"columns",isAllSelected:"isAllSelected",leftFixedLeafCount:"fixedLeafColumnsLength",rightFixedLeafCount:"rightFixedLeafColumnsLength",columnsCount:function(e){return e.columns.length},leftFixedCount:function(e){return e.fixedColumns.length},rightFixedCount:function(e){return e.rightFixedColumns.length}})),created:function(){this.filterPanels={}},mounted:function(){var e=this;this.$nextTick(function(){var t=e.defaultSort,o=t.prop,r=t.order;e.store.commit("sort",{prop:o,order:r,init:!0})})},beforeDestroy:function(){var e=this.filterPanels;for(var t in e)e.hasOwnProperty(t)&&e[t]&&e[t].$destroy(!0)},methods:{isCellHidden:function(e,t){for(var o=0,r=0;r=this.leftFixedLeafCount:"right"===this.fixed?o=this.columnsCount-this.rightFixedLeafCount},getHeaderRowStyle:function(e){var t=this.table.headerRowStyle;return"function"==typeof t?t.call(null,{rowIndex:e}):t},getHeaderRowClass:function(e){var t=[],o=this.table.headerRowClassName;return"string"==typeof o?t.push(o):"function"==typeof o&&t.push(o.call(null,{rowIndex:e})),t.join(" ")},getHeaderCellStyle:function(e,t,o,r){var n=this.table.headerCellStyle;return"function"==typeof n?n.call(null,{rowIndex:e,columnIndex:t,row:o,column:r}):n},getHeaderCellClass:function(e,t,o,r){var n=[r.id,r.order,r.headerAlign,r.className,r.labelClassName];0===e&&this.isCellHidden(t,o)&&n.push("is-hidden"),r.children||n.push("is-leaf"),r.sortable&&n.push("is-sortable");var i=this.table.headerCellClassName;return"string"==typeof i?n.push(i):"function"==typeof i&&n.push(i.call(null,{rowIndex:e,columnIndex:t,row:o,column:r})),n.join(" ")},toggleAllSelection:function(e){e.stopPropagation(),this.store.commit("toggleAllSelection")},handleFilterClick:function(e,t){e.stopPropagation();var o=e.target,r="TH"===o.tagName?o:o.parentNode;if(!Object(B.hasClass)(r,"noclick")){r=r.querySelector(".el-table__column-filter-trigger")||r;var n=this.$parent,i=this.filterPanels[t.id];if(i&&t.filterOpened)return void(i.showPopper=!1);i||(i=new w.a(ue),this.filterPanels[t.id]=i,t.filterPlacement&&(i.placement=t.filterPlacement),i.table=n,i.cell=r,i.column=t,!this.$isServer&&i.$mount(document.createElement("div"))),setTimeout(function(){i.showPopper=!0},16)}},handleHeaderClick:function(e,t){!t.filters&&t.sortable?this.handleSortClick(e,t):t.filterable&&!t.sortable&&this.handleFilterClick(e,t),this.$parent.$emit("header-click",t,e)},handleHeaderContextMenu:function(e,t){this.$parent.$emit("header-contextmenu",t,e)},handleMouseDown:function(e,t){var o=this;if(!this.$isServer&&!(t.children&&t.children.length>0)&&this.draggingColumn&&this.border){this.dragging=!0,this.$parent.resizeProxyVisible=!0;var r=this.$parent,n=r.$el,i=n.getBoundingClientRect().left,l=this.$el.querySelector("th."+t.id),a=l.getBoundingClientRect(),s=a.left-i+30;Object(B.addClass)(l,"noclick"),this.dragState={startMouseLeft:e.clientX,startLeft:a.right-i,startColumnLeft:a.left-i,tableLeft:i};var c=r.$refs.resizeProxy;c.style.left=this.dragState.startLeft+"px",document.onselectstart=function(){return!1},document.ondragstart=function(){return!1};var d=function(e){var t=e.clientX-o.dragState.startMouseLeft,r=o.dragState.startLeft+t;c.style.left=Math.max(s,r)+"px"},u=function n(){if(o.dragging){var i=o.dragState,a=i.startColumnLeft,s=i.startLeft,u=parseInt(c.style.left,10),p=u-a;t.width=t.realWidth=p,r.$emit("header-dragend",t.width,s-a,t,e),o.store.scheduleLayout(),document.body.style.cursor="",o.dragging=!1,o.draggingColumn=null,o.dragState={},r.resizeProxyVisible=!1}document.removeEventListener("mousemove",d),document.removeEventListener("mouseup",n),document.onselectstart=null,document.ondragstart=null,setTimeout(function(){Object(B.removeClass)(l,"noclick")},0)};document.addEventListener("mousemove",d),document.addEventListener("mouseup",u)}},handleMouseMove:function(e,t){if(!(t.children&&t.children.length>0)){for(var o=e.target;o&&"TH"!==o.tagName;)o=o.parentNode;if(t&&t.resizable&&!this.dragging&&this.border){var r=o.getBoundingClientRect(),n=document.body.style;r.width>12&&r.right-e.pageX<8?(n.cursor="col-resize",Object(B.hasClass)(o,"is-sortable")&&(o.style.cursor="col-resize"),this.draggingColumn=t):this.dragging||(n.cursor="",Object(B.hasClass)(o,"is-sortable")&&(o.style.cursor="pointer"),this.draggingColumn=null)}}},handleMouseOut:function(){this.$isServer||(document.body.style.cursor="")},toggleOrder:function(e){var t=e.order,o=e.sortOrders;if(""===t)return o[0];var r=o.indexOf(t||null);return o[r>o.length-2?0:r+1]},handleSortClick:function(e,t,o){e.stopPropagation();for(var r=t.order===o?null:o||this.toggleOrder(t),n=e.target;n&&"TH"!==n.tagName;)n=n.parentNode;if(n&&"TH"===n.tagName&&Object(B.hasClass)(n,"noclick"))return void Object(B.removeClass)(n,"noclick");if(t.sortable){var i=this.store.states,l=i.sortProp,a=void 0,s=i.sortingColumn;(s!==t||s===t&&null===s.order)&&(s&&(s.order=null),i.sortingColumn=t,l=t.property),a=t.order=r||null,i.sortProp=l,i.sortOrder=a,this.store.commit("changeSortCondition")}}},data:function(){return{draggingColumn:null,dragging:!1,dragState:{}}}},ge=Object.assign||function(e){for(var t=1;t=this.leftFixedLeafCount;if("right"===this.fixed){for(var r=0,n=0;n=this.columnsCount-this.rightFixedCount},getRowClasses:function(e,t){var o=[e.id,e.align,e.labelClassName];return e.className&&o.push(e.className),this.isCellHidden(t,this.columns,e)&&o.push("is-hidden"),e.children||o.push("is-leaf"),o}}},_e=Object.assign||function(e){for(var t=1;t0){var r=o.scrollTop;t.pixelY<0&&0!==r&&e.preventDefault(),t.pixelY>0&&o.scrollHeight-o.clientHeight>r&&e.preventDefault(),o.scrollTop+=Math.ceil(t.pixelY/5)}else o.scrollLeft+=Math.ceil(t.pixelX/5)},handleHeaderFooterMousewheel:function(e,t){var o=t.pixelX,r=t.pixelY;Math.abs(o)>=Math.abs(r)&&(this.bodyWrapper.scrollLeft+=t.pixelX/5)},syncPostion:Object(d.throttle)(20,function(){var e=this.bodyWrapper,t=e.scrollLeft,o=e.scrollTop,r=e.offsetWidth,n=e.scrollWidth,i=this.$refs,l=i.headerWrapper,a=i.footerWrapper,s=i.fixedBodyWrapper,c=i.rightFixedBodyWrapper;l&&(l.scrollLeft=t),a&&(a.scrollLeft=t),s&&(s.scrollTop=o),c&&(c.scrollTop=o);var d=n-r-1;this.scrollPosition=t>=d?"right":0===t?"left":"middle"}),bindEvents:function(){this.bodyWrapper.addEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(u.addResizeListener)(this.$el,this.resizeListener)},unbindEvents:function(){this.bodyWrapper.removeEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(u.removeResizeListener)(this.$el,this.resizeListener)},resizeListener:function(){if(this.$ready){var e=!1,t=this.$el,o=this.resizeState,r=o.width,n=o.height,i=t.offsetWidth;r!==i&&(e=!0);var l=t.offsetHeight;(this.height||this.shouldUpdateHeight)&&n!==l&&(e=!0),e&&(this.resizeState.width=i,this.resizeState.height=l,this.doLayout())}},doLayout:function(){this.shouldUpdateHeight&&this.layout.updateElsHeight(),this.layout.updateColumnsWidth()},sort:function(e,t){this.store.commit("sort",{prop:e,order:t})},toggleAllSelection:function(){this.store.commit("toggleAllSelection")}},computed:_e({tableSize:function(){return this.size||(this.$ELEMENT||{}).size},bodyWrapper:function(){return this.$refs.bodyWrapper},shouldUpdateHeight:function(){return this.height||this.maxHeight||this.fixedColumns.length>0||this.rightFixedColumns.length>0},bodyWidth:function(){var e=this.layout,t=e.bodyWidth,o=e.scrollY,r=e.gutterWidth;return t?t-(o?r:0)+"px":""},bodyHeight:function(){var e=this.layout,t=e.headerHeight,o=void 0===t?0:t,r=e.bodyHeight,n=e.footerHeight,i=void 0===n?0:n;if(this.height)return{height:r?r+"px":""};if(this.maxHeight){var l=Object(C.j)(this.maxHeight);if("number"==typeof l)return{"max-height":l-i-(this.showHeader?o:0)+"px"}}return{}},fixedBodyHeight:function(){if(this.height)return{height:this.layout.fixedBodyHeight?this.layout.fixedBodyHeight+"px":""};if(this.maxHeight){var e=Object(C.j)(this.maxHeight);if("number"==typeof e)return e=this.layout.scrollX?e-this.layout.gutterWidth:e,this.showHeader&&(e-=this.layout.headerHeight),e-=this.layout.footerHeight,{"max-height":e+"px"}}return{}},fixedHeight:function(){return this.maxHeight?this.showSummary?{bottom:0}:{bottom:this.layout.scrollX&&this.data.length?this.layout.gutterWidth+"px":""}:this.showSummary?{height:this.layout.tableHeight?this.layout.tableHeight+"px":""}:{height:this.layout.viewportHeight?this.layout.viewportHeight+"px":""}},emptyBlockStyle:function(){if(this.data&&this.data.length)return null;var e="100%";return this.layout.appendHeight&&(e="calc(100% - "+this.layout.appendHeight+"px)"),{width:this.bodyWidth,height:e}}},n({selection:"selection",columns:"columns",tableData:"data",fixedColumns:"fixedColumns",rightFixedColumns:"rightFixedColumns"})),watch:{height:{immediate:!0,handler:function(e){this.layout.setHeight(e)}},maxHeight:{immediate:!0,handler:function(e){this.layout.setMaxHeight(e)}},currentRowKey:{immediate:!0,handler:function(e){this.rowKey&&this.store.setCurrentRowKey(e)}},data:{immediate:!0,handler:function(e){this.store.commit("setData",e)}},expandRowKeys:{immediate:!0,handler:function(e){e&&this.store.setExpandRowKeysAdapter(e)}}},created:function(){var e=this;this.tableId="el-table_"+ve++,this.debouncedUpdateLayout=Object(d.debounce)(50,function(){return e.doLayout()})},mounted:function(){var e=this;this.bindEvents(),this.store.updateColumns(),this.doLayout(),this.resizeState={width:this.$el.offsetWidth,height:this.$el.offsetHeight},this.store.states.columns.forEach(function(t){t.filteredValue&&t.filteredValue.length&&e.store.commit("filterChange",{column:t,values:t.filteredValue,silent:!0})}),this.$ready=!0},destroyed:function(){this.unbindEvents()},data:function(){var e=this.treeProps,t=e.hasChildren,o=void 0===t?"hasChildren":t,n=e.children,i=void 0===n?"children":n;return this.store=r(this,{rowKey:this.rowKey,defaultExpandAll:this.defaultExpandAll,selectOnIndeterminate:this.selectOnIndeterminate,indent:this.indent,lazy:this.lazy,lazyColumnIdentifier:o,childrenColumnName:i}),{layout:new R({store:this.store,table:this,fit:this.fit,showHeader:this.showHeader}),isHidden:!1,renderExpanded:null,resizeProxyVisible:!1,resizeState:{width:null,height:null},isGroup:!1,scrollPosition:"left"}}},ye=xe,we=Object(ce.a)(ye,l,a,!1,null,null,null);we.options.__file="packages/table/src/table.vue";var ke=we.exports;ke.install=function(e){e.component(ke.name,ke)},t.default=ke}])},function(e,t,o){var r=o(160);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(161);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(162);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(163);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(164);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(165);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(166);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(167);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(168);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(169);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(170);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(171);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(172);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){var r=o(173);"string"==typeof r&&(r=[[e.i,r,""]]);var n={hmr:!0};n.transform=void 0,n.insertInto=void 0,o(3)(r,n),r.locals&&(e.exports=r.locals)},function(e,t,o){"use strict";var r=o(204),n=o(74),i=(o(202),o(46)),l=o.i(i.a)(n.a,r.a,r.b,!1,null,null,null);t.a=l.exports},function(e,t,o){"use strict";function r(e){return e&&DataView.prototype.isPrototypeOf(e)}function n(e){if("string"!=typeof e&&(e=String(e)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(e)||""===e)throw new TypeError("Invalid character in header field name");return e.toLowerCase()}function i(e){return"string"!=typeof e&&(e=String(e)),e}function l(e){var t={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return w.iterable&&(t[Symbol.iterator]=function(){return t}),t}function a(e){this.map={},e instanceof a?e.forEach(function(e,t){this.append(t,e)},this):Array.isArray(e)?e.forEach(function(e){this.append(e[0],e[1])},this):e&&Object.getOwnPropertyNames(e).forEach(function(t){this.append(t,e[t])},this)}function s(e){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}function c(e){return new Promise(function(t,o){e.onload=function(){t(e.result)},e.onerror=function(){o(e.error)}})}function d(e){var t=new FileReader,o=c(t);return t.readAsArrayBuffer(e),o}function u(e){var t=new FileReader,o=c(t);return t.readAsText(e),o}function p(e){for(var t=new Uint8Array(e),o=new Array(t.length),r=0;r-1?t:e}function g(e,t){if(!(this instanceof g))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');t=t||{};var o=t.body;if(e instanceof g){if(e.bodyUsed)throw new TypeError("Already read");this.url=e.url,this.credentials=e.credentials,t.headers||(this.headers=new a(e.headers)),this.method=e.method,this.mode=e.mode,this.signal=e.signal,o||null==e._bodyInit||(o=e._bodyInit,e.bodyUsed=!0)}else this.url=String(e);if(this.credentials=t.credentials||this.credentials||"same-origin",!t.headers&&this.headers||(this.headers=new a(t.headers)),this.method=b(t.method||this.method||"GET"),this.mode=t.mode||this.mode||null,this.signal=t.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&o)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(o),!("GET"!==this.method&&"HEAD"!==this.method||"no-store"!==t.cache&&"no-cache"!==t.cache)){var r=/([?&])_=[^&]*/;if(r.test(this.url))this.url=this.url.replace(r,"$1_="+(new Date).getTime());else{var n=/\?/;this.url+=(n.test(this.url)?"&":"?")+"_="+(new Date).getTime()}}}function m(e){var t=new FormData;return e.trim().split("&").forEach(function(e){if(e){var o=e.split("="),r=o.shift().replace(/\+/g," "),n=o.join("=").replace(/\+/g," ");t.append(decodeURIComponent(r),decodeURIComponent(n))}}),t}function _(e){var t=new a;return e.replace(/\r?\n[\t ]+/g," ").split("\r").map(function(e){return 0===e.indexOf("\n")?e.substr(1,e.length):e}).forEach(function(e){var o=e.split(":"),r=o.shift().trim();if(r){var n=o.join(":").trim();t.append(r,n)}}),t}function v(e,t){if(!(this instanceof v))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');t||(t={}),this.type="default",this.status=void 0===t.status?200:t.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in t?t.statusText:"",this.headers=new a(t.headers),this.url=t.url||"",this._initBody(e)}function x(e,t){return new Promise(function(o,r){function n(){s.abort()}var l=new g(e,t);if(l.signal&&l.signal.aborted)return r(new S("Aborted","AbortError"));var s=new XMLHttpRequest;s.onload=function(){var e={status:s.status,statusText:s.statusText,headers:_(s.getAllResponseHeaders()||"")};e.url="responseURL"in s?s.responseURL:e.headers.get("X-Request-URL");var t="response"in s?s.response:s.responseText;setTimeout(function(){o(new v(t,e))},0)},s.onerror=function(){setTimeout(function(){r(new TypeError("Network request failed"))},0)},s.ontimeout=function(){setTimeout(function(){r(new TypeError("Network request failed"))},0)},s.onabort=function(){setTimeout(function(){r(new S("Aborted","AbortError"))},0)},s.open(l.method,function(e){try{return""===e&&y.location.href?y.location.href:e}catch(t){return e}}(l.url),!0),"include"===l.credentials?s.withCredentials=!0:"omit"===l.credentials&&(s.withCredentials=!1),"responseType"in s&&(w.blob?s.responseType="blob":w.arrayBuffer&&l.headers.get("Content-Type")&&-1!==l.headers.get("Content-Type").indexOf("application/octet-stream")&&(s.responseType="arraybuffer")),!t||"object"!=typeof t.headers||t.headers instanceof a?l.headers.forEach(function(e,t){s.setRequestHeader(t,e)}):Object.getOwnPropertyNames(t.headers).forEach(function(e){s.setRequestHeader(e,i(t.headers[e]))}),l.signal&&(l.signal.addEventListener("abort",n),s.onreadystatechange=function(){4===s.readyState&&l.signal.removeEventListener("abort",n)}),s.send(void 0===l._bodyInit?null:l._bodyInit)})}var y="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||void 0!==y&&y,w={searchParams:"URLSearchParams"in y,iterable:"Symbol"in y&&"iterator"in Symbol,blob:"FileReader"in y&&"Blob"in y&&function(){try{return new Blob,!0}catch(e){return!1}}(),formData:"FormData"in y,arrayBuffer:"ArrayBuffer"in y};if(w.arrayBuffer)var k=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],F=ArrayBuffer.isView||function(e){return e&&k.indexOf(Object.prototype.toString.call(e))>-1};a.prototype.append=function(e,t){e=n(e),t=i(t);var o=this.map[e];this.map[e]=o?o+", "+t:t},a.prototype.delete=function(e){delete this.map[n(e)]},a.prototype.get=function(e){return e=n(e),this.has(e)?this.map[e]:null},a.prototype.has=function(e){return this.map.hasOwnProperty(n(e))},a.prototype.set=function(e,t){this.map[n(e)]=i(t)},a.prototype.forEach=function(e,t){for(var o in this.map)this.map.hasOwnProperty(o)&&e.call(t,this.map[o],o,this)},a.prototype.keys=function(){var e=[];return this.forEach(function(t,o){e.push(o)}),l(e)},a.prototype.values=function(){var e=[];return this.forEach(function(t){e.push(t)}),l(e)},a.prototype.entries=function(){var e=[];return this.forEach(function(t,o){e.push([o,t])}),l(e)},w.iterable&&(a.prototype[Symbol.iterator]=a.prototype.entries);var C=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];g.prototype.clone=function(){return new g(this,{body:this._bodyInit})},h.call(g.prototype),h.call(v.prototype),v.prototype.clone=function(){return new v(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new a(this.headers),url:this.url})},v.error=function(){var e=new v(null,{status:0,statusText:""});return e.type="error",e};var E=[301,302,303,307,308];v.redirect=function(e,t){if(-1===E.indexOf(t))throw new RangeError("Invalid status code");return new v(null,{status:t,headers:{location:e}})};var S=y.DOMException;try{new S}catch(e){S=function(e,t){this.message=e,this.name=t;var o=Error(e);this.stack=o.stack},S.prototype=Object.create(Error.prototype),S.prototype.constructor=S}x.polyfill=!0,y.fetch||(y.fetch=x,y.Headers=a,y.Request=g,y.Response=v)},function(e,t,o){"use strict";function r(e){this.rules=null,this._messages=d.a,this.define(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=o(54),i=o.n(n),l=o(20),a=o.n(l),s=o(1),c=o(116),d=o(105);r.prototype={messages:function(e){return e&&(this._messages=o.i(s.a)(o.i(d.b)(),e)),this._messages},define:function(e){if(!e)throw new Error("Cannot configure a schema with no rules");if("object"!==(void 0===e?"undefined":a()(e))||Array.isArray(e))throw new Error("Rules must be an object");this.rules={};var t=void 0,o=void 0;for(t in e)e.hasOwnProperty(t)&&(o=e[t],this.rules[t]=Array.isArray(o)?o:[o])},validate:function(e){function t(e){var t=void 0,o=void 0,r=[],n={};for(t=0;t1&&void 0!==arguments[1]?arguments[1]:{},c=arguments[2],u=e,p=l,f=c;if("function"==typeof p&&(f=p,p={}),!this.rules||0===Object.keys(this.rules).length)return void(f&&f());if(p.messages){var h=this.messages();h===d.a&&(h=o.i(d.b)()),o.i(s.a)(h,p.messages),p.messages=h}else p.messages=this.messages();var b=void 0,g=void 0,m={};(p.keys||Object.keys(this.rules)).forEach(function(t){b=n.rules[t],g=u[t],b.forEach(function(o){var r=o;"function"==typeof r.transform&&(u===e&&(u=i()({},u)),g=u[t]=r.transform(g)),r="function"==typeof r?{validator:r}:i()({},r),r.validator=n.getValidationMethod(r),r.field=t,r.fullField=r.fullField||t,r.type=n.getType(r),r.validator&&(m[t]=m[t]||[],m[t].push({rule:r,value:g,source:u,field:t}))})});var _={};o.i(s.b)(m,p,function(e,t){function n(e,t){return i()({},t,{fullField:c.fullField+"."+e})}function l(){var l=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],a=l;if(Array.isArray(a)||(a=[a]),a.length&&o.i(s.c)("async-validator:",a),a.length&&c.message&&(a=[].concat(c.message)),a=a.map(o.i(s.d)(c)),p.first&&a.length)return _[c.field]=1,t(a);if(d){if(c.required&&!e.value)return a=c.message?[].concat(c.message).map(o.i(s.d)(c)):p.error?[p.error(c,o.i(s.e)(p.messages.required,c.field))]:[],t(a);var u={};if(c.defaultField)for(var f in e.value)e.value.hasOwnProperty(f)&&(u[f]=c.defaultField);u=i()({},u,e.rule.fields);for(var h in u)if(u.hasOwnProperty(h)){var b=Array.isArray(u[h])?u[h]:[u[h]];u[h]=b.map(n.bind(null,h))}var g=new r(u);g.messages(p.messages),e.rule.options&&(e.rule.options.messages=p.messages,e.rule.options.error=p.error),g.validate(e.value,e.rule.options||p,function(e){t(e&&e.length?a.concat(e):e)})}else t(a)}var c=e.rule,d=!("object"!==c.type&&"array"!==c.type||"object"!==a()(c.fields)&&"object"!==a()(c.defaultField));d=d&&(c.required||!c.required&&e.value),c.field=e.field;var u=c.validator(c,e.value,l,e.source,p);u&&u.then&&u.then(function(){return l()},function(e){return l(e)})},function(e){t(e)})},getType:function(e){if(void 0===e.type&&e.pattern instanceof RegExp&&(e.type="pattern"),"function"!=typeof e.validator&&e.type&&!c.a.hasOwnProperty(e.type))throw new Error(o.i(s.e)("Unknown rule type %s",e.type));return e.type||"string"},getValidationMethod:function(e){if("function"==typeof e.validator)return e.validator;var t=Object.keys(e),o=t.indexOf("message");return-1!==o&&t.splice(o,1),1===t.length&&"required"===t[0]?c.a.required:c.a[this.getType(e)]||!1}},r.register=function(e,t){if("function"!=typeof t)throw new Error("Cannot register a validator by type, validator is not a function");c.a[e]=t},r.messages=d.a,t.default=r},function(e,t,o){"use strict";function r(){return{default:"Validation error on field %s",required:"%s is required",enum:"%s must be one of %s",whitespace:"%s cannot be empty",date:{format:"%s date %s is invalid for format %s",parse:"%s date could not be parsed, %s is invalid ",invalid:"%s date %s is invalid"},types:{string:"%s is not a %s",method:"%s is not a %s (function)",array:"%s is not an %s",object:"%s is not an %s",number:"%s is not a %s",date:"%s is not a %s",boolean:"%s is not a %s",integer:"%s is not an %s",float:"%s is not a %s",regexp:"%s is not a valid %s",email:"%s is not a valid %s",url:"%s is not a valid %s",hex:"%s is not a valid %s"},string:{len:"%s must be exactly %s characters",min:"%s must be at least %s characters",max:"%s cannot be longer than %s characters",range:"%s must be between %s and %s characters"},number:{len:"%s must equal %s",min:"%s cannot be less than %s",max:"%s cannot be greater than %s",range:"%s must be between %s and %s"},array:{len:"%s must be exactly %s in length",min:"%s cannot be less than %s in length",max:"%s cannot be greater than %s in length",range:"%s must be between %s and %s in length"},pattern:{mismatch:"%s value %s does not match pattern %s"},clone:function(){var e=JSON.parse(JSON.stringify(this));return e.clone=this.clone,e}}}t.b=r,o.d(t,"a",function(){return n});var n=r()},function(e,t,o){"use strict";function r(e,t,o,r,l){e[i]=Array.isArray(e[i])?e[i]:[],-1===e[i].indexOf(t)&&r.push(n.e(l.messages[i],e.fullField,e[i].join(", ")))}var n=o(1),i="enum";t.a=r},function(e,t,o){"use strict";function r(e,t,o,r,i){if(e.pattern)if(e.pattern instanceof RegExp)e.pattern.lastIndex=0,e.pattern.test(t)||r.push(n.e(i.messages.pattern.mismatch,e.fullField,t,e.pattern));else if("string"==typeof e.pattern){var l=new RegExp(e.pattern);l.test(t)||r.push(n.e(i.messages.pattern.mismatch,e.fullField,t,e.pattern))}}var n=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,o,r,i){var l="number"==typeof e.len,a="number"==typeof e.min,s="number"==typeof e.max,c=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,d=t,u=null,p="number"==typeof t,f="string"==typeof t,h=Array.isArray(t);if(p?u="number":f?u="string":h&&(u="array"),!u)return!1;h&&(d=t.length),f&&(d=t.replace(c,"_").length),l?d!==e.len&&r.push(n.e(i.messages[u].len,e.fullField,e.len)):a&&!s&&de.max?r.push(n.e(i.messages[u].max,e.fullField,e.max)):a&&s&&(de.max)&&r.push(n.e(i.messages[u].range,e.fullField,e.min,e.max))}var n=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,n,s){if(e.required&&void 0===t)return void o.i(a.a)(e,t,r,n,s);var d=["integer","float","array","regexp","object","method","email","number","date","url","hex"],u=e.type;d.indexOf(u)>-1?c[u](t)||n.push(l.e(s.messages.types[u],e.fullField,e.type)):u&&(void 0===t?"undefined":i()(t))!==e.type&&n.push(l.e(s.messages.types[u],e.fullField,e.type))}var n=o(20),i=o.n(n),l=o(1),a=o(50),s={email:/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,url:new RegExp("^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$","i"),hex:/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i},c={integer:function(e){return c.number(e)&&parseInt(e,10)===e},float:function(e){return c.number(e)&&!c.integer(e)},array:function(e){return Array.isArray(e)},regexp:function(e){if(e instanceof RegExp)return!0;try{return!!new RegExp(e)}catch(e){return!1}},date:function(e){return"function"==typeof e.getTime&&"function"==typeof e.getMonth&&"function"==typeof e.getYear},number:function(e){return!isNaN(e)&&"number"==typeof e},object:function(e){return"object"===(void 0===e?"undefined":i()(e))&&!c.array(e)},method:function(e){return"function"==typeof e},email:function(e){return"string"==typeof e&&!!e.match(s.email)&&e.length<255},url:function(e){return"string"==typeof e&&!!e.match(s.url)},hex:function(e){return"string"==typeof e&&!!e.match(s.hex)}};t.a=r},function(e,t,o){"use strict";function r(e,t,o,r,i){(/^\s+$/.test(t)||""===t)&&r.push(n.e(i.messages.whitespace,e.fullField))}var n=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t,"array")&&!e.required)return r();n.a.required(e,t,l,s,a,"array"),o.i(i.f)(t,"array")||(n.a.type(e,t,l,s,a),n.a.range(e,t,l,s,a))}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(n.f)(t)&&!e.required)return r();i.a.required(e,t,l,s,a),void 0!==t&&i.a.type(e,t,l,s,a)}r(s)}var n=o(1),i=o(5);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();if(n.a.required(e,t,l,s,a),!o.i(i.f)(t)){var c=void 0;c="number"==typeof t?new Date(t):t,n.a.type(e,c,l,s,a),c&&n.a.range(e,c.getTime(),l,s,a)}}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,a,s){var c=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,a,c,s),t&&n.a[l](e,t,a,c,s)}r(c)}var n=o(5),i=o(1),l="enum";t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,l,s,a),void 0!==t&&(n.a.type(e,t,l,s,a),n.a.range(e,t,l,s,a))}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";var r=o(124),n=o(118),i=o(119),l=o(112),a=o(122),s=o(117),c=o(115),d=o(111),u=o(120),p=o(114),f=o(121),h=o(113),b=o(123),g=o(125);t.a={string:r.a,method:n.a,number:i.a,boolean:l.a,regexp:a.a,integer:s.a,float:c.a,array:d.a,object:u.a,enum:p.a,pattern:f.a,date:h.a,url:g.a,hex:g.a,email:g.a,required:b.a}},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,l,s,a),void 0!==t&&(n.a.type(e,t,l,s,a),n.a.range(e,t,l,s,a))}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,l,s,a),void 0!==t&&n.a.type(e,t,l,s,a)}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,l,s,a),void 0!==t&&(n.a.type(e,t,l,s,a),n.a.range(e,t,l,s,a))}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,l,s,a),void 0!==t&&n.a.type(e,t,l,s,a)}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t,"string")&&!e.required)return r();n.a.required(e,t,l,s,a),o.i(i.f)(t,"string")||n.a.pattern(e,t,l,s,a)}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t)&&!e.required)return r();n.a.required(e,t,l,s,a),o.i(i.f)(t)||n.a.type(e,t,l,s,a)}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,o,r,n){var a=[],s=Array.isArray(t)?"array":void 0===t?"undefined":i()(t);l.a.required(e,t,r,a,n,s),o(a)}var n=o(20),i=o.n(n),l=o(5);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t,"string")&&!e.required)return r();n.a.required(e,t,l,s,a,"string"),o.i(i.f)(t,"string")||(n.a.type(e,t,l,s,a),n.a.range(e,t,l,s,a),n.a.pattern(e,t,l,s,a),!0===e.whitespace&&n.a.whitespace(e,t,l,s,a))}r(s)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";function r(e,t,r,l,a){var s=e.type,c=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(o.i(i.f)(t,s)&&!e.required)return r();n.a.required(e,t,l,c,a,s),o.i(i.f)(t,s)||n.a.type(e,t,l,c,a)}r(c)}var n=o(5),i=o(1);t.a=r},function(e,t,o){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=o(97),n=(o.n(r),o(0)),i=(o.n(n),o(84)),l=o.n(i),a=o(96),s=(o.n(a),o(83)),c=o.n(s),d=o(93),u=(o.n(d),o(48)),p=o.n(u),f=o(94),h=(o.n(f),o(81)),b=o.n(h),g=o(95),m=(o.n(g),o(82)),_=o.n(m),v=o(99),x=(o.n(v),o(86)),y=o.n(x),w=o(100),k=(o.n(w),o(87)),F=o.n(k),C=o(89),E=(o.n(C),o(78)),S=o.n(E),z=o(98),O=(o.n(z),o(85)),A=o.n(O),$=o(90),T=(o.n($),o(79)),j=o.n(T),M=o(91),L=(o.n(M),o(80)),I=o.n(L),P=o(88),N=(o.n(P),o(47)),D=o.n(N),R=o(4),B=o(49),H=o.n(B),W=o(29),q=o.n(W),U=o(92),V=(o.n(U),o(101)),Y=(o.n(V),o(102)),K=o(77);o(103),q.a.use(H.a),R.default.use(D.a),R.default.use(I.a),R.default.use(j.a),R.default.use(A.a),R.default.use(S.a),R.default.use(F.a),R.default.use(y.a),R.default.use(_.a),R.default.use(b.a),R.default.use(p.a),R.default.prototype.$msgbox=c.a,R.default.prototype.$confirm=c.a.confirm,R.default.prototype.$message=l.a,R.default.config.productionTip=!1,new R.default({el:"#app",router:K.a,template:"",components:{App:Y.a}})},function(e,t,o){e.exports={default:o(130),__esModule:!0}},function(e,t,o){e.exports={default:o(131),__esModule:!0}},function(e,t,o){e.exports={default:o(132),__esModule:!0}},function(e,t,o){o(152),e.exports=o(17).Object.assign},function(e,t,o){o(155),o(153),o(156),o(157),e.exports=o(17).Symbol},function(e,t,o){o(154),o(158),e.exports=o(42).f("iterator")},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){e.exports=function(){}},function(e,t,o){var r=o(13),n=o(150),i=o(149);e.exports=function(e){return function(t,o,l){var a,s=r(t),c=n(s.length),d=i(l,c);if(e&&o!=o){for(;c>d;)if((a=s[d++])!=a)return!0}else for(;c>d;d++)if((e||d in s)&&s[d]===o)return e||d||0;return!e&&-1}}},function(e,t,o){var r=o(133);e.exports=function(e,t,o){if(r(e),void 0===t)return e;switch(o){case 1:return function(o){return e.call(t,o)};case 2:return function(o,r){return e.call(t,o,r)};case 3:return function(o,r,n){return e.call(t,o,r,n)}}return function(){return e.apply(t,arguments)}}},function(e,t,o){var r=o(23),n=o(34),i=o(24);e.exports=function(e){var t=r(e),o=n.f;if(o)for(var l,a=o(e),s=i.f,c=0;a.length>c;)s.call(e,l=a[c++])&&t.push(l);return t}},function(e,t,o){var r=o(6).document;e.exports=r&&r.documentElement},function(e,t,o){var r=o(55);e.exports=Array.isArray||function(e){return"Array"==r(e)}},function(e,t,o){"use strict";var r=o(60),n=o(25),i=o(35),l={};o(11)(l,o(14)("iterator"),function(){return this}),e.exports=function(e,t,o){e.prototype=r(l,{next:n(1,o)}),i(e,t+" Iterator")}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,o){var r=o(26)("meta"),n=o(19),i=o(10),l=o(12).f,a=0,s=Object.isExtensible||function(){return!0},c=!o(18)(function(){return s(Object.preventExtensions({}))}),d=function(e){l(e,r,{value:{i:"O"+ ++a,w:{}}})},u=function(e,t){if(!n(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!i(e,r)){if(!s(e))return"F";if(!t)return"E";d(e)}return e[r].i},p=function(e,t){if(!i(e,r)){if(!s(e))return!0;if(!t)return!1;d(e)}return e[r].w},f=function(e){return c&&h.NEED&&s(e)&&!i(e,r)&&d(e),e},h=e.exports={KEY:r,NEED:!1,fastKey:u,getWeak:p,onFreeze:f}},function(e,t,o){"use strict";var r=o(9),n=o(23),i=o(34),l=o(24),a=o(39),s=o(58),c=Object.assign;e.exports=!c||o(18)(function(){var e={},t={},o=Symbol(),r="abcdefghijklmnopqrst";return e[o]=7,r.split("").forEach(function(e){t[e]=e}),7!=c({},e)[o]||Object.keys(c({},t)).join("")!=r})?function(e,t){for(var o=a(e),c=arguments.length,d=1,u=i.f,p=l.f;c>d;)for(var f,h=s(arguments[d++]),b=u?n(h).concat(u(h)):n(h),g=b.length,m=0;g>m;)f=b[m++],r&&!p.call(h,f)||(o[f]=h[f]);return o}:c},function(e,t,o){var r=o(12),n=o(21),i=o(23);e.exports=o(9)?Object.defineProperties:function(e,t){n(e);for(var o,l=i(t),a=l.length,s=0;a>s;)r.f(e,o=l[s++],t[o]);return e}},function(e,t,o){var r=o(24),n=o(25),i=o(13),l=o(40),a=o(10),s=o(57),c=Object.getOwnPropertyDescriptor;t.f=o(9)?c:function(e,t){if(e=i(e),t=l(t,!0),s)try{return c(e,t)}catch(e){}if(a(e,t))return n(!r.f.call(e,t),e[t])}},function(e,t,o){var r=o(13),n=o(61).f,i={}.toString,l="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],a=function(e){try{return n(e)}catch(e){return l.slice()}};e.exports.f=function(e){return l&&"[object Window]"==i.call(e)?a(e):n(r(e))}},function(e,t,o){var r=o(10),n=o(39),i=o(36)("IE_PROTO"),l=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=n(e),r(e,i)?e[i]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?l:null}},function(e,t,o){var r=o(38),n=o(30);e.exports=function(e){return function(t,o){var i,l,a=String(n(t)),s=r(o),c=a.length;return s<0||s>=c?e?"":void 0:(i=a.charCodeAt(s),i<55296||i>56319||s+1===c||(l=a.charCodeAt(s+1))<56320||l>57343?e?a.charAt(s):i:e?a.slice(s,s+2):l-56320+(i-55296<<10)+65536)}}},function(e,t,o){var r=o(38),n=Math.max,i=Math.min;e.exports=function(e,t){return e=r(e),e<0?n(e+t,0):i(e,t)}},function(e,t,o){var r=o(38),n=Math.min;e.exports=function(e){return e>0?n(r(e),9007199254740991):0}},function(e,t,o){"use strict";var r=o(134),n=o(141),i=o(33),l=o(13);e.exports=o(59)(Array,"Array",function(e,t){this._t=l(e),this._i=0,this._k=t},function(){var e=this._t,t=this._k,o=this._i++;return!e||o>=e.length?(this._t=void 0,n(1)):"keys"==t?n(0,o):"values"==t?n(0,e[o]):n(0,[o,e[o]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},function(e,t,o){var r=o(32);r(r.S+r.F,"Object",{assign:o(143)})},function(e,t){},function(e,t,o){"use strict";var r=o(148)(!0);o(59)(String,"String",function(e){this._t=String(e),this._i=0},function(){var e,t=this._t,o=this._i;return o>=t.length?{value:void 0,done:!0}:(e=r(t,o),this._i+=e.length,{value:e,done:!1})})},function(e,t,o){"use strict";var r=o(6),n=o(10),i=o(9),l=o(32),a=o(63),s=o(142).KEY,c=o(18),d=o(37),u=o(35),p=o(26),f=o(14),h=o(42),b=o(41),g=o(137),m=o(139),_=o(21),v=o(19),x=o(39),y=o(13),w=o(40),k=o(25),F=o(60),C=o(146),E=o(145),S=o(34),z=o(12),O=o(23),A=E.f,$=z.f,T=C.f,j=r.Symbol,M=r.JSON,L=M&&M.stringify,I=f("_hidden"),P=f("toPrimitive"),N={}.propertyIsEnumerable,D=d("symbol-registry"),R=d("symbols"),B=d("op-symbols"),H=Object.prototype,W="function"==typeof j&&!!S.f,q=r.QObject,U=!q||!q.prototype||!q.prototype.findChild,V=i&&c(function(){return 7!=F($({},"a",{get:function(){return $(this,"a",{value:7}).a}})).a})?function(e,t,o){var r=A(H,t);r&&delete H[t],$(e,t,o),r&&e!==H&&$(H,t,r)}:$,Y=function(e){var t=R[e]=F(j.prototype);return t._k=e,t},K=W&&"symbol"==typeof j.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof j},X=function(e,t,o){return e===H&&X(B,t,o),_(e),t=w(t,!0),_(o),n(R,t)?(o.enumerable?(n(e,I)&&e[I][t]&&(e[I][t]=!1),o=F(o,{enumerable:k(0,!1)})):(n(e,I)||$(e,I,k(1,{})),e[I][t]=!0),V(e,t,o)):$(e,t,o)},G=function(e,t){_(e);for(var o,r=g(t=y(t)),n=0,i=r.length;i>n;)X(e,o=r[n++],t[o]);return e},J=function(e,t){return void 0===t?F(e):G(F(e),t)},Z=function(e){var t=N.call(this,e=w(e,!0));return!(this===H&&n(R,e)&&!n(B,e))&&(!(t||!n(this,e)||!n(R,e)||n(this,I)&&this[I][e])||t)},Q=function(e,t){if(e=y(e),t=w(t,!0),e!==H||!n(R,t)||n(B,t)){var o=A(e,t);return!o||!n(R,t)||n(e,I)&&e[I][t]||(o.enumerable=!0),o}},ee=function(e){for(var t,o=T(y(e)),r=[],i=0;o.length>i;)n(R,t=o[i++])||t==I||t==s||r.push(t);return r},te=function(e){for(var t,o=e===H,r=T(o?B:y(e)),i=[],l=0;r.length>l;)!n(R,t=r[l++])||o&&!n(H,t)||i.push(R[t]);return i};W||(j=function(){if(this instanceof j)throw TypeError("Symbol is not a constructor!");var e=p(arguments.length>0?arguments[0]:void 0),t=function(o){this===H&&t.call(B,o),n(this,I)&&n(this[I],e)&&(this[I][e]=!1),V(this,e,k(1,o))};return i&&U&&V(H,e,{configurable:!0,set:t}),Y(e)},a(j.prototype,"toString",function(){return this._k}),E.f=Q,z.f=X,o(61).f=C.f=ee,o(24).f=Z,S.f=te,i&&!o(22)&&a(H,"propertyIsEnumerable",Z,!0),h.f=function(e){return Y(f(e))}),l(l.G+l.W+l.F*!W,{Symbol:j});for(var oe="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),re=0;oe.length>re;)f(oe[re++]);for(var ne=O(f.store),ie=0;ne.length>ie;)b(ne[ie++]);l(l.S+l.F*!W,"Symbol",{for:function(e){return n(D,e+="")?D[e]:D[e]=j(e)},keyFor:function(e){if(!K(e))throw TypeError(e+" is not a symbol!");for(var t in D)if(D[t]===e)return t},useSetter:function(){U=!0},useSimple:function(){U=!1}}),l(l.S+l.F*!W,"Object",{create:J,defineProperty:X,defineProperties:G,getOwnPropertyDescriptor:Q,getOwnPropertyNames:ee,getOwnPropertySymbols:te});var le=c(function(){S.f(1)});l(l.S+l.F*le,"Object",{getOwnPropertySymbols:function(e){return S.f(x(e))}}),M&&l(l.S+l.F*(!W||c(function(){var e=j();return"[null]"!=L([e])||"{}"!=L({a:e})||"{}"!=L(Object(e))})),"JSON",{stringify:function(e){for(var t,o,r=[e],n=1;arguments.length>n;)r.push(arguments[n++]);if(o=t=r[1],(v(t)||void 0!==e)&&!K(e))return m(t)||(t=function(e,t){if("function"==typeof o&&(t=o.call(this,e,t)),!K(t))return t}),r[1]=t,L.apply(M,r)}}),j.prototype[P]||o(11)(j.prototype,P,j.prototype.valueOf),u(j,"Symbol"),u(Math,"Math",!0),u(r.JSON,"JSON",!0)},function(e,t,o){o(41)("asyncIterator")},function(e,t,o){o(41)("observable")},function(e,t,o){o(151);for(var r=o(6),n=o(11),i=o(33),l=o(14)("toStringTag"),a="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s.el-button.is-active,.el-button-group>.el-button.is-disabled,.el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover{z-index:1}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button{margin-left:10px}.el-button:focus,.el-button:hover{color:#409EFF;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#FFF;border-color:#409EFF;color:#409EFF}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#FFF;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#FFF;border-color:#EBEEF5;color:#C0C4CC}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#FFF;background-color:#409EFF;border-color:#409EFF}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#FFF}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#FFF;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409EFF;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409EFF;border-color:#409EFF;color:#FFF}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#FFF;background-color:#67C23A;border-color:#67C23A}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#FFF}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#FFF}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#FFF;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67C23A;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67C23A;border-color:#67C23A;color:#FFF}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#FFF;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#FFF;background-color:#E6A23C;border-color:#E6A23C}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#FFF}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#FFF}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#FFF;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#E6A23C;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#E6A23C;border-color:#E6A23C;color:#FFF}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#FFF;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#FFF;background-color:#F56C6C;border-color:#F56C6C}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#FFF}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#FFF}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#FFF;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#F56C6C;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#F56C6C;border-color:#F56C6C;color:#FFF}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#FFF;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#FFF;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#FFF}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#FFF}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#FFF;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#FFF}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#FFF;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--text,.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover,.el-button--text:active{border-color:transparent}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--mini,.el-button--small{font-size:12px;border-radius:3px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small,.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini.is-circle{padding:7px}.el-button--text{color:#409EFF;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;background-color:transparent}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group::after,.el-button-group::before{display:table;content:\"\"}.el-button-group::after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:not(:last-child){margin-right:-1px}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,".el-col-pull-0,.el-col-pull-1,.el-col-pull-10,.el-col-pull-11,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-2,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-push-0,.el-col-push-1,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-2,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9{position:relative}[class*=el-col-]{float:left;box-sizing:border-box}.el-col-0{display:none;width:0%}.el-col-offset-0{margin-left:0}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{position:relative;right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0%}.el-col-xs-offset-0{margin-left:0}.el-col-xs-pull-0{position:relative;right:0}.el-col-xs-push-0{position:relative;left:0}.el-col-xs-1{width:4.16667%}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{width:8.33333%}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{width:12.5%}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{display:none;width:0%}.el-col-sm-offset-0{margin-left:0}.el-col-sm-pull-0{position:relative;right:0}.el-col-sm-push-0{position:relative;left:0}.el-col-sm-1{width:4.16667%}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{width:8.33333%}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{width:12.5%}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{display:none;width:0%}.el-col-md-offset-0{margin-left:0}.el-col-md-pull-0{position:relative;right:0}.el-col-md-push-0{position:relative;left:0}.el-col-md-1{width:4.16667%}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{width:8.33333%}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{width:12.5%}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{display:none;width:0%}.el-col-lg-offset-0{margin-left:0}.el-col-lg-pull-0{position:relative;right:0}.el-col-lg-push-0{position:relative;left:0}.el-col-lg-1{width:4.16667%}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{width:8.33333%}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{width:12.5%}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{display:none;width:0%}.el-col-xl-offset-0{margin-left:0}.el-col-xl-pull-0{position:relative;right:0}.el-col-xl-push-0{position:relative;left:0}.el-col-xl-1{width:4.16667%}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{width:8.33333%}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{width:12.5%}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,"",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,'.el-form--inline .el-form-item,.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form-item::after,.el-form-item__content::after{clear:both}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{margin-right:10px}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#F56C6C;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:\'*\';color:#F56C6C;margin-right:4px}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus{border-color:#F56C6C}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#F56C6C}.el-form-item--feedback .el-input__validateIcon{display:inline-block}',""])},function(e,t,o){t=e.exports=o(2)(!1);var r=o(64),n=r(o(73)),i=r(o(72));t.push([e.i,'@charset "UTF-8";.el-pagination--small .arrow.disabled,.el-table .hidden-columns,.el-table td.is-hidden>*,.el-table th.is-hidden>*,.el-table--hidden{visibility:hidden}.el-input__suffix,.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-dropdown .el-dropdown-selfdefine:focus:active,.el-dropdown .el-dropdown-selfdefine:focus:not(.focusing),.el-message__closeBtn:focus,.el-message__content:focus,.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing),.el-rate:active,.el-rate:focus,.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing),.el-upload-list__item.is-success:active,.el-upload-list__item.is-success:not(.focusing):focus{outline-width:0}@font-face{font-family:element-icons;src:url('+n+') format("woff"),url('+i+') format("truetype");font-weight:400;font-display:"auto";font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\\e6a0"}.el-icon-ice-cream-square:before{content:"\\e6a3"}.el-icon-lollipop:before{content:"\\e6a4"}.el-icon-potato-strips:before{content:"\\e6a5"}.el-icon-milk-tea:before{content:"\\e6a6"}.el-icon-ice-drink:before{content:"\\e6a7"}.el-icon-ice-tea:before{content:"\\e6a9"}.el-icon-coffee:before{content:"\\e6aa"}.el-icon-orange:before{content:"\\e6ab"}.el-icon-pear:before{content:"\\e6ac"}.el-icon-apple:before{content:"\\e6ad"}.el-icon-cherry:before{content:"\\e6ae"}.el-icon-watermelon:before{content:"\\e6af"}.el-icon-grape:before{content:"\\e6b0"}.el-icon-refrigerator:before{content:"\\e6b1"}.el-icon-goblet-square-full:before{content:"\\e6b2"}.el-icon-goblet-square:before{content:"\\e6b3"}.el-icon-goblet-full:before{content:"\\e6b4"}.el-icon-goblet:before{content:"\\e6b5"}.el-icon-cold-drink:before{content:"\\e6b6"}.el-icon-coffee-cup:before{content:"\\e6b8"}.el-icon-water-cup:before{content:"\\e6b9"}.el-icon-hot-water:before{content:"\\e6ba"}.el-icon-ice-cream:before{content:"\\e6bb"}.el-icon-dessert:before{content:"\\e6bc"}.el-icon-sugar:before{content:"\\e6bd"}.el-icon-tableware:before{content:"\\e6be"}.el-icon-burger:before{content:"\\e6bf"}.el-icon-knife-fork:before{content:"\\e6c1"}.el-icon-fork-spoon:before{content:"\\e6c2"}.el-icon-chicken:before{content:"\\e6c3"}.el-icon-food:before{content:"\\e6c4"}.el-icon-dish-1:before{content:"\\e6c5"}.el-icon-dish:before{content:"\\e6c6"}.el-icon-moon-night:before{content:"\\e6ee"}.el-icon-moon:before{content:"\\e6f0"}.el-icon-cloudy-and-sunny:before{content:"\\e6f1"}.el-icon-partly-cloudy:before{content:"\\e6f2"}.el-icon-cloudy:before{content:"\\e6f3"}.el-icon-sunny:before{content:"\\e6f6"}.el-icon-sunset:before{content:"\\e6f7"}.el-icon-sunrise-1:before{content:"\\e6f8"}.el-icon-sunrise:before{content:"\\e6f9"}.el-icon-heavy-rain:before{content:"\\e6fa"}.el-icon-lightning:before{content:"\\e6fb"}.el-icon-light-rain:before{content:"\\e6fc"}.el-icon-wind-power:before{content:"\\e6fd"}.el-icon-baseball:before{content:"\\e712"}.el-icon-soccer:before{content:"\\e713"}.el-icon-football:before{content:"\\e715"}.el-icon-basketball:before{content:"\\e716"}.el-icon-ship:before{content:"\\e73f"}.el-icon-truck:before{content:"\\e740"}.el-icon-bicycle:before{content:"\\e741"}.el-icon-mobile-phone:before{content:"\\e6d3"}.el-icon-service:before{content:"\\e6d4"}.el-icon-key:before{content:"\\e6e2"}.el-icon-unlock:before{content:"\\e6e4"}.el-icon-lock:before{content:"\\e6e5"}.el-icon-watch:before{content:"\\e6fe"}.el-icon-watch-1:before{content:"\\e6ff"}.el-icon-timer:before{content:"\\e702"}.el-icon-alarm-clock:before{content:"\\e703"}.el-icon-map-location:before{content:"\\e704"}.el-icon-delete-location:before{content:"\\e705"}.el-icon-add-location:before{content:"\\e706"}.el-icon-location-information:before{content:"\\e707"}.el-icon-location-outline:before{content:"\\e708"}.el-icon-location:before{content:"\\e79e"}.el-icon-place:before{content:"\\e709"}.el-icon-discover:before{content:"\\e70a"}.el-icon-first-aid-kit:before{content:"\\e70b"}.el-icon-trophy-1:before{content:"\\e70c"}.el-icon-trophy:before{content:"\\e70d"}.el-icon-medal:before{content:"\\e70e"}.el-icon-medal-1:before{content:"\\e70f"}.el-icon-stopwatch:before{content:"\\e710"}.el-icon-mic:before{content:"\\e711"}.el-icon-copy-document:before{content:"\\e718"}.el-icon-full-screen:before{content:"\\e719"}.el-icon-switch-button:before{content:"\\e71b"}.el-icon-aim:before{content:"\\e71c"}.el-icon-crop:before{content:"\\e71d"}.el-icon-odometer:before{content:"\\e71e"}.el-icon-time:before{content:"\\e71f"}.el-icon-bangzhu:before{content:"\\e724"}.el-icon-close-notification:before{content:"\\e726"}.el-icon-microphone:before{content:"\\e727"}.el-icon-turn-off-microphone:before{content:"\\e728"}.el-icon-position:before{content:"\\e729"}.el-icon-postcard:before{content:"\\e72a"}.el-icon-message:before{content:"\\e72b"}.el-icon-chat-line-square:before{content:"\\e72d"}.el-icon-chat-dot-square:before{content:"\\e72e"}.el-icon-chat-dot-round:before{content:"\\e72f"}.el-icon-chat-square:before{content:"\\e730"}.el-icon-chat-line-round:before{content:"\\e731"}.el-icon-chat-round:before{content:"\\e732"}.el-icon-set-up:before{content:"\\e733"}.el-icon-turn-off:before{content:"\\e734"}.el-icon-open:before{content:"\\e735"}.el-icon-connection:before{content:"\\e736"}.el-icon-link:before{content:"\\e737"}.el-icon-cpu:before{content:"\\e738"}.el-icon-thumb:before{content:"\\e739"}.el-icon-female:before{content:"\\e73a"}.el-icon-male:before{content:"\\e73b"}.el-icon-guide:before{content:"\\e73c"}.el-icon-news:before{content:"\\e73e"}.el-icon-price-tag:before{content:"\\e744"}.el-icon-discount:before{content:"\\e745"}.el-icon-wallet:before{content:"\\e747"}.el-icon-coin:before{content:"\\e748"}.el-icon-money:before{content:"\\e749"}.el-icon-bank-card:before{content:"\\e74a"}.el-icon-box:before{content:"\\e74b"}.el-icon-present:before{content:"\\e74c"}.el-icon-sell:before{content:"\\e6d5"}.el-icon-sold-out:before{content:"\\e6d6"}.el-icon-shopping-bag-2:before{content:"\\e74d"}.el-icon-shopping-bag-1:before{content:"\\e74e"}.el-icon-shopping-cart-2:before{content:"\\e74f"}.el-icon-shopping-cart-1:before{content:"\\e750"}.el-icon-shopping-cart-full:before{content:"\\e751"}.el-icon-smoking:before{content:"\\e752"}.el-icon-no-smoking:before{content:"\\e753"}.el-icon-house:before{content:"\\e754"}.el-icon-table-lamp:before{content:"\\e755"}.el-icon-school:before{content:"\\e756"}.el-icon-office-building:before{content:"\\e757"}.el-icon-toilet-paper:before{content:"\\e758"}.el-icon-notebook-2:before{content:"\\e759"}.el-icon-notebook-1:before{content:"\\e75a"}.el-icon-files:before{content:"\\e75b"}.el-icon-collection:before{content:"\\e75c"}.el-icon-receiving:before{content:"\\e75d"}.el-icon-suitcase-1:before{content:"\\e760"}.el-icon-suitcase:before{content:"\\e761"}.el-icon-film:before{content:"\\e763"}.el-icon-collection-tag:before{content:"\\e765"}.el-icon-data-analysis:before{content:"\\e766"}.el-icon-pie-chart:before{content:"\\e767"}.el-icon-data-board:before{content:"\\e768"}.el-icon-data-line:before{content:"\\e76d"}.el-icon-reading:before{content:"\\e769"}.el-icon-magic-stick:before{content:"\\e76a"}.el-icon-coordinate:before{content:"\\e76b"}.el-icon-mouse:before{content:"\\e76c"}.el-icon-brush:before{content:"\\e76e"}.el-icon-headset:before{content:"\\e76f"}.el-icon-umbrella:before{content:"\\e770"}.el-icon-scissors:before{content:"\\e771"}.el-icon-mobile:before{content:"\\e773"}.el-icon-attract:before{content:"\\e774"}.el-icon-monitor:before{content:"\\e775"}.el-icon-search:before{content:"\\e778"}.el-icon-takeaway-box:before{content:"\\e77a"}.el-icon-paperclip:before{content:"\\e77d"}.el-icon-printer:before{content:"\\e77e"}.el-icon-document-add:before{content:"\\e782"}.el-icon-document:before{content:"\\e785"}.el-icon-document-checked:before{content:"\\e786"}.el-icon-document-copy:before{content:"\\e787"}.el-icon-document-delete:before{content:"\\e788"}.el-icon-document-remove:before{content:"\\e789"}.el-icon-tickets:before{content:"\\e78b"}.el-icon-folder-checked:before{content:"\\e77f"}.el-icon-folder-delete:before{content:"\\e780"}.el-icon-folder-remove:before{content:"\\e781"}.el-icon-folder-add:before{content:"\\e783"}.el-icon-folder-opened:before{content:"\\e784"}.el-icon-folder:before{content:"\\e78a"}.el-icon-edit-outline:before{content:"\\e764"}.el-icon-edit:before{content:"\\e78c"}.el-icon-date:before{content:"\\e78e"}.el-icon-c-scale-to-original:before{content:"\\e7c6"}.el-icon-view:before{content:"\\e6ce"}.el-icon-loading:before{content:"\\e6cf"}.el-icon-rank:before{content:"\\e6d1"}.el-icon-sort-down:before{content:"\\e7c4"}.el-icon-sort-up:before{content:"\\e7c5"}.el-icon-sort:before{content:"\\e6d2"}.el-icon-finished:before{content:"\\e6cd"}.el-icon-refresh-left:before{content:"\\e6c7"}.el-icon-refresh-right:before{content:"\\e6c8"}.el-icon-refresh:before{content:"\\e6d0"}.el-icon-video-play:before{content:"\\e7c0"}.el-icon-video-pause:before{content:"\\e7c1"}.el-icon-d-arrow-right:before{content:"\\e6dc"}.el-icon-d-arrow-left:before{content:"\\e6dd"}.el-icon-arrow-up:before{content:"\\e6e1"}.el-icon-arrow-down:before{content:"\\e6df"}.el-icon-arrow-right:before{content:"\\e6e0"}.el-icon-arrow-left:before{content:"\\e6de"}.el-icon-top-right:before{content:"\\e6e7"}.el-icon-top-left:before{content:"\\e6e8"}.el-icon-top:before{content:"\\e6e6"}.el-icon-bottom:before{content:"\\e6eb"}.el-icon-right:before{content:"\\e6e9"}.el-icon-back:before{content:"\\e6ea"}.el-icon-bottom-right:before{content:"\\e6ec"}.el-icon-bottom-left:before{content:"\\e6ed"}.el-icon-caret-top:before{content:"\\e78f"}.el-icon-caret-bottom:before{content:"\\e790"}.el-icon-caret-right:before{content:"\\e791"}.el-icon-caret-left:before{content:"\\e792"}.el-icon-d-caret:before{content:"\\e79a"}.el-icon-share:before{content:"\\e793"}.el-icon-menu:before{content:"\\e798"}.el-icon-s-grid:before{content:"\\e7a6"}.el-icon-s-check:before{content:"\\e7a7"}.el-icon-s-data:before{content:"\\e7a8"}.el-icon-s-opportunity:before{content:"\\e7aa"}.el-icon-s-custom:before{content:"\\e7ab"}.el-icon-s-claim:before{content:"\\e7ad"}.el-icon-s-finance:before{content:"\\e7ae"}.el-icon-s-comment:before{content:"\\e7af"}.el-icon-s-flag:before{content:"\\e7b0"}.el-icon-s-marketing:before{content:"\\e7b1"}.el-icon-s-shop:before{content:"\\e7b4"}.el-icon-s-open:before{content:"\\e7b5"}.el-icon-s-management:before{content:"\\e7b6"}.el-icon-s-ticket:before{content:"\\e7b7"}.el-icon-s-release:before{content:"\\e7b8"}.el-icon-s-home:before{content:"\\e7b9"}.el-icon-s-promotion:before{content:"\\e7ba"}.el-icon-s-operation:before{content:"\\e7bb"}.el-icon-s-unfold:before{content:"\\e7bc"}.el-icon-s-fold:before{content:"\\e7a9"}.el-icon-s-platform:before{content:"\\e7bd"}.el-icon-s-order:before{content:"\\e7be"}.el-icon-s-cooperation:before{content:"\\e7bf"}.el-icon-bell:before{content:"\\e725"}.el-icon-message-solid:before{content:"\\e799"}.el-icon-video-camera:before{content:"\\e772"}.el-icon-video-camera-solid:before{content:"\\e796"}.el-icon-camera:before{content:"\\e779"}.el-icon-camera-solid:before{content:"\\e79b"}.el-icon-download:before{content:"\\e77c"}.el-icon-upload2:before{content:"\\e77b"}.el-icon-upload:before{content:"\\e7c3"}.el-icon-picture-outline-round:before{content:"\\e75f"}.el-icon-picture-outline:before{content:"\\e75e"}.el-icon-picture:before{content:"\\e79f"}.el-icon-close:before{content:"\\e6db"}.el-icon-check:before{content:"\\e6da"}.el-icon-plus:before{content:"\\e6d9"}.el-icon-minus:before{content:"\\e6d8"}.el-icon-help:before{content:"\\e73d"}.el-icon-s-help:before{content:"\\e7b3"}.el-icon-circle-close:before{content:"\\e78d"}.el-icon-circle-check:before{content:"\\e720"}.el-icon-circle-plus-outline:before{content:"\\e723"}.el-icon-remove-outline:before{content:"\\e722"}.el-icon-zoom-out:before{content:"\\e776"}.el-icon-zoom-in:before{content:"\\e777"}.el-icon-error:before{content:"\\e79d"}.el-icon-success:before{content:"\\e79c"}.el-icon-circle-plus:before{content:"\\e7a0"}.el-icon-remove:before{content:"\\e7a2"}.el-icon-info:before{content:"\\e7a1"}.el-icon-question:before{content:"\\e7a4"}.el-icon-warning-outline:before{content:"\\e6c9"}.el-icon-warning:before{content:"\\e7a3"}.el-icon-goods:before{content:"\\e7c2"}.el-icon-s-goods:before{content:"\\e7b2"}.el-icon-star-off:before{content:"\\e717"}.el-icon-star-on:before{content:"\\e797"}.el-icon-more-outline:before{content:"\\e6cc"}.el-icon-more:before{content:"\\e794"}.el-icon-phone-outline:before{content:"\\e6cb"}.el-icon-phone:before{content:"\\e795"}.el-icon-user:before{content:"\\e6e3"}.el-icon-user-solid:before{content:"\\e7a5"}.el-icon-setting:before{content:"\\e6ca"}.el-icon-s-tools:before{content:"\\e7ac"}.el-icon-delete:before{content:"\\e6d7"}.el-icon-delete-solid:before{content:"\\e7c9"}.el-icon-eleme:before{content:"\\e7c7"}.el-icon-platform-eleme:before{content:"\\e7ca"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{transform:rotateZ(0)}100%{transform:rotateZ(360deg)}}@keyframes rotating{0%{transform:rotateZ(0)}100%{transform:rotateZ(360deg)}}.el-pagination{white-space:nowrap;padding:2px 5px;color:#303133;font-weight:700}.el-pagination::after,.el-pagination::before{display:table;content:""}.el-pagination::after{clear:both}.el-pagination button,.el-pagination span:not([class*=suffix]){display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;vertical-align:top;box-sizing:border-box}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield;line-height:normal}.el-pagination .el-input__suffix{right:0;transform:scale(.8)}.el-pagination .el-select .el-input{width:100px;margin:0 5px}.el-pagination .el-select .el-input .el-input__inner{padding-right:25px;border-radius:3px}.el-pagination button{border:none;padding:0 6px;background:0 0}.el-pagination button:focus{outline:0}.el-pagination button:hover{color:#409EFF}.el-pagination button:disabled{color:#C0C4CC;background-color:#FFF;cursor:not-allowed}.el-pagination .btn-next,.el-pagination .btn-prev{background:center center no-repeat #FFF;background-size:16px;cursor:pointer;margin:0;color:#303133}.el-pagination .btn-next .el-icon,.el-pagination .btn-prev .el-icon{display:block;font-size:12px;font-weight:700}.el-pagination .btn-prev{padding-right:12px}.el-pagination .btn-next{padding-left:12px}.el-pagination .el-pager li.disabled{color:#C0C4CC;cursor:not-allowed}.el-pager li,.el-pager li.btn-quicknext:hover,.el-pager li.btn-quickprev:hover{cursor:pointer}.el-pagination--small .btn-next,.el-pagination--small .btn-prev,.el-pagination--small .el-pager li,.el-pagination--small .el-pager li.btn-quicknext,.el-pagination--small .el-pager li.btn-quickprev,.el-pagination--small .el-pager li:last-child{border-color:transparent;font-size:12px;line-height:22px;height:22px;min-width:22px}.el-pagination--small .more::before,.el-pagination--small li.more::before{line-height:24px}.el-pagination--small button,.el-pagination--small span:not([class*=suffix]){height:22px;line-height:22px}.el-pagination--small .el-pagination__editor,.el-pagination--small .el-pagination__editor.el-input .el-input__inner{height:22px}.el-pagination__sizes{margin:0 10px 0 0;font-weight:400;color:#606266}.el-pagination__sizes .el-input .el-input__inner{font-size:13px;padding-left:8px}.el-pagination__sizes .el-input .el-input__inner:hover{border-color:#409EFF}.el-pagination__total{margin-right:10px;font-weight:400;color:#606266}.el-pagination__jump{margin-left:24px;font-weight:400;color:#606266}.el-pagination__jump .el-input__inner{padding:0 3px}.el-pagination__rightwrapper{float:right}.el-pagination__editor{line-height:18px;padding:0 2px;height:28px;text-align:center;margin:0 2px;box-sizing:border-box;border-radius:3px}.el-pager,.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev{padding:0}.el-pagination__editor.el-input{width:50px}.el-pagination__editor.el-input .el-input__inner{height:28px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button,.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev,.el-pagination.is-background .el-pager li{margin:0 5px;background-color:#f4f4f5;color:#606266;min-width:30px;border-radius:2px}.el-pagination.is-background .btn-next.disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-prev.disabled,.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .el-pager li.disabled{color:#C0C4CC}.el-pagination.is-background .el-pager li:not(.disabled):hover{color:#409EFF}.el-pagination.is-background .el-pager li:not(.disabled).active{background-color:#409EFF;color:#FFF}.el-dialog,.el-pager li{background:#FFF;-webkit-box-sizing:border-box}.el-pagination.is-background.el-pagination--small .btn-next,.el-pagination.is-background.el-pagination--small .btn-prev,.el-pagination.is-background.el-pagination--small .el-pager li{margin:0 3px;min-width:22px}.el-pager,.el-pager li{vertical-align:top;margin:0;display:inline-block}.el-pager{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;list-style:none;font-size:0}.el-date-table,.el-table th{-webkit-user-select:none;-moz-user-select:none}.el-pager .more::before{line-height:30px}.el-pager li{padding:0 4px;font-size:13px;min-width:35.5px;height:28px;line-height:28px;box-sizing:border-box;text-align:center}.el-menu--collapse .el-menu .el-submenu,.el-menu--popup{min-width:200px}.el-pager li.btn-quicknext,.el-pager li.btn-quickprev{line-height:28px;color:#303133}.el-pager li.btn-quicknext.disabled,.el-pager li.btn-quickprev.disabled{color:#C0C4CC}.el-pager li.active+li{border-left:0}.el-pager li:hover{color:#409EFF}.el-pager li.active{color:#409EFF;cursor:default}@-webkit-keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}.el-dialog{position:relative;margin:0 auto 50px;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.3);box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#409EFF}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px;word-break:break-all}.el-dialog__footer{padding:10px 20px 20px;text-align:right;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}.el-autocomplete{position:relative;display:inline-block}.el-autocomplete-suggestion{margin:5px 0;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;border:1px solid #E4E7ED;box-sizing:border-box;background-color:#FFF}.el-dropdown-menu,.el-menu--collapse .el-submenu .el-menu{z-index:10;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-autocomplete-suggestion__wrap{max-height:280px;padding:10px 0;box-sizing:border-box}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{padding:0 20px;margin:0;line-height:34px;cursor:pointer;color:#606266;font-size:14px;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-autocomplete-suggestion li.highlighted,.el-autocomplete-suggestion li:hover{background-color:#F5F7FA}.el-autocomplete-suggestion li.divider{margin-top:6px;border-top:1px solid #000}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{text-align:center;height:100px;line-height:100px;font-size:20px;color:#999}.el-autocomplete-suggestion.is-loading li::after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-autocomplete-suggestion.is-loading li:hover{background-color:#FFF}.el-autocomplete-suggestion.is-loading .el-icon-loading{vertical-align:middle}.el-dropdown{display:inline-block;position:relative;color:#606266;font-size:14px}.el-dropdown .el-button-group{display:block}.el-dropdown .el-button-group .el-button{float:none}.el-dropdown .el-dropdown__caret-button{padding-left:5px;padding-right:5px;position:relative;border-left:none}.el-dropdown .el-dropdown__caret-button::before{content:\'\';position:absolute;display:block;width:1px;top:5px;bottom:5px;left:0;background:rgba(255,255,255,.5)}.el-dropdown .el-dropdown__caret-button.el-button--default::before{background:rgba(220,223,230,.5)}.el-dropdown .el-dropdown__caret-button:hover::before{top:0;bottom:0}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{padding-left:0}.el-dropdown__icon{font-size:12px;margin:0 3px}.el-dropdown-menu{position:absolute;top:0;left:0;padding:10px 0;margin:5px 0;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-dropdown-menu__item{list-style:none;line-height:36px;padding:0 20px;margin:0;font-size:14px;color:#606266;cursor:pointer;outline:0}.el-dropdown-menu__item:focus,.el-dropdown-menu__item:not(.is-disabled):hover{background-color:#ecf5ff;color:#66b1ff}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{position:relative;margin-top:6px;border-top:1px solid #EBEEF5}.el-dropdown-menu__item--divided:before{content:\'\';height:6px;display:block;margin:0 -20px;background-color:#FFF}.el-dropdown-menu__item.is-disabled{cursor:default;color:#bbb;pointer-events:none}.el-dropdown-menu--medium{padding:6px 0}.el-dropdown-menu--medium .el-dropdown-menu__item{line-height:30px;padding:0 17px;font-size:14px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:6px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:6px;margin:0 -17px}.el-dropdown-menu--small{padding:6px 0}.el-dropdown-menu--small .el-dropdown-menu__item{line-height:27px;padding:0 15px;font-size:13px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:4px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:4px;margin:0 -15px}.el-dropdown-menu--mini{padding:3px 0}.el-dropdown-menu--mini .el-dropdown-menu__item{line-height:24px;padding:0 10px;font-size:12px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:3px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:3px;margin:0 -10px}.el-menu{border-right:solid 1px #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0;background-color:#FFF}.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu::after,.el-menu::before{display:table;content:""}.el-menu::after{clear:both}.el-menu.el-menu--horizontal{border-bottom:solid 1px #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409EFF;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#FFF;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409EFF;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-submenu{position:relative}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;border:1px solid #E4E7ED;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;position:relative;-webkit-box-sizing:border-box;white-space:nowrap;list-style:none}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:none}.el-menu--popup{z-index:100;border:none;padding:5px 0;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409EFF}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;transition:transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409EFF}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:rotateZ(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.el-radio-button__inner,.el-radio-group{display:inline-block;line-height:1;vertical-align:middle}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{transition:.2s;opacity:0}.el-radio-group{font-size:0}.el-radio-button{position:relative;display:inline-block;outline:0}.el-radio-button__inner{white-space:nowrap;background:#FFF;border:1px solid #DCDFE6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;position:relative;cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#409EFF}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;box-shadow:-1px 0 0 0 #409EFF}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#F2F6FC}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-popover,.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active):not(.is-disabled){box-shadow:0 0 2px 2px #409EFF}.el-switch{display:inline-flex;align-items:center;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.el-switch__core,.el-switch__label{display:inline-block;cursor:pointer}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{transition:.2s;height:20px;font-size:14px;font-weight:500;vertical-align:middle;color:#303133}.el-switch__label.is-active{color:#409EFF}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{line-height:1;font-size:14px;display:inline-block}.el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.el-switch__core{margin:0;position:relative;width:40px;height:20px;border:1px solid #DCDFE6;outline:0;border-radius:10px;box-sizing:border-box;background:#DCDFE6;transition:border-color .3s,background-color .3s;vertical-align:middle}.el-switch__core:after{content:"";position:absolute;top:1px;left:1px;border-radius:100%;transition:all .3s;width:16px;height:16px;background-color:#FFF}.el-switch.is-checked .el-switch__core{border-color:#409EFF;background-color:#409EFF}.el-switch.is-checked .el-switch__core::after{left:100%;margin-left:-17px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter,.el-switch .label-fade-leave-active{opacity:0}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #E4E7ED;border-radius:4px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#409EFF;background-color:#FFF}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#F5F7FA}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\\e6da";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;box-sizing:border-box}.el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#FFF}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#F5F7FA}.el-select-dropdown__item.selected{color:#409EFF;font-weight:700}.el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type)::after{content:\'\';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#E4E7ED}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{display:inline-block;position:relative}.el-select .el-select__tags>span{display:contents}.el-select:hover .el-input__inner{border-color:#C0C4CC}.el-select .el-input__inner{cursor:pointer;padding-right:35px}.el-select .el-input__inner:focus{border-color:#409EFF}.el-select .el-input .el-select__caret{color:#C0C4CC;font-size:14px;transition:transform .3s;transform:rotateZ(180deg);cursor:pointer}.el-select .el-input .el-select__caret.is-reverse{transform:rotateZ(0)}.el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;transform:rotateZ(180deg);border-radius:100%;color:#C0C4CC;transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-select .el-input .el-select__caret.is-show-close:hover{color:#909399}.el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.el-select .el-input.is-disabled .el-input__inner:hover{border-color:#E4E7ED}.el-select .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-select>.el-input{display:block}.el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.el-select__input.is-mini{height:14px}.el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#C0C4CC;line-height:18px;font-size:14px}.el-select__close:hover{color:#909399}.el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;transform:translateY(-50%);display:flex;align-items:center;flex-wrap:wrap}.el-select .el-tag__close{margin-top:-2px}.el-select .el-tag{box-sizing:border-box;border-color:transparent;margin:2px 0 2px 6px;background-color:#f0f2f5}.el-select .el-tag__close.el-icon-close{background-color:#C0C4CC;right:-7px;top:0;color:#FFF}.el-select .el-tag__close.el-icon-close:hover{background-color:#909399}.el-table,.el-table__expanded-cell{background-color:#FFF}.el-select .el-tag__close.el-icon-close::before{display:block;transform:translate(0,.5px)}.el-table{position:relative;overflow:hidden;box-sizing:border-box;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table--mini,.el-table--small,.el-table__expand-icon{font-size:12px}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:flex;justify-content:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;transition:transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit td.gutter,.el-table--fit th.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th{background:#F5F7FA}.el-table th,.el-table tr{background-color:#FFF}.el-table td,.el-table th{padding:12px 0;min-width:0;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table td.is-center,.el-table th.is-center{text-align:center}.el-table td.is-right,.el-table th.is-right{text-align:right}.el-table td.gutter,.el-table th.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table--medium td,.el-table--medium th{padding:10px 0}.el-table--small td,.el-table--small th{padding:8px 0}.el-table--mini td,.el-table--mini th{padding:6px 0}.el-table .cell,.el-table--border td:first-child .cell,.el-table--border th:first-child .cell{padding-left:10px}.el-table tr input[type=checkbox]{margin:0}.el-table td,.el-table th.is-leaf{border-bottom:1px solid #EBEEF5}.el-table th.is-sortable{cursor:pointer}.el-table th{overflow:hidden;-ms-user-select:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-table th>.cell{display:inline-block;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th>.cell.highlight{color:#409EFF}.el-table th.required>div::before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td div{box-sizing:border-box}.el-table td.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #EBEEF5}.el-table--border::after,.el-table--group::after,.el-table::before{content:\'\';position:absolute;background-color:#EBEEF5;z-index:1}.el-table--border::after,.el-table--group::after{top:0;right:0;width:1px;height:100%}.el-table::before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border td,.el-table--border th,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #EBEEF5}.el-table--border th.gutter:last-of-type{border-bottom:1px solid #EBEEF5;border-bottom-width:1px}.el-table--border th,.el-table__fixed-right-patch{border-bottom:1px solid #EBEEF5}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right::before,.el-table__fixed::before{content:\'\';position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#EBEEF5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#FFF}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td{border-top:1px solid #EBEEF5;background-color:#F5F7FA;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td{border-top:1px solid #EBEEF5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td,.el-table__header-wrapper tbody td{background-color:#F5F7FA;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{box-shadow:none}.el-picker-panel,.el-table-filter{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #EBEEF5}.el-table .caret-wrapper{display:inline-flex;flex-direction:column;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#C0C4CC;top:5px}.el-table .sort-caret.descending{border-top-color:#C0C4CC;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409EFF}.el-table .descending .sort-caret.descending{border-top-color:#409EFF}.el-table .hidden-columns{position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td{background:#FAFAFA}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td,.el-table__body tr.hover-row.el-table__row--striped.current-row>td,.el-table__body tr.hover-row.el-table__row--striped>td,.el-table__body tr.hover-row>td{background-color:#F5F7FA}.el-table__body tr.current-row>td{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #EBEEF5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;transform:scale(.75)}.el-table--enable-row-transition .el-table__body td{transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#F5F7FA}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #EBEEF5;border-radius:2px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-date-table td,.el-date-table td div{height:30px;-webkit-box-sizing:border-box}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409EFF;color:#FFF}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #EBEEF5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-date-table td.in-range div,.el-date-table td.in-range div:hover,.el-date-table.is-week-mode .el-date-table__row.current div,.el-date-table.is-week-mode .el-date-table__row:hover div{background-color:#F2F6FC}.el-table-filter__bottom button:hover{color:#409EFF}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-date-table{font-size:12px;-ms-user-select:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:#606266}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td{width:32px;padding:4px 0;box-sizing:border-box;text-align:center;cursor:pointer;position:relative}.el-date-table td div{padding:3px 0;box-sizing:border-box}.el-date-table td span{width:24px;height:24px;display:block;margin:0 auto;line-height:24px;position:absolute;left:50%;transform:translateX(-50%);border-radius:50%}.el-date-table td.next-month,.el-date-table td.prev-month{color:#C0C4CC}.el-date-table td.today{position:relative}.el-date-table td.today span{color:#409EFF;font-weight:700}.el-date-table td.today.end-date span,.el-date-table td.today.start-date span{color:#FFF}.el-date-table td.available:hover{color:#409EFF}.el-date-table td.current:not(.disabled) span{color:#FFF;background-color:#409EFF}.el-date-table td.end-date div,.el-date-table td.start-date div{color:#FFF}.el-date-table td.end-date span,.el-date-table td.start-date span{background-color:#409EFF}.el-date-table td.start-date div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table td.end-date div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td.disabled div{background-color:#F5F7FA;opacity:1;cursor:not-allowed;color:#C0C4CC}.el-date-table td.selected div{margin-left:5px;margin-right:5px;background-color:#F2F6FC;border-radius:15px}.el-date-table td.selected div:hover{background-color:#F2F6FC}.el-date-table td.selected span{background-color:#409EFF;color:#FFF;border-radius:15px}.el-date-table td.week{font-size:80%;color:#606266}.el-month-table,.el-year-table{font-size:12px;border-collapse:collapse}.el-date-table th{padding:5px;color:#606266;font-weight:400;border-bottom:solid 1px #EBEEF5}.el-month-table{margin:-1px}.el-month-table td{text-align:center;padding:8px 0;cursor:pointer}.el-month-table td div{height:48px;padding:6px 0;box-sizing:border-box}.el-month-table td.today .cell{color:#409EFF;font-weight:700}.el-month-table td.today.end-date .cell,.el-month-table td.today.start-date .cell{color:#FFF}.el-month-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-month-table td.disabled .cell:hover{color:#C0C4CC}.el-month-table td .cell{width:60px;height:36px;display:block;line-height:36px;color:#606266;margin:0 auto;border-radius:18px}.el-month-table td .cell:hover{color:#409EFF}.el-month-table td.in-range div,.el-month-table td.in-range div:hover{background-color:#F2F6FC}.el-month-table td.end-date div,.el-month-table td.start-date div{color:#FFF}.el-month-table td.end-date .cell,.el-month-table td.start-date .cell{color:#FFF;background-color:#409EFF}.el-month-table td.start-date div{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-month-table td.end-date div{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-month-table td.current:not(.disabled) .cell{color:#409EFF}.el-year-table{margin:-1px}.el-year-table .el-icon{color:#303133}.el-year-table td{text-align:center;padding:20px 3px;cursor:pointer}.el-year-table td.today .cell{color:#409EFF;font-weight:700}.el-year-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-year-table td.disabled .cell:hover{color:#C0C4CC}.el-year-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#606266;margin:0 auto}.el-year-table td .cell:hover,.el-year-table td.current:not(.disabled) .cell{color:#409EFF}.el-date-range-picker{width:646px}.el-date-range-picker.has-sidebar{width:756px}.el-date-range-picker table{table-layout:fixed;width:100%}.el-date-range-picker .el-picker-panel__body{min-width:513px}.el-date-range-picker .el-picker-panel__content{margin:0}.el-date-range-picker__header{position:relative;text-align:center;height:28px}.el-date-range-picker__header [class*=arrow-left]{float:left}.el-date-range-picker__header [class*=arrow-right]{float:right}.el-date-range-picker__header div{font-size:16px;font-weight:500;margin-right:50px}.el-date-range-picker__content{float:left;width:50%;box-sizing:border-box;margin:0;padding:16px}.el-date-range-picker__content.is-left{border-right:1px solid #e4e4e4}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;box-sizing:border-box}.el-date-range-picker__time-header>.el-icon-arrow-right{font-size:20px;vertical-align:middle;display:table-cell;color:#303133}.el-date-range-picker__time-picker-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-range-picker__time-picker-wrap .el-picker-panel{position:absolute;top:13px;right:0;z-index:1;background:#FFF}.el-date-picker{width:322px}.el-date-picker.has-sidebar.has-time{width:434px}.el-date-picker.has-sidebar{width:438px}.el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-picker .el-picker-panel__content{width:292px}.el-date-picker table{table-layout:fixed;width:100%}.el-date-picker__editor-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;box-sizing:border-box}.el-date-picker__header{margin:12px;text-align:center}.el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:solid 1px #EBEEF5}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#606266}.el-date-picker__header-label.active,.el-date-picker__header-label:hover{color:#409EFF}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{padding:10px;text-align:center}.el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.time-select{margin:5px 0;min-width:0}.time-select .el-picker-panel__content{max-height:200px;margin:0}.time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.time-select-item.selected:not(.disabled){color:#409EFF;font-weight:700}.time-select-item.disabled{color:#E4E7ED;cursor:not-allowed}.time-select-item:hover{background-color:#F5F7FA;font-weight:700;cursor:pointer}.el-date-editor{position:relative;display:inline-block;text-align:left}.el-date-editor.el-input,.el-date-editor.el-input__inner{width:220px}.el-date-editor--monthrange.el-input,.el-date-editor--monthrange.el-input__inner{width:300px}.el-date-editor--daterange.el-input,.el-date-editor--daterange.el-input__inner,.el-date-editor--timerange.el-input,.el-date-editor--timerange.el-input__inner{width:350px}.el-date-editor--datetimerange.el-input,.el-date-editor--datetimerange.el-input__inner{width:400px}.el-date-editor--dates .el-input__inner{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .el-icon-circle-close{cursor:pointer}.el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#C0C4CC;float:left;line-height:32px}.el-date-editor .el-range-input,.el-date-editor .el-range-separator{height:100%;margin:0;text-align:center;display:inline-block;font-size:14px}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;padding:0;width:39%;color:#606266}.el-date-editor .el-range-input:-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::-moz-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::placeholder{color:#C0C4CC}.el-date-editor .el-range-separator{padding:0 5px;line-height:32px;width:5%;color:#303133}.el-date-editor .el-range__close-icon{font-size:14px;color:#C0C4CC;width:25px;display:inline-block;float:right;line-height:32px}.el-range-editor.el-input__inner{display:inline-flex;align-items:center;padding:3px 10px}.el-range-editor .el-range-input{line-height:1}.el-range-editor.is-active,.el-range-editor.is-active:hover{border-color:#409EFF}.el-range-editor--medium.el-input__inner{height:36px}.el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.el-range-editor--medium .el-range-input{font-size:14px}.el-range-editor--medium .el-range__close-icon,.el-range-editor--medium .el-range__icon{line-height:28px}.el-range-editor--small.el-input__inner{height:32px}.el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.el-range-editor--small .el-range-input{font-size:13px}.el-range-editor--small .el-range__close-icon,.el-range-editor--small .el-range__icon{line-height:24px}.el-range-editor--mini.el-input__inner{height:28px}.el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.el-range-editor--mini .el-range-input{font-size:12px}.el-range-editor--mini .el-range__close-icon,.el-range-editor--mini .el-range__icon{line-height:20px}.el-range-editor.is-disabled{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled:focus,.el-range-editor.is-disabled:hover{border-color:#E4E7ED}.el-range-editor.is-disabled input{background-color:#F5F7FA;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled input:-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::-moz-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::placeholder{color:#C0C4CC}.el-range-editor.is-disabled .el-range-separator{color:#C0C4CC}.el-picker-panel{color:#606266;border:1px solid #E4E7ED;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#FFF;border-radius:4px;line-height:30px;margin:5px 0}.el-popover,.el-time-panel{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-picker-panel__body-wrapper::after,.el-picker-panel__body::after{content:"";display:table;clear:both}.el-picker-panel__content{position:relative;margin:15px}.el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#FFF;position:relative;font-size:0}.el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#606266;padding-left:12px;text-align:left;outline:0;cursor:pointer}.el-picker-panel__shortcut:hover{color:#409EFF}.el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#409EFF}.el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.el-picker-panel__icon-btn{font-size:12px;color:#303133;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.el-picker-panel__icon-btn:hover{color:#409EFF}.el-picker-panel__icon-btn.is-disabled{color:#bbb}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;box-sizing:border-box;padding-top:6px;background-color:#FFF;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__input.el-input .el-input__inner,.el-time-spinner__list{padding:0;text-align:center}.el-time-spinner__wrapper.is-arrow{box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#FFF;cursor:default}.el-time-spinner__arrow{font-size:12px;color:#909399;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.el-time-spinner__arrow:hover{color:#409EFF}.el-time-spinner__arrow.el-icon-arrow-up{top:10px}.el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__list{margin:0;list-style:none}.el-time-spinner__list::after,.el-time-spinner__list::before{content:\'\';display:block;width:100%;height:80px}.el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#606266}.el-time-spinner__item:hover:not(.disabled):not(.active){background:#F5F7FA;cursor:pointer}.el-time-spinner__item.active:not(.disabled){color:#303133;font-weight:700}.el-time-spinner__item.disabled{color:#C0C4CC;cursor:not-allowed}.el-time-panel{margin:5px 0;border:1px solid #E4E7ED;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;box-sizing:content-box}.el-slider__button,.el-slider__button-wrapper{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content::after,.el-time-panel__content::before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #E4E7ED;border-bottom:1px solid #E4E7ED}.el-time-panel__content::after{left:50%;margin-left:12%;margin-right:12%}.el-time-panel__content::before{padding-left:50%;margin-right:12%;margin-left:12%}.el-time-panel__content.has-seconds::after{left:calc(100% / 3 * 2)}.el-time-panel__content.has-seconds::before{padding-left:calc(100% / 3)}.el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;box-sizing:border-box}.el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#303133}.el-time-panel__btn.confirm{font-weight:800;color:#409EFF}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{position:relative;text-align:center;padding:10px}.el-time-range-picker__cell{box-sizing:border-box;margin:0;padding:4px 7px 7px;width:50%;display:inline-block}.el-time-range-picker__header{margin-bottom:5px;text-align:center;font-size:14px}.el-time-range-picker__body{border-radius:2px;border:1px solid #E4E7ED}.el-popover{position:absolute;background:#FFF;min-width:150px;border:1px solid #EBEEF5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#FFF;border-radius:4px;border:1px solid #EBEEF5;font-size:18px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;overflow:hidden;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper::after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus,.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#F56C6C}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409EFF}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__status{position:absolute;top:50%;transform:translateY(-50%);font-size:24px!important}.el-message-box__status::before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67C23A}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#E6A23C}.el-message-box__status.el-icon-error{color:#F56C6C}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#F56C6C;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:flex;align-items:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@keyframes msgbox-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#C0C4CC}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#409EFF;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{display:inline-block;margin-right:10px;vertical-align:top}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item::after{clear:both}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__content::after{clear:both}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#F56C6C;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:\'*\';color:#F56C6C;margin-right:4px}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#F56C6C}.el-form-item--feedback .el-input__validateIcon{display:inline-block}.el-tabs__header{padding:0;position:relative;margin:0 0 15px}.el-tabs__active-bar{position:absolute;bottom:0;left:0;height:2px;background-color:#409EFF;z-index:1;transition:transform .3s cubic-bezier(.645,.045,.355,1);list-style:none}.el-tabs__new-tab{float:right;border:1px solid #d3dce6;height:18px;width:18px;line-height:18px;margin:12px 0 9px 10px;border-radius:3px;text-align:center;font-size:12px;color:#d3dce6;cursor:pointer;transition:all .15s}.el-collapse-item__arrow,.el-tabs__nav{-webkit-transition:-webkit-transform .3s}.el-tabs__new-tab .el-icon-plus{transform:scale(.8,.8)}.el-tabs__new-tab:hover{color:#409EFF}.el-tabs__nav-wrap{overflow:hidden;margin-bottom:-1px;position:relative}.el-tabs__nav-wrap::after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background-color:#E4E7ED;z-index:1}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap::after,.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap::after{content:none}.el-tabs__nav-wrap.is-scrollable{padding:0 20px;box-sizing:border-box}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{position:absolute;cursor:pointer;line-height:44px;font-size:12px;color:#909399}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;position:relative;transition:transform .3s;float:left;z-index:2}.el-tabs__nav.is-stretch{min-width:100%;display:flex}.el-tabs__nav.is-stretch>*{flex:1;text-align:center}.el-tabs__item{padding:0 20px;height:40px;box-sizing:border-box;line-height:40px;display:inline-block;list-style:none;font-size:14px;font-weight:500;color:#303133;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:0}.el-tabs__item:focus.is-active.is-focus:not(:active){box-shadow:0 0 2px 2px #409EFF inset;border-radius:3px}.el-tabs__item .el-icon-close{border-radius:50%;text-align:center;transition:all .3s cubic-bezier(.645,.045,.355,1);margin-left:5px}.el-tabs__item .el-icon-close:before{transform:scale(.9);display:inline-block}.el-tabs__item .el-icon-close:hover{background-color:#C0C4CC;color:#FFF}.el-tabs__item.is-active{color:#409EFF}.el-tabs__item:hover{color:#409EFF;cursor:pointer}.el-tabs__item.is-disabled{color:#C0C4CC;cursor:default}.el-tabs__content{overflow:hidden;position:relative}.el-tabs--card>.el-tabs__header{border-bottom:1px solid #E4E7ED}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid #E4E7ED;border-bottom:none;border-radius:4px 4px 0 0;box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__active-bar{display:none}.el-tabs--card>.el-tabs__header .el-tabs__item .el-icon-close{position:relative;font-size:12px;width:0;height:14px;vertical-align:middle;line-height:15px;overflow:hidden;top:-1px;right:-2px;transform-origin:100% 50%}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .el-icon-close,.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .el-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid #E4E7ED;transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:#FFF}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--border-card{background:#FFF;border:1px solid #DCDFE6;box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:#F5F7FA;border-bottom:1px solid #E4E7ED;margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__item{transition:all .3s cubic-bezier(.645,.045,.355,1);border:1px solid transparent;margin-top:-1px;color:#909399}.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item,.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child{margin-left:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:#409EFF;background-color:#FFF;border-right-color:#DCDFE6;border-left-color:#DCDFE6}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:#409EFF}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:#C0C4CC}.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2),.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),.el-tabs--top .el-tabs__item.is-top:nth-child(2){padding-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:last-child,.el-tabs--bottom .el-tabs__item.is-top:last-child,.el-tabs--top .el-tabs__item.is-bottom:last-child,.el-tabs--top .el-tabs__item.is-top:last-child{padding-right:0}.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--bottom .el-tabs__header.is-bottom{margin-bottom:0;margin-top:10px}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid #DCDFE6}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-top:-1px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid transparent}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-scroll,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{top:0;bottom:auto;width:2px;height:auto}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{height:30px;line-height:30px;width:100%;text-align:center;cursor:pointer}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i{transform:rotateZ(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{left:auto;top:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{right:auto;bottom:0}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__nav-wrap.is-left::after{right:0;left:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left::after,.el-tabs--left .el-tabs__nav-wrap.is-right::after,.el-tabs--right .el-tabs__nav-wrap.is-left::after,.el-tabs--right .el-tabs__nav-wrap.is-right::after{height:100%;width:2px;bottom:auto;top:0}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{float:none}.el-tabs--left .el-tabs__item.is-left,.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-right{display:block}.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left,.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs--left .el-tabs__header.is-left{float:left;margin-bottom:0;margin-right:10px}.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid #E4E7ED;border-bottom:none;border-top:1px solid #E4E7ED;text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid #E4E7ED;border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid #E4E7ED;border-right-color:#fff;border-left:none;border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-radius:4px 0 0 4px;border-bottom:1px solid #E4E7ED;border-right:none}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid #dfe4ed}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid transparent;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 transparent}.el-tabs--right .el-tabs__header.is-right{float:right;margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right::after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid #E4E7ED}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid #E4E7ED;border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid #E4E7ED;border-left-color:#fff;border-right:none;border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-radius:0 4px 4px 0;border-bottom:1px solid #E4E7ED;border-left:none}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid #dfe4ed}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid transparent;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 transparent}.slideInLeft-transition,.slideInRight-transition{display:inline-block}.slideInRight-enter{-webkit-animation:slideInRight-enter .3s;animation:slideInRight-enter .3s}.slideInRight-leave{position:absolute;left:0;right:0;-webkit-animation:slideInRight-leave .3s;animation:slideInRight-leave .3s}.slideInLeft-enter{-webkit-animation:slideInLeft-enter .3s;animation:slideInLeft-enter .3s}.slideInLeft-leave{position:absolute;left:0;right:0;-webkit-animation:slideInLeft-leave .3s;animation:slideInLeft-leave .3s}@-webkit-keyframes slideInRight-enter{0%{opacity:0;transform-origin:0 0;transform:translateX(100%)}to{opacity:1;transform-origin:0 0;transform:translateX(0)}}@keyframes slideInRight-enter{0%{opacity:0;transform-origin:0 0;transform:translateX(100%)}to{opacity:1;transform-origin:0 0;transform:translateX(0)}}@-webkit-keyframes slideInRight-leave{0%{transform-origin:0 0;transform:translateX(0);opacity:1}100%{transform-origin:0 0;transform:translateX(100%);opacity:0}}@keyframes slideInRight-leave{0%{transform-origin:0 0;transform:translateX(0);opacity:1}100%{transform-origin:0 0;transform:translateX(100%);opacity:0}}@-webkit-keyframes slideInLeft-enter{0%{opacity:0;transform-origin:0 0;transform:translateX(-100%)}to{opacity:1;transform-origin:0 0;transform:translateX(0)}}@keyframes slideInLeft-enter{0%{opacity:0;transform-origin:0 0;transform:translateX(-100%)}to{opacity:1;transform-origin:0 0;transform:translateX(0)}}@-webkit-keyframes slideInLeft-leave{0%{transform-origin:0 0;transform:translateX(0);opacity:1}100%{transform-origin:0 0;transform:translateX(-100%);opacity:0}}@keyframes slideInLeft-leave{0%{transform-origin:0 0;transform:translateX(0);opacity:1}100%{transform-origin:0 0;transform:translateX(-100%);opacity:0}}.el-tree{position:relative;cursor:default;background:#FFF;color:#606266}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);color:#909399;font-size:14px}.el-tree__drop-indicator{position:absolute;left:0;right:0;height:1px;background-color:#409EFF}.el-tree-node{white-space:nowrap;outline:0}.el-tree-node:focus>.el-tree-node__content{background-color:#F5F7FA}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:#409EFF;color:#fff}.el-tree-node__content{display:flex;align-items:center;height:26px;cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree-node__content:hover{background-color:#F5F7FA}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:#C0C4CC;font-size:12px;transform:rotate(0);transition:transform .3s ease-in-out}.el-tree-node__expand-icon.expanded{transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__label{font-size:14px}.el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#C0C4CC}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#f0f7ff}.el-alert{width:100%;padding:8px 16px;margin:0;box-sizing:border-box;border-radius:4px;position:relative;background-color:#FFF;overflow:hidden;opacity:1;display:flex;align-items:center;transition:opacity .2s}.el-alert.is-light .el-alert__closebtn{color:#C0C4CC}.el-alert.is-dark .el-alert__closebtn,.el-alert.is-dark .el-alert__description{color:#FFF}.el-alert.is-center{justify-content:center}.el-alert--success.is-light{background-color:#f0f9eb;color:#67C23A}.el-alert--success.is-light .el-alert__description{color:#67C23A}.el-alert--success.is-dark{background-color:#67C23A;color:#FFF}.el-alert--info.is-light{background-color:#f4f4f5;color:#909399}.el-alert--info.is-dark{background-color:#909399;color:#FFF}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning.is-light{background-color:#fdf6ec;color:#E6A23C}.el-alert--warning.is-light .el-alert__description{color:#E6A23C}.el-alert--warning.is-dark{background-color:#E6A23C;color:#FFF}.el-alert--error.is-light{background-color:#fef0f0;color:#F56C6C}.el-alert--error.is-light .el-alert__description{color:#F56C6C}.el-alert--error.is-dark{background-color:#F56C6C;color:#FFF}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert-fade-enter,.el-alert-fade-leave-active,.el-loading-fade-enter,.el-loading-fade-leave-active,.el-notification-fade-leave-active{opacity:0}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-notification{display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;box-sizing:border-box;border:1px solid #EBEEF5;position:fixed;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{margin-left:13px;margin-right:8px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67C23A}.el-notification .el-icon-error{color:#F56C6C}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#E6A23C}.el-notification-fade-enter.right{right:0;transform:translateX(100%)}.el-notification-fade-enter.left{left:0;transform:translateX(-100%)}.el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.el-input-number .el-input{display:block}.el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.el-input-number__decrease,.el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#F5F7FA;color:#606266;cursor:pointer;font-size:13px}.el-input-number__decrease:hover,.el-input-number__increase:hover{color:#409EFF}.el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#409EFF}.el-input-number__decrease.is-disabled,.el-input-number__increase.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #DCDFE6}.el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #DCDFE6}.el-input-number.is-disabled .el-input-number__decrease,.el-input-number.is-disabled .el-input-number__increase{border-color:#E4E7ED;color:#E4E7ED}.el-input-number.is-disabled .el-input-number__decrease:hover,.el-input-number.is-disabled .el-input-number__increase:hover{color:#E4E7ED;cursor:not-allowed}.el-input-number--medium{width:200px;line-height:34px}.el-input-number--medium .el-input-number__decrease,.el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.el-input-number--small{width:130px;line-height:30px}.el-input-number--small .el-input-number__decrease,.el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.el-input-number--small .el-input-number__decrease [class*=el-icon],.el-input-number--small .el-input-number__increase [class*=el-icon]{transform:scale(.9)}.el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.el-input-number--mini{width:130px;line-height:26px}.el-input-number--mini .el-input-number__decrease,.el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.el-input-number--mini .el-input-number__decrease [class*=el-icon],.el-input-number--mini .el-input-number__increase [class*=el-icon]{transform:scale(.8)}.el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.el-input-number.is-controls-right .el-input-number__decrease,.el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #DCDFE6}.el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #DCDFE6;border-radius:0 0 4px}.el-input-number.is-controls-right[class*=medium] [class*=decrease],.el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.el-input-number.is-controls-right[class*=small] [class*=decrease],.el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.el-input-number.is-controls-right[class*=mini] [class*=decrease],.el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.el-progress-bar__inner::after,.el-row::after,.el-row::before,.el-slider::after,.el-slider::before,.el-slider__button-wrapper::after,.el-upload-cover::after{content:""}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#FFF}.el-tooltip__popper.is-light{background:#FFF;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#FFF}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#FFF}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#FFF}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#FFF}.el-slider::after,.el-slider::before{display:table}.el-slider__button-wrapper .el-tooltip,.el-slider__button-wrapper::after{vertical-align:middle;display:inline-block}.el-slider::after{clear:both}.el-slider__runway{width:100%;height:6px;margin:16px 0;background-color:#E4E7ED;border-radius:3px;position:relative;cursor:pointer;vertical-align:middle}.el-slider__runway.show-input{margin-right:160px;width:auto}.el-slider__runway.disabled{cursor:default}.el-slider__runway.disabled .el-slider__bar{background-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button{border-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button-wrapper.dragging,.el-slider__runway.disabled .el-slider__button-wrapper.hover,.el-slider__runway.disabled .el-slider__button-wrapper:hover{cursor:not-allowed}.el-slider__runway.disabled .el-slider__button.dragging,.el-slider__runway.disabled .el-slider__button.hover,.el-slider__runway.disabled .el-slider__button:hover{transform:scale(1);cursor:not-allowed}.el-slider__button-wrapper,.el-slider__stop{-webkit-transform:translateX(-50%);position:absolute}.el-slider__input{float:right;margin-top:3px;width:130px}.el-slider__input.el-input-number--mini{margin-top:5px}.el-slider__input.el-input-number--medium{margin-top:0}.el-slider__input.el-input-number--large{margin-top:-2px}.el-slider__bar{height:6px;background-color:#409EFF;border-top-left-radius:3px;border-bottom-left-radius:3px;position:absolute}.el-slider__button-wrapper{height:36px;width:36px;z-index:1001;top:-15px;transform:translateX(-50%);background-color:transparent;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:normal}.el-slider__button-wrapper::after{height:100%}.el-slider__button-wrapper.hover,.el-slider__button-wrapper:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button-wrapper.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__button{width:16px;height:16px;border:2px solid #409EFF;background-color:#FFF;border-radius:50%;transition:.2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-image-viewer__btn,.el-step__icon-inner{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-slider__button.dragging,.el-slider__button.hover,.el-slider__button:hover{transform:scale(1.2)}.el-slider__button.hover,.el-slider__button:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__stop{height:6px;width:6px;border-radius:100%;background-color:#FFF;transform:translateX(-50%)}.el-slider__marks{top:0;left:12px;width:18px;height:100%}.el-slider__marks-text{position:absolute;transform:translateX(-50%);font-size:14px;color:#909399;margin-top:15px}.el-slider.is-vertical{position:relative}.el-slider.is-vertical .el-slider__runway{width:6px;height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:6px;height:auto;border-radius:0 0 3px 3px}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:-15px;transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{transform:translateY(50%)}.el-slider.is-vertical.el-slider--with-input{padding-bottom:58px}.el-slider.is-vertical.el-slider--with-input .el-slider__input{overflow:visible;float:none;position:absolute;bottom:22px;width:36px;margin-top:15px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input__inner{text-align:center;padding-left:5px;padding-right:5px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{top:32px;margin-top:-1px;border:1px solid #DCDFE6;line-height:20px;box-sizing:border-box;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease{width:18px;right:18px;border-bottom-left-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{width:19px;border-bottom-right-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase~.el-input .el-input__inner{border-bottom-left-radius:0;border-bottom-right-radius:0}.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__increase{border-color:#C0C4CC}.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__increase{border-color:#409EFF}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;transform:translateY(50%)}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-col-pull-0,.el-col-pull-1,.el-col-pull-10,.el-col-pull-11,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-2,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-push-0,.el-col-push-1,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-2,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9,.el-row{position:relative}.el-loading-spinner .el-loading-text{color:#409EFF;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#409EFF;stroke-linecap:round}.el-loading-spinner i{color:#409EFF}@-webkit-keyframes loading-rotate{100%{transform:rotate(360deg)}}@keyframes loading-rotate{100%{transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.el-row{box-sizing:border-box}.el-row::after,.el-row::before{display:table}.el-row::after{clear:both}.el-row--flex{display:flex}.el-col-0,.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{justify-content:center}.el-row--flex.is-justify-end{justify-content:flex-end}.el-row--flex.is-justify-space-between{justify-content:space-between}.el-row--flex.is-justify-space-around{justify-content:space-around}.el-row--flex.is-align-middle{align-items:center}.el-row--flex.is-align-bottom{align-items:flex-end}[class*=el-col-]{float:left;box-sizing:border-box}.el-upload--picture-card,.el-upload-dragger{-webkit-box-sizing:border-box;cursor:pointer}.el-col-0{width:0%}.el-col-offset-0{margin-left:0}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{position:relative;right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0%}.el-col-xs-offset-0{margin-left:0}.el-col-xs-pull-0{position:relative;right:0}.el-col-xs-push-0{position:relative;left:0}.el-col-xs-1{width:4.16667%}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{width:8.33333%}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{width:12.5%}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{display:none;width:0%}.el-col-sm-offset-0{margin-left:0}.el-col-sm-pull-0{position:relative;right:0}.el-col-sm-push-0{position:relative;left:0}.el-col-sm-1{width:4.16667%}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{width:8.33333%}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{width:12.5%}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{display:none;width:0%}.el-col-md-offset-0{margin-left:0}.el-col-md-pull-0{position:relative;right:0}.el-col-md-push-0{position:relative;left:0}.el-col-md-1{width:4.16667%}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{width:8.33333%}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{width:12.5%}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{display:none;width:0%}.el-col-lg-offset-0{margin-left:0}.el-col-lg-pull-0{position:relative;right:0}.el-col-lg-push-0{position:relative;left:0}.el-col-lg-1{width:4.16667%}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{width:8.33333%}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{width:12.5%}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{display:none;width:0%}.el-col-xl-offset-0{margin-left:0}.el-col-xl-pull-0{position:relative;right:0}.el-col-xl-push-0{position:relative;left:0}.el-col-xl-1{width:4.16667%}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{width:8.33333%}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{width:12.5%}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-upload{display:inline-block;text-align:center;cursor:pointer;outline:0}.el-upload__input{display:none}.el-upload__tip{font-size:12px;color:#606266;margin-top:7px}.el-upload iframe{position:absolute;z-index:-1;top:0;left:0;opacity:0;filter:alpha(opacity=0)}.el-upload--picture-card{background-color:#fbfdff;border:1px dashed #c0ccda;border-radius:6px;box-sizing:border-box;width:148px;height:148px;line-height:146px;vertical-align:top}.el-upload--picture-card i{font-size:28px;color:#8c939d}.el-upload--picture-card:hover,.el-upload:focus{border-color:#409EFF;color:#409EFF}.el-upload:focus .el-upload-dragger{border-color:#409EFF}.el-upload-dragger{background-color:#fff;border:1px dashed #d9d9d9;border-radius:6px;box-sizing:border-box;width:360px;height:180px;text-align:center;position:relative;overflow:hidden}.el-upload-dragger .el-icon-upload{font-size:67px;color:#C0C4CC;margin:40px 0 16px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:1px solid #DCDFE6;margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:#606266;font-size:14px;text-align:center}.el-upload-dragger .el-upload__text em{color:#409EFF;font-style:normal}.el-upload-dragger:hover{border-color:#409EFF}.el-upload-dragger.is-dragover{background-color:rgba(32,159,255,.06);border:2px dashed #409EFF}.el-upload-list{margin:0;padding:0;list-style:none}.el-upload-list__item{transition:all .5s cubic-bezier(.55,0,.1,1);font-size:14px;color:#606266;line-height:1.8;margin-top:5px;position:relative;box-sizing:border-box;border-radius:4px;width:100%}.el-upload-list__item .el-progress{position:absolute;top:20px;width:100%}.el-upload-list__item .el-progress__text{position:absolute;right:0;top:-13px}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item:first-child{margin-top:10px}.el-upload-list__item .el-icon-upload-success{color:#67C23A}.el-upload-list__item .el-icon-close{display:none;position:absolute;top:5px;right:5px;cursor:pointer;opacity:.75;color:#606266}.el-upload-list__item .el-icon-close:hover{opacity:1}.el-upload-list__item .el-icon-close-tip{display:none;position:absolute;top:5px;right:5px;font-size:12px;cursor:pointer;opacity:1;color:#409EFF}.el-upload-list__item:hover{background-color:#F5F7FA}.el-upload-list__item:hover .el-icon-close{display:inline-block}.el-upload-list__item:hover .el-progress__text{display:none}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:block}.el-upload-list__item.is-success .el-upload-list__item-name:focus,.el-upload-list__item.is-success .el-upload-list__item-name:hover{color:#409EFF;cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon-close-tip{display:inline-block}.el-upload-list__item.is-success:active .el-icon-close-tip,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:not(.focusing):focus .el-icon-close-tip{display:none}.el-upload-list.is-disabled .el-upload-list__item:hover .el-upload-list__item-status-label{display:block}.el-upload-list__item-name{color:#606266;display:block;margin-right:40px;overflow:hidden;padding-left:4px;text-overflow:ellipsis;transition:color .3s;white-space:nowrap}.el-upload-list__item-name [class^=el-icon]{height:100%;margin-right:7px;color:#909399;line-height:inherit}.el-upload-list__item-status-label{position:absolute;right:5px;top:0;line-height:inherit;display:none}.el-upload-list__item-delete{position:absolute;right:10px;top:0;font-size:12px;color:#606266;display:none}.el-upload-list__item-delete:hover{color:#409EFF}.el-upload-list--picture-card{margin:0;display:inline;vertical-align:top}.el-upload-list--picture-card .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;box-sizing:border-box;width:148px;height:148px;margin:0 8px 8px 0;display:inline-block}.el-upload-list--picture-card .el-upload-list__item .el-icon-check,.el-upload-list--picture-card .el-upload-list__item .el-icon-circle-check{color:#FFF}.el-upload-list--picture-card .el-upload-list__item .el-icon-close,.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;transform:rotate(45deg);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-list--picture-card .el-upload-list__item-status-label i{font-size:12px;margin-top:11px;transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{position:absolute;width:100%;height:100%;left:0;top:0;cursor:default;text-align:center;color:#fff;opacity:0;font-size:20px;background-color:rgba(0,0,0,.5);transition:opacity .3s}.el-upload-list--picture-card .el-upload-list__item-actions::after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-upload-list--picture-card .el-upload-list__item-actions span{display:none;cursor:pointer}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:15px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{position:static;font-size:inherit;color:inherit}.el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-block}.el-upload-list--picture-card .el-progress{top:50%;left:50%;transform:translate(-50%,-50%);bottom:auto;width:126px}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{overflow:hidden;z-index:0;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;box-sizing:border-box;margin-top:10px;padding:10px 10px 10px 90px;height:92px}.el-upload-list--picture .el-upload-list__item .el-icon-check,.el-upload-list--picture .el-upload-list__item .el-icon-circle-check{color:#FFF}.el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{background:0 0;box-shadow:none;top:-2px;right:-12px}.el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name{line-height:70px;margin-top:0}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.el-upload-list--picture .el-upload-list__item-thumbnail{vertical-align:middle;display:inline-block;width:70px;height:70px;float:left;position:relative;z-index:1;margin-left:-80px;background-color:#FFF}.el-upload-list--picture .el-upload-list__item-name{display:block;margin-top:20px}.el-upload-list--picture .el-upload-list__item-name i{font-size:70px;line-height:1;position:absolute;left:9px;top:10px}.el-upload-list--picture .el-upload-list__item-status-label{position:absolute;right:-17px;top:-7px;width:46px;height:26px;background:#13ce66;text-align:center;transform:rotate(45deg);box-shadow:0 1px 1px #ccc}.el-upload-list--picture .el-upload-list__item-status-label i{font-size:12px;margin-top:12px;transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;z-index:10;cursor:default}.el-upload-cover::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-cover img{display:block;width:100%;height:100%}.el-upload-cover__label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;transform:rotate(45deg);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-cover__label i{font-size:12px;margin-top:11px;transform:rotate(-45deg);color:#fff}.el-upload-cover__progress{display:inline-block;vertical-align:middle;position:static;width:243px}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{position:absolute;top:0;left:0;width:100%;height:100%}.el-upload-cover__interact{position:absolute;bottom:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.72);text-align:center}.el-upload-cover__interact .btn{display:inline-block;color:#FFF;font-size:14px;cursor:pointer;vertical-align:middle;transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);margin-top:60px}.el-upload-cover__interact .btn span{opacity:0;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#FFF;display:block;font-size:24px;line-height:inherit;margin:0 auto 5px}.el-upload-cover__title{position:absolute;bottom:0;left:0;background-color:#FFF;height:36px;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;text-align:left;padding:0 10px;margin:0;line-height:36px;font-size:14px;color:#303133}.el-upload-cover+.el-upload__inner{opacity:0;position:relative;z-index:1}.el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle,.el-progress--dashboard{display:inline-block}.el-progress--circle .el-progress__text,.el-progress--dashboard .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;transform:translate(0,-50%)}.el-progress--circle .el-progress__text i,.el-progress--dashboard .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.el-progress-bar,.el-progress-bar__inner::after,.el-progress-bar__innerText,.el-spinner{display:inline-block;vertical-align:middle}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67C23A}.el-progress.is-success .el-progress__text{color:#67C23A}.el-progress.is-warning .el-progress-bar__inner{background-color:#E6A23C}.el-progress.is-warning .el-progress__text{color:#E6A23C}.el-progress.is-exception .el-progress-bar__inner{background-color:#F56C6C}.el-progress.is-exception .el-progress__text{color:#F56C6C}.el-progress-bar{padding-right:50px;width:100%;margin-right:-55px;box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#EBEEF5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#409EFF;text-align:right;border-radius:100px;line-height:1;white-space:nowrap;transition:width .6s ease}.el-card,.el-message{border-radius:4px;overflow:hidden}.el-progress-bar__inner::after{height:100%}.el-progress-bar__innerText{color:#FFF;font-size:12px;margin:0 5px}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-time-spinner{width:100%;white-space:nowrap}.el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{transform:rotate(360deg)}}@keyframes rotate{100%{transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}.el-message{min-width:380px;box-sizing:border-box;border-width:1px;border-style:solid;border-color:#EBEEF5;position:fixed;left:50%;top:20px;transform:translateX(-50%);background-color:#edf2fc;transition:opacity .3s,transform .4s,top .4s;padding:15px 15px 15px 20px;display:flex;align-items:center}.el-message.is-center{justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67C23A}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#E6A23C}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#F56C6C}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;transform:translateY(-50%);cursor:pointer;color:#C0C4CC;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67C23A}.el-message .el-icon-error{color:#F56C6C}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#E6A23C}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;transform:translate(-50%,-100%)}.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#F56C6C;border-radius:10px;color:#FFF;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #FFF}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;transform:translateY(-50%) translateX(100%)}.el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#409EFF}.el-badge__content--success{background-color:#67C23A}.el-badge__content--warning{background-color:#E6A23C}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#F56C6C}.el-card{border:1px solid #EBEEF5;background-color:#FFF;color:#303133;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #EBEEF5;box-sizing:border-box}.el-card__body{padding:20px}.el-rate{height:20px;line-height:1}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#C0C4CC;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-step.is-vertical,.el-steps{display:-ms-flexbox}.el-rate__text{font-size:14px;vertical-align:middle}.el-steps{display:flex}.el-steps--simple{padding:13px 8%;border-radius:4px;background:#F5F7FA}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{height:100%;flex-flow:column}.el-step{position:relative;flex-shrink:1}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{flex-basis:auto!important;flex-shrink:0;flex-grow:0}.el-step:last-of-type .el-step__description,.el-step:last-of-type .el-step__main{padding-right:0}.el-step__head{position:relative;width:100%}.el-step__head.is-process{color:#303133;border-color:#303133}.el-step__head.is-wait{color:#C0C4CC;border-color:#C0C4CC}.el-step__head.is-success{color:#67C23A;border-color:#67C23A}.el-step__head.is-error{color:#F56C6C;border-color:#F56C6C}.el-step__head.is-finish{color:#409EFF;border-color:#409EFF}.el-step__icon{position:relative;z-index:1;display:inline-flex;justify-content:center;align-items:center;width:24px;height:24px;font-size:14px;box-sizing:border-box;background:#FFF;transition:.15s ease-out}.el-step__icon.is-text{border-radius:50%;border:2px solid;border-color:inherit}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{display:inline-block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-align:center;font-weight:700;line-height:1;color:inherit}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{transform:translateY(1px)}.el-step__line{position:absolute;border-color:inherit;background-color:#C0C4CC}.el-step__line-inner{display:block;border-width:1px;border-style:solid;border-color:inherit;transition:.15s ease-out;box-sizing:border-box;width:0;height:0}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{font-weight:700;color:#303133}.el-step__title.is-wait{color:#C0C4CC}.el-step__title.is-success{color:#67C23A}.el-step__title.is-error{color:#F56C6C}.el-step__title.is-finish{color:#409EFF}.el-step__description{padding-right:10%;margin-top:-5px;font-size:12px;line-height:20px;font-weight:400}.el-step__description.is-process{color:#303133}.el-step__description.is-wait{color:#C0C4CC}.el-step__description.is-success{color:#67C23A}.el-step__description.is-error{color:#F56C6C}.el-step__description.is-finish{color:#409EFF}.el-step.is-horizontal{display:inline-block}.el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.el-step.is-vertical{display:flex}.el-step.is-vertical .el-step__head{flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{padding-left:10px;flex-grow:1}.el-step.is-vertical .el-step__title{line-height:24px;padding-bottom:8px}.el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.el-step.is-vertical .el-step__icon.is-icon{width:24px}.el-step.is-center .el-step__head,.el-step.is-center .el-step__main{text-align:center}.el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.el-step.is-center .el-step__line{left:50%;right:-50%}.el-step.is-simple{display:flex;align-items:center}.el-step.is-simple .el-step__head{width:auto;font-size:0;padding-right:10px}.el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.el-step.is-simple .el-step__icon-inner.is-status{transform:scale(.8) translateY(1px)}.el-step.is-simple .el-step__main{position:relative;display:flex;align-items:stretch;flex-grow:1}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{max-width:50%;word-break:break-all}.el-step.is-simple .el-step__arrow{flex-grow:1;display:flex;align-items:center;justify-content:center}.el-step.is-simple .el-step__arrow::after,.el-step.is-simple .el-step__arrow::before{content:\'\';display:inline-block;position:absolute;height:15px;width:1px;background:#C0C4CC}.el-step.is-simple .el-step__arrow::before{transform:rotate(-45deg) translateY(-4px);transform-origin:0 0}.el-step.is-simple .el-step__arrow::after{transform:rotate(45deg) translateY(4px);transform-origin:100% 100%}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-carousel{position:relative}.el-carousel--horizontal{overflow-x:hidden}.el-carousel--vertical{overflow-y:hidden}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#FFF;position:absolute;top:50%;z-index:10;transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;margin:0;padding:0;z-index:2}.el-carousel__indicators--horizontal{bottom:0;left:50%;transform:translateX(-50%)}.el-carousel__indicators--vertical{right:0;top:50%;transform:translateY(-50%)}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#C0C4CC;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{background-color:transparent;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{display:inline-block;padding:12px 4px}.el-carousel__indicator--vertical{padding:4px 12px}.el-carousel__indicator--vertical .el-carousel__button{width:2px;height:15px}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#FFF;border:none;outline:0;padding:0;margin:0;cursor:pointer;transition:.3s}.el-carousel__item,.el-carousel__mask{height:100%;top:0;left:0;position:absolute}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{transform:translateY(-50%) translateX(10px);opacity:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{transition:transform .4s ease-in-out}.el-carousel__item--card{width:50%;transition:transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#FFF;opacity:.24;transition:.2s}.el-fade-in-enter,.el-fade-in-leave-active,.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:opacity .2s linear}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{transition:opacity .2s linear}.el-fade-in-enter-active,.el-fade-in-leave-active{transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transform:scale(1,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45,.45)}.collapse-transition{transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.horizontal-collapse-transition{transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;transform:translateY(-30px)}.el-opacity-transition{transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-collapse{border-top:1px solid #EBEEF5;border-bottom:1px solid #EBEEF5}.el-collapse-item.is-disabled .el-collapse-item__header{color:#bbb;cursor:not-allowed}.el-collapse-item__header{display:flex;align-items:center;height:48px;line-height:48px;background-color:#FFF;color:#303133;cursor:pointer;border-bottom:1px solid #EBEEF5;font-size:13px;font-weight:500;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin:0 8px 0 auto;transition:transform .3s;font-weight:300}.el-collapse-item__arrow.is-active{transform:rotate(90deg)}.el-collapse-item__header.focusing:focus:not(:hover){color:#409EFF}.el-collapse-item__header.is-active{border-bottom-color:transparent}.el-collapse-item__wrap{will-change:height;background-color:#FFF;overflow:hidden;box-sizing:border-box;border-bottom:1px solid #EBEEF5}.el-cascader__tags,.el-tag{-webkit-box-sizing:border-box}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item:last-child{margin-bottom:-1px}.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#EBEEF5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#FFF;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#EBEEF5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#FFF}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#EBEEF5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#FFF;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#EBEEF5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#FFF}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409EFF;border-width:1px;border-style:solid;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409EFF}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67C23A}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close::before{display:block}.el-tag--dark{background-color:#409eff;border-color:#409eff;color:#fff}.el-tag--dark.is-hit{border-color:#409EFF}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#FFF;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67C23A}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409EFF}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67C23A}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}.el-cascader{display:inline-block;position:relative;font-size:14px;line-height:40px}.el-cascader:not(.is-disabled):hover .el-input__inner{cursor:pointer;border-color:#C0C4CC}.el-cascader .el-input .el-input__inner:focus,.el-cascader .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-cascader .el-input{cursor:pointer}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-icon-arrow-down{transition:transform .3s;font-size:14px}.el-cascader .el-input .el-icon-arrow-down.is-reverse{transform:rotateZ(180deg)}.el-cascader .el-input .el-icon-circle-close:hover{color:#909399}.el-cascader--medium{font-size:14px;line-height:36px}.el-cascader--small{font-size:13px;line-height:32px}.el-cascader--mini{font-size:12px;line-height:28px}.el-cascader.is-disabled .el-cascader__label{z-index:2;color:#C0C4CC}.el-cascader__dropdown{margin:5px 0;font-size:14px;background:#FFF;border:1px solid #E4E7ED;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-cascader__tags{position:absolute;left:0;right:30px;top:50%;transform:translateY(-50%);display:flex;flex-wrap:wrap;line-height:normal;text-align:left;box-sizing:border-box}.el-cascader__tags .el-tag{display:inline-flex;align-items:center;max-width:100%;margin:2px 0 2px 6px;text-overflow:ellipsis;background:#f0f2f5}.el-cascader__tags .el-tag:not(.is-hit){border-color:transparent}.el-cascader__tags .el-tag>span{flex:1;overflow:hidden;text-overflow:ellipsis}.el-cascader__tags .el-tag .el-icon-close{flex:none;background-color:#C0C4CC;color:#FFF}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:#909399}.el-cascader__suggestion-panel{border-radius:4px}.el-cascader__suggestion-list{max-height:204px;margin:0;padding:6px 0;font-size:14px;color:#606266;text-align:center}.el-cascader__suggestion-item{display:flex;justify-content:space-between;align-items:center;height:34px;padding:0 15px;text-align:left;outline:0;cursor:pointer}.el-cascader__suggestion-item:focus,.el-cascader__suggestion-item:hover{background:#F5F7FA}.el-cascader__suggestion-item.is-checked{color:#409EFF;font-weight:700}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__empty-text{margin:10px 0;color:#C0C4CC}.el-cascader__search-input{flex:1;height:24px;min-width:60px;margin:2px 0 2px 15px;padding:0;color:#606266;border:none;outline:0;box-sizing:border-box}.el-cascader__search-input:-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::-moz-placeholder{color:#C0C4CC}.el-cascader__search-input::placeholder{color:#C0C4CC}.el-color-predefine{display:flex;font-size:12px;margin-top:8px;width:280px}.el-color-predefine__colors{display:flex;flex:1;flex-wrap:wrap}.el-color-predefine__color-selector{margin:0 0 8px 8px;width:20px;height:20px;border-radius:4px;cursor:pointer}.el-color-predefine__color-selector:nth-child(10n+1){margin-left:0}.el-color-predefine__color-selector.selected{box-shadow:0 0 3px 2px #409EFF}.el-color-predefine__color-selector>div{display:flex;height:100%;border-radius:3px}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{position:relative;box-sizing:border-box;width:280px;height:12px;background-color:red;padding:0 2px}.el-color-hue-slider__bar{position:relative;background:linear-gradient(to right,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%);height:100%}.el-color-hue-slider__thumb{position:absolute;cursor:pointer;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:linear-gradient(to bottom,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-svpanel{position:relative;width:280px;height:180px}.el-color-svpanel__black,.el-color-svpanel__white{position:absolute;top:0;left:0;right:0;bottom:0}.el-color-svpanel__white{background:linear-gradient(to right,#fff,rgba(255,255,255,0))}.el-color-svpanel__black{background:linear-gradient(to top,#000,rgba(0,0,0,0))}.el-color-svpanel__cursor{position:absolute}.el-color-svpanel__cursor>div{cursor:head;width:4px;height:4px;box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;transform:translate(-2px,-2px)}.el-color-alpha-slider{position:relative;box-sizing:border-box;width:280px;height:12px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-alpha-slider__bar{position:relative;background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%);height:100%}.el-color-alpha-slider__thumb{position:absolute;cursor:pointer;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:linear-gradient(to bottom,rgba(255,255,255,0) 0,#fff 100%)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-dropdown{width:300px}.el-color-dropdown__main-wrapper{margin-bottom:6px}.el-color-dropdown__main-wrapper::after{content:"";display:table;clear:both}.el-color-dropdown__btns{margin-top:6px;text-align:right}.el-color-dropdown__value{float:left;line-height:26px;font-size:12px;color:#000;width:160px}.el-color-dropdown__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-color-dropdown__btn[disabled]{color:#ccc;cursor:not-allowed}.el-color-dropdown__btn:hover{color:#409EFF;border-color:#409EFF}.el-color-dropdown__link-btn{cursor:pointer;color:#409EFF;text-decoration:none;padding:15px;font-size:12px}.el-color-dropdown__link-btn:hover{color:tint(#409EFF,20%)}.el-color-picker{display:inline-block;position:relative;line-height:normal;height:40px}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed}.el-color-picker--medium{height:36px}.el-color-picker--medium .el-color-picker__trigger{height:36px;width:36px}.el-color-picker--medium .el-color-picker__mask{height:34px;width:34px}.el-color-picker--small{height:32px}.el-color-picker--small .el-color-picker__trigger{height:32px;width:32px}.el-color-picker--small .el-color-picker__mask{height:30px;width:30px}.el-color-picker--small .el-color-picker__empty,.el-color-picker--small .el-color-picker__icon{transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker--mini{height:28px}.el-color-picker--mini .el-color-picker__trigger{height:28px;width:28px}.el-color-picker--mini .el-color-picker__mask{height:26px;width:26px}.el-color-picker--mini .el-color-picker__empty,.el-color-picker--mini .el-color-picker__icon{transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker__mask{height:38px;width:38px;border-radius:4px;position:absolute;top:1px;left:1px;z-index:1;cursor:not-allowed;background-color:rgba(255,255,255,.7)}.el-color-picker__trigger{display:inline-block;box-sizing:border-box;height:40px;width:40px;padding:4px;border:1px solid #e6e6e6;border-radius:4px;font-size:0;position:relative;cursor:pointer}.el-color-picker__color{position:relative;display:block;box-sizing:border-box;border:1px solid #999;border-radius:2px;width:100%;height:100%;text-align:center}.el-color-picker__color.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-picker__color-inner{position:absolute;left:0;top:0;right:0;bottom:0}.el-color-picker__empty,.el-color-picker__icon{top:50%;left:50%;font-size:12px;position:absolute}.el-color-picker__empty{color:#999;transform:translate3d(-50%,-50%,0)}.el-color-picker__icon{display:inline-block;width:100%;transform:translate3d(-50%,-50%,0);color:#FFF;text-align:center}.el-color-picker__panel{position:absolute;z-index:10;padding:6px;box-sizing:content-box;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-textarea{position:relative;display:inline-block;width:100%;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#FFF;background-image:none;border:1px solid #DCDFE6;border-radius:4px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::-moz-placeholder{color:#C0C4CC}.el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea__inner:hover{border-color:#C0C4CC}.el-textarea__inner:focus{outline:0;border-color:#409EFF}.el-textarea .el-input__count{color:#909399;background:#FFF;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::-moz-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea.is-exceed .el-textarea__inner{border-color:#F56C6C}.el-textarea.is-exceed .el-input__count{color:#F56C6C}.el-input{position:relative;font-size:14px;display:inline-block;width:100%}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner{background:#fff}.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#C0C4CC;font-size:14px;cursor:pointer;transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:inline-flex;align-items:center;color:#909399;font-size:12px}.el-input .el-input__count .el-input__count-inner{background:#FFF;line-height:initial;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#FFF;background-image:none;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;color:#606266;display:inline-block;font-size:inherit;height:40px;line-height:40px;outline:0;padding:0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__prefix,.el-input__suffix{position:absolute;top:0;-webkit-transition:all .3s;height:100%;color:#C0C4CC;text-align:center}.el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input__inner::-moz-placeholder{color:#C0C4CC}.el-input__inner::placeholder{color:#C0C4CC}.el-input__inner:hover{border-color:#C0C4CC}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409EFF;outline:0}.el-input__suffix{right:5px;transition:all .3s}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;transition:all .3s;line-height:40px}.el-input__icon:after{content:\'\';height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::-moz-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-link,.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-input.is-exceed .el-input__inner{border-color:#F56C6C}.el-input.is-exceed .el-input__suffix .el-input__count{color:#F56C6C}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#F5F7FA;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #DCDFE6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--prepend .el-input__inner,.el-input-group__append{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-transfer{font-size:14px}.el-transfer__buttons{display:inline-block;vertical-align:middle;padding:0 30px}.el-transfer__button{display:block;margin:0 auto;padding:10px;border-radius:50%;color:#FFF;background-color:#409EFF;font-size:0}.el-transfer-panel__item+.el-transfer-panel__item,.el-transfer__button [class*=el-icon-]+span{margin-left:0}.el-transfer__button.is-with-texts{border-radius:4px}.el-transfer__button.is-disabled,.el-transfer__button.is-disabled:hover{border:1px solid #DCDFE6;background-color:#F5F7FA;color:#C0C4CC}.el-transfer__button:first-child{margin-bottom:10px}.el-transfer__button:nth-child(2){margin:0}.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer-panel{border:1px solid #EBEEF5;border-radius:4px;overflow:hidden;background:#FFF;display:inline-block;vertical-align:middle;width:200px;max-height:100%;box-sizing:border-box;position:relative}.el-transfer-panel__body{height:246px}.el-transfer-panel__body.is-with-footer{padding-bottom:40px}.el-transfer-panel__list{margin:0;padding:6px 0;list-style:none;height:246px;overflow:auto;box-sizing:border-box}.el-transfer-panel__list.is-filterable{height:194px;padding-top:0}.el-transfer-panel__item{height:30px;line-height:30px;padding-left:15px;display:block!important}.el-transfer-panel__item.el-checkbox{color:#606266}.el-transfer-panel__item:hover{color:#409EFF}.el-transfer-panel__item.el-checkbox .el-checkbox__label{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;box-sizing:border-box;padding-left:24px;line-height:30px}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;margin:15px;box-sizing:border-box;display:block;width:auto}.el-transfer-panel__filter .el-input__inner{height:32px;width:100%;font-size:12px;display:inline-block;box-sizing:border-box;border-radius:16px;padding-right:10px;padding-left:30px}.el-transfer-panel__filter .el-input__icon{margin-left:5px}.el-transfer-panel .el-transfer-panel__header{height:40px;line-height:40px;background:#F5F7FA;margin:0;padding-left:15px;border-bottom:1px solid #EBEEF5;box-sizing:border-box;color:#000}.el-transfer-panel .el-transfer-panel__header .el-checkbox{display:block;line-height:40px}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{font-size:16px;color:#303133;font-weight:400}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label span{position:absolute;right:15px;color:#909399;font-size:12px;font-weight:400}.el-divider__text,.el-link{font-weight:500;font-size:14px}.el-transfer-panel .el-transfer-panel__footer{height:40px;background:#FFF;margin:0;padding:0;border-top:1px solid #EBEEF5;position:absolute;bottom:0;left:0;width:100%;z-index:1}.el-transfer-panel .el-transfer-panel__footer::after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-container,.el-timeline-item__node{display:-ms-flexbox}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{padding-left:20px;color:#606266}.el-transfer-panel .el-transfer-panel__empty{margin:0;height:30px;line-height:30px;padding:6px 15px 0;color:#909399;text-align:center}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-transfer-panel .el-checkbox__inner{height:14px;width:14px;border-radius:3px}.el-transfer-panel .el-checkbox__inner::after{height:6px;width:3px;left:4px}.el-container{display:flex;flex-direction:row;flex:1;flex-basis:auto;box-sizing:border-box;min-width:0}.el-container.is-vertical,.el-drawer{-webkit-box-orient:vertical}.el-aside,.el-header{-webkit-box-sizing:border-box}.el-container.is-vertical{flex-direction:column}.el-header{padding:0 20px;box-sizing:border-box;flex-shrink:0}.el-aside{overflow:auto;box-sizing:border-box;flex-shrink:0}.el-footer,.el-main{-webkit-box-sizing:border-box}.el-main{display:block;flex:1;flex-basis:auto;overflow:auto;box-sizing:border-box;padding:20px}.el-footer{padding:0 20px;box-sizing:border-box;flex-shrink:0}.el-timeline{margin:0;font-size:14px;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline-item{position:relative;padding-bottom:20px}.el-timeline-item__wrapper{position:relative;padding-left:28px;top:-3px}.el-timeline-item__tail{position:absolute;left:4px;height:100%;border-left:2px solid #E4E7ED}.el-timeline-item__icon{color:#FFF;font-size:13px}.el-timeline-item__node{position:absolute;background-color:#E4E7ED;border-radius:50%;display:flex;justify-content:center;align-items:center}.el-image__error,.el-timeline-item__dot{display:-ms-flexbox}.el-timeline-item__node--normal{left:-1px;width:12px;height:12px}.el-timeline-item__node--large{left:-2px;width:14px;height:14px}.el-timeline-item__node--primary{background-color:#409EFF}.el-timeline-item__node--success{background-color:#67C23A}.el-timeline-item__node--warning{background-color:#E6A23C}.el-timeline-item__node--danger{background-color:#F56C6C}.el-timeline-item__node--info{background-color:#909399}.el-timeline-item__dot{position:absolute;display:flex;justify-content:center;align-items:center}.el-timeline-item__content{color:#303133}.el-timeline-item__timestamp{color:#909399;line-height:1;font-size:13px}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-link{display:inline-flex;flex-direction:row;align-items:center;justify-content:center;vertical-align:middle;position:relative;text-decoration:none;outline:0;padding:0}.el-link.is-underline:hover:after{content:"";position:absolute;left:0;right:0;height:0;bottom:0;border-bottom:1px solid #409EFF}.el-link.el-link--default:after,.el-link.el-link--primary.is-underline:hover:after,.el-link.el-link--primary:after{border-color:#409EFF}.el-link.is-disabled{cursor:not-allowed}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link.el-link--default{color:#606266}.el-link.el-link--default:hover{color:#409EFF}.el-link.el-link--default.is-disabled{color:#C0C4CC}.el-link.el-link--primary{color:#409EFF}.el-link.el-link--primary:hover{color:#66b1ff}.el-link.el-link--primary.is-disabled{color:#a0cfff}.el-link.el-link--danger.is-underline:hover:after,.el-link.el-link--danger:after{border-color:#F56C6C}.el-link.el-link--danger{color:#F56C6C}.el-link.el-link--danger:hover{color:#f78989}.el-link.el-link--danger.is-disabled{color:#fab6b6}.el-link.el-link--success.is-underline:hover:after,.el-link.el-link--success:after{border-color:#67C23A}.el-link.el-link--success{color:#67C23A}.el-link.el-link--success:hover{color:#85ce61}.el-link.el-link--success.is-disabled{color:#b3e19d}.el-link.el-link--warning.is-underline:hover:after,.el-link.el-link--warning:after{border-color:#E6A23C}.el-link.el-link--warning{color:#E6A23C}.el-link.el-link--warning:hover{color:#ebb563}.el-link.el-link--warning.is-disabled{color:#f3d19e}.el-link.el-link--info.is-underline:hover:after,.el-link.el-link--info:after{border-color:#909399}.el-link.el-link--info{color:#909399}.el-link.el-link--info:hover{color:#a6a9ad}.el-link.el-link--info.is-disabled{color:#c8c9cc}.el-divider{background-color:#DCDFE6;position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative}.el-divider__text{position:absolute;background-color:#FFF;padding:0 20px;color:#303133}.el-image__error,.el-image__placeholder{background:#F5F7FA}.el-divider__text.is-left{left:20px;transform:translateY(-50%)}.el-divider__text.is-center{left:50%;transform:translateX(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;transform:translateY(-50%)}.el-image__error,.el-image__inner,.el-image__placeholder{width:100%;height:100%}.el-image{position:relative;display:inline-block;overflow:hidden}.el-image__inner{vertical-align:top}.el-image__inner--center{position:relative;top:50%;left:50%;transform:translate(-50%,-50%);display:block}.el-image__error{display:flex;justify-content:center;align-items:center;font-size:14px;color:#C0C4CC;vertical-align:middle}.el-image__preview{cursor:pointer}.el-image-viewer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0}.el-image-viewer__btn{position:absolute;z-index:1;display:flex;align-items:center;justify-content:center;border-radius:50%;opacity:.8;cursor:pointer;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-button,.el-checkbox{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-image-viewer__close{top:40px;right:40px;width:40px;height:40px;font-size:24px;color:#fff;background-color:#606266}.el-image-viewer__canvas{width:100%;height:100%;display:flex;justify-content:center;align-items:center}.el-image-viewer__actions{left:50%;bottom:30px;transform:translateX(-50%);width:282px;height:44px;padding:0 23px;background-color:#606266;border-color:#fff;border-radius:22px}.el-image-viewer__actions__inner{width:100%;height:100%;text-align:justify;cursor:default;font-size:23px;color:#fff;display:flex;align-items:center;justify-content:space-around}.el-image-viewer__next,.el-image-viewer__prev{top:50%;width:44px;height:44px;font-size:24px;color:#fff;background-color:#606266;border-color:#fff}.el-image-viewer__prev{transform:translateY(-50%);left:40px}.el-image-viewer__next{transform:translateY(-50%);right:40px;text-indent:2px}.el-image-viewer__mask{position:absolute;width:100%;height:100%;top:0;left:0;opacity:.5;background:#000}.viewer-fade-enter-active{-webkit-animation:viewer-fade-in .3s;animation:viewer-fade-in .3s}.viewer-fade-leave-active{-webkit-animation:viewer-fade-out .3s;animation:viewer-fade-out .3s}@-webkit-keyframes viewer-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@keyframes viewer-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes viewer-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}@keyframes viewer-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:.1s;font-weight:500;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button{margin-left:10px}.el-button:focus,.el-button:hover{color:#409EFF;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#FFF;border-color:#409EFF;color:#409EFF}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#FFF;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#FFF;border-color:#EBEEF5;color:#C0C4CC}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:\'\';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#FFF;background-color:#409EFF;border-color:#409EFF}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#FFF}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#FFF;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409EFF;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409EFF;border-color:#409EFF;color:#FFF}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#FFF;background-color:#67C23A;border-color:#67C23A}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#FFF}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#FFF}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#FFF;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67C23A;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67C23A;border-color:#67C23A;color:#FFF}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#FFF;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#FFF;background-color:#E6A23C;border-color:#E6A23C}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#FFF}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#FFF}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#FFF;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#E6A23C;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#E6A23C;border-color:#E6A23C;color:#FFF}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#FFF;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#FFF;background-color:#F56C6C;border-color:#F56C6C}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#FFF}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#FFF}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#FFF;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#F56C6C;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#F56C6C;border-color:#F56C6C;color:#FFF}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#FFF;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#FFF;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#FFF}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#FFF}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#FFF;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#FFF}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#FFF;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--text,.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover,.el-button--text:active{border-color:transparent}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--mini,.el-button--small{font-size:12px;border-radius:3px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small,.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini.is-circle{padding:7px}.el-button--text{color:#409EFF;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;background-color:transparent}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group::after,.el-button-group::before{display:table;content:""}.el-button-group::after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button.is-disabled{z-index:1}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:not(:last-child){margin-right:-1px}.el-button-group>.el-button.is-active,.el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover{z-index:1}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-calendar{background-color:#fff}.el-calendar__header{display:flex;justify-content:space-between;padding:12px 20px;border-bottom:1px solid #EBEEF5}.el-backtop,.el-page-header{display:-ms-flexbox}.el-calendar__title{color:#000;align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{padding:12px 0;color:#606266;font-weight:400}.el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{color:#C0C4CC}.el-backtop,.el-calendar-table td.is-today{color:#409EFF}.el-calendar-table td{border-bottom:1px solid #EBEEF5;border-right:1px solid #EBEEF5;vertical-align:top;transition:background-color .2s ease}.el-calendar-table td.is-selected{background-color:#F2F8FE}.el-calendar-table tr:first-child td{border-top:1px solid #EBEEF5}.el-calendar-table tr td:first-child{border-left:1px solid #EBEEF5}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{box-sizing:border-box;padding:8px;height:85px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:#F2F8FE}.el-backtop{position:fixed;background-color:#FFF;width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 0 6px rgba(0,0,0,.12);cursor:pointer;z-index:5}.el-backtop:hover{background-color:#F2F6FC}.el-page-header{display:flex;line-height:24px}.el-page-header__left{display:flex;cursor:pointer;margin-right:40px;position:relative}.el-page-header__left::after{content:"";position:absolute;width:1px;height:16px;right:-20px;top:50%;transform:translateY(-50%);background-color:#DCDFE6}.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-page-header__left .el-icon-back{font-size:18px;margin-right:6px;align-self:center}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{font-size:18px;color:#303133}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin-right:30px}.el-checkbox-button__inner,.el-radio{font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409EFF}.el-checkbox.is-bordered.is-disabled{border-color:#EBEEF5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#DCDFE6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#C0C4CC}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#C0C4CC}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#C0C4CC;border-color:#C0C4CC}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409EFF;border-color:#409EFF}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#C0C4CC;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner::after{transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409EFF}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409EFF}.el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:\'\';position:absolute;display:block;background-color:#FFF;height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #DCDFE6;border-radius:2px;box-sizing:border-box;width:14px;height:14px;background-color:#FFF;z-index:1;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409EFF}.el-checkbox__inner::after{box-sizing:content-box;content:"";border:1px solid #FFF;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{display:inline-block;position:relative}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409EFF}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-radio,.el-radio__input{line-height:1;white-space:nowrap;outline:0}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409EFF}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#EBEEF5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409EFF}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-radio,.el-radio--medium.is-bordered .el-radio__label{font-size:14px}.el-radio{color:#606266;cursor:pointer;margin-right:30px}.el-cascader-node>.el-radio,.el-radio:last-child{margin-right:0}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;height:40px}.el-radio.is-bordered.is-checked{border-color:#409EFF}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#EBEEF5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#F5F7FA;border-color:#E4E7ED}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio__input{cursor:pointer;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#F5F7FA}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#C0C4CC}.el-radio__input.is-disabled+span.el-radio__label{color:#C0C4CC;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#409EFF;background:#409EFF}.el-radio__input.is-checked .el-radio__inner::after{transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#409EFF}.el-radio__input.is-focus .el-radio__inner{border-color:#409EFF}.el-radio__inner{border:1px solid #DCDFE6;border-radius:100%;width:14px;height:14px;background-color:#FFF;cursor:pointer;box-sizing:border-box}.el-radio__inner:hover{border-color:#409EFF}.el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#FFF;content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(0);transition:transform .15s ease-in}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{box-shadow:0 0 2px 2px #409EFF}.el-radio__label{font-size:14px;padding-left:10px}.el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;transition:opacity 340ms ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(144,147,153,.3);transition:.3s background-color}.el-scrollbar__thumb:hover{background-color:rgba(144,147,153,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;transition:opacity 120ms ease-out}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-cascader-panel{display:flex;border-radius:4px;font-size:14px}.el-cascader-panel.is-bordered{border:1px solid #E4E7ED;border-radius:4px}.el-cascader-menu{min-width:180px;box-sizing:border-box;color:#606266;border-right:solid 1px #E4E7ED}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu:last-child .el-cascader-node{padding-right:20px}.el-cascader-menu__wrap{height:204px}.el-cascader-menu__list{position:relative;min-height:100%;margin:0;padding:6px 0;list-style:none;box-sizing:border-box}.el-avatar,.el-drawer{-webkit-box-sizing:border-box;overflow:hidden}.el-cascader-menu__hover-zone{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.el-cascader-menu__empty-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:#C0C4CC}.el-cascader-node{position:relative;display:flex;align-items:center;padding:0 30px 0 20px;height:34px;line-height:34px;outline:0}.el-cascader-node.is-selectable.in-active-path{color:#606266}.el-cascader-node.in-active-path,.el-cascader-node.is-active,.el-cascader-node.is-selectable.in-checked-path{color:#409EFF;font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):focus,.el-cascader-node:not(.is-disabled):hover{background:#F5F7FA}.el-cascader-node.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{flex:1;padding:0 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-avatar{display:inline-block;box-sizing:border-box;text-align:center;color:#fff;background:#C0C4CC;width:40px;height:40px;line-height:40px;font-size:14px}.el-avatar>img{display:block;height:100%;vertical-align:middle}.el-drawer,.el-drawer__header{display:-ms-flexbox}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:4px}.el-avatar--icon{font-size:18px}.el-avatar--large{width:40px;height:40px;line-height:40px}.el-avatar--medium{width:36px;height:36px;line-height:36px}.el-avatar--small{width:28px;height:28px;line-height:28px}.el-drawer.btt,.el-drawer.ttb,.el-drawer__container{left:0;right:0;width:100%}.el-drawer.ltr,.el-drawer.rtl,.el-drawer__container{top:0;bottom:0;height:100%}@-webkit-keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes rtl-drawer-in{0%{transform:translate(100%,0)}100%{transform:translate(0,0)}}@keyframes rtl-drawer-in{0%{transform:translate(100%,0)}100%{transform:translate(0,0)}}@-webkit-keyframes rtl-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(100%,0)}}@keyframes rtl-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(100%,0)}}@-webkit-keyframes ltr-drawer-in{0%{transform:translate(-100%,0)}100%{transform:translate(0,0)}}@keyframes ltr-drawer-in{0%{transform:translate(-100%,0)}100%{transform:translate(0,0)}}@-webkit-keyframes ltr-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(-100%,0)}}@keyframes ltr-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(-100%,0)}}@-webkit-keyframes ttb-drawer-in{0%{transform:translate(0,-100%)}100%{transform:translate(0,0)}}@keyframes ttb-drawer-in{0%{transform:translate(0,-100%)}100%{transform:translate(0,0)}}@-webkit-keyframes ttb-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(0,-100%)}}@keyframes ttb-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(0,-100%)}}@-webkit-keyframes btt-drawer-in{0%{transform:translate(0,100%)}100%{transform:translate(0,0)}}@keyframes btt-drawer-in{0%{transform:translate(0,100%)}100%{transform:translate(0,0)}}@-webkit-keyframes btt-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(0,100%)}}@keyframes btt-drawer-out{0%{transform:translate(0,0)}100%{transform:translate(0,100%)}}.el-drawer{position:absolute;box-sizing:border-box;background-color:#FFF;display:flex;flex-direction:column;box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);outline:0}.el-drawer.rtl{-webkit-animation:rtl-drawer-out .3s;animation:rtl-drawer-out .3s;right:0}.el-drawer__open .el-drawer.rtl{-webkit-animation:rtl-drawer-in .3s 1ms;animation:rtl-drawer-in .3s 1ms}.el-drawer.ltr{-webkit-animation:ltr-drawer-out .3s;animation:ltr-drawer-out .3s;left:0}.el-drawer__open .el-drawer.ltr{-webkit-animation:ltr-drawer-in .3s 1ms;animation:ltr-drawer-in .3s 1ms}.el-drawer.ttb{-webkit-animation:ttb-drawer-out .3s;animation:ttb-drawer-out .3s;top:0}.el-drawer__open .el-drawer.ttb{-webkit-animation:ttb-drawer-in .3s 1ms;animation:ttb-drawer-in .3s 1ms}.el-drawer.btt{-webkit-animation:btt-drawer-out .3s;animation:btt-drawer-out .3s;bottom:0}.el-drawer__open .el-drawer.btt{-webkit-animation:btt-drawer-in .3s 1ms;animation:btt-drawer-in .3s 1ms}.el-drawer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:hidden;margin:0}.el-drawer__header{align-items:center;color:#72767b;display:flex;margin-bottom:32px;padding:20px 20px 0}.el-drawer__header>:first-child{flex:1}.el-drawer__title{margin:0;flex:1;line-height:inherit;font-size:1rem}.el-drawer__close-btn{border:none;cursor:pointer;font-size:20px;color:inherit;background-color:transparent}.el-drawer__body{flex:1}.el-drawer__body>*{box-sizing:border-box}.el-drawer__container{position:relative}.el-drawer-fade-enter-active{-webkit-animation:el-drawer-fade-in .3s;animation:el-drawer-fade-in .3s}.el-drawer-fade-leave-active{animation:el-drawer-fade-in .3s reverse}.el-popconfirm__main{display:flex;align-items:center}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin:0}',""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,".el-textarea{position:relative;display:inline-block;width:100%;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#FFF;background-image:none;border:1px solid #DCDFE6;border-radius:4px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::-moz-placeholder{color:#C0C4CC}.el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea__inner:hover{border-color:#C0C4CC}.el-textarea__inner:focus{outline:0;border-color:#409EFF}.el-textarea .el-input__count{color:#909399;background:#FFF;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::-moz-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea.is-exceed .el-textarea__inner{border-color:#F56C6C}.el-textarea.is-exceed .el-input__count{color:#F56C6C}.el-input{position:relative;font-size:14px;display:inline-block;width:100%}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner{background:#fff}.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#C0C4CC;font-size:14px;cursor:pointer;transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:inline-flex;align-items:center;color:#909399;font-size:12px}.el-input .el-input__count .el-input__count-inner{background:#FFF;line-height:initial;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#FFF;background-image:none;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;color:#606266;display:inline-block;font-size:inherit;height:40px;line-height:40px;outline:0;padding:0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__prefix,.el-input__suffix{position:absolute;top:0;-webkit-transition:all .3s;text-align:center;height:100%;color:#C0C4CC}.el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input__inner::-moz-placeholder{color:#C0C4CC}.el-input__inner::placeholder{color:#C0C4CC}.el-input__inner:hover{border-color:#C0C4CC}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409EFF;outline:0}.el-input__suffix{right:5px;transition:all .3s;pointer-events:none}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;transition:all .3s;line-height:40px}.el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::-moz-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-input.is-exceed .el-input__inner{border-color:#F56C6C}.el-input.is-exceed .el-input__suffix .el-input__count{color:#F56C6C}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#F5F7FA;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #DCDFE6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--prepend .el-input__inner,.el-input-group__append{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input__inner::-ms-clear{display:none;width:0;height:0}",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,"",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,'.el-fade-in-enter,.el-fade-in-leave-active,.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-menu--collapse .el-menu .el-submenu,.el-menu--popup{min-width:200px}.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:opacity .2s linear}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{transition:opacity .2s linear}.el-fade-in-enter-active,.el-fade-in-leave-active{transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transform:scale(1,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45,.45)}.collapse-transition{transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.horizontal-collapse-transition{transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;transform:translateY(-30px)}.el-opacity-transition{transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-menu{border-right:solid 1px #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0;background-color:#FFF}.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu::after,.el-menu::before{display:table;content:""}.el-menu::after{clear:both}.el-menu.el-menu--horizontal{border-bottom:solid 1px #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409EFF;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#FFF;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409EFF;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-submenu{position:relative}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;z-index:10;border:1px solid #E4E7ED;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;list-style:none;position:relative;white-space:nowrap}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:none}.el-menu--popup{z-index:100;border:none;padding:5px 0;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409EFF}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;transition:transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409EFF}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:rotateZ(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{transition:.2s;opacity:0}',""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,".el-button-group>.el-button.is-active,.el-button-group>.el-button.is-disabled,.el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover{z-index:1}.el-button,.el-input__inner{-webkit-appearance:none;outline:0}.el-message-box,.el-popup-parent--hidden{overflow:hidden}.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;color:#606266;text-align:center;box-sizing:border-box;margin:0;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button{margin-left:10px}.el-button:focus,.el-button:hover{color:#409EFF;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#FFF;border-color:#409EFF;color:#409EFF}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#FFF;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#FFF;border-color:#EBEEF5;color:#C0C4CC}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#FFF;background-color:#409EFF;border-color:#409EFF}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#FFF}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#FFF;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409EFF;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409EFF;border-color:#409EFF;color:#FFF}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#FFF;background-color:#67C23A;border-color:#67C23A}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#FFF}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#FFF}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#FFF;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67C23A;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67C23A;border-color:#67C23A;color:#FFF}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#FFF;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#FFF;background-color:#E6A23C;border-color:#E6A23C}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#FFF}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#FFF}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#FFF;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#E6A23C;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#E6A23C;border-color:#E6A23C;color:#FFF}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#FFF;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#FFF;background-color:#F56C6C;border-color:#F56C6C}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#FFF}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#FFF}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#FFF;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#F56C6C;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#F56C6C;border-color:#F56C6C;color:#FFF}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#FFF;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#FFF;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#FFF}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#FFF}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#FFF;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#FFF}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#FFF;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--text,.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover,.el-button--text:active{border-color:transparent}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--mini,.el-button--small{font-size:12px;border-radius:3px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small,.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini.is-circle{padding:7px}.el-button--text{color:#409EFF;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;background-color:transparent}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group::after,.el-button-group::before{display:table;content:\"\"}.el-button-group::after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:not(:last-child){margin-right:-1px}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:first-child{border-right-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:last-child{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:rgba(255,255,255,.5);border-right-color:rgba(255,255,255,.5)}.el-textarea{position:relative;display:inline-block;width:100%;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#FFF;background-image:none;border:1px solid #DCDFE6;border-radius:4px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::-moz-placeholder{color:#C0C4CC}.el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea__inner:hover{border-color:#C0C4CC}.el-textarea__inner:focus{outline:0;border-color:#409EFF}.el-textarea .el-input__count{color:#909399;background:#FFF;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::-moz-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea.is-exceed .el-textarea__inner{border-color:#F56C6C}.el-textarea.is-exceed .el-input__count{color:#F56C6C}.el-input{position:relative;font-size:14px;display:inline-block;width:100%}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner{background:#fff}.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#C0C4CC;font-size:14px;cursor:pointer;transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:inline-flex;align-items:center;color:#909399;font-size:12px}.el-input .el-input__count .el-input__count-inner{background:#FFF;line-height:initial;display:inline-block;padding:0 5px}.el-input__inner{background-color:#FFF;background-image:none;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;color:#606266;display:inline-block;font-size:inherit;height:40px;line-height:40px;padding:0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__prefix,.el-input__suffix{position:absolute;-webkit-transition:all .3s;text-align:center;height:100%;color:#C0C4CC;top:0}.el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input__inner::-moz-placeholder{color:#C0C4CC}.el-input__inner::placeholder{color:#C0C4CC}.el-input__inner:hover{border-color:#C0C4CC}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409EFF;outline:0}.el-input__suffix{right:5px;transition:all .3s;pointer-events:none}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;transition:all .3s;line-height:40px}.el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::-moz-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-input.is-exceed .el-input__inner{border-color:#F56C6C}.el-input.is-exceed .el-input__suffix .el-input__count{color:#F56C6C}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#F5F7FA;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #DCDFE6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--prepend .el-input__inner,.el-input-group__append{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#FFF;border-radius:4px;border:1px solid #EBEEF5;font-size:18px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper::after{content:\"\";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409EFF}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#F56C6C}.el-message-box__status{position:absolute;top:50%;transform:translateY(-50%);font-size:24px!important}.el-message-box__status::before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67C23A}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#E6A23C}.el-message-box__status.el-icon-error{color:#F56C6C}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#F56C6C;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:flex;align-items:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@keyframes msgbox-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}100%{transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{transform:translate3d(0,0,0);opacity:1}100%{transform:translate3d(0,-20px,0);opacity:0}}",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,".el-message__closeBtn:focus,.el-message__content:focus{outline-width:0}.el-message{min-width:380px;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#EBEEF5;position:fixed;left:50%;top:20px;transform:translateX(-50%);background-color:#edf2fc;transition:opacity .3s,transform .4s,top .4s;overflow:hidden;padding:15px 15px 15px 20px;display:flex;align-items:center}.el-message.is-center{justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67C23A}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#E6A23C}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#F56C6C}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;transform:translateY(-50%);cursor:pointer;color:#C0C4CC;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67C23A}.el-message .el-icon-error{color:#F56C6C}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#E6A23C}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;transform:translate(-50%,-100%)}",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,'.el-row{position:relative;box-sizing:border-box}.el-row::after,.el-row::before{display:table;content:""}.el-row::after{clear:both}.el-row--flex{display:flex}.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{justify-content:center}.el-row--flex.is-justify-end{justify-content:flex-end}.el-row--flex.is-justify-space-between{justify-content:space-between}.el-row--flex.is-justify-space-around{justify-content:space-around}.el-row--flex.is-align-middle{align-items:center}.el-row--flex.is-align-bottom{align-items:flex-end}',""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,'@charset "UTF-8";.el-checkbox,.el-checkbox__input{white-space:nowrap;display:inline-block;position:relative}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin-right:30px}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409EFF}.el-checkbox.is-bordered.is-disabled{border-color:#EBEEF5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#DCDFE6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#C0C4CC}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#C0C4CC}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#C0C4CC;border-color:#C0C4CC}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409EFF;border-color:#409EFF}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#C0C4CC;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner::after{transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409EFF}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409EFF}.el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:\'\';position:absolute;display:block;background-color:#FFF;height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #DCDFE6;border-radius:2px;box-sizing:border-box;width:14px;height:14px;background-color:#FFF;z-index:1;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409EFF}.el-checkbox__inner::after{box-sizing:content-box;content:"";border:1px solid #FFF;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox-button__inner,.el-tag{-webkit-box-sizing:border-box;white-space:nowrap}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{position:relative;display:inline-block}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;vertical-align:middle;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409EFF}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409EFF}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#EBEEF5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409EFF}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409EFF;border-width:1px;border-style:solid;border-radius:4px;box-sizing:border-box}.el-tag.is-hit{border-color:#409EFF}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67C23A}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close::before{display:block}.el-tag--dark{background-color:#409eff;border-color:#409eff;color:#fff}.el-tag--dark.is-hit{border-color:#409EFF}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#FFF;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67C23A}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409EFF}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67C23A}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #EBEEF5;border-radius:2px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409EFF;color:#FFF}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #EBEEF5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-table-filter__bottom button:hover{color:#409EFF}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}',""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,'@charset "UTF-8";.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-table,.el-table__append-wrapper{overflow:hidden}.el-table td.is-hidden>*,.el-table th.is-hidden>*,.el-table--hidden{visibility:hidden}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin-right:30px}.el-checkbox-button__inner,.el-table th{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409EFF}.el-checkbox.is-bordered.is-disabled{border-color:#EBEEF5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#DCDFE6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#C0C4CC}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#C0C4CC}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#C0C4CC;border-color:#C0C4CC}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409EFF;border-color:#409EFF}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#C0C4CC;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner::after{transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409EFF}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409EFF}.el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:\'\';position:absolute;display:block;background-color:#FFF;height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #DCDFE6;border-radius:2px;box-sizing:border-box;width:14px;height:14px;background-color:#FFF;z-index:1;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409EFF}.el-checkbox__inner::after{box-sizing:content-box;content:"";border:1px solid #FFF;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{position:relative;display:inline-block}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409EFF}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409EFF}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#EBEEF5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409EFF}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409EFF;border-width:1px;border-style:solid;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409EFF}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67C23A}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close::before{display:block}.el-tag--dark{background-color:#409eff;border-color:#409eff;color:#fff}.el-tag--dark.is-hit{border-color:#409EFF}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#FFF;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67C23A}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409EFF}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67C23A}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#FFF}.el-table,.el-table__expanded-cell{background-color:#FFF}.el-tooltip__popper.is-light{background:#FFF;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#FFF}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#FFF}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#FFF}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#FFF}.el-table{position:relative;box-sizing:border-box;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table--mini,.el-table--small,.el-table__expand-icon{font-size:12px}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:flex;justify-content:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;transition:transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit td.gutter,.el-table--fit th.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th{background:#F5F7FA}.el-table th,.el-table tr{background-color:#FFF}.el-table td,.el-table th{padding:12px 0;min-width:0;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table td.is-center,.el-table th.is-center{text-align:center}.el-table td.is-right,.el-table th.is-right{text-align:right}.el-table td.gutter,.el-table th.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table--medium td,.el-table--medium th{padding:10px 0}.el-table--small td,.el-table--small th{padding:8px 0}.el-table--mini td,.el-table--mini th{padding:6px 0}.el-table .cell,.el-table--border td:first-child .cell,.el-table--border th:first-child .cell{padding-left:10px}.el-table tr input[type=checkbox]{margin:0}.el-table td,.el-table th.is-leaf{border-bottom:1px solid #EBEEF5}.el-table th.is-sortable{cursor:pointer}.el-table th{overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-table th>.cell{display:inline-block;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th>.cell.highlight{color:#409EFF}.el-table th.required>div::before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td div{box-sizing:border-box}.el-table td.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #EBEEF5}.el-table--border::after,.el-table--group::after,.el-table::before{content:\'\';position:absolute;background-color:#EBEEF5;z-index:1}.el-table--border::after,.el-table--group::after{top:0;right:0;width:1px;height:100%}.el-table::before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border td,.el-table--border th,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #EBEEF5}.el-table--border th.gutter:last-of-type{border-bottom:1px solid #EBEEF5;border-bottom-width:1px}.el-table--border th,.el-table__fixed-right-patch{border-bottom:1px solid #EBEEF5}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right::before,.el-table__fixed::before{content:\'\';position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#EBEEF5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#FFF}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td{border-top:1px solid #EBEEF5;background-color:#F5F7FA;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td{border-top:1px solid #EBEEF5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td,.el-table__header-wrapper tbody td{background-color:#F5F7FA;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{box-shadow:none}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #EBEEF5}.el-table .caret-wrapper{display:inline-flex;flex-direction:column;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#C0C4CC;top:5px}.el-table .sort-caret.descending{border-top-color:#C0C4CC;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409EFF}.el-table .descending .sort-caret.descending{border-top-color:#409EFF}.el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td{background:#FAFAFA}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td,.el-table__body tr.hover-row.el-table__row--striped.current-row>td,.el-table__body tr.hover-row.el-table__row--striped>td,.el-table__body tr.hover-row>td{background-color:#F5F7FA}.el-table__body tr.current-row>td{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #EBEEF5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;transform:scale(.75)}.el-table--enable-row-transition .el-table__body td{transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#F5F7FA}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}',""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,".el-form-item span{margin-left:15px}.demo-table-expand{font-size:0}.demo-table-expand label{width:90px;color:#99a9bf}.demo-table-expand .el-form-item{margin-right:0;margin-bottom:0;width:50%}",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,"\nbody {\n background-color: #fafafa;\n margin: 0px;\n font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,sans-serif;\n}\nheader {\n width: 100%;\n height: 60px;\n}\n.header-color {\n background: #58B7FF;\n}\n#content {\n margin-top: 20px;\n padding-right: 40px;\n}\n.brand {\n color: #fff;\n background-color: transparent;\n margin-left: 20px;\n float: left;\n line-height: 25px;\n font-size: 25px;\n padding: 15px 15px;\n height: 30px;\n text-decoration: none;\n}\n",""])},function(e,t,o){t=e.exports=o(2)(!1),t.push([e.i,"\n#head {\n margin-bottom: 30px;\n}\n",""])},function(e,t,o){"use strict";function r(e){return!!e&&"object"==typeof e}function n(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||i(e)}function i(e){return e.$$typeof===f}function l(e){return Array.isArray(e)?[]:{}}function a(e,t){return t&&!0===t.clone&&u(e)?d(l(e),e,t):e}function s(e,t,o){var r=e.slice();return t.forEach(function(t,n){void 0===r[n]?r[n]=a(t,o):u(t)?r[n]=d(e[n],t,o):-1===e.indexOf(t)&&r.push(a(t,o))}),r}function c(e,t,o){var r={};return u(e)&&Object.keys(e).forEach(function(t){r[t]=a(e[t],o)}),Object.keys(t).forEach(function(n){u(t[n])&&e[n]?r[n]=d(e[n],t[n],o):r[n]=a(t[n],o)}),r}function d(e,t,o){var r=Array.isArray(t),n=Array.isArray(e),i=o||{arrayMerge:s};return r===n?r?(i.arrayMerge||s)(e,t,o):c(e,t,o):a(t,o)}var u=function(e){return r(e)&&!n(e)},p="function"==typeof Symbol&&Symbol.for,f=p?Symbol.for("react.element"):60103;d.all=function(e,t){if(!Array.isArray(e)||e.length<2)throw new Error("first argument should be an array with at least two elements");return e.reduce(function(e,o){return d(e,o,t)})};var h=d;e.exports=h},function(e,t,o){e.exports=function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,o){if(1&o&&(e=t(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var n in e)t.d(r,n,function(t){return e[t]}.bind(null,n));return r},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=86)}({0:function(e,t,o){"use strict";function r(e,t,o,r,n,i,l,a){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=o,s._compiled=!0),r&&(s.functional=!0),i&&(s._scopeId="data-v-"+i);var c;if(l?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(l)},s._ssrRegister=c):n&&(c=a?function(){n.call(this,this.$root.$options.shadowRoot)}:n),c)if(s.functional){s._injectStyles=c;var d=s.render;s.render=function(e,t){return c.call(t),d(e,t)}}else{var u=s.beforeCreate;s.beforeCreate=u?[].concat(u,c):[c]}return{exports:e,options:s}}o.d(t,"a",function(){return r})},4:function(e,t){e.exports=o(15)},86:function(e,t,o){"use strict";o.r(t);var r=function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{staticClass:"el-checkbox-group",attrs:{role:"group","aria-label":"checkbox-group"}},[e._t("default")],2)},n=[];r._withStripped=!0;var i=o(4),l=o.n(i),a={name:"ElCheckboxGroup",componentName:"ElCheckboxGroup",mixins:[l.a],inject:{elFormItem:{default:""}},props:{value:{},disabled:Boolean,min:Number,max:Number,size:String,fill:String,textColor:String},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},checkboxGroupSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size}},watch:{value:function(e){this.dispatch("ElFormItem","el.form.change",[e])}}},s=a,c=o(0),d=Object(c.a)(s,r,n,!1,null,null,null);d.options.__file="packages/checkbox/src/checkbox-group.vue";var u=d.exports;u.install=function(e){e.component(u.name,u)},t.default=u}})},function(e,t,o){"use strict";t.__esModule=!0;var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.default=function(e){function t(e){for(var t=arguments.length,o=Array(t>1?t-1:0),l=1;l=0;t--){var o=e.childNodes[t];if(r.Utils.attemptFocus(o)||r.Utils.focusLastDescendant(o))return!0}return!1},r.Utils.attemptFocus=function(e){if(!r.Utils.isFocusable(e))return!1;r.Utils.IgnoreUtilFocusChanges=!0;try{e.focus()}catch(e){}return r.Utils.IgnoreUtilFocusChanges=!1,document.activeElement===e},r.Utils.isFocusable=function(e){if(e.tabIndex>0||0===e.tabIndex&&null!==e.getAttribute("tabIndex"))return!0;if(e.disabled)return!1;switch(e.nodeName){case"A":return!!e.href&&"ignore"!==e.rel;case"INPUT":return"hidden"!==e.type&&"file"!==e.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},r.Utils.triggerEvent=function(e,t){var o=void 0;o=/^mouse|click/.test(t)?"MouseEvents":/^key/.test(t)?"KeyboardEvent":"HTMLEvents";for(var r=document.createEvent(o),n=arguments.length,i=Array(n>2?n-2:0),l=2;l0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};!(o&&o.context&&r.target&&n.target)||e.contains(r.target)||e.contains(n.target)||e===r.target||o.context.popperElm&&(o.context.popperElm.contains(r.target)||o.context.popperElm.contains(n.target))||(t.expression&&e[s].methodName&&o.context[e[s].methodName]?o.context[e[s].methodName]():e[s].bindingFn&&e[s].bindingFn())}}t.__esModule=!0;var n=o(4),i=function(e){return e&&e.__esModule?e:{default:e}}(n),l=o(7),a=[],s="@@clickoutsideContext",c=void 0,d=0;!i.default.prototype.$isServer&&(0,l.on)(document,"mousedown",function(e){return c=e}),!i.default.prototype.$isServer&&(0,l.on)(document,"mouseup",function(e){a.forEach(function(t){return t[s].documentHandler(e,c)})}),t.default={bind:function(e,t,o){a.push(e);var n=d++;e[s]={id:n,documentHandler:r(e,t,o),methodName:t.expression,bindingFn:t.value}},update:function(e,t,o){e[s].documentHandler=r(e,t,o),e[s].methodName=t.expression,e[s].bindingFn=t.value},unbind:function(e){for(var t=a.length,o=0;o1&&console.warn("WARNING: the given `parent` query("+e.parent+") matched more than one element, the first one will be used"),0===a.length)throw"ERROR: the given `parent` doesn't exists!";a=a[0]}return a.length>1&&a instanceof Element==0&&(console.warn("WARNING: you have passed as parent a list of elements, the first one will be used"),a=a[0]),a.appendChild(i),i},e.prototype._getPosition=function(e,t){var o=l(t);return this._options.forceAbsolute?"absolute":s(t,o)?"fixed":"absolute"},e.prototype._getOffsets=function(e,o,r){r=r.split("-")[0];var n={};n.position=this.state.position;var i="fixed"===n.position,a=f(o,l(e),i),s=t(e);return-1!==["right","left"].indexOf(r)?(n.top=a.top+a.height/2-s.height/2,n.left="left"===r?a.left-s.width:a.right):(n.left=a.left+a.width/2-s.width/2,n.top="top"===r?a.top-s.height:a.bottom),n.width=s.width,n.height=s.height,{popper:n,reference:a}},e.prototype._setupEventListeners=function(){if(this.state.updateBound=this.update.bind(this),b.addEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement){var e=a(this._reference);e!==b.document.body&&e!==b.document.documentElement||(e=b),e.addEventListener("scroll",this.state.updateBound),this.state.scrollTarget=e}},e.prototype._removeEventListeners=function(){b.removeEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement&&this.state.scrollTarget&&(this.state.scrollTarget.removeEventListener("scroll",this.state.updateBound),this.state.scrollTarget=null),this.state.updateBound=null},e.prototype._getBoundaries=function(e,t,o){var r,n,i={};if("window"===o){var s=b.document.body,c=b.document.documentElement;n=Math.max(s.scrollHeight,s.offsetHeight,c.clientHeight,c.scrollHeight,c.offsetHeight),r=Math.max(s.scrollWidth,s.offsetWidth,c.clientWidth,c.scrollWidth,c.offsetWidth),i={top:0,right:r,bottom:n,left:0}}else if("viewport"===o){var d=l(this._popper),p=a(this._popper),f=u(d),h="fixed"===e.offsets.popper.position?0:function(e){return e==document.body?Math.max(document.documentElement.scrollTop,document.body.scrollTop):e.scrollTop}(p),g="fixed"===e.offsets.popper.position?0:function(e){return e==document.body?Math.max(document.documentElement.scrollLeft,document.body.scrollLeft):e.scrollLeft}(p);i={top:0-(f.top-h),right:b.document.documentElement.clientWidth-(f.left-g),bottom:b.document.documentElement.clientHeight-(f.top-h),left:0-(f.left-g)}}else i=l(this._popper)===o?{top:0,left:0,right:o.clientWidth,bottom:o.clientHeight}:u(o);return i.left+=t,i.right-=t,i.top=i.top+t,i.bottom=i.bottom-t,i},e.prototype.runModifiers=function(e,t,o){var r=t.slice();return void 0!==o&&(r=this._options.modifiers.slice(0,n(this._options.modifiers,o))),r.forEach(function(t){d(t)&&(e=t.call(this,e))}.bind(this)),e},e.prototype.isModifierRequired=function(e,t){var o=n(this._options.modifiers,e);return!!this._options.modifiers.slice(0,o).filter(function(e){return e===t}).length},e.prototype.modifiers={},e.prototype.modifiers.applyStyle=function(e){var t,o={position:e.offsets.popper.position},r=Math.round(e.offsets.popper.left),n=Math.round(e.offsets.popper.top);return this._options.gpuAcceleration&&(t=h("transform"))?(o[t]="translate3d("+r+"px, "+n+"px, 0)",o.top=0,o.left=0):(o.left=r,o.top=n),Object.assign(o,e.styles),c(this._popper,o),this._popper.setAttribute("x-placement",e.placement),this.isModifierRequired(this.modifiers.applyStyle,this.modifiers.arrow)&&e.offsets.arrow&&c(e.arrowElement,e.offsets.arrow),e},e.prototype.modifiers.shift=function(e){var t=e.placement,o=t.split("-")[0],n=t.split("-")[1];if(n){var i=e.offsets.reference,l=r(e.offsets.popper),a={y:{start:{top:i.top},end:{top:i.top+i.height-l.height}},x:{start:{left:i.left},end:{left:i.left+i.width-l.width}}},s=-1!==["bottom","top"].indexOf(o)?"x":"y";e.offsets.popper=Object.assign(l,a[s][n])}return e},e.prototype.modifiers.preventOverflow=function(e){var t=this._options.preventOverflowOrder,o=r(e.offsets.popper),n={left:function(){var t=o.left;return o.lefte.boundaries.right&&(t=Math.min(o.left,e.boundaries.right-o.width)),{left:t}},top:function(){var t=o.top;return o.tope.boundaries.bottom&&(t=Math.min(o.top,e.boundaries.bottom-o.height)),{top:t}}};return t.forEach(function(t){e.offsets.popper=Object.assign(o,n[t]())}),e},e.prototype.modifiers.keepTogether=function(e){var t=r(e.offsets.popper),o=e.offsets.reference,n=Math.floor;return t.rightn(o.right)&&(e.offsets.popper.left=n(o.right)),t.bottomn(o.bottom)&&(e.offsets.popper.top=n(o.bottom)),e},e.prototype.modifiers.flip=function(e){if(!this.isModifierRequired(this.modifiers.flip,this.modifiers.preventOverflow))return console.warn("WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!"),e;if(e.flipped&&e.placement===e._originalPlacement)return e;var t=e.placement.split("-")[0],n=o(t),i=e.placement.split("-")[1]||"",l=[];return l="flip"===this._options.flipBehavior?[t,n]:this._options.flipBehavior,l.forEach(function(a,s){if(t===a&&l.length!==s+1){t=e.placement.split("-")[0],n=o(t);var c=r(e.offsets.popper),d=-1!==["right","bottom"].indexOf(t);(d&&Math.floor(e.offsets.reference[t])>Math.floor(c[n])||!d&&Math.floor(e.offsets.reference[t])a[f]&&(e.offsets.popper[u]+=s[u]+h-a[f]);var b=s[u]+(n||s[d]/2-h/2),g=b-a[u];return g=Math.max(Math.min(a[d]-h-8,g),8),i[u]=g,i[p]="",e.offsets.arrow=i,e.arrowElement=o,e},Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(e){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var t=Object(e),o=1;o0){var r=t[t.length-1];if(r.id===e)r.modalClass&&r.modalClass.trim().split(/\s+/).forEach(function(e){return(0,i.removeClass)(o,e)}),t.pop(),t.length>0&&(o.style.zIndex=t[t.length-1].zIndex);else for(var n=t.length-1;n>=0;n--)if(t[n].id===e){t.splice(n,1);break}}0===t.length&&(this.modalFade&&(0,i.addClass)(o,"v-modal-leave"),setTimeout(function(){0===t.length&&(o.parentNode&&o.parentNode.removeChild(o),o.style.display="none",u.modalDom=void 0),(0,i.removeClass)(o,"v-modal-leave")},200))}};Object.defineProperty(u,"zIndex",{configurable:!0,get:function(){return a||(s=s||(n.default.prototype.$ELEMENT||{}).zIndex||2e3,a=!0),s},set:function(e){s=e}});var p=function(){if(!n.default.prototype.$isServer&&u.modalStack.length>0){var e=u.modalStack[u.modalStack.length-1];if(!e)return;return u.getInstance(e.id)}};n.default.prototype.$isServer||window.addEventListener("keydown",function(e){if(27===e.keyCode){var t=p();t&&t.closeOnPressEscape&&(t.handleClose?t.handleClose():t.handleAction?t.handleAction("cancel"):t.close())}}),t.default=u},function(e,t,o){"use strict";function r(e){return void 0!==e&&null!==e}function n(e){return/([(\uAC00-\uD7AF)|(\u3130-\u318F)])+/gi.test(e)}t.__esModule=!0,t.isDef=r,t.isKorean=n},function(e,t,o){"use strict";function r(e){return"[object String]"===Object.prototype.toString.call(e)}function n(e){return"[object Object]"===Object.prototype.toString.call(e)}function i(e){return e&&e.nodeType===Node.ELEMENT_NODE}t.__esModule=!0,t.isString=r,t.isObject=n,t.isHtmlElement=i,t.isFunction=function(e){var t={};return e&&"[object Function]"===t.toString.call(e)},t.isUndefined=function(e){return void 0===e},t.isDefined=function(e){return void 0!==e&&null!==e}},function(e,t,o){e.exports=o(191)},function(e,t,o){"use strict";var r=!("undefined"==typeof window||!window.document||!window.document.createElement),n={canUseDOM:r,canUseWorkers:"undefined"!=typeof Worker,canUseEventListeners:r&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:r&&!!window.screen,isInWorker:!r};e.exports=n},function(e,t){function o(){if(!_){_=!0;var e=navigator.userAgent,t=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(e),o=/(Mac OS X)|(Windows)|(Linux)/.exec(e);if(h=/\b(iPhone|iP[ao]d)/.exec(e),b=/\b(iP[ao]d)/.exec(e),p=/Android/i.exec(e),g=/FBAN\/\w+;/i.exec(e),m=/Mobile/i.exec(e),f=!!/Win64/.exec(e),t){(r=t[1]?parseFloat(t[1]):t[5]?parseFloat(t[5]):NaN)&&document&&document.documentMode&&(r=document.documentMode);var v=/(?:Trident\/(\d+.\d+))/.exec(e);s=v?parseFloat(v[1])+4:r,n=t[2]?parseFloat(t[2]):NaN,i=t[3]?parseFloat(t[3]):NaN,l=t[4]?parseFloat(t[4]):NaN,l?(t=/(?:Chrome\/(\d+\.\d+))/.exec(e),a=t&&t[1]?parseFloat(t[1]):NaN):a=NaN}else r=n=i=a=l=NaN;if(o){if(o[1]){var x=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(e);c=!x||parseFloat(x[1].replace("_","."))}else c=!1;d=!!o[2],u=!!o[3]}else c=d=u=!1}}var r,n,i,l,a,s,c,d,u,p,f,h,b,g,m,_=!1,v={ie:function(){return o()||r},ieCompatibilityMode:function(){return o()||s>r},ie64:function(){return v.ie()&&f},firefox:function(){return o()||n},opera:function(){return o()||i},webkit:function(){return o()||l},safari:function(){return v.webkit()},chrome:function(){return o()||a},windows:function(){return o()||d},osx:function(){return o()||c},linux:function(){return o()||u},iphone:function(){return o()||h},mobile:function(){return o()||h||b||p||m},nativeApp:function(){return o()||g},android:function(){return o()||p},ipad:function(){return o()||b}};e.exports=v},function(e,t,o){"use strict";function r(e,t){if(!i.canUseDOM||t&&!("addEventListener"in document))return!1;var o="on"+e,r=o in document;if(!r){var l=document.createElement("div");l.setAttribute(o,"return;"),r="function"==typeof l[o]}return!r&&n&&"wheel"===e&&(r=document.implementation.hasFeature("Events.wheel","3.0")),r}var n,i=o(188);i.canUseDOM&&(n=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("","")),e.exports=r},function(e,t,o){"use strict";function r(e){var t=0,o=0,r=0,n=0;return"detail"in e&&(o=e.detail),"wheelDelta"in e&&(o=-e.wheelDelta/120),"wheelDeltaY"in e&&(o=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=o,o=0),r=t*l,n=o*l,"deltaY"in e&&(n=e.deltaY),"deltaX"in e&&(r=e.deltaX),(r||n)&&e.deltaMode&&(1==e.deltaMode?(r*=a,n*=a):(r*=s,n*=s)),r&&!t&&(t=r<1?-1:1),n&&!o&&(o=n<1?-1:1),{spinX:t,spinY:o,pixelX:r,pixelY:n}}var n=o(189),i=o(190),l=10,a=40,s=800;r.getEventType=function(){return n.firefox()?"DOMMouseScroll":i("wheel")?"wheel":"mousewheel"},e.exports=r},function(e,t){function o(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function n(e){if(d===setTimeout)return setTimeout(e,0);if((d===o||!d)&&setTimeout)return d=setTimeout,setTimeout(e,0);try{return d(e,0)}catch(t){try{return d.call(null,e,0)}catch(t){return d.call(this,e,0)}}}function i(e){if(u===clearTimeout)return clearTimeout(e);if((u===r||!u)&&clearTimeout)return u=clearTimeout,clearTimeout(e);try{return u(e)}catch(t){try{return u.call(null,e)}catch(t){return u.call(this,e)}}}function l(){b&&f&&(b=!1,f.length?h=f.concat(h):g=-1,h.length&&a())}function a(){if(!b){var e=n(l);b=!0;for(var t=h.length;t;){for(f=h,h=[];++g1)for(var o=1;o0},e.prototype.connect_=function(){f&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),v?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){f&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,o=void 0===t?"":t;_.some(function(e){return!!~o.indexOf(e)})&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),y=function(e,t){for(var o=0,r=Object.keys(t);o0},e}(),z="undefined"!=typeof WeakMap?new WeakMap:new p,O=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var o=x.getInstance(),r=new S(t,o,this);z.set(this,r)}return e}();["observe","unobserve","disconnect"].forEach(function(e){O.prototype[e]=function(){var t;return(t=z.get(this))[e].apply(t,arguments)}});var A=function(){return void 0!==h.ResizeObserver?h.ResizeObserver:O}();t.default=A}.call(t,o(28))},function(e,t,o){(function(e,t){!function(e,o){"use strict";function r(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),o=0;o=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},o(194),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(t,o(28))},function(e,t,o){"use strict";var r=o(205),n=o(75),i=(o(203),o(46)),l=o.i(i.a)(n.a,r.a,r.b,!1,null,null,null);t.a=l.exports},function(e,t,o){"use strict";var r=o(206),n=o(76),i=o(46),l=o.i(i.a)(n.a,r.a,r.b,!1,null,null,null);t.a=l.exports},function(e,t,o){"use strict";var r=o(195);o.n(r)},function(e,t,o){"use strict";var r=o(196);o.n(r)},function(e,t,o){"use strict";var r=o(207);o.d(t,"a",function(){return r.a}),o.d(t,"b",function(){return r.b})},function(e,t,o){"use strict";var r=o(208);o.d(t,"a",function(){return r.a}),o.d(t,"b",function(){return r.b})},function(e,t,o){"use strict";var r=o(209);o.d(t,"a",function(){return r.a}),o.d(t,"b",function(){return r.b})},function(e,t,o){"use strict";o.d(t,"a",function(){return r}),o.d(t,"b",function(){return n});var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("div",{attrs:{id:"app"}},[o("header",{staticClass:"grid-content header-color"},[o("el-row",[o("a",{staticClass:"brand",attrs:{href:"#"}},[e._v("frp client")])])],1),e._v(" "),o("section",[o("el-row",[o("el-col",{attrs:{id:"side-nav",xs:24,md:4}},[o("el-menu",{attrs:{"default-active":"1",mode:"vertical",theme:"light",router:"false"},on:{select:e.handleSelect}},[o("el-menu-item",{attrs:{index:"/"}},[e._v("Overview")]),e._v(" "),o("el-menu-item",{attrs:{index:"/configure"}},[e._v("Configure")]),e._v(" "),o("el-menu-item",{attrs:{index:""}},[e._v("Help")])],1)],1),e._v(" "),o("el-col",{attrs:{xs:24,md:20}},[o("div",{attrs:{id:"content"}},[o("router-view")],1)])],1)],1),e._v(" "),o("footer")])},n=[]},function(e,t,o){"use strict";o.d(t,"a",function(){return r}),o.d(t,"b",function(){return n});var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("div",[o("el-row",{attrs:{id:"head"}},[o("el-button",{attrs:{type:"primary"},on:{click:e.fetchData}},[e._v("Refresh")]),e._v(" "),o("el-button",{attrs:{type:"primary"},on:{click:e.uploadConfig}},[e._v("Upload")])],1),e._v(" "),o("el-input",{attrs:{type:"textarea",autosize:"",placeholder:"frpc configrue file, can not be empty..."},model:{value:e.textarea,callback:function(t){e.textarea=t},expression:"textarea"}})],1)},n=[]},function(e,t,o){"use strict";o.d(t,"a",function(){return r}),o.d(t,"b",function(){return n});var r=function(){var e=this,t=e.$createElement,o=e._self._c||t;return o("div",[o("el-row",[o("el-col",{attrs:{md:24}},[o("div",[o("el-table",{staticStyle:{width:"100%"},attrs:{data:e.status,stripe:"","default-sort":{prop:"type",order:"ascending"}}},[o("el-table-column",{attrs:{prop:"name",label:"name"}}),e._v(" "),o("el-table-column",{attrs:{prop:"type",label:"type",width:"150"}}),e._v(" "),o("el-table-column",{attrs:{prop:"local_addr",label:"local address",width:"200"}}),e._v(" "),o("el-table-column",{attrs:{prop:"plugin",label:"plugin",width:"200"}}),e._v(" "),o("el-table-column",{attrs:{prop:"remote_addr",label:"remote address"}}),e._v(" "),o("el-table-column",{attrs:{prop:"status",label:"status",width:"150"}}),e._v(" "),o("el-table-column",{attrs:{prop:"err",label:"info"}})],1)],1)])],1)],1)},n=[]},function(e,t,o){"use strict";function r(e,t){for(var o in t)e[o]=t[o];return e}function n(e){try{return decodeURIComponent(e)}catch(e){}return e}function i(e,t,o){void 0===t&&(t={});var r,n=o||l;try{r=n(e||"")}catch(e){r={}}for(var i in t){var a=t[i];r[i]=Array.isArray(a)?a.map(Ve):Ve(a)}return r}function l(e){var t={};return(e=e.trim().replace(/^(\?|#|&)/,""))?(e.split("&").forEach(function(e){var o=e.replace(/\+/g," ").split("="),r=n(o.shift()),i=o.length>0?n(o.join("=")):null;void 0===t[r]?t[r]=i:Array.isArray(t[r])?t[r].push(i):t[r]=[t[r],i]}),t):t}function a(e){var t=e?Object.keys(e).map(function(t){var o=e[t];if(void 0===o)return"";if(null===o)return Ue(t);if(Array.isArray(o)){var r=[];return o.forEach(function(e){void 0!==e&&(null===e?r.push(Ue(t)):r.push(Ue(t)+"="+Ue(e)))}),r.join("&")}return Ue(t)+"="+Ue(o)}).filter(function(e){return e.length>0}).join("&"):null;return t?"?"+t:""}function s(e,t,o,r){var n=r&&r.options.stringifyQuery,i=t.query||{};try{i=c(i)}catch(e){}var l={name:t.name||e&&e.name,meta:e&&e.meta||{},path:t.path||"/",hash:t.hash||"",query:i,params:t.params||{},fullPath:u(t,n),matched:e?d(e):[]};return o&&(l.redirectedFrom=u(o,n)),Object.freeze(l)}function c(e){if(Array.isArray(e))return e.map(c);if(e&&"object"==typeof e){var t={};for(var o in e)t[o]=c(e[o]);return t}return e}function d(e){for(var t=[];e;)t.unshift(e),e=e.parent;return t}function u(e,t){var o=e.path,r=e.query;void 0===r&&(r={});var n=e.hash;void 0===n&&(n="");var i=t||a;return(o||"/")+i(r)+n}function p(e,t){return t===Ke?e===t:!!t&&(e.path&&t.path?e.path.replace(Ye,"")===t.path.replace(Ye,"")&&e.hash===t.hash&&f(e.query,t.query):!(!e.name||!t.name)&&e.name===t.name&&e.hash===t.hash&&f(e.query,t.query)&&f(e.params,t.params))}function f(e,t){if(void 0===e&&(e={}),void 0===t&&(t={}),!e||!t)return e===t;var o=Object.keys(e).sort(),r=Object.keys(t).sort();return o.length===r.length&&o.every(function(o,n){var i=e[o];if(r[n]!==o)return!1;var l=t[o];return null==i||null==l?i===l:"object"==typeof i&&"object"==typeof l?f(i,l):String(i)===String(l)})}function h(e,t){return 0===e.path.replace(Ye,"/").indexOf(t.path.replace(Ye,"/"))&&(!t.hash||e.hash===t.hash)&&b(e.query,t.query)}function b(e,t){for(var o in t)if(!(o in e))return!1;return!0}function g(e){for(var t=0;t=0&&(t=e.slice(r),e=e.slice(0,r));var n=e.indexOf("?");return n>=0&&(o=e.slice(n+1),e=e.slice(0,n)),{path:e,query:o,hash:t}}function y(e){return e.replace(/\/\//g,"/")}function w(e,t){for(var o,r=[],n=0,i=0,l="",a=t&&t.delimiter||"/";null!=(o=ot.exec(e));){var s=o[0],c=o[1],d=o.index;if(l+=e.slice(i,d),i=d+s.length,c)l+=c[1];else{var u=e[i],p=o[2],f=o[3],h=o[4],b=o[5],g=o[6],m=o[7];l&&(r.push(l),l="");var _=null!=p&&null!=u&&u!==p,v="+"===g||"*"===g,x="?"===g||"*"===g,y=o[2]||a,w=h||b;r.push({name:f||n++,prefix:p||"",delimiter:y,optional:x,repeat:v,partial:_,asterisk:!!m,pattern:w?z(w):m?".*":"[^"+S(y)+"]+?"})}}return i-1&&(n.params[p]=o.params[p]);return n.path=I(a.path,n.params,'named route "'+i+'"'),l(a,n,r)}if(n.path){n.params={};for(var f=0;f=e.length?o():e[n]?t(e[n],function(){r(n+1)}):r(n+1)};r(0)}function ue(e,t){return be(e,t,bt.redirected,'Redirected when going from "'+e.fullPath+'" to "'+ge(t)+'" via a navigation guard.')}function pe(e,t){var o=be(e,t,bt.duplicated,'Avoided redundant navigation to current location: "'+e.fullPath+'".');return o.name="NavigationDuplicated",o}function fe(e,t){return be(e,t,bt.cancelled,'Navigation cancelled from "'+e.fullPath+'" to "'+t.fullPath+'" with a new navigation.')}function he(e,t){return be(e,t,bt.aborted,'Navigation aborted from "'+e.fullPath+'" to "'+t.fullPath+'" via a navigation guard.')}function be(e,t,o,r){var n=new Error(r);return n._isRouter=!0,n.from=e,n.to=t,n.type=o,n}function ge(e){if("string"==typeof e)return e;if("path"in e)return e.path;var t={};return gt.forEach(function(o){o in e&&(t[o]=e[o])}),JSON.stringify(t,null,2)}function me(e){return Object.prototype.toString.call(e).indexOf("Error")>-1}function _e(e,t){return me(e)&&e._isRouter&&(null==t||e.type===t)}function ve(e){return function(t,o,r){var n=!1,i=0,l=null;xe(e,function(e,t,o,a){if("function"==typeof e&&void 0===e.cid){n=!0,i++;var s,c=ke(function(t){we(t)&&(t=t.default),e.resolved="function"==typeof t?t:rt.extend(t),o.components[a]=t,--i<=0&&r()}),d=ke(function(e){var t="Failed to resolve async component "+a+": "+e;l||(l=me(e)?e:new Error(t),r(l))});try{s=e(c,d)}catch(e){d(e)}if(s)if("function"==typeof s.then)s.then(c,d);else{var u=s.component;u&&"function"==typeof u.then&&u.then(c,d)}}}),n||r()}}function xe(e,t){return ye(e.map(function(e){return Object.keys(e.components).map(function(o){return t(e.components[o],e.instances[o],e,o)})}))}function ye(e){return Array.prototype.concat.apply([],e)}function we(e){return e.__esModule||mt&&"Module"===e[Symbol.toStringTag]}function ke(e){var t=!1;return function(){for(var o=[],r=arguments.length;r--;)o[r]=arguments[r];if(!t)return t=!0,e.apply(this,o)}}function Fe(e){if(!e)if(ct){var t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^https?:\/\/[^\/]+/,"")}else e="/";return"/"!==e.charAt(0)&&(e="/"+e),e.replace(/\/$/,"")}function Ce(e,t){var o,r=Math.max(e.length,t.length);for(o=0;o=0?t.slice(0,o):t)+"#"+e}function Ne(e){ht?se(Pe(e)):window.location.hash=e}function De(e){ht?ce(Pe(e)):window.location.replace(Pe(e))}function Re(e,t){return e.push(t),function(){var o=e.indexOf(t);o>-1&&e.splice(o,1)}}function Be(e,t,o){var r="hash"===o?"#"+t:t;return e?y(e+"/"+r):r}var He=/[!'()*]/g,We=function(e){return"%"+e.charCodeAt(0).toString(16)},qe=/%2C/g,Ue=function(e){return encodeURIComponent(e).replace(He,We).replace(qe,",")},Ve=function(e){return null==e||"object"==typeof e?e:String(e)},Ye=/\/?$/,Ke=s(null,{path:"/"}),Xe={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"}},render:function(e,t){var o=t.props,n=t.children,i=t.parent,l=t.data;l.routerView=!0;for(var a=i.$createElement,s=o.name,c=i.$route,d=i._routerViewCache||(i._routerViewCache={}),u=0,p=!1;i&&i._routerRoot!==i;){var f=i.$vnode?i.$vnode.data:{};f.routerView&&u++,f.keepAlive&&i._directInactive&&i._inactive&&(p=!0),i=i.$parent}if(l.routerViewDepth=u,p){var h=d[s],b=h&&h.component;return b?(h.configProps&&m(b,l,h.route,h.configProps),a(b,l,n)):a()}var _=c.matched[u],v=_&&_.components[s];if(!_||!v)return d[s]=null,a();d[s]={component:v},l.registerRouteInstance=function(e,t){var o=_.instances[s];(t&&o!==e||!t&&o===e)&&(_.instances[s]=t)},(l.hook||(l.hook={})).prepatch=function(e,t){_.instances[s]=t.componentInstance},l.hook.init=function(e){e.data.keepAlive&&e.componentInstance&&e.componentInstance!==_.instances[s]&&(_.instances[s]=e.componentInstance),g(c)};var x=_.props&&_.props[s];return x&&(r(d[s],{route:c,configProps:x}),m(v,l,c,x)),a(v,l,n)}},Ge=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)},Je=L,Ze=w,Qe=k,et=E,tt=M,ot=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");Je.parse=Ze,Je.compile=Qe,Je.tokensToFunction=et,Je.tokensToRegExp=tt;var rt,nt=Object.create(null),it=[String,Object],lt=[String,Array],at=function(){},st={name:"RouterLink",props:{to:{type:it,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,ariaCurrentValue:{type:String,default:"page"},event:{type:lt,default:"click"}},render:function(e){var t=this,o=this.$router,n=this.$route,i=o.resolve(this.to,n,this.append),l=i.location,a=i.route,c=i.href,d={},u=o.options.linkActiveClass,f=o.options.linkExactActiveClass,b=null==u?"router-link-active":u,g=null==f?"router-link-exact-active":f,m=null==this.activeClass?b:this.activeClass,_=null==this.exactActiveClass?g:this.exactActiveClass,v=a.redirectedFrom?s(null,P(a.redirectedFrom),null,o):a;d[_]=p(n,v),d[m]=this.exact?d[_]:h(n,v);var x=d[_]?this.ariaCurrentValue:null,y=function(e){N(e)&&(t.replace?o.replace(l,at):o.push(l,at))},w={click:N};Array.isArray(this.event)?this.event.forEach(function(e){w[e]=y}):w[this.event]=y;var k={class:d},F=!this.$scopedSlots.$hasNormal&&this.$scopedSlots.default&&this.$scopedSlots.default({href:c,route:a,navigate:y,isActive:d[m],isExactActive:d[_]});if(F){if(1===F.length)return F[0];if(F.length>1||!F.length)return 0===F.length?e():e("span",{},F)}if("a"===this.tag)k.on=w,k.attrs={href:c,"aria-current":x};else{var C=D(this.$slots.default);if(C){C.isStatic=!1;var E=C.data=r({},C.data);E.on=E.on||{};for(var S in E.on){var z=E.on[S];S in w&&(E.on[S]=Array.isArray(z)?z:[z])}for(var O in w)O in E.on?E.on[O].push(w[O]):E.on[O]=y;var A=C.data.attrs=r({},C.data.attrs);A.href=c,A["aria-current"]=x}else k.on=w}return e(this.tag,k,this.$slots.default)}},ct="undefined"!=typeof window,dt=ct&&window.performance&&window.performance.now?window.performance:Date,ut=K(),pt=Object.create(null),ft=/^#\d/,ht=ct&&function(){var e=window.navigator.userAgent;return(-1===e.indexOf("Android 2.")&&-1===e.indexOf("Android 4.0")||-1===e.indexOf("Mobile Safari")||-1!==e.indexOf("Chrome")||-1!==e.indexOf("Windows Phone"))&&window.history&&"function"==typeof window.history.pushState}(),bt={redirected:2,aborted:4,cancelled:8,duplicated:16},gt=["params","query","hash"],mt="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,_t=function(e,t){this.router=e,this.base=Fe(t),this.current=Ke,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};_t.prototype.listen=function(e){this.cb=e},_t.prototype.onReady=function(e,t){this.ready?e():(this.readyCbs.push(e),t&&this.readyErrorCbs.push(t))},_t.prototype.onError=function(e){this.errorCbs.push(e)},_t.prototype.transitionTo=function(e,t,o){var r,n=this;try{r=this.router.match(e,this.current)}catch(e){throw this.errorCbs.forEach(function(t){t(e)}),e}var i=this.current;this.confirmTransition(r,function(){n.updateRoute(r),t&&t(r),n.ensureURL(),n.router.afterHooks.forEach(function(e){e&&e(r,i)}),n.ready||(n.ready=!0,n.readyCbs.forEach(function(e){e(r)}))},function(e){o&&o(e),e&&!n.ready&&(_e(e,bt.redirected)&&i===Ke||(n.ready=!0,n.readyErrorCbs.forEach(function(t){t(e)})))})},_t.prototype.confirmTransition=function(e,t,o){var r=this,n=this.current;this.pending=e;var i=function(e){!_e(e)&&me(e)&&(r.errorCbs.length?r.errorCbs.forEach(function(t){t(e)}):console.error(e)),o&&o(e)},l=e.matched.length-1,a=n.matched.length-1;if(p(e,n)&&l===a&&e.matched[l]===n.matched[a])return this.ensureURL(),i(pe(n,e));var s=Ce(this.current.matched,e.matched),c=s.updated,d=s.deactivated,u=s.activated,f=[].concat(ze(d),this.router.beforeHooks,Oe(c),u.map(function(e){return e.beforeEnter}),ve(u)),h=function(t,o){if(r.pending!==e)return i(fe(n,e));try{t(e,n,function(t){!1===t?(r.ensureURL(!0),i(he(n,e))):me(t)?(r.ensureURL(!0),i(t)):"string"==typeof t||"object"==typeof t&&("string"==typeof t.path||"string"==typeof t.name)?(i(ue(n,e)),"object"==typeof t&&t.replace?r.replace(t):r.push(t)):o(t)})}catch(e){i(e)}};de(f,h,function(){de($e(u).concat(r.router.resolveHooks),h,function(){if(r.pending!==e)return i(fe(n,e));r.pending=null,t(e),r.router.app&&r.router.app.$nextTick(function(){g(e)})})})},_t.prototype.updateRoute=function(e){this.current=e,this.cb&&this.cb(e)},_t.prototype.setupListeners=function(){},_t.prototype.teardown=function(){this.listeners.forEach(function(e){e()}),this.listeners=[],this.current=Ke,this.pending=null};var vt=function(e){function t(t,o){e.call(this,t,o),this._startLocation=je(this.base)}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.setupListeners=function(){var e=this;if(!(this.listeners.length>0)){var t=this.router,o=t.options.scrollBehavior,r=ht&&o;r&&this.listeners.push(J());var n=function(){var o=e.current,n=je(e.base);e.current===Ke&&n===e._startLocation||e.transitionTo(n,function(e){r&&Z(t,e,o,!0)})};window.addEventListener("popstate",n),this.listeners.push(function(){window.removeEventListener("popstate",n)})}},t.prototype.go=function(e){window.history.go(e)},t.prototype.push=function(e,t,o){var r=this,n=this,i=n.current;this.transitionTo(e,function(e){se(y(r.base+e.fullPath)),Z(r.router,e,i,!1),t&&t(e)},o)},t.prototype.replace=function(e,t,o){var r=this,n=this,i=n.current;this.transitionTo(e,function(e){ce(y(r.base+e.fullPath)),Z(r.router,e,i,!1),t&&t(e)},o)},t.prototype.ensureURL=function(e){if(je(this.base)!==this.current.fullPath){var t=y(this.base+this.current.fullPath);e?se(t):ce(t)}},t.prototype.getCurrentLocation=function(){return je(this.base)},t}(_t),xt=function(e){function t(t,o,r){e.call(this,t,o),r&&Me(this.base)||Le()}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.setupListeners=function(){var e=this;if(!(this.listeners.length>0)){var t=this.router,o=t.options.scrollBehavior,r=ht&&o;r&&this.listeners.push(J());var n=function(){var t=e.current;Le()&&e.transitionTo(Ie(),function(o){r&&Z(e.router,o,t,!0),ht||De(o.fullPath)})},i=ht?"popstate":"hashchange";window.addEventListener(i,n),this.listeners.push(function(){window.removeEventListener(i,n)})}},t.prototype.push=function(e,t,o){var r=this,n=this,i=n.current;this.transitionTo(e,function(e){Ne(e.fullPath),Z(r.router,e,i,!1),t&&t(e)},o)},t.prototype.replace=function(e,t,o){var r=this,n=this,i=n.current;this.transitionTo(e,function(e){De(e.fullPath),Z(r.router,e,i,!1),t&&t(e)},o)},t.prototype.go=function(e){window.history.go(e)},t.prototype.ensureURL=function(e){var t=this.current.fullPath;Ie()!==t&&(e?Ne(t):De(t))},t.prototype.getCurrentLocation=function(){return Ie()},t}(_t),yt=function(e){function t(t,o){e.call(this,t,o),this.stack=[],this.index=-1}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.push=function(e,t,o){var r=this;this.transitionTo(e,function(e){r.stack=r.stack.slice(0,r.index+1).concat(e),r.index++,t&&t(e)},o)},t.prototype.replace=function(e,t,o){var r=this;this.transitionTo(e,function(e){r.stack=r.stack.slice(0,r.index).concat(e),t&&t(e)},o)},t.prototype.go=function(e){var t=this,o=this.index+e;if(!(o<0||o>=this.stack.length)){var r=this.stack[o];this.confirmTransition(r,function(){var e=t.current;t.index=o,t.updateRoute(r),t.router.afterHooks.forEach(function(t){t&&t(r,e)})},function(e){_e(e,bt.duplicated)&&(t.index=o)})}},t.prototype.getCurrentLocation=function(){var e=this.stack[this.stack.length-1];return e?e.fullPath:"/"},t.prototype.ensureURL=function(){},t}(_t),wt=function(e){void 0===e&&(e={}),this.app=null,this.apps=[],this.options=e,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=U(e.routes||[],this);var t=e.mode||"hash";switch(this.fallback="history"===t&&!ht&&!1!==e.fallback,this.fallback&&(t="hash"),ct||(t="abstract"),this.mode=t,t){case"history":this.history=new vt(this,e.base);break;case"hash":this.history=new xt(this,e.base,this.fallback);break;case"abstract":this.history=new yt(this,e.base)}},kt={currentRoute:{configurable:!0}};wt.prototype.match=function(e,t,o){return this.matcher.match(e,t,o)},kt.currentRoute.get=function(){return this.history&&this.history.current},wt.prototype.init=function(e){var t=this;if(this.apps.push(e),e.$once("hook:destroyed",function(){var o=t.apps.indexOf(e);o>-1&&t.apps.splice(o,1),t.app===e&&(t.app=t.apps[0]||null),t.app||t.history.teardown()}),!this.app){this.app=e;var o=this.history;if(o instanceof vt||o instanceof xt){var r=function(e){var r=o.current,n=t.options.scrollBehavior;ht&&n&&"fullPath"in e&&Z(t,e,r,!1)},n=function(e){o.setupListeners(),r(e)};o.transitionTo(o.getCurrentLocation(),n,n)}o.listen(function(e){t.apps.forEach(function(t){t._route=e})})}},wt.prototype.beforeEach=function(e){return Re(this.beforeHooks,e)},wt.prototype.beforeResolve=function(e){return Re(this.resolveHooks,e)},wt.prototype.afterEach=function(e){return Re(this.afterHooks,e)},wt.prototype.onReady=function(e,t){this.history.onReady(e,t)},wt.prototype.onError=function(e){this.history.onError(e)},wt.prototype.push=function(e,t,o){var r=this;if(!t&&!o&&"undefined"!=typeof Promise)return new Promise(function(t,o){r.history.push(e,t,o)});this.history.push(e,t,o)},wt.prototype.replace=function(e,t,o){var r=this;if(!t&&!o&&"undefined"!=typeof Promise)return new Promise(function(t,o){r.history.replace(e,t,o)});this.history.replace(e,t,o)},wt.prototype.go=function(e){this.history.go(e)},wt.prototype.back=function(){this.go(-1)},wt.prototype.forward=function(){this.go(1)},wt.prototype.getMatchedComponents=function(e){var t=e?e.matched?e:this.resolve(e).route:this.currentRoute;return t?[].concat.apply([],t.matched.map(function(e){return Object.keys(e.components).map(function(t){return e.components[t]})})):[]},wt.prototype.resolve=function(e,t,o){t=t||this.history.current;var r=P(e,t,o,this),n=this.match(r,t),i=n.redirectedFrom||n.fullPath;return{location:r,route:n,href:Be(this.history.base,i,this.mode),normalizedTo:r,resolved:n}},wt.prototype.addRoutes=function(e){this.matcher.addRoutes(e),this.history.current!==Ke&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(wt.prototype,kt),wt.install=R,wt.version="3.4.9",wt.isNavigationFailure=_e,wt.NavigationFailureType=bt,ct&&window.Vue&&window.Vue.use(wt),t.a=wt}],[126]); \ No newline at end of file diff --git a/assets/frps/embed.go b/assets/frps/embed.go deleted file mode 100644 index 3fc0d3a5524..00000000000 --- a/assets/frps/embed.go +++ /dev/null @@ -1,14 +0,0 @@ -package frpc - -import ( - "embed" - - "github.com/fatedier/frp/assets" -) - -//go:embed static/* -var content embed.FS - -func init() { - assets.Register(content) -} diff --git a/assets/frps/static/535877f50039c0cb49a6196a5b7517cd.woff b/assets/frps/static/535877f50039c0cb49a6196a5b7517cd.woff deleted file mode 100644 index 02b9a2539e4..00000000000 Binary files a/assets/frps/static/535877f50039c0cb49a6196a5b7517cd.woff and /dev/null differ diff --git a/assets/frps/static/732389ded34cb9c52dd88271f1345af9.ttf b/assets/frps/static/732389ded34cb9c52dd88271f1345af9.ttf deleted file mode 100644 index 91b74de3677..00000000000 Binary files a/assets/frps/static/732389ded34cb9c52dd88271f1345af9.ttf and /dev/null differ diff --git a/assets/frps/static/index.html b/assets/frps/static/index.html deleted file mode 100644 index 7a35c049605..00000000000 --- a/assets/frps/static/index.html +++ /dev/null @@ -1 +0,0 @@ - frps dashboard
\ No newline at end of file diff --git a/assets/frps/static/manifest.js b/assets/frps/static/manifest.js deleted file mode 100644 index bf66eb524ab..00000000000 --- a/assets/frps/static/manifest.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];lt.get("hoverLayerThreshold")&&!X.node&&n.traverse(function(e){e.isGroup||(e.useHoverLayer=!0)})}function x(e,t){var n=0;t.group.traverse(function(e){"group"===e.type||e.ignore||n++});var o=+e.get("progressive"),i=n>e.get("progressiveThreshold")&&o&&!X.node;i&&t.group.traverse(function(e){e.isGroup||(e.progressive=i?Math.floor(n++/o):-1,i&&e.stopAnimation(!0))});var r=e.get("blendMode")||null;t.group.traverse(function(e){e.isGroup||e.setStyle("blend",r)})}function y(e,t){var n=e.get("z"),o=e.get("zlevel");t.group.traverse(function(e){"group"!==e.type&&(null!=n&&(e.z=n),null!=o&&(e.zlevel=o))})}function _(e){var t=e._coordSysMgr;return Y.extend(new ee(e),{getCoordinateSystems:Y.bind(t.getCoordinateSystems,t),getComponentByElement:function(t){for(;t;){var n=t.__ecComponentInfo;if(null!=n)return e._model.getComponent(n.mainType,n.index);t=t.parent}}})}function w(e){function t(e,t){for(var o=0;o=0&&Y.each(e,function(e){var i=e.coordinateSystem;if(i&&i.containPoint)n|=!!i.containPoint(t);else if("seriesModels"===o){var r=this._chartsMap[e.__viewId];r&&r.containPoint&&(n|=r.containPoint(t,e))}},this)},this),!!n},Se.getVisual=function(e,t){var n=this._model;e=ce.parseFinder(n,e,{defaultMainType:"series"});var o=e.seriesModel,i=o.getData(),r=e.hasOwnProperty("dataIndexInside")?e.dataIndexInside:e.hasOwnProperty("dataIndex")?i.indexOfRawIndex(e.dataIndex):null;return null!=r?i.getItemVisual(r,t):i.getVisual(t)},Se.getViewOfComponentModel=function(e){return this._componentsMap[e.__viewId]},Se.getViewOfSeriesModel=function(e){return this._chartsMap[e.__viewId]};var Me={update:function(e){var t=this._model,n=this._api,o=this._coordSysMgr,i=this._zr;if(t){t.restoreData(),o.create(this._model,this._api),p.call(this,t,n),h.call(this,t),o.update(t,n),m.call(this,t,e),v.call(this,t,e);var r=t.get("backgroundColor")||"transparent",a=i.painter;if(a.isSingleCanvas&&a.isSingleCanvas())i.configLayer(0,{clearColor:r});else{if(!X.canvasSupported){var l=Z.parse(r);r=Z.stringify(l,"rgb"),0===l[3]&&(r="transparent")}r.colorStops||r.image?(i.configLayer(0,{clearColor:r}),this.__hasGradientOrPatternBg=!0,this._dom.style.background="transparent"):(this.__hasGradientOrPatternBg&&i.configLayer(0,{clearColor:null}),this.__hasGradientOrPatternBg=!1,this._dom.style.background=r)}he(Oe,function(e){e(t,n)})}},updateView:function(e){var t=this._model;t&&(t.eachSeries(function(e){e.getData().clearAllVisual()}),m.call(this,t,e),d.call(this,"updateView",t,e))},updateVisual:function(e){var t=this._model;t&&(t.eachSeries(function(e){e.getData().clearAllVisual()}),m.call(this,t,e,!0),d.call(this,"updateVisual",t,e))},updateLayout:function(e){var t=this._model;t&&(g.call(this,t,e),d.call(this,"updateLayout",t,e))},prepareAndUpdate:function(e){var t=this._model;f.call(this,"component",t),f.call(this,"chart",t),Me.update.call(this,e)}};Se.resize=function(e){this[_e]=!0,this._zr.resize(e);var t=this._model&&this._model.resetOption("media");Me[t?"prepareAndUpdate":"update"].call(this),this._loadingFX&&this._loadingFX.resize(),this[_e]=!1;var n=e&&e.silent;c.call(this,n),u.call(this,n)},Se.showLoading=function(e,t){if(Y.isObject(e)&&(t=e,e=""),e=e||"default",this.hideLoading(),De[e]){var n=De[e](this._api,t),o=this._zr;this._loadingFX=n,o.add(n)}},Se.hideLoading=function(){this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null},Se.makeActionFromEvent=function(e){var t=Y.extend({},e);return t.type=Te[e.type],t},Se.dispatchAction=function(e,t){if(Y.isObject(t)||(t={silent:!!t}),Ae[e.type]&&this._model){if(this[_e])return void this._pendingActions.push(e);s.call(this,e,t.silent),t.flush?this._zr.flush(!0):!1!==t.flush&&X.browser.weChat&&this._throttledZrFlush(),c.call(this,t.silent),u.call(this,t.silent)}},Se.on=o("on"),Se.off=o("off"),Se.one=o("one");var Ce=["click","dblclick","mouseover","mouseout","mousemove","mousedown","mouseup","globalout","contextmenu"];Se._initEvents=function(){he(Ce,function(e){this._zr.on(e,function(t){var n,o=this.getModel(),i=t.target;if("globalout"===e)n={};else if(i&&null!=i.dataIndex){var r=i.dataModel||o.getSeriesByIndex(i.seriesIndex);n=r&&r.getDataParams(i.dataIndex,i.dataType)||{}}else i&&i.eventData&&(n=Y.extend({},i.eventData));n&&(n.event=t,n.type=e,this.trigger(e,n))},this)},this),he(Te,function(e,t){this._messageCenter.on(t,function(e){this.trigger(t,e)},this)},this)},Se.isDisposed=function(){return this._disposed},Se.clear=function(){this.setOption({series:[]},!0)},Se.dispose=function(){if(!this._disposed){this._disposed=!0;var e=this._api,t=this._model;he(this._componentsViews,function(n){n.dispose(t,e)}),he(this._chartsViews,function(n){n.dispose(t,e)}),this._zr.dispose(),delete ze[this.id]}},Y.mixin(r,J);var Ae={},Te={},Ee=[],Ie=[],Oe=[],Le=[],Pe={},De={},ze={},Re={},Ne=new Date-0,Be=new Date-0,Fe="_echarts_instance_",Ve={},je=M;N(2e3,fe),I(oe),B("default",pe),P({type:"highlight",event:"highlight",update:"highlight"},Y.noop),P({type:"downplay",event:"downplay",update:"downplay"},Y.noop);var $e={};t.version="3.8.5",t.dependencies=me,t.PRIORITY=ye,t.init=k,t.connect=S,t.disConnect=M,t.disconnect=je,t.dispose=C,t.getInstanceByDom=A,t.getInstanceById=T,t.registerTheme=E,t.registerPreprocessor=I,t.registerProcessor=O,t.registerPostUpdate=L,t.registerAction=P,t.registerCoordinateSystem=D,t.getCoordinateSystemDimensions=z,t.registerLayout=R,t.registerVisual=N,t.registerLoading=B,t.extendComponentModel=F,t.extendComponentView=V,t.extendSeriesModel=j,t.extendChartView=$,t.setCanvasCreator=H,t.registerMap=W,t.getMap=G,t.dataTool=$e;var He=n(214);!function(){for(var e in He)He.hasOwnProperty(e)&&(t[e]=He[e])}()},function(e,t,n){function o(e){return X.extend(e)}function i(e,t){return U.extendFromString(e,t)}function r(e,t,n,o){var i=U.createFromString(e,t),r=i.getBoundingRect();return n&&("center"===o&&(n=l(n,r)),s(i,n)),i}function a(e,t,n){var o=new J({style:{image:e,x:t.x,y:t.y,width:t.width,height:t.height},onload:function(e){if("center"===n){var i={width:e.width,height:e.height};o.setStyle(l(t,i))}}});return o}function l(e,t){var n,o=t.width/t.height,i=e.height*o;return i<=e.width?n=e.height:(i=e.width,n=i/o),{x:e.x+e.width/2-i/2,y:e.y+e.height/2-n/2,width:i,height:n}}function s(e,t){if(e.applyTransform){var n=e.getBoundingRect(),o=n.calculateTransform(t);e.applyTransform(o)}}function c(e){var t=e.shape,n=e.style.lineWidth;return he(2*t.x1)===he(2*t.x2)&&(t.x1=t.x2=d(t.x1,n,!0)),he(2*t.y1)===he(2*t.y2)&&(t.y1=t.y2=d(t.y1,n,!0)),e}function u(e){var t=e.shape,n=e.style.lineWidth,o=t.x,i=t.y,r=t.width,a=t.height;return t.x=d(t.x,n,!0),t.y=d(t.y,n,!0),t.width=Math.max(d(o+r,n,!1)-t.x,0===r?0:1),t.height=Math.max(d(i+a,n,!1)-t.y,0===a?0:1),e}function d(e,t,n){var o=he(2*e);return(o+he(t))%2==0?o/2:(o+(n?1:-1))/2}function f(e){return null!=e&&"none"!=e}function p(e){return"string"==typeof e?q.lift(e,-.1):e}function h(e){if(e.__hoverStlDirty){var t=e.style.stroke,n=e.style.fill,o=e.__hoverStl;o.fill=o.fill||(f(n)?p(n):null),o.stroke=o.stroke||(f(t)?p(t):null);var i={};for(var r in o)null!=o[r]&&(i[r]=e.style[r]);e.__normalStl=i,e.__hoverStlDirty=!1}}function g(e){if(!e.__isHover){if(h(e),e.useHoverLayer)e.__zr&&e.__zr.addHover(e,e.__hoverStl);else{var t=e.style,n=t.insideRollbackOpt;n&&P(t),t.extendFrom(e.__hoverStl),n&&(L(t,t.insideOriginalTextPosition,n),null==t.textFill&&(t.textFill=n.autoColor)),e.dirty(!1),e.z2+=1}e.__isHover=!0}}function m(e){if(e.__isHover){var t=e.__normalStl;e.useHoverLayer?e.__zr&&e.__zr.removeHover(e):(t&&e.setStyle(t),e.z2-=1),e.__isHover=!1}}function v(e){"group"===e.type?e.traverse(function(e){"group"!==e.type&&g(e)}):g(e)}function b(e){"group"===e.type?e.traverse(function(e){"group"!==e.type&&m(e)}):m(e)}function x(e,t){e.__hoverStl=e.hoverStyle||t||{},e.__hoverStlDirty=!0,e.__isHover&&h(e)}function y(e){this.__hoverSilentOnTouch&&e.zrByTouch||!this.__isEmphasis&&v(this)}function _(e){this.__hoverSilentOnTouch&&e.zrByTouch||!this.__isEmphasis&&b(this)}function w(){this.__isEmphasis=!0,v(this)}function k(){this.__isEmphasis=!1,b(this)}function S(e,t,n){e.__hoverSilentOnTouch=n&&n.hoverSilentOnTouch,"group"===e.type?e.traverse(function(e){"group"!==e.type&&x(e,t)}):x(e,t),e.on("mouseover",y).on("mouseout",_),e.on("emphasis",w).on("normal",k)}function M(e,t,n,o,i,r,a){i=i||ve;var l=i.labelFetcher,s=i.labelDataIndex,c=i.labelDimIndex,u=n.getShallow("show"),d=o.getShallow("show"),f=u||d?G.retrieve2(l?l.getFormattedLabel(s,"normal",null,c):null,i.defaultText):null,p=u?f:null,h=d?G.retrieve2(l?l.getFormattedLabel(s,"emphasis",null,c):null,f):null;null==p&&null==h||(C(e,n,r,i),C(t,o,a,i,!0)),e.text=p,t.text=h}function C(e,t,n,o,i){return T(e,t,o,i),n&&G.extend(e,n),e.host&&e.host.dirty&&e.host.dirty(!1),e}function A(e,t,n){var o,i={isRectText:!0};!1===n?o=!0:i.autoColor=n,T(e,t,i,o),e.host&&e.host.dirty&&e.host.dirty(!1)}function T(e,t,n,o){if(n=n||ve,n.isRectText){var i=t.getShallow("position")||(o?null:"inside");"outside"===i&&(i="top"),e.textPosition=i,e.textOffset=t.getShallow("offset");var r=t.getShallow("rotate");null!=r&&(r*=Math.PI/180),e.textRotation=r,e.textDistance=G.retrieve2(t.getShallow("distance"),o?null:5)}var a,l=t.ecModel,s=l&&l.option.textStyle,c=E(t);if(c){a={};for(var u in c)if(c.hasOwnProperty(u)){var d=t.getModel(["rich",u]);I(a[u]={},d,s,n,o)}}return e.rich=a,I(e,t,s,n,o,!0),n.forceRich&&!n.textStyle&&(n.textStyle={}),e}function E(e){for(var t;e&&e!==e.ecModel;){var n=(e.option||ve).rich;if(n){t=t||{};for(var o in n)n.hasOwnProperty(o)&&(t[o]=1)}e=e.parentModel}return t}function I(e,t,n,o,i,r){if(n=!i&&n||ve,e.textFill=O(t.getShallow("color"),o)||n.color,e.textStroke=O(t.getShallow("textBorderColor"),o)||n.textBorderColor,e.textStrokeWidth=G.retrieve2(t.getShallow("textBorderWidth"),n.textBorderWidth),!i){if(r){var a=e.textPosition;e.insideRollback=L(e,a,o),e.insideOriginalTextPosition=a,e.insideRollbackOpt=o}null==e.textFill&&(e.textFill=o.autoColor)}e.fontStyle=t.getShallow("fontStyle")||n.fontStyle,e.fontWeight=t.getShallow("fontWeight")||n.fontWeight,e.fontSize=t.getShallow("fontSize")||n.fontSize,e.fontFamily=t.getShallow("fontFamily")||n.fontFamily,e.textAlign=t.getShallow("align"),e.textVerticalAlign=t.getShallow("verticalAlign")||t.getShallow("baseline"),e.textLineHeight=t.getShallow("lineHeight"),e.textWidth=t.getShallow("width"),e.textHeight=t.getShallow("height"),e.textTag=t.getShallow("tag"),r&&o.disableBox||(e.textBackgroundColor=O(t.getShallow("backgroundColor"),o),e.textPadding=t.getShallow("padding"),e.textBorderColor=O(t.getShallow("borderColor"),o),e.textBorderWidth=t.getShallow("borderWidth"),e.textBorderRadius=t.getShallow("borderRadius"),e.textBoxShadowColor=t.getShallow("shadowColor"),e.textBoxShadowBlur=t.getShallow("shadowBlur"),e.textBoxShadowOffsetX=t.getShallow("shadowOffsetX"),e.textBoxShadowOffsetY=t.getShallow("shadowOffsetY")),e.textShadowColor=t.getShallow("textShadowColor")||n.textShadowColor,e.textShadowBlur=t.getShallow("textShadowBlur")||n.textShadowBlur,e.textShadowOffsetX=t.getShallow("textShadowOffsetX")||n.textShadowOffsetX,e.textShadowOffsetY=t.getShallow("textShadowOffsetY")||n.textShadowOffsetY}function O(e,t){return"auto"!==e?e:t&&t.autoColor?t.autoColor:null}function L(e,t,n){var o,i=n.useInsideStyle;return null==e.textFill&&!1!==i&&(!0===i||n.isRectText&&t&&"string"==typeof t&&t.indexOf("inside")>=0)&&(o={textFill:null,textStroke:e.textStroke,textStrokeWidth:e.textStrokeWidth},e.textFill="#fff",null==e.textStroke&&(e.textStroke=n.autoColor,null==e.textStrokeWidth&&(e.textStrokeWidth=2))),o}function P(e){var t=e.insideRollback;t&&(e.textFill=t.textFill,e.textStroke=t.textStroke,e.textStrokeWidth=t.textStrokeWidth)}function D(e,t){var n=t||t.getModel("textStyle");return[e.fontStyle||n&&n.getShallow("fontStyle")||"",e.fontWeight||n&&n.getShallow("fontWeight")||"",(e.fontSize||n&&n.getShallow("fontSize")||12)+"px",e.fontFamily||n&&n.getShallow("fontFamily")||"sans-serif"].join(" ")}function z(e,t,n,o,i,r){if("function"==typeof i&&(r=i,i=null),o&&o.isAnimationEnabled()){var a=e?"Update":"",l=o.getShallow("animationDuration"+a),s=o.getShallow("animationEasing"+a),c=o.getShallow("animationDelay"+a);"function"==typeof c&&(c=c(i,o.getAnimationDelayParams?o.getAnimationDelayParams(t,i):null)),"function"==typeof l&&(l=l(i)),l>0?t.animateTo(n,l,c||0,s,r,!!r):(t.stopAnimation(),t.attr(n),r&&r())}else t.stopAnimation(),t.attr(n),r&&r()}function R(e,t,n,o,i){z(!0,e,t,n,o,i)}function N(e,t,n,o,i){z(!1,e,t,n,o,i)}function B(e,t){for(var n=Y.identity([]);e&&e!==t;)Y.mul(n,e.getLocalTransform(),n),e=e.parent;return n}function F(e,t,n){return t&&!G.isArrayLike(t)&&(t=K.getLocalTransform(t)),n&&(t=Y.invert([],t)),Z.applyTransform([],e,t)}function V(e,t,n){var o=0===t[4]||0===t[5]||0===t[0]?1:Math.abs(2*t[4]/t[0]),i=0===t[4]||0===t[5]||0===t[2]?1:Math.abs(2*t[4]/t[2]),r=["left"===e?-o:"right"===e?o:0,"top"===e?-i:"bottom"===e?i:0];return r=F(r,t,n),Math.abs(r[0])>Math.abs(r[1])?r[0]>0?"right":"left":r[1]>0?"bottom":"top"}function j(e,t,n,o){function i(e){var t={position:Z.clone(e.position),rotation:e.rotation};return e.shape&&(t.shape=G.extend({},e.shape)),t}if(e&&t){var r=function(e){var t={};return e.traverse(function(e){!e.isGroup&&e.anid&&(t[e.anid]=e)}),t}(e);t.traverse(function(e){if(!e.isGroup&&e.anid){var t=r[e.anid];if(t){var o=i(e);e.attr(i(t)),R(e,o,n,e.dataIndex)}}})}}function $(e,t){return G.map(e,function(e){var n=e[0];n=ge(n,t.x),n=me(n,t.x+t.width);var o=e[1];return o=ge(o,t.y),o=me(o,t.y+t.height),[n,o]})}function H(e,t){var n=ge(e.x,t.x),o=me(e.x+e.width,t.x+t.width),i=ge(e.y,t.y),r=me(e.y+e.height,t.y+t.height);if(o>=n&&r>=i)return{x:n,y:i,width:o-n,height:r-i}}function W(e,t,n){t=G.extend({rectHover:!0},t);var o=t.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},e)return 0===e.indexOf("image://")?(o.image=e.slice(8),G.defaults(o,n),new J(t)):r(e.replace("path://",""),t,n,"center")}var G=n(0),U=n(692),q=n(32),Y=n(24),Z=n(7),X=n(16),K=n(136),J=n(73);t.Image=J;var Q=n(96);t.Group=Q;var ee=n(74);t.Text=ee;var te=n(679);t.Circle=te;var ne=n(685);t.Sector=ne;var oe=n(684);t.Ring=oe;var ie=n(681);t.Polygon=ie;var re=n(682);t.Polyline=re;var ae=n(683);t.Rect=ae;var le=n(680);t.Line=le;var se=n(678);t.BezierCurve=se;var ce=n(677);t.Arc=ce;var ue=n(673);t.CompoundPath=ue;var de=n(246);t.LinearGradient=de;var fe=n(674);t.RadialGradient=fe;var pe=n(10);t.BoundingRect=pe;var he=Math.round,ge=Math.max,me=Math.min,ve={},be=U.mergePath;t.extendShape=o,t.extendPath=i,t.makePath=r,t.makeImage=a,t.mergePath=be,t.resizePath=s,t.subPixelOptimizeLine=c,t.subPixelOptimizeRect=u,t.subPixelOptimize=d,t.setHoverStyle=S,t.setLabelStyle=M,t.setTextStyle=C,t.setText=A,t.getFont=D,t.updateProps=R,t.initProps=N,t.getTransform=B,t.applyTransform=F,t.transformDirection=V,t.groupTransition=j,t.clipPointsByRect=$,t.clipRectByRect=H,t.createIcon=W},function(e,t,n){function o(e){return e.replace(/^\s+/,"").replace(/\s+$/,"")}function i(e,t,n,o){var i=t[1]-t[0],r=n[1]-n[0];if(0===i)return 0===r?n[0]:(n[0]+n[1])/2;if(o)if(i>0){if(e<=t[0])return n[0];if(e>=t[1])return n[1]}else{if(e>=t[0])return n[0];if(e<=t[1])return n[1]}else{if(e===t[0])return n[0];if(e===t[1])return n[1]}return(e-t[0])/i*r+n[0]}function r(e,t){switch(e){case"center":case"middle":e="50%";break;case"left":case"top":e="0%";break;case"right":case"bottom":e="100%"}return"string"==typeof e?o(e).match(/%$/)?parseFloat(e)/100*t:parseFloat(e):null==e?NaN:+e}function a(e,t,n){return null==t&&(t=10),t=Math.min(Math.max(0,t),20),e=(+e).toFixed(t),n?e:+e}function l(e){return e.sort(function(e,t){return e-t}),e}function s(e){if(e=+e,isNaN(e))return 0;for(var t=1,n=0;Math.round(e*t)/t!==e;)t*=10,n++;return n}function c(e){var t=e.toString(),n=t.indexOf("e");if(n>0){var o=+t.slice(n+1);return o<0?-o:0}var i=t.indexOf(".");return i<0?0:t.length-1-i}function u(e,t){var n=Math.log,o=Math.LN10,i=Math.floor(n(e[1]-e[0])/o),r=Math.round(n(Math.abs(t[1]-t[0]))/o),a=Math.min(Math.max(-i+r,0),20);return isFinite(a)?a:20}function d(e,t,n){if(!e[t])return 0;var o=y.reduce(e,function(e,t){return e+(isNaN(t)?0:t)},0);if(0===o)return 0;for(var i=Math.pow(10,n),r=y.map(e,function(e){return(isNaN(e)?0:e)/o*i*100}),a=100*i,l=y.map(r,function(e){return Math.floor(e)}),s=y.reduce(l,function(e,t){return e+t},0),c=y.map(r,function(e,t){return e-l[t]});su&&(u=c[f],d=f);++l[d],c[d]=0,++s}return l[t]/i}function f(e){var t=2*Math.PI;return(e%t+t)%t}function p(e){return e>-_&&e<_}function h(e){if(e instanceof Date)return e;if("string"==typeof e){var t=w.exec(e);if(!t)return new Date(NaN);if(t[8]){var n=+t[4]||0;return"Z"!==t[8].toUpperCase()&&(n-=t[8].slice(0,3)),new Date(Date.UTC(+t[1],+(t[2]||1)-1,+t[3]||1,n,+(t[5]||0),+t[6]||0,+t[7]||0))}return new Date(+t[1],+(t[2]||1)-1,+t[3]||1,+t[4]||0,+(t[5]||0),+t[6]||0,+t[7]||0)}return null==e?new Date(NaN):new Date(Math.round(e))}function g(e){return Math.pow(10,m(e))}function m(e){return Math.floor(Math.log(e)/Math.LN10)}function v(e,t){var n,o=m(e),i=Math.pow(10,o),r=e/i;return n=t?r<1.5?1:r<2.5?2:r<4?3:r<7?5:10:r<1?1:r<2?2:r<3?3:r<5?5:10,e=n*i,o>=-20?+e.toFixed(o<0?-o:0):e}function b(e){function t(e,n,o){return e.interval[o]=0}var y=n(0),_=1e-4,w=/^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d\d)(?::(\d\d)(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/;t.linearMap=i,t.parsePercent=r,t.round=a,t.asc=l,t.getPrecision=s,t.getPrecisionSafe=c,t.getPixelPrecision=u,t.getPercentWithPrecision=d,t.MAX_SAFE_INTEGER=9007199254740991,t.remRadian=f,t.isRadianAroundZero=p,t.parseDate=h,t.quantity=g,t.nice=v,t.reformIntervals=b,t.isNumeric=x},function(e,t,n){(function(e){var n;"undefined"!=typeof window?n=window.__DEV__:void 0!==e&&(n=e.__DEV__),void 0===n&&(n=!0);var o=n;t.__DEV__=o}).call(t,n(48))},function(e,t,n){function o(e){return e instanceof Array?e:null==e?[]:[e]}function i(e,t){if(e)for(var n=e.emphasis=e.emphasis||{},o=e.normal=e.normal||{},i=0,r=t.length;i=n.length&&n.push({option:e})}}),n}function u(e){var t=x.createHashMap();k(e,function(e,n){var o=e.exist;o&&t.set(o.id,e)}),k(e,function(e,n){var o=e.option;x.assert(!o||null==o.id||!t.get(o.id)||t.get(o.id)===e,"id duplicates: "+(o&&o.id)),o&&null!=o.id&&t.set(o.id,e),!e.keyInfo&&(e.keyInfo={})}),k(e,function(e,n){var o=e.exist,i=e.option,r=e.keyInfo;if(S(i)){if(r.name=null!=i.name?i.name+"":o?o.name:"\0-",o)r.id=o.id;else if(null!=i.id)r.id=i.id+"";else{var a=0;do{r.id="\0"+r.name+"\0"+a++}while(t.get(r.id))}t.set(r.id,e)}})}function d(e){return S(e)&&e.id&&0===(e.id+"").indexOf("\0_ec_\0")}function f(e,t){function n(e,t,n){for(var i=0,r=e.length;io||s.newline?(r=0,u=m,a+=l+n,l=p.height):l=Math.max(l,p.height)}else{var v=p.height+(g?-g.y+p.y:0);d=a+v,d>i||s.newline?(r+=l+n,a=0,d=v,l=p.width):l=Math.max(l,p.width)}s.newline||(f[0]=r,f[1]=a,"horizontal"===e?r=u+n:a=d+n)})}function i(e,t,n){var o=t.width,i=t.height,r=h(e.x,o),a=h(e.y,i),l=h(e.x2,o),s=h(e.y2,i);return(isNaN(r)||isNaN(parseFloat(e.x)))&&(r=0),(isNaN(l)||isNaN(parseFloat(e.x2)))&&(l=o),(isNaN(a)||isNaN(parseFloat(e.y)))&&(a=0),(isNaN(s)||isNaN(parseFloat(e.y2)))&&(s=i),n=g.normalizeCssArray(n||0),{width:Math.max(l-r-n[1]-n[3],0),height:Math.max(s-a-n[0]-n[2],0)}}function r(e,t,n){n=g.normalizeCssArray(n||0);var o=t.width,i=t.height,r=h(e.left,o),a=h(e.top,i),l=h(e.right,o),s=h(e.bottom,i),c=h(e.width,o),u=h(e.height,i),d=n[2]+n[0],p=n[1]+n[3],m=e.aspect;switch(isNaN(c)&&(c=o-l-p-r),isNaN(u)&&(u=i-s-d-a),null!=m&&(isNaN(c)&&isNaN(u)&&(m>o/i?c=.8*o:u=.8*i),isNaN(c)&&(c=m*u),isNaN(u)&&(u=c/m)),isNaN(r)&&(r=o-l-c-p),isNaN(a)&&(a=i-s-u-d),e.left||e.right){case"center":r=o/2-c/2-n[3];break;case"right":r=o-c-p}switch(e.top||e.bottom){case"middle":case"center":a=i/2-u/2-n[0];break;case"bottom":a=i-u-d}r=r||0,a=a||0,isNaN(c)&&(c=o-p-r-(l||0)),isNaN(u)&&(u=i-d-a-(s||0));var v=new f(r+n[3],a+n[0],c,u);return v.margin=n,v}function a(e,t,n,o,i){var a=!i||!i.hv||i.hv[0],l=!i||!i.hv||i.hv[1],s=i&&i.boundingMode||"all";if(a||l){var c;if("raw"===s)c="group"===e.type?new f(0,0,+t.width||0,+t.height||0):e.getBoundingRect();else if(c=e.getBoundingRect(),e.needLocalTransform()){var u=e.getLocalTransform();c=c.clone(),c.applyTransform(u)}t=r(d.defaults({width:c.width,height:c.height},t),n,o);var p=e.position,h=a?t.x-c.x:0,g=l?t.y-c.y:0;e.attr("position","raw"===s?[h,g]:[p[0]+h,p[1]+g])}}function l(e,t){return null!=e[b[t][0]]||null!=e[b[t][1]]&&null!=e[b[t][2]]}function s(e,t,n){function o(n,o){var a={},s=0,c={},u=0;if(m(n,function(t){c[t]=e[t]}),m(n,function(e){i(t,e)&&(a[e]=c[e]=t[e]),r(a,e)&&s++,r(c,e)&&u++}),l[o])return r(t,n[1])?c[n[2]]=null:r(t,n[2])&&(c[n[1]]=null),c;if(2!==u&&s){if(s>=2)return a;for(var d=0;d1?"."+e[1]:""))}function i(e,t){return e=(e||"").toLowerCase().replace(/-(.)/g,function(e,t){return t.toUpperCase()}),t&&e&&(e=e.charAt(0).toUpperCase()+e.slice(1)),e}function r(e){return String(e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function a(e,t,n){d.isArray(t)||(t=[t]);var o=t.length;if(!o)return"";for(var i=t[0].$vars||[],a=0;a':""}function c(e,t,n){"week"!==e&&"month"!==e&&"quarter"!==e&&"half-year"!==e&&"year"!==e||(e="MM-dd\nyyyy");var o=p.parseDate(t),i=n?"UTC":"",r=o["get"+i+"FullYear"](),a=o["get"+i+"Month"]()+1,l=o["get"+i+"Date"](),s=o["get"+i+"Hours"](),c=o["get"+i+"Minutes"](),u=o["get"+i+"Seconds"]();return e=e.replace("MM",v(a)).replace("M",a).replace("yyyy",r).replace("yy",r%100).replace("dd",v(l)).replace("d",l).replace("hh",v(s)).replace("h",s).replace("mm",v(c)).replace("m",c).replace("ss",v(u)).replace("s",u)}function u(e){return e?e.charAt(0).toUpperCase()+e.substr(1):e}var d=n(0),f=n(27),p=n(3),h=d.normalizeCssArray,g=["a","b","c","d","e","f","g"],m=function(e,t){return"{"+e+(null==t?"":t)+"}"},v=function(e){return e<10?"0"+e:e},b=f.truncateText,x=f.getBoundingRect;t.addCommas=o,t.toCamelCase=i,t.normalizeCssArray=h,t.encodeHTML=r,t.formatTpl=a,t.formatTplSimple=l,t.getTooltipMarker=s,t.formatTime=c,t.capitalFirst=u,t.truncateText=b,t.getTextRect=x},function(e,t,n){var o=n(341);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){function o(e,t,n,o){n<0&&(e+=n,n=-n),o<0&&(t+=o,o=-o),this.x=e,this.y=t,this.width=n,this.height=o}var i=n(7),r=n(24),a=i.applyTransform,l=Math.min,s=Math.max;o.prototype={constructor:o,union:function(e){var t=l(e.x,this.x),n=l(e.y,this.y);this.width=s(e.x+e.width,this.x+this.width)-t,this.height=s(e.y+e.height,this.y+this.height)-n,this.x=t,this.y=n},applyTransform:function(){var e=[],t=[],n=[],o=[];return function(i){if(i){e[0]=n[0]=this.x,e[1]=o[1]=this.y,t[0]=o[0]=this.x+this.width,t[1]=n[1]=this.y+this.height,a(e,e,i),a(t,t,i),a(n,n,i),a(o,o,i),this.x=l(e[0],t[0],n[0],o[0]),this.y=l(e[1],t[1],n[1],o[1]);var r=s(e[0],t[0],n[0],o[0]),c=s(e[1],t[1],n[1],o[1]);this.width=r-this.x,this.height=c-this.y}}}(),calculateTransform:function(e){var t=this,n=e.width/t.width,o=e.height/t.height,i=r.create();return r.translate(i,i,[-t.x,-t.y]),r.scale(i,i,[n,o]),r.translate(i,i,[e.x,e.y]),i},intersect:function(e){if(!e)return!1;e instanceof o||(e=o.create(e));var t=this,n=t.x,i=t.x+t.width,r=t.y,a=t.y+t.height,l=e.x,s=e.x+e.width,c=e.y,u=e.y+e.height;return!(i=n.x&&e<=n.x+n.width&&t>=n.y&&t<=n.y+n.height},clone:function(){return new o(this.x,this.y,this.width,this.height)},copy:function(e){this.x=e.x,this.y=e.y,this.width=e.width,this.height=e.height},plain:function(){return{x:this.x,y:this.y,width:this.width,height:this.height}}},o.create=function(e){return new o(e.x,e.y,e.width,e.height)};var c=o;e.exports=c},function(e,t,n){"use strict";function o(){for(var e=arguments.length,t=Array(e),n=0;n=r)return e;switch(e){case"%s":return String(t[o++]);case"%d":return Number(t[o++]);case"%j":try{return JSON.stringify(t[o++])}catch(e){return"[Circular]"}break;default:return e}}),l=t[o];o=0?r[c]=new u.constructor(a[c].length):r[c]=a[c]}return i}var l=n(4),s=(l.__DEV__,n(0)),c=n(12),u=n(56),d=n(5),f=s.isObject,p="undefined"==typeof window?t:window,h={float:void 0===p.Float64Array?Array:p.Float64Array,int:void 0===p.Int32Array?Array:p.Int32Array,ordinal:Array,number:Array,time:Array},g=["stackedOn","hasItemOption","_nameList","_idList","_rawData"];i.prototype.pure=!1,i.prototype.count=function(){return this._array.length},i.prototype.getItem=function(e){return this._array[e]};var m=function(e,t){e=e||["x","y"];for(var n={},o=[],i=0;i0&&(k+="__ec__"+p[w]),p[w]++),k&&(f[g]=k)}this._nameList=t,this._idList=f},v.count=function(){return this.indices.length},v.get=function(e,t,n){var o=this._storage,i=this.indices[t];if(null==i||!o[e])return NaN;var r=o[e][i];if(n){var a=this._dimensionInfos[e];if(a&&a.stackable)for(var l=this.stackedOn;l;){var s=l.get(e,t);(r>=0&&s>0||r<=0&&s<0)&&(r+=s),l=l.stackedOn}}return r},v.getValues=function(e,t,n){var o=[];s.isArray(e)||(n=t,t=e,e=this.dimensions);for(var i=0,r=e.length;is&&(s=r));return this._extent[e+!!t]=[l,s]}return[1/0,-1/0]},v.getSum=function(e,t){var n=this._storage[e],o=0;if(n)for(var i=0,r=this.count();ie))return r;i=r-1}}return-1},v.indicesOfNearest=function(e,t,n,o){var i=this._storage,r=i[e],a=[];if(!r)return a;null==o&&(o=1/0);for(var l=Number.MAX_VALUE,s=-1,c=0,u=this.count();c=0&&s<0)&&(l=f,s=d,a.length=0),a.push(c))}return a},v.getRawIndex=function(e){var t=this.indices[e];return null==t?-1:t},v.getRawDataItem=function(e){return this._rawData.getItem(this.getRawIndex(e))},v.getName=function(e){return this._nameList[this.indices[e]]||""},v.getId=function(e){return this._idList[this.indices[e]]||this.getRawIndex(e)+""},v.each=function(e,t,n,o){"function"==typeof e&&(o=n,n=t,t=e,e=[]),e=s.map(r(e),this.getDimension,this);var i=[],a=e.length,l=this.indices;o=o||this;for(var c=0;ch-g&&(f=h-g,u.length=f);for(var m=0;m=0;r--)o=i.merge(o,e[r],!0);l.set(this,"__defaultOption",o)}return l.get(this,"__defaultOption")},getReferringComponents:function(e){return this.ecModel.queryComponents({mainType:e,index:this.get(e+"Index",!0),id:this.get(e+"Id",!0)})}});l.enableClassManagement(d,{registerWhenExtend:!0}),a.enableSubTypeDefaulter(d),a.enableTopologicalTravel(d,o),i.mixin(d,c);var f=d;e.exports=f},function(e,t){var n={};n="undefined"==typeof navigator?{browser:{},os:{},node:!0,canvasSupported:!0,svgSupported:!0}:function(e){var t={},n={},o=e.match(/Firefox\/([\d.]+)/),i=e.match(/MSIE\s([\d.]+)/)||e.match(/Trident\/.+?rv:(([\d.]+))/),r=e.match(/Edge\/([\d.]+)/),a=/micromessenger/i.test(e);return o&&(n.firefox=!0,n.version=o[1]),i&&(n.ie=!0,n.version=i[1]),r&&(n.edge=!0,n.version=r[1]),a&&(n.weChat=!0),{browser:n,os:t,node:!1,canvasSupported:!!document.createElement("canvas").getContext,svgSupported:"undefined"!=typeof SVGRect,touchEventsSupported:"ontouchstart"in window&&!n.ie&&!n.edge,pointerEventsSupported:"onpointerdown"in window&&(n.edge||n.ie&&n.version>=11)}}(navigator.userAgent);var o=n;e.exports=o},function(e,t,n){function o(e){i.call(this,e),this.path=null}var i=n(97),r=n(0),a=n(61),l=n(669),s=n(247),c=s.prototype.getCanvasPattern,u=Math.abs,d=new a(!0);o.prototype={constructor:o,type:"path",__dirtyPath:!0,strokeContainThreshold:5,brush:function(e,t){var n=this.style,o=this.path||d,i=n.hasStroke(),r=n.hasFill(),a=n.fill,l=n.stroke,s=r&&!!a.colorStops,u=i&&!!l.colorStops,f=r&&!!a.image,p=i&&!!l.image;if(n.bind(e,this,t),this.setTransform(e),this.__dirty){var h;s&&(h=h||this.getBoundingRect(),this._fillGradient=n.getGradient(e,a,h)),u&&(h=h||this.getBoundingRect(),this._strokeGradient=n.getGradient(e,l,h))}s?e.fillStyle=this._fillGradient:f&&(e.fillStyle=c.call(a,e)),u?e.strokeStyle=this._strokeGradient:p&&(e.strokeStyle=c.call(l,e));var g=n.lineDash,m=n.lineDashOffset,v=!!e.setLineDash,b=this.getGlobalScale();o.setScale(b[0],b[1]),this.__dirtyPath||g&&!v&&i?(o.beginPath(e),g&&!v&&(o.setLineDash(g),o.setLineDashOffset(m)),this.buildPath(o,this.shape,!1),this.path&&(this.__dirtyPath=!1)):(e.beginPath(),this.path.rebuildPath(e)),r&&o.fill(e),g&&v&&(e.setLineDash(g),e.lineDashOffset=m),i&&o.stroke(e),g&&v&&e.setLineDash([]),this.restoreTransform(e),null!=n.text&&this.drawRectText(e,this.getBoundingRect())},buildPath:function(e,t,n){},createPathProxy:function(){this.path=new a},getBoundingRect:function(){var e=this._rect,t=this.style,n=!e;if(n){var o=this.path;o||(o=this.path=new a),this.__dirtyPath&&(o.beginPath(),this.buildPath(o,this.shape,!1)),e=o.getBoundingRect()}if(this._rect=e,t.hasStroke()){var i=this._rectWithStroke||(this._rectWithStroke=e.clone());if(this.__dirty||n){i.copy(e);var r=t.lineWidth,l=t.strokeNoScale?this.getLineScale():1;t.hasFill()||(r=Math.max(r,this.strokeContainThreshold||4)),l>1e-10&&(i.width+=r/l,i.height+=r/l,i.x-=r/l/2,i.y-=r/l/2)}return i}return e},contain:function(e,t){var n=this.transformCoordToLocal(e,t),o=this.getBoundingRect(),i=this.style;if(e=n[0],t=n[1],o.contain(e,t)){var r=this.path.data;if(i.hasStroke()){var a=i.lineWidth,s=i.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(i.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),l.containStroke(r,a/s,e,t)))return!0}if(i.hasFill())return l.contain(r,e,t)}return!1},dirty:function(e){null==e&&(e=!0),e&&(this.__dirtyPath=e,this._rect=null),this.__dirty=!0,this.__zr&&this.__zr.refresh(),this.__clipTarget&&this.__clipTarget.dirty()},animateShape:function(e){return this.animate("shape",e)},attrKV:function(e,t){"shape"===e?(this.setShape(t),this.__dirtyPath=!0,this._rect=null):i.prototype.attrKV.call(this,e,t)},setShape:function(e,t){var n=this.shape;if(n){if(r.isObject(e))for(var o in e)e.hasOwnProperty(o)&&(n[o]=e[o]);else n[e]=t;this.dirty(!0)}return this},getLineScale:function(){var e=this.transform;return e&&u(e[0]-1)>1e-10&&u(e[3]-1)>1e-10?Math.sqrt(u(e[0]*e[3]-e[2]*e[1])):1}},o.extend=function(e){var t=function(t){o.call(this,t),e.style&&this.style.extendFrom(e.style,!1);var n=e.shape;if(n){this.shape=this.shape||{};var i=this.shape;for(var r in n)!i.hasOwnProperty(r)&&n.hasOwnProperty(r)&&(i[r]=n[r])}e.init&&e.init.call(this,t)};r.inherits(t,o);for(var n in e)"style"!==n&&"shape"!==n&&(t.prototype[n]=e[n]);return t},r.inherits(o,i);var f=o;e.exports=f},function(e,t,n){(function(t){function n(e,t){var n=e[1]||"",i=e[3];if(!i)return n;if(t){var r=o(i);return[n].concat(i.sources.map(function(e){return"/*# sourceURL="+i.sourceRoot+e+" */"})).concat([r]).join("\n")}return[n].join("\n")}function o(e){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+new t(JSON.stringify(e)).toString("base64")+" */"}e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var o=n(t,e);return t[2]?"@media "+t[2]+"{"+o+"}":o}).join("")},t.i=function(e,n){"string"==typeof e&&(e=[[null,e,""]]);for(var o={},i=0;i":"")+u.join(a?"
":", ")}(r):s(c(r)),d=o.getName(e),f=o.getItemVisual(e,"color");i.isObject(f)&&f.colorStops&&(f=(f.colorStops[0]||{}).color),f=f||"transparent";var g=u(f),m=this.name;return"\0-"===m&&(m=""),m=m?s(m)+(t?": ":"
"):"",t?g+m+a:m+g+(d?s(d)+": "+a:a)},isAnimationEnabled:function(){if(r.node)return!1;var e=this.getShallow("animation");return e&&this.getData().count()>this.getShallow("animationThreshold")&&(e=!1),e},restoreData:function(){f(this,"data",p(this,"dataBeforeProcessed").cloneShallow())},getColorFromPalette:function(e,t){var n=this.ecModel,o=m.getColorFromPalette.call(this,e,t);return o||(o=n.getColorFromPalette(e,t)),o},getAxisTooltipData:null,getTooltipPosition:null});i.mixin(y,h.dataFormatMixin),i.mixin(y,m);var _=y;e.exports=_},function(e,t){function n(e,t){for(var n=0;n=0&&b.splice(t,1)}function a(e){var t=document.createElement("style");return t.type="text/css",i(e,t),t}function l(e){var t=document.createElement("link");return t.rel="stylesheet",i(e,t),t}function s(e,t){var n,o,i;if(t.singleton){var s=v++;n=m||(m=a(t)),o=c.bind(null,n,s,!1),i=c.bind(null,n,s,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=l(t),o=d.bind(null,n),i=function(){r(n),n.href&&URL.revokeObjectURL(n.href)}):(n=a(t),o=u.bind(null,n),i=function(){r(n)});return o(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;o(e=t)}else i()}}function c(e,t,n,o){var i=n?"":o.css;if(e.styleSheet)e.styleSheet.cssText=x(t,i);else{var r=document.createTextNode(i),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(r,a[t]):e.appendChild(r)}}function u(e,t){var n=t.css,o=t.media;if(o&&e.setAttribute("media",o),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}function d(e,t){var n=t.css,o=t.sourceMap;o&&(n+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */");var i=new Blob([n],{type:"text/css"}),r=e.href;e.href=URL.createObjectURL(i),r&&URL.revokeObjectURL(r)}var f={},p=function(e){var t;return function(){return void 0===t&&(t=e.apply(this,arguments)),t}},h=p(function(){return/msie [6-9]\b/.test(self.navigator.userAgent.toLowerCase())}),g=p(function(){return document.head||document.getElementsByTagName("head")[0]}),m=null,v=0,b=[];e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");t=t||{},void 0===t.singleton&&(t.singleton=h()),void 0===t.insertAt&&(t.insertAt="bottom");var i=o(e);return n(i,t),function(e){for(var r=[],a=0;a=0&&Math.floor(t)===t&&isFinite(e)}function f(e){return i(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function p(e){return null==e?"":Array.isArray(e)||c(e)&&e.toString===Mr?JSON.stringify(e,null,2):String(e)}function h(e){var t=parseFloat(e);return isNaN(t)?e:t}function g(e,t){for(var n=Object.create(null),o=e.split(","),i=0;i-1)return e.splice(n,1)}}function v(e,t){return Tr.call(e,t)}function b(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}function x(e,t){function n(n){var o=arguments.length;return o?o>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function y(e,t){return e.bind(t)}function _(e,t){t=t||0;for(var n=e.length-t,o=new Array(n);n--;)o[n]=e[n+t];return o}function w(e,t){for(var n in t)e[n]=t[n];return e}function k(e){for(var t={},n=0;n-1)if(r&&!v(i,"default"))a=!1;else if(""===a||a===Pr(e)){var s=ie(String,i.type);(s<0||l0&&(a=_e(a,(t||"")+"_"+n),ye(a[0])&&ye(c)&&(u[s]=D(c.text+a[0].text),a.shift()),u.push.apply(u,a)):l(a)?ye(c)?u[s]=D(c.text+a):""!==a&&u.push(D(a)):ye(a)&&ye(c)?u[s]=D(c.text+a.text):(r(e._isVList)&&i(a.tag)&&o(a.key)&&i(t)&&(a.key="__vlist"+t+"_"+n+"__"),u.push(a)));return u}function we(e){var t=e.$options.provide;t&&(e._provided="function"==typeof t?t.call(e):t)}function ke(e){var t=Se(e.$options.inject,e);t&&(R(!1),Object.keys(t).forEach(function(n){V(e,n,t[n])}),R(!0))}function Se(e,t){if(e){for(var n=Object.create(null),o=aa?Reflect.ownKeys(e):Object.keys(e),i=0;i0,r=e?!!e.$stable:!i,a=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(r&&n&&n!==Sr&&a===n.$key&&!i&&!n.$hasNormal)return n;o={};for(var l in e)e[l]&&"$"!==l[0]&&(o[l]=Te(t,l,e[l]))}else o={};for(var s in t)s in o||(o[s]=Ee(t,s));return e&&Object.isExtensible(e)&&(e._normalized=o),E(o,"$stable",r),E(o,"$key",a),E(o,"$hasNormal",i),o}function Te(e,t,n){var o=function(){var e=arguments.length?n.apply(null,arguments):n({});return e=e&&"object"==typeof e&&!Array.isArray(e)?[e]:xe(e),e&&(0===e.length||1===e.length&&e[0].isComment)?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:o,enumerable:!0,configurable:!0}),o}function Ee(e,t){return function(){return e[t]}}function Ie(e,t){var n,o,r,a,l;if(Array.isArray(e)||"string"==typeof e)for(n=new Array(e.length),o=0,r=e.length;oHa&&Ba[n].id>e.id;)n--;Ba.splice(n+1,0,e)}else Ba.push(e);ja||(ja=!0,ue(St))}}function Et(e,t,n){Za.get=function(){return this[t][n]},Za.set=function(e){this[t][n]=e},Object.defineProperty(e,n,Za)}function It(e){e._watchers=[];var t=e.$options;t.props&&Ot(e,t.props),t.methods&&Bt(e,t.methods),t.data?Lt(e):F(e._data={},!0),t.computed&&Dt(e,t.computed),t.watch&&t.watch!==Qr&&Ft(e,t.watch)}function Ot(e,t){var n=e.$options.propsData||{},o=e._props={},i=e.$options._propKeys=[];!e.$parent||R(!1);for(var r in t)!function(r){i.push(r);var a=ee(r,t,n,e);V(o,r,a),r in e||Et(e,"_props",r)}(r);R(!0)}function Lt(e){var t=e.$options.data;t=e._data="function"==typeof t?Pt(t,e):t||{},c(t)||(t={});for(var n=Object.keys(t),o=e.$options.props,i=(e.$options.methods,n.length);i--;){var r=n[i];o&&v(o,r)||T(r)||Et(e,"_data",r)}F(t,!0)}function Pt(e,t){L();try{return e.call(t,t)}catch(e){return re(e,t,"data()"),{}}finally{P()}}function Dt(e,t){var n=e._computedWatchers=Object.create(null),o=ia();for(var i in t){var r=t[i],a="function"==typeof r?r:r.get;o||(n[i]=new Ya(e,a||S,S,Xa)),i in e||zt(e,i,r)}}function zt(e,t,n){var o=!ia();"function"==typeof n?(Za.get=o?Rt(t):Nt(n),Za.set=S):(Za.get=n.get?o&&!1!==n.cache?Rt(t):Nt(n.get):S,Za.set=n.set||S),Object.defineProperty(e,t,Za)}function Rt(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),ca.target&&t.depend(),t.value}}function Nt(e){return function(){return e.call(this,this)}}function Bt(e,t){e.$options.props;for(var n in t)e[n]="function"!=typeof t[n]?S:Dr(t[n],e)}function Ft(e,t){for(var n in t){var o=t[n];if(Array.isArray(o))for(var i=0;i-1)return this;var n=_(arguments,1);return n.unshift(this),"function"==typeof e.install?e.install.apply(e,n):"function"==typeof e&&e.apply(null,n),t.push(e),this}}function Ut(e){e.mixin=function(e){return this.options=J(this.options,e),this}}function qt(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,o=n.cid,i=e._Ctor||(e._Ctor={});if(i[o])return i[o];var r=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=J(n.options,e),a.super=n,a.options.props&&Yt(a),a.options.computed&&Zt(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,Br.forEach(function(e){a[e]=n[e]}),r&&(a.options.components[r]=a),a.superOptions=n.options,a.extendOptions=e,a.sealedOptions=w({},a.options),i[o]=a,a}}function Yt(e){var t=e.options.props;for(var n in t)Et(e.prototype,"_props",n)}function Zt(e){var t=e.options.computed;for(var n in t)zt(e.prototype,n,t[n])}function Xt(e){Br.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&c(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}function Kt(e){return e&&(e.Ctor.options.name||e.tag)}function Jt(e,t){return Array.isArray(e)?e.indexOf(t)>-1:"string"==typeof e?e.split(",").indexOf(t)>-1:!!u(e)&&e.test(t)}function Qt(e,t){var n=e.cache,o=e.keys,i=e._vnode;for(var r in n){var a=n[r];if(a){var l=Kt(a.componentOptions);l&&!t(l)&&en(n,r,o,i)}}}function en(e,t,n,o){var i=e[t];!i||o&&i.tag===o.tag||i.componentInstance.$destroy(),e[t]=null,m(n,t)}function tn(e){for(var t=e.data,n=e,o=e;i(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(t=nn(o.data,t));for(;i(n=n.parent);)n&&n.data&&(t=nn(t,n.data));return on(t.staticClass,t.class)}function nn(e,t){return{staticClass:rn(e.staticClass,t.staticClass),class:i(e.class)?[e.class,t.class]:t.class}}function on(e,t){return i(e)||i(t)?rn(e,an(t)):""}function rn(e,t){return e?t?e+" "+t:e:t||""}function an(e){return Array.isArray(e)?ln(e):s(e)?sn(e):"string"==typeof e?e:""}function ln(e){for(var t,n="",o=0,r=e.length;o-1?Al[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Al[e]=/HTMLUnknownElement/.test(t.toString())}function dn(e){if("string"==typeof e){return document.querySelector(e)||document.createElement("div")}return e}function fn(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function pn(e,t){return document.createElementNS(wl[e],t)}function hn(e){return document.createTextNode(e)}function gn(e){return document.createComment(e)}function mn(e,t,n){e.insertBefore(t,n)}function vn(e,t){e.removeChild(t)}function bn(e,t){e.appendChild(t)}function xn(e){return e.parentNode}function yn(e){return e.nextSibling}function _n(e){return e.tagName}function wn(e,t){e.textContent=t}function kn(e,t){e.setAttribute(t,"")}function Sn(e,t){var n=e.data.ref;if(i(n)){var o=e.context,r=e.componentInstance||e.elm,a=o.$refs;t?Array.isArray(a[n])?m(a[n],r):a[n]===r&&(a[n]=void 0):e.data.refInFor?Array.isArray(a[n])?a[n].indexOf(r)<0&&a[n].push(r):a[n]=[r]:a[n]=r}}function Mn(e,t){return e.key===t.key&&(e.tag===t.tag&&e.isComment===t.isComment&&i(e.data)===i(t.data)&&Cn(e,t)||r(e.isAsyncPlaceholder)&&e.asyncFactory===t.asyncFactory&&o(t.asyncFactory.error))}function Cn(e,t){if("input"!==e.tag)return!0;var n,o=i(n=e.data)&&i(n=n.attrs)&&n.type,r=i(n=t.data)&&i(n=n.attrs)&&n.type;return o===r||Tl(o)&&Tl(r)}function An(e,t,n){var o,r,a={};for(o=t;o<=n;++o)r=e[o].key,i(r)&&(a[r]=o);return a}function Tn(e,t){(e.data.directives||t.data.directives)&&En(e,t)}function En(e,t){var n,o,i,r=e===Ol,a=t===Ol,l=In(e.data.directives,e.context),s=In(t.data.directives,t.context),c=[],u=[];for(n in s)o=l[n],i=s[n],o?(i.oldValue=o.value,i.oldArg=o.arg,Ln(i,"update",t,e),i.def&&i.def.componentUpdated&&u.push(i)):(Ln(i,"bind",t,e),i.def&&i.def.inserted&&c.push(i));if(c.length){var d=function(){for(var n=0;n-1?zn(e,t,n):vl(t)?_l(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):hl(t)?e.setAttribute(t,ml(t,n)):xl(t)?_l(n)?e.removeAttributeNS(bl,yl(t)):e.setAttributeNS(bl,t,n):zn(e,t,n)}function zn(e,t,n){if(_l(n))e.removeAttribute(t);else{if(Yr&&!Zr&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var o=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",o)};e.addEventListener("input",o),e.__ieph=!0}e.setAttribute(t,n)}}function Rn(e,t){var n=t.elm,r=t.data,a=e.data;if(!(o(r.staticClass)&&o(r.class)&&(o(a)||o(a.staticClass)&&o(a.class)))){var l=tn(t),s=n._transitionClasses;i(s)&&(l=rn(l,an(s))),l!==n._prevClass&&(n.setAttribute("class",l),n._prevClass=l)}}function Nn(e){function t(){(a||(a=[])).push(e.slice(h,i).trim()),h=i+1}var n,o,i,r,a,l=!1,s=!1,c=!1,u=!1,d=0,f=0,p=0,h=0;for(i=0;i=0&&" "===(m=e.charAt(g));g--);m&&Bl.test(m)||(u=!0)}}else void 0===r?(h=i+1,r=e.slice(0,i).trim()):t();if(void 0===r?r=e.slice(0,i).trim():0!==h&&t(),a)for(i=0;i-1?{exp:e.slice(0,il),key:'"'+e.slice(il+1)+'"'}:{exp:e,key:null};for(nl=e,il=rl=al=0;!no();)ol=to(),oo(ol)?ro(ol):91===ol&&io(ol);return{exp:e.slice(0,rl),key:e.slice(rl+1,al)}}function to(){return nl.charCodeAt(++il)}function no(){return il>=tl}function oo(e){return 34===e||39===e}function io(e){var t=1;for(rl=il;!no();)if(e=to(),oo(e))ro(e);else if(91===e&&t++,93===e&&t--,0===t){al=il;break}}function ro(e){for(var t=e;!no()&&(e=to())!==t;);}function ao(e,t,n){ll=n;var o=t.value,i=t.modifiers,r=e.tag,a=e.attrsMap.type;if(e.component)return Jn(e,o,i),!1;if("select"===r)co(e,o,i);else if("input"===r&&"checkbox"===a)lo(e,o,i);else if("input"===r&&"radio"===a)so(e,o,i);else if("input"===r||"textarea"===r)uo(e,o,i);else if(!Vr.isReservedTag(r))return Jn(e,o,i),!1;return!0}function lo(e,t,n){var o=n&&n.number,i=Yn(e,"value")||"null",r=Yn(e,"true-value")||"true",a=Yn(e,"false-value")||"false";jn(e,"checked","Array.isArray("+t+")?_i("+t+","+i+")>-1"+("true"===r?":("+t+")":":_q("+t+","+r+")")),Un(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+r+"):("+a+");if(Array.isArray($$a)){var $$v="+(o?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Qn(t,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Qn(t,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Qn(t,"$$c")+"}",null,!0)}function so(e,t,n){var o=n&&n.number,i=Yn(e,"value")||"null";i=o?"_n("+i+")":i,jn(e,"checked","_q("+t+","+i+")"),Un(e,"change",Qn(t,i),null,!0)}function co(e,t,n){var o=n&&n.number,i='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(o?"_n(val)":"val")+"})",r="var $$selectedVal = "+i+";";r=r+" "+Qn(t,"$event.target.multiple ? $$selectedVal : $$selectedVal[0]"),Un(e,"change",r,null,!0)}function uo(e,t,n){var o=e.attrsMap.type,i=n||{},r=i.lazy,a=i.number,l=i.trim,s=!r&&"range"!==o,c=r?"change":"range"===o?Fl:"input",u="$event.target.value";l&&(u="$event.target.value.trim()"),a&&(u="_n("+u+")");var d=Qn(t,u);s&&(d="if($event.target.composing)return;"+d),jn(e,"value","("+t+")"),Un(e,c,d,null,!0),(l||a)&&Un(e,"blur","$forceUpdate()")}function fo(e){if(i(e[Fl])){var t=Yr?"change":"input";e[t]=[].concat(e[Fl],e[t]||[]),delete e[Fl]}i(e[Vl])&&(e.change=[].concat(e[Vl],e.change||[]),delete e[Vl])}function po(e,t,n){var o=sl;return function i(){null!==t.apply(null,arguments)&&go(e,i,n,o)}}function ho(e,t,n,o){if(jl){var i=Wa,r=t;t=r._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=i||e.timeStamp<=0||e.target.ownerDocument!==document)return r.apply(this,arguments)}}sl.addEventListener(e,t,ea?{capture:n,passive:o}:n)}function go(e,t,n,o){(o||sl).removeEventListener(e,t._wrapper||t,n)}function mo(e,t){if(!o(e.data.on)||!o(t.data.on)){var n=t.data.on||{},i=e.data.on||{};sl=t.elm,fo(n),he(n,i,ho,go,po,t.context),sl=void 0}}function vo(e,t){if(!o(e.data.domProps)||!o(t.data.domProps)){var n,r,a=t.elm,l=e.data.domProps||{},s=t.data.domProps||{};i(s.__ob__)&&(s=t.data.domProps=w({},s));for(n in l)n in s||(a[n]="");for(n in s){if(r=s[n],"textContent"===n||"innerHTML"===n){if(t.children&&(t.children.length=0),r===l[n])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===n&&"PROGRESS"!==a.tagName){a._value=r;var c=o(r)?"":String(r);bo(a,c)&&(a.value=c)}else if("innerHTML"===n&&Sl(a.tagName)&&o(a.innerHTML)){cl=cl||document.createElement("div"),cl.innerHTML=""+r+"";for(var u=cl.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;u.firstChild;)a.appendChild(u.firstChild)}else if(r!==l[n])try{a[n]=r}catch(e){}}}}function bo(e,t){return!e.composing&&("OPTION"===e.tagName||xo(e,t)||yo(e,t))}function xo(e,t){var n=!0;try{n=document.activeElement!==e}catch(e){}return n&&e.value!==t}function yo(e,t){var n=e.value,o=e._vModifiers;if(i(o)){if(o.number)return h(n)!==h(t);if(o.trim)return n.trim()!==t.trim()}return n!==t}function _o(e){var t=wo(e.style);return e.staticStyle?w(e.staticStyle,t):t}function wo(e){return Array.isArray(e)?k(e):"string"==typeof e?Wl(e):e}function ko(e,t){var n,o={};if(t)for(var i=e;i.componentInstance;)(i=i.componentInstance._vnode)&&i.data&&(n=_o(i.data))&&w(o,n);(n=_o(e.data))&&w(o,n);for(var r=e;r=r.parent;)r.data&&(n=_o(r.data))&&w(o,n);return o}function So(e,t){var n=t.data,r=e.data;if(!(o(n.staticStyle)&&o(n.style)&&o(r.staticStyle)&&o(r.style))){var a,l,s=t.elm,c=r.staticStyle,u=r.normalizedStyle||r.style||{},d=c||u,f=wo(t.data.style)||{};t.data.normalizedStyle=i(f.__ob__)?w({},f):f;var p=ko(t,!0);for(l in d)o(p[l])&&ql(s,l,"");for(l in p)(a=p[l])!==d[l]&&ql(s,l,null==a?"":a)}}function Mo(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(Kl).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Co(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(Kl).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{for(var n=" "+(e.getAttribute("class")||"")+" ",o=" "+t+" ";n.indexOf(o)>=0;)n=n.replace(o," ");n=n.trim(),n?e.setAttribute("class",n):e.removeAttribute("class")}}function Ao(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&w(t,Jl(e.name||"v")),w(t,e),t}return"string"==typeof e?Jl(e):void 0}}function To(e){as(function(){as(e)})}function Eo(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),Mo(e,t))}function Io(e,t){e._transitionClasses&&m(e._transitionClasses,t),Co(e,t)}function Oo(e,t,n){var o=Lo(e,t),i=o.type,r=o.timeout,a=o.propCount;if(!i)return n();var l=i===es?os:rs,s=0,c=function(){e.removeEventListener(l,u),n()},u=function(t){t.target===e&&++s>=a&&c()};setTimeout(function(){s0&&(n=es,u=a,d=r.length):t===ts?c>0&&(n=ts,u=c,d=s.length):(u=Math.max(a,c),n=u>0?a>c?es:ts:null,d=n?n===es?r.length:s.length:0),{type:n,timeout:u,propCount:d,hasTransform:n===es&&ls.test(o[ns+"Property"])}}function Po(e,t){for(;e.length1}function Fo(e,t){!0!==t.data.show&&zo(t)}function Vo(e,t,n){jo(e,t,n),(Yr||Xr)&&setTimeout(function(){jo(e,t,n)},0)}function jo(e,t,n){var o=t.value,i=e.multiple;if(!i||Array.isArray(o)){for(var r,a,l=0,s=e.options.length;l-1,a.selected!==r&&(a.selected=r);else if(M(Ho(a),o))return void(e.selectedIndex!==l&&(e.selectedIndex=l));i||(e.selectedIndex=-1)}}function $o(e,t){return t.every(function(t){return!M(t,e)})}function Ho(e){return"_value"in e?e._value:e.value}function Wo(e){e.target.composing=!0}function Go(e){e.target.composing&&(e.target.composing=!1,Uo(e.target,"input"))}function Uo(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function qo(e){return!e.componentInstance||e.data&&e.data.transition?e:qo(e.componentInstance._vnode)}function Yo(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?Yo(ct(t.children)):e}function Zo(e){var t={},n=e.$options;for(var o in n.propsData)t[o]=e[o];var i=n._parentListeners;for(var r in i)t[Ir(r)]=i[r];return t}function Xo(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}function Ko(e){for(;e=e.parent;)if(e.data.transition)return!0}function Jo(e,t){return t.key===e.key&&t.tag===e.tag}function Qo(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb()}function ei(e){e.data.newPos=e.elm.getBoundingClientRect()}function ti(e){var t=e.data.pos,n=e.data.newPos,o=t.left-n.left,i=t.top-n.top;if(o||i){e.data.moved=!0;var r=e.elm.style;r.transform=r.WebkitTransform="translate("+o+"px,"+i+"px)",r.transitionDuration="0s"}}function ni(e,t){var n=t?Ns(t):zs;if(n.test(e)){for(var o,i,r,a=[],l=[],s=n.lastIndex=0;o=n.exec(e);){(i=o.index)>s&&(l.push(r=e.slice(s,i)),a.push(JSON.stringify(r)));var c=Nn(o[1].trim());a.push("_s("+c+")"),l.push({"@binding":c}),s=i+o[0].length}return s=0&&a[i].lowerCasedTag!==l;i--);else i=0;if(i>=0){for(var s=a.length-1;s>=i;s--)t.end&&t.end(a[s].tag,n,o);a.length=i,r=i&&a[i-1].tag}else"br"===l?t.start&&t.start(e,[],!0,n,o):"p"===l&&(t.start&&t.start(e,[],!1,n,o),t.end&&t.end(e,n,o))}for(var i,r,a=[],l=t.expectHTML,s=t.isUnaryTag||zr,c=t.canBeLeftOpenTag||zr,u=0;e;){if(i=e,r&&ec(r)){var d=0,f=r.toLowerCase(),p=tc[f]||(tc[f]=new RegExp("([\\s\\S]*?)(]*>)","i")),h=e.replace(p,function(e,n,o){return d=o.length,ec(f)||"noscript"===f||(n=n.replace(//g,"$1").replace(//g,"$1")),ac(f,n)&&(n=n.slice(1)),t.chars&&t.chars(n),""});u+=e.length-h.length,e=h,o(f,u-d,u)}else{var g=e.indexOf("<");if(0===g){if(Js.test(e)){var m=e.indexOf("--\x3e");if(m>=0){t.shouldKeepComment&&t.comment(e.substring(4,m),u,u+m+3),n(m+3);continue}}if(Qs.test(e)){var v=e.indexOf("]>");if(v>=0){n(v+2);continue}}var b=e.match(Ks);if(b){n(b[0].length);continue}var x=e.match(Xs);if(x){var y=u;n(x[0].length),o(x[1],y,u);continue}var _=function(){var t=e.match(Ys);if(t){var o={tagName:t[1],attrs:[],start:u};n(t[0].length);for(var i,r;!(i=e.match(Zs))&&(r=e.match(Gs)||e.match(Ws));)r.start=u,n(r[0].length),r.end=u,o.attrs.push(r);if(i)return o.unarySlash=i[1],n(i[0].length),o.end=u,o}}();if(_){!function(e){var n=e.tagName,i=e.unarySlash;l&&("p"===r&&Hs(n)&&o(r),c(n)&&r===n&&o(n));for(var u=s(n)||!!i,d=e.attrs.length,f=new Array(d),p=0;p=0){for(k=e.slice(g);!(Xs.test(k)||Ys.test(k)||Js.test(k)||Qs.test(k)||(S=k.indexOf("<",1))<0);)g+=S,k=e.slice(g);w=e.substring(0,g)}g<0&&(w=e),w&&n(w.length),t.chars&&w&&t.chars(w,u-w.length,u)}if(e===i){t.chars&&t.chars(e);break}}o()}function ci(e,t,n){return{type:1,tag:e,attrsList:t,attrsMap:Ii(t),rawAttrsMap:{},parent:n,children:[]}}function ui(e,t){function n(e){if(o(e),u||e.processed||(e=pi(e,t)),l.length||e===r||r.if&&(e.elseif||e.else)&&_i(r,{exp:e.elseif,block:e}),a&&!e.forbidden)if(e.elseif||e.else)xi(e,a);else{if(e.slotScope){var n=e.slotTarget||'"default"';(a.scopedSlots||(a.scopedSlots={}))[n]=e}a.children.push(e),e.parent=a}e.children=e.children.filter(function(e){return!e.slotScope}),o(e),e.pre&&(u=!1),Ts(e.tag)&&(d=!1);for(var i=0;i>>0}function sr(e){return 1===e.type&&("slot"===e.tag||e.children.some(sr))}function cr(e,t){var n=e.attrsMap["slot-scope"];if(e.if&&!e.ifProcessed&&!n)return er(e,t,cr,"null");if(e.for&&!e.forProcessed)return nr(e,t,cr);var o=e.slotScope===yc?"":String(e.slotScope),i="function("+o+"){return "+("template"===e.tag?e.if&&n?"("+e.if+")?"+(ur(e,t)||"undefined")+":undefined":ur(e,t)||"undefined":Ki(e,t))+"}",r=o?"":",proxy:true";return"{key:"+(e.slotTarget||'"default"')+",fn:"+i+r+"}"}function ur(e,t,n,o,i){var r=e.children;if(r.length){var a=r[0];if(1===r.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var l=n?t.maybeComponent(a)?",1":",0":"";return""+(o||Ki)(a,t)+l}var s=n?dr(r,t.maybeComponent):0,c=i||pr;return"["+r.map(function(e){return c(e,t)}).join(",")+"]"+(s?","+s:"")}}function dr(e,t){for(var n=0,o=0;o':'
',Ds.innerHTML.indexOf(" ")>0}function kr(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}var Sr=Object.freeze({}),Mr=Object.prototype.toString,Cr=g("slot,component",!0),Ar=g("key,ref,slot,slot-scope,is"),Tr=Object.prototype.hasOwnProperty,Er=/-(\w)/g,Ir=b(function(e){return e.replace(Er,function(e,t){return t?t.toUpperCase():""})}),Or=b(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),Lr=/\B([A-Z])/g,Pr=b(function(e){return e.replace(Lr,"-$1").toLowerCase()}),Dr=Function.prototype.bind?y:x,zr=function(e,t,n){return!1},Rr=function(e){return e},Nr="data-server-rendered",Br=["component","directive","filter"],Fr=["beforeCreate","created","beforeMount","mounted","beforeUpdate","updated","beforeDestroy","destroyed","activated","deactivated","errorCaptured","serverPrefetch"],Vr={optionMergeStrategies:Object.create(null),silent:!1,productionTip:!1,devtools:!1,performance:!1,errorHandler:null,warnHandler:null,ignoredElements:[],keyCodes:Object.create(null),isReservedTag:zr,isReservedAttr:zr,isUnknownElement:zr,getTagNamespace:S,parsePlatformTagName:Rr,mustUseProp:zr,async:!0,_lifecycleHooks:Fr},jr=/a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/,$r=new RegExp("[^"+jr.source+".$_\\d]"),Hr="__proto__"in{},Wr="undefined"!=typeof window,Gr="undefined"!=typeof WXEnvironment&&!!WXEnvironment.platform,Ur=Gr&&WXEnvironment.platform.toLowerCase(),qr=Wr&&window.navigator.userAgent.toLowerCase(),Yr=qr&&/msie|trident/.test(qr),Zr=qr&&qr.indexOf("msie 9.0")>0,Xr=qr&&qr.indexOf("edge/")>0,Kr=(qr&&qr.indexOf("android"),qr&&/iphone|ipad|ipod|ios/.test(qr)||"ios"===Ur),Jr=(qr&&/chrome\/\d+/.test(qr),qr&&/phantomjs/.test(qr),qr&&qr.match(/firefox\/(\d+)/)),Qr={}.watch,ea=!1;if(Wr)try{var ta={};Object.defineProperty(ta,"passive",{get:function(){ea=!0}}),window.addEventListener("test-passive",null,ta)}catch(e){}var na,oa,ia=function(){return void 0===na&&(na=!Wr&&!Gr&&void 0!==e&&e.process&&"server"===e.process.env.VUE_ENV),na},ra=Wr&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,aa="undefined"!=typeof Symbol&&O(Symbol)&&"undefined"!=typeof Reflect&&O(Reflect.ownKeys);oa="undefined"!=typeof Set&&O(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var la=S,sa=0,ca=function(){this.id=sa++,this.subs=[]};ca.prototype.addSub=function(e){this.subs.push(e)},ca.prototype.removeSub=function(e){m(this.subs,e)},ca.prototype.depend=function(){ca.target&&ca.target.addDep(this)},ca.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;tdocument.createEvent("Event").timeStamp&&(Ga=function(){return Ua.now()})}var qa=0,Ya=function(e,t,n,o,i){this.vm=e,i&&(e._watcher=this),e._watchers.push(this),o?(this.deep=!!o.deep,this.user=!!o.user,this.lazy=!!o.lazy,this.sync=!!o.sync,this.before=o.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++qa,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new oa,this.newDepIds=new oa,this.expression="","function"==typeof t?this.getter=t:(this.getter=I(t),this.getter||(this.getter=S)),this.value=this.lazy?void 0:this.get()};Ya.prototype.get=function(){L(this);var e,t=this.vm;try{e=this.getter.call(t,t)}catch(e){if(!this.user)throw e;re(e,t,'getter for watcher "'+this.expression+'"')}finally{this.deep&&de(e),P(),this.cleanupDeps()}return e},Ya.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},Ya.prototype.cleanupDeps=function(){for(var e=this.deps.length;e--;){var t=this.deps[e];this.newDepIds.has(t.id)||t.removeSub(this)}var n=this.depIds;this.depIds=this.newDepIds,this.newDepIds=n,this.newDepIds.clear(),n=this.deps,this.deps=this.newDeps,this.newDeps=n,this.newDeps.length=0},Ya.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():Tt(this)},Ya.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||s(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){re(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},Ya.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},Ya.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},Ya.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||m(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var Za={enumerable:!0,configurable:!0,get:S,set:S},Xa={lazy:!0},Ka=0;!function(e){e.prototype._init=function(e){var t=this;t._uid=Ka++,t._isVue=!0,e&&e._isComponent?jt(t,e):t.$options=J($t(t.constructor),e||{},t),t._renderProxy=t,t._self=t,mt(t),ut(t),it(t),wt(t,"beforeCreate"),ke(t),It(t),we(t),wt(t,"created"),t.$options.el&&t.$mount(t.$options.el)}}(Wt),function(e){var t={};t.get=function(){return this._data};var n={};n.get=function(){return this._props},Object.defineProperty(e.prototype,"$data",t),Object.defineProperty(e.prototype,"$props",n),e.prototype.$set=j,e.prototype.$delete=$,e.prototype.$watch=function(e,t,n){var o=this;if(c(t))return Vt(o,e,t,n);n=n||{},n.user=!0;var i=new Ya(o,e,t,n);if(n.immediate)try{t.call(o,i.value)}catch(e){re(e,o,'callback for immediate watcher "'+i.expression+'"')}return function(){i.teardown()}}}(Wt),function(e){var t=/^hook:/;e.prototype.$on=function(e,n){var o=this;if(Array.isArray(e))for(var i=0,r=e.length;i1?_(n):n;for(var o=_(arguments,1),i='event handler for "'+e+'"',r=0,a=n.length;rparseInt(this.max)&&en(s,c[0],c,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}},el={KeepAlive:Qa};!function(e){var t={};t.get=function(){return Vr},Object.defineProperty(e,"config",t),e.util={warn:la,extend:w,mergeOptions:J,defineReactive:V},e.set=j,e.delete=$,e.nextTick=ue,e.observable=function(e){return F(e),e},e.options=Object.create(null),Br.forEach(function(t){e.options[t+"s"]=Object.create(null)}),e.options._base=e,w(e.options.components,el),Gt(e),Ut(e),qt(e),Xt(e)}(Wt),Object.defineProperty(Wt.prototype,"$isServer",{get:ia}),Object.defineProperty(Wt.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Wt,"FunctionalRenderContext",{value:Ge}),Wt.version="2.6.12";var tl,nl,ol,il,rl,al,ll,sl,cl,ul,dl=g("style,class"),fl=g("input,textarea,option,select,progress"),pl=function(e,t,n){return"value"===n&&fl(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},hl=g("contenteditable,draggable,spellcheck"),gl=g("events,caret,typing,plaintext-only"),ml=function(e,t){return _l(t)||"false"===t?"false":"contenteditable"===e&&gl(t)?t:"true"},vl=g("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),bl="http://www.w3.org/1999/xlink",xl=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},yl=function(e){return xl(e)?e.slice(6,e.length):""},_l=function(e){return null==e||!1===e},wl={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},kl=g("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template,blockquote,iframe,tfoot"),Sl=g("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view",!0),Ml=function(e){return"pre"===e},Cl=function(e){return kl(e)||Sl(e)},Al=Object.create(null),Tl=g("text,number,password,search,email,tel,url"),El=Object.freeze({createElement:fn,createElementNS:pn,createTextNode:hn,createComment:gn,insertBefore:mn,removeChild:vn,appendChild:bn,parentNode:xn,nextSibling:yn,tagName:_n,setTextContent:wn,setStyleScope:kn}),Il={create:function(e,t){Sn(t)},update:function(e,t){e.data.ref!==t.data.ref&&(Sn(e,!0),Sn(t))},destroy:function(e){Sn(e,!0)}},Ol=new da("",{},[]),Ll=["create","activate","update","remove","destroy"],Pl={create:Tn,update:Tn,destroy:function(e){Tn(e,Ol)}},Dl=Object.create(null),zl=[Il,Pl],Rl={create:Pn,update:Pn},Nl={create:Rn,update:Rn},Bl=/[\w).+\-_$\]]/,Fl="__r",Vl="__c",jl=wa&&!(Jr&&Number(Jr[1])<=53),$l={create:mo,update:mo},Hl={create:vo,update:vo},Wl=b(function(e){var t={},n=/;(?![^(]*\))/g,o=/:(.+)/;return e.split(n).forEach(function(e){if(e){var n=e.split(o);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}),Gl=/^--/,Ul=/\s*!important$/,ql=function(e,t,n){if(Gl.test(t))e.style.setProperty(t,n);else if(Ul.test(n))e.style.setProperty(Pr(t),n.replace(Ul,""),"important");else{var o=Zl(t);if(Array.isArray(n))for(var i=0,r=n.length;ih?(d=o(n[v+1])?null:n[v+1].elm,b(e,d,n,p,v,r)):p>v&&y(t,f,h)}function k(e,t,n,o){for(var r=n;r\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Gs=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Us="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+jr.source+"]*",qs="((?:"+Us+"\\:)?"+Us+")",Ys=new RegExp("^<"+qs),Zs=/^\s*(\/?)>/,Xs=new RegExp("^<\\/"+qs+"[^>]*>"),Ks=/^]+>/i,Js=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},oc=/&(?:lt|gt|quot|amp|#39);/g,ic=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,rc=g("pre,textarea",!0),ac=function(e,t){return e&&rc(e)&&"\n"===t[0]},lc=/^@|^v-on:/,sc=/^v-|^@|^:|^#/,cc=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,uc=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,dc=/^\(|\)$/g,fc=/^\[.*\]$/,pc=/:(.*)$/,hc=/^:|^\.|^v-bind:/,gc=/\.[^.\]]+(?=[^\]]*$)/g,mc=/^v-slot(:|$)|^#/,vc=/[\r\n]/,bc=/\s+/g,xc=b(Vs.decode),yc="_empty_",_c=/^xmlns:NS\d+/,wc=/^NS\d+:/,kc={preTransformNode:Di},Sc=[Bs,Fs,kc],Mc={model:ao,text:Ri,html:Ni},Cc={expectHTML:!0,modules:Sc,directives:Mc,isPreTag:Ml,isUnaryTag:js,mustUseProp:pl,canBeLeftOpenTag:$s,isReservedTag:Cl,getTagNamespace:cn,staticKeys:function(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}(Sc)},Ac=b(Fi),Tc=/^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/,Ec=/\([^)]*?\);*$/,Ic=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Oc={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Lc={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},Pc=function(e){return"if("+e+")return null;"},Dc={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Pc("$event.target !== $event.currentTarget"),ctrl:Pc("!$event.ctrlKey"),shift:Pc("!$event.shiftKey"),alt:Pc("!$event.altKey"),meta:Pc("!$event.metaKey"),left:Pc("'button' in $event && $event.button !== 0"),middle:Pc("'button' in $event && $event.button !== 1"),right:Pc("'button' in $event && $event.button !== 2")},zc={on:Yi,bind:Zi,cloak:S},Rc=function(e){this.options=e,this.warn=e.warn||Fn,this.transforms=Vn(e.modules,"transformCode"),this.dataGenFns=Vn(e.modules,"genData"),this.directives=w(w({},zc),e.directives);var t=e.isReservedTag||zr;this.maybeComponent=function(e){return!!e.component||!t(e.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1},Nc=(new RegExp("\\b"+"do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,super,throw,while,yield,delete,export,import,return,switch,default,extends,finally,continue,debugger,function,arguments".split(",").join("\\b|\\b")+"\\b"),new RegExp("\\b"+"delete,typeof,void".split(",").join("\\s*\\([^\\)]*\\)|\\b")+"\\s*\\([^\\)]*\\)"),function(e){return function(t){function n(n,o){var i=Object.create(t),r=[],a=[],l=function(e,t,n){(n?a:r).push(e)};if(o){o.modules&&(i.modules=(t.modules||[]).concat(o.modules)),o.directives&&(i.directives=w(Object.create(t.directives||null),o.directives));for(var s in o)"modules"!==s&&"directives"!==s&&(i[s]=o[s])}i.warn=l;var c=e(n.trim(),i);return c.errors=r,c.tips=a,c}return{compile:n,compileToFunctions:_r(n)}}}(function(e,t){var n=ui(e.trim(),t);!1!==t.optimize&&Bi(n,t);var o=Xi(n,t);return{ast:n,render:o.render,staticRenderFns:o.staticRenderFns}})),Bc=Nc(Cc),Fc=(Bc.compile,Bc.compileToFunctions),Vc=!!Wr&&wr(!1),jc=!!Wr&&wr(!0),$c=b(function(e){var t=dn(e);return t&&t.innerHTML}),Hc=Wt.prototype.$mount;Wt.prototype.$mount=function(e,t){if((e=e&&dn(e))===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var o=n.template;if(o)if("string"==typeof o)"#"===o.charAt(0)&&(o=$c(o));else{if(!o.nodeType)return this;o=o.innerHTML}else e&&(o=kr(e));if(o){var i=Fc(o,{outputSourceRange:!1,shouldDecodeNewlines:Vc,shouldDecodeNewlinesForHref:jc,delimiters:n.delimiters,comments:n.comments},this),r=i.render,a=i.staticRenderFns;n.render=r,n.staticRenderFns=a}}return Hc.call(this,e,t)},Wt.compile=Fc,t.default=Wt}.call(t,n(48),n(631).setImmediate)},function(e,t,n){function o(e,t){var n,o,i,r=e.type,a=t.getMin(),l=t.getMax(),s=null!=a,c=null!=l,u=e.getExtent();return"ordinal"===r?n=(t.get("data")||[]).length:(o=t.get("boundaryGap"),d.isArray(o)||(o=[o||0,o||0]),"boolean"==typeof o[0]&&(o=[0,0]),o[0]=m.parsePercent(o[0],1),o[1]=m.parsePercent(o[1],1),i=u[1]-u[0]||Math.abs(u[0])),null==a&&(a="ordinal"===r?n?0:NaN:u[0]-o[0]*i),null==l&&(l="ordinal"===r?n?n-1:NaN:u[1]+o[1]*i),"dataMin"===a?a=u[0]:"function"==typeof a&&(a=a({min:u[0],max:u[1]})),"dataMax"===l?l=u[1]:"function"==typeof l&&(l=l({min:u[0],max:u[1]})),(null==a||!isFinite(a))&&(a=NaN),(null==l||!isFinite(l))&&(l=NaN),e.setBlank(d.eqNaN(a)||d.eqNaN(l)),t.getNeedCrossZero()&&(a>0&&l>0&&!s&&(a=0),a<0&&l<0&&!c&&(l=0)),[a,l]}function i(e,t){var n=o(e,t),i=null!=t.getMin(),r=null!=t.getMax(),a=t.get("splitNumber");"log"===e.type&&(e.base=t.get("logBase"));var l=e.type;e.setExtent(n[0],n[1]),e.niceExtent({splitNumber:a,fixMin:i,fixMax:r,minInterval:"interval"===l||"time"===l?t.get("minInterval"):null,maxInterval:"interval"===l||"time"===l?t.get("maxInterval"):null});var s=t.get("interval");null!=s&&e.setInterval&&e.setInterval(s)}function r(e,t){if(t=t||e.get("type"))switch(t){case"category":return new p(e.getCategories(),[1/0,-1/0]);case"value":return new h;default:return(g.getClass(t)||h).create(e)}}function a(e){var t=e.scale.getExtent(),n=t[0],o=t[1];return!(n>0&&o>0||n<0&&o<0)}function l(e,t,n,o,i){var r,a=0,l=0,s=(o-i)/180*Math.PI,c=1;t.length>40&&(c=Math.floor(t.length/40));for(var u=0;u1?c:(a+1)*c-1}function s(e,t){var n=e.scale,o=n.getTicksLabels(),i=n.getTicks();return"string"==typeof t?(t=function(e){return function(t){return e.replace("{value}",null!=t?t:"")}}(t),d.map(o,t)):"function"==typeof t?d.map(i,function(n,o){return t(c(e,n),o)},this):o}function c(e,t){return"category"===e.type?e.scale.getLabel(t):t}var u=n(4),d=(u.__DEV__,n(0)),f=n(27),p=n(604),h=n(90),g=n(91),m=n(3);n(605),n(603),t.getScaleExtent=o,t.niceScaleExtent=i,t.createScaleByModel=r,t.ifAxisCrossZero=a,t.getAxisLabelInterval=l,t.getFormattedLabels=s,t.getAxisRawValue=c},function(e,t,n){function o(e,t){if("image"!==this.type){var n=this.style,o=this.shape;o&&"line"===o.symbolType?n.stroke=e:this.__isEmptyBrush?(n.stroke=e,n.fill=t||"#fff"):(n.fill&&(n.fill=e),n.stroke&&(n.stroke=e)),this.dirty(!1)}}function i(e,t,n,i,r,s,c){var u=0===e.indexOf("empty");u&&(e=e.substr(5,1).toLowerCase()+e.substr(6));var d;return d=0===e.indexOf("image://")?a.makeImage(e.slice(8),new l(t,n,i,r),c?"center":"cover"):0===e.indexOf("path://")?a.makePath(e.slice(7),{},new l(t,n,i,r),c?"center":"cover"):new g({shape:{symbolType:e,x:t,y:n,width:i,height:r}}),d.__isEmptyBrush=u,d.setColor=o,d.setColor(s),d}var r=n(0),a=n(2),l=n(10),s=a.extendShape({type:"triangle",shape:{cx:0,cy:0,width:0,height:0},buildPath:function(e,t){var n=t.cx,o=t.cy,i=t.width/2,r=t.height/2;e.moveTo(n,o-r),e.lineTo(n+i,o+r),e.lineTo(n-i,o+r),e.closePath()}}),c=a.extendShape({type:"diamond",shape:{cx:0,cy:0,width:0,height:0},buildPath:function(e,t){var n=t.cx,o=t.cy,i=t.width/2,r=t.height/2;e.moveTo(n,o-r),e.lineTo(n+i,o),e.lineTo(n,o+r),e.lineTo(n-i,o),e.closePath()}}),u=a.extendShape({type:"pin",shape:{x:0,y:0,width:0,height:0},buildPath:function(e,t){var n=t.x,o=t.y,i=t.width/5*3,r=Math.max(i,t.height),a=i/2,l=a*a/(r-a),s=o-r+a+l,c=Math.asin(l/a),u=Math.cos(c)*a,d=Math.sin(c),f=Math.cos(c),p=.6*a,h=.7*a;e.moveTo(n-u,s+l),e.arc(n,s,a,Math.PI-c,2*Math.PI+c),e.bezierCurveTo(n+u-d*p,s+l+f*p,n,o-h,n,o),e.bezierCurveTo(n,o-h,n-u+d*p,s+l+f*p,n-u,s+l),e.closePath()}}),d=a.extendShape({type:"arrow",shape:{x:0,y:0,width:0,height:0},buildPath:function(e,t){var n=t.height,o=t.width,i=t.x,r=t.y,a=o/3*2;e.moveTo(i,r),e.lineTo(i+a,r+n),e.lineTo(i,r+n/4*3),e.lineTo(i-a,r+n),e.lineTo(i,r),e.closePath()}}),f={line:a.Line,rect:a.Rect,roundRect:a.Rect,square:a.Rect,circle:a.Circle,diamond:c,pin:u,arrow:d,triangle:s},p={line:function(e,t,n,o,i){i.x1=e,i.y1=t+o/2,i.x2=e+n,i.y2=t+o/2},rect:function(e,t,n,o,i){i.x=e,i.y=t,i.width=n,i.height=o},roundRect:function(e,t,n,o,i){i.x=e,i.y=t,i.width=n,i.height=o,i.r=Math.min(n,o)/4},square:function(e,t,n,o,i){var r=Math.min(n,o);i.x=e,i.y=t,i.width=r,i.height=r},circle:function(e,t,n,o,i){i.cx=e+n/2,i.cy=t+o/2,i.r=Math.min(n,o)/2},diamond:function(e,t,n,o,i){i.cx=e+n/2,i.cy=t+o/2,i.width=n,i.height=o},pin:function(e,t,n,o,i){i.x=e+n/2,i.y=t+o/2,i.width=n,i.height=o},arrow:function(e,t,n,o,i){i.x=e+n/2,i.y=t+o/2,i.width=n,i.height=o},triangle:function(e,t,n,o,i){i.cx=e+n/2,i.cy=t+o/2,i.width=n,i.height=o}},h={};r.each(f,function(e,t){h[t]=new e});var g=a.extendShape({type:"symbol",shape:{symbolType:"",x:0,y:0,width:0,height:0},beforeBrush:function(){var e=this.style;"pin"===this.shape.symbolType&&"inside"===e.textPosition&&(e.textPosition=["50%","40%"],e.textAlign="center",e.textVerticalAlign="middle")},buildPath:function(e,t,n){var o=t.symbolType,i=h[o];"none"!==t.symbolType&&(i||(o="rect",i=h[o]),p[o](t.x,t.y,t.width,t.height,i.shape),i.buildPath(e,i.shape,n))}});t.createSymbol=i},function(e,t){function n(){var e=new u(6);return o(e),e}function o(e){return e[0]=1,e[1]=0,e[2]=0,e[3]=1,e[4]=0,e[5]=0,e}function i(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4],e[5]=t[5],e}function r(e,t,n){var o=t[0]*n[0]+t[2]*n[1],i=t[1]*n[0]+t[3]*n[1],r=t[0]*n[2]+t[2]*n[3],a=t[1]*n[2]+t[3]*n[3],l=t[0]*n[4]+t[2]*n[5]+t[4],s=t[1]*n[4]+t[3]*n[5]+t[5];return e[0]=o,e[1]=i,e[2]=r,e[3]=a,e[4]=l,e[5]=s,e}function a(e,t,n){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4]+n[0],e[5]=t[5]+n[1],e}function l(e,t,n){var o=t[0],i=t[2],r=t[4],a=t[1],l=t[3],s=t[5],c=Math.sin(n),u=Math.cos(n);return e[0]=o*u+a*c,e[1]=-o*c+a*u,e[2]=i*u+l*c,e[3]=-i*c+u*l,e[4]=u*r+c*s,e[5]=u*s-c*r,e}function s(e,t,n){var o=n[0],i=n[1];return e[0]=t[0]*o,e[1]=t[1]*i,e[2]=t[2]*o,e[3]=t[3]*i,e[4]=t[4]*o,e[5]=t[5]*i,e}function c(e,t){var n=t[0],o=t[2],i=t[4],r=t[1],a=t[3],l=t[5],s=n*a-r*o;return s?(s=1/s,e[0]=a*s,e[1]=-r*s,e[2]=-o*s,e[3]=n*s,e[4]=(o*l-a*i)*s,e[5]=(r*i-n*l)*s,e):null}var u="undefined"==typeof Float32Array?Array:Float32Array;t.create=n,t.identity=o,t.copy=i,t.mul=r,t.translate=a,t.rotate=l,t.scale=s,t.invert=c},function(e,t,n){function o(e,t,n){function o(e,t,n){d[t]?e.otherDims[t]=n:(e.coordDim=t,e.coordDimIndex=n,m.set(t,!0))}function a(e,t,n){if(n||null!=t.get(e)){for(var o=0;null!=t.get(e+o);)o++;e+=o}return t.set(e,!0),e}t=t||[],n=n||{},e=(e||[]).slice();var p=(n.dimsDef||[]).slice(),h=r.createHashMap(n.encodeDef),g=r.createHashMap(),m=r.createHashMap(),v=[],b=n.dimCount;if(null==b){var x=i(t[0]);b=Math.max(r.isArray(x)&&x.length||1,e.length,p.length),s(e,function(e){var t=e.dimsDef;t&&(b=Math.max(b,t.length))})}for(var y=0;yI&&(E=0,T={}),E++,T[n]=i,i}function r(e,t,n,o,i,r,s){return r?l(e,t,n,o,i,r,s):a(e,t,n,o,i,s)}function a(e,t,n,o,r,a){var l=v(e,t,r,a),u=i(e,t);r&&(u+=r[1]+r[3]);var d=l.outerHeight,f=s(0,u,n),p=c(0,d,o),h=new _(f,p,u,d);return h.lineHeight=l.lineHeight,h}function l(e,t,n,o,i,r,a){var l=b(e,{rich:r,truncate:a,font:t,textAlign:n,textPadding:i}),u=l.outerWidth,d=l.outerHeight,f=s(0,u,n),p=c(0,d,o);return new _(f,p,u,d)}function s(e,t,n){return"right"===n?e-=t:"center"===n&&(e-=t/2),e}function c(e,t,n){return"middle"===n?e-=t/2:"bottom"===n&&(e-=t),e}function u(e,t,n){var o=t.x,i=t.y,r=t.height,a=t.width,l=r/2,s="left",c="top";switch(e){case"left":o-=n,i+=l,s="right",c="middle";break;case"right":o+=n+a,i+=l,c="middle";break;case"top":o+=a/2,i-=n,s="center",c="bottom";break;case"bottom":o+=a/2,i+=r+n,s="center";break;case"inside":o+=a/2,i+=l,s="center",c="middle";break;case"insideLeft":o+=n,i+=l,c="middle";break;case"insideRight":o+=a-n,i+=l,s="right",c="middle";break;case"insideTop":o+=a/2,i+=n,s="center";break;case"insideBottom":o+=a/2,i+=r-n,s="center",c="bottom";break;case"insideTopLeft":o+=n,i+=n;break;case"insideTopRight":o+=a-n,i+=n,s="right";break;case"insideBottomLeft":o+=n,i+=r-n,c="bottom";break;case"insideBottomRight":o+=a-n,i+=r-n,s="right",c="bottom"}return{x:o,y:i,textAlign:s,textVerticalAlign:c}}function d(e,t,n,o,i){if(!t)return"";var r=(e+"").split("\n");i=f(t,n,o,i);for(var a=0,l=r.length;a=a;s++)l-=a;var c=i(n);return c>l&&(n="",c=0),l=e-c,o.ellipsis=n,o.ellipsisWidth=c,o.contentWidth=l,o.containerWidth=e,o}function p(e,t){var n=t.containerWidth,o=t.font,r=t.contentWidth;if(!n)return"";var a=i(e,o);if(a<=n)return e;for(var l=0;;l++){if(a<=r||l>=t.maxIterations){e+=t.ellipsis;break}var s=0===l?h(e,r,t.ascCharWidth,t.cnCharWidth):a>0?Math.floor(e.length*r/a):0;e=e.substr(0,s),a=i(e,o)}return""===e&&(e=t.placeholder),e}function h(e,t,n,o){for(var i=0,r=0,a=e.length;rs)e="",r=[];else if(null!=c)for(var u=f(c-(n?n[1]+n[3]:0),t,o.ellipsis,{minChar:o.minChar,placeholder:o.placeholder}),d=0,h=r.length;dr&&x(n,e.substring(r,a)),x(n,o[2],o[1]),r=O.lastIndex}rm)return{lines:[],width:0,height:0};S.textWidth=i(S.text,E);var L=M.textWidth,P=null==L||"auto"===L;if("string"==typeof L&&"%"===L.charAt(L.length-1))S.percentWidth=L,u.push(S),L=0;else{if(P){L=S.textWidth;var D=M.textBackgroundColor,z=D&&D.image;z&&(z=w.findExistImage(z),w.isImageReady(z)&&(L=Math.max(L,z.width*I/z.height)))}var R=T?T[1]+T[3]:0;L+=R;var N=null!=h?h-_:null;null!=N&&N-1}function i(e,t){if(e){for(var n=e.className,i=(t||"").split(" "),r=0,a=i.length;ro.top&&n.right>o.left&&n.left=0){var r="touchend"!=o?t.targetTouches[0]:t.changedTouches[0];r&&i(e,r,t,n)}else i(e,t,t,n),t.zrDelta=t.wheelDelta?t.wheelDelta/120:-(t.detail||0)/3;var a=t.button;return null==t.which&&void 0!==a&&p.test(t.type)&&(t.which=1&a?1:2&a?3:4&a?2:0),t}function l(e,t,n){f?e.addEventListener(t,n):e.attachEvent("on"+t,n)}function s(e,t,n){f?e.removeEventListener(t,n):e.detachEvent("on"+t,n)}function c(e){return e.which>1}var u=n(49);t.Dispatcher=u;var d=n(15),f="undefined"!=typeof window&&!!window.addEventListener,p=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,h=f?function(e){e.preventDefault(),e.stopPropagation(),e.cancelBubble=!0}:function(e){e.returnValue=!1,e.cancelBubble=!0};t.clientToLocal=i,t.normalizeEvent=a,t.addEventListener=l,t.removeEventListener=s,t.stop=h,t.notLeftMouse=c},function(e,t,n){function o(e){return e=Math.round(e),e<0?0:e>255?255:e}function i(e){return e=Math.round(e),e<0?0:e>360?360:e}function r(e){return e<0?0:e>1?1:e}function a(e){return o(e.length&&"%"===e.charAt(e.length-1)?parseFloat(e)/100*255:parseInt(e,10))}function l(e){return r(e.length&&"%"===e.charAt(e.length-1)?parseFloat(e)/100:parseFloat(e))}function s(e,t,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?e+(t-e)*n*6:2*n<1?t:3*n<2?e+(t-e)*(2/3-n)*6:e}function c(e,t,n){return e+(t-e)*n}function u(e,t,n,o,i){return e[0]=t,e[1]=n,e[2]=o,e[3]=i,e}function d(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e}function f(e,t){C&&d(C,t),C=M.put(e,C||t.slice())}function p(e,t){if(e){t=t||[];var n=M.get(e);if(n)return d(t,n);e+="";var o=e.replace(/ /g,"").toLowerCase();if(o in S)return d(t,S[o]),f(e,t),t;if("#"!==o.charAt(0)){var i=o.indexOf("("),r=o.indexOf(")");if(-1!==i&&r+1===o.length){var s=o.substr(0,i),c=o.substr(i+1,r-(i+1)).split(","),p=1;switch(s){case"rgba":if(4!==c.length)return void u(t,0,0,0,1);p=l(c.pop());case"rgb":return 3!==c.length?void u(t,0,0,0,1):(u(t,a(c[0]),a(c[1]),a(c[2]),p),f(e,t),t);case"hsla":return 4!==c.length?void u(t,0,0,0,1):(c[3]=l(c[3]),h(c,t),f(e,t),t);case"hsl":return 3!==c.length?void u(t,0,0,0,1):(h(c,t),f(e,t),t);default:return}}u(t,0,0,0,1)}else{if(4===o.length){var g=parseInt(o.substr(1),16);return g>=0&&g<=4095?(u(t,(3840&g)>>4|(3840&g)>>8,240&g|(240&g)>>4,15&g|(15&g)<<4,1),f(e,t),t):void u(t,0,0,0,1)}if(7===o.length){var g=parseInt(o.substr(1),16);return g>=0&&g<=16777215?(u(t,(16711680&g)>>16,(65280&g)>>8,255&g,1),f(e,t),t):void u(t,0,0,0,1)}}}}function h(e,t){var n=(parseFloat(e[0])%360+360)%360/360,i=l(e[1]),r=l(e[2]),a=r<=.5?r*(i+1):r+i-r*i,c=2*r-a;return t=t||[],u(t,o(255*s(c,a,n+1/3)),o(255*s(c,a,n)),o(255*s(c,a,n-1/3)),1),4===e.length&&(t[3]=e[3]),t}function g(e){if(e){var t,n,o=e[0]/255,i=e[1]/255,r=e[2]/255,a=Math.min(o,i,r),l=Math.max(o,i,r),s=l-a,c=(l+a)/2;if(0===s)t=0,n=0;else{n=c<.5?s/(l+a):s/(2-l-a);var u=((l-o)/6+s/2)/s,d=((l-i)/6+s/2)/s,f=((l-r)/6+s/2)/s;o===l?t=f-d:i===l?t=1/3+u-f:r===l&&(t=2/3+d-u),t<0&&(t+=1),t>1&&(t-=1)}var p=[360*t,n,c];return null!=e[3]&&p.push(e[3]),p}}function m(e,t){var n=p(e);if(n){for(var o=0;o<3;o++)n[o]=t<0?n[o]*(1-t)|0:(255-n[o])*t+n[o]|0;return w(n,4===n.length?"rgba":"rgb")}}function v(e){var t=p(e);if(t)return((1<<24)+(t[0]<<16)+(t[1]<<8)+ +t[2]).toString(16).slice(1)}function b(e,t,n){if(t&&t.length&&e>=0&&e<=1){n=n||[];var i=e*(t.length-1),a=Math.floor(i),l=Math.ceil(i),s=t[a],u=t[l],d=i-a;return n[0]=o(c(s[0],u[0],d)),n[1]=o(c(s[1],u[1],d)),n[2]=o(c(s[2],u[2],d)),n[3]=r(c(s[3],u[3],d)),n}}function x(e,t,n){if(t&&t.length&&e>=0&&e<=1){var i=e*(t.length-1),a=Math.floor(i),l=Math.ceil(i),s=p(t[a]),u=p(t[l]),d=i-a,f=w([o(c(s[0],u[0],d)),o(c(s[1],u[1],d)),o(c(s[2],u[2],d)),r(c(s[3],u[3],d))],"rgba");return n?{color:f,leftIndex:a,rightIndex:l,value:i}:f}}function y(e,t,n,o){if(e=p(e))return e=g(e),null!=t&&(e[0]=i(t)),null!=n&&(e[1]=l(n)),null!=o&&(e[2]=l(o)),w(h(e),"rgba")}function _(e,t){if((e=p(e))&&null!=t)return e[3]=r(t),w(e,"rgba")}function w(e,t){if(e&&e.length){var n=e[0]+","+e[1]+","+e[2];return"rgba"!==t&&"hsva"!==t&&"hsla"!==t||(n+=","+e[3]),t+"("+n+")"}}var k=n(244),S={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]},M=new k(20),C=null,A=b,T=x;t.parse=p,t.lift=m,t.toHex=v,t.fastLerp=b,t.fastMapToColor=A,t.lerp=x,t.mapToColor=T,t.modifyHSL=y,t.modifyAlpha=_,t.stringify=w},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){function o(e){for(var t=0;t=0&&i(e)?function(e,t,n,o){return m(e)&&(y.hasItemOption=!0),o===x?n:g(h(e),p[o])}:function(e,t,n,o){var i=h(e),r=g(i&&i[o],p[o]);m(e)&&(y.hasItemOption=!0);var a=c&&c.categoryAxesModels;return a&&a[t]&&"string"==typeof r&&(w[t]=w[t]||a[t].getCategories(),(r=u.indexOf(w[t],r))<0&&!isNaN(r)&&(r=+r)),r};return y.hasItemOption=!1,y.initData(e,_,k),y}function a(e){return"category"!==e&&"time"!==e}function l(e){return"category"===e?"ordinal":"time"===e?"time":"float"}function s(e,t){var n,o=[],i=e&&e.dimensions[e.categoryIndex];if(i&&(n=e.categoryAxesModels[i.name]),n){var r=n.getCategories();if(r){var a=t.length;if(u.isArray(t[0])&&t[0].length>1){o=[];for(var l=0;l=n&&e<=o},containData:function(e){return this.contain(this.dataToCoord(e))},getExtent:function(){return this._extent.slice()},getPixelPrecision:function(e){return r.getPixelPrecision(e||this.scale.getExtent(),this._extent)},setExtent:function(e,t){var n=this._extent;n[0]=e,n[1]=t},dataToCoord:function(e,t){var n=this._extent,i=this.scale;return e=i.normalize(e),this.onBand&&"ordinal"===i.type&&(n=n.slice(),o(n,i.count())),l(e,s,n,t)},coordToData:function(e,t){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&(n=n.slice(),o(n,i.count()));var r=l(e,n,s,t);return this.scale.scale(r)},pointToData:function(e,t){},getTicksCoords:function(e){if(this.onBand&&!e){for(var t=this.getBands(),n=[],o=0;o0&&void 0!==arguments[0]?arguments[0]:"";return String(e).replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")},t.arrayFindIndex=function(e,t){for(var n=0;n!==e.length;++n)if(t(e[n]))return n;return-1}),m=(t.arrayFind=function(e,t){var n=g(e,t);return-1!==n?e[n]:void 0},t.coerceTruthyValueToArray=function(e){return Array.isArray(e)?e:e?[e]:[]},t.isIE=function(){return!f.default.prototype.$isServer&&!isNaN(Number(document.documentMode))},t.isEdge=function(){return!f.default.prototype.$isServer&&navigator.userAgent.indexOf("Edge")>-1},t.isFirefox=function(){return!f.default.prototype.$isServer&&!!window.navigator.userAgent.match(/firefox/i)},t.autoprefixer=function(e){if("object"!==(void 0===e?"undefined":u(e)))return e;var t=["ms-","webkit-"];return["transform","transition","animation"].forEach(function(n){var o=e[n];n&&o&&t.forEach(function(t){e[t+n]=o})}),e},t.kebabCase=function(e){var t=/([^-])([A-Z])/g;return e.replace(t,"$1-$2").replace(t,"$1-$2").toLowerCase()},t.capitalize=function(e){return(0,p.isString)(e)?e.charAt(0).toUpperCase()+e.slice(1):e},t.looseEqual=function(e,t){var n=(0,p.isObject)(e),o=(0,p.isObject)(t);return n&&o?JSON.stringify(e)===JSON.stringify(t):!n&&!o&&String(e)===String(t)}),v=t.arrayEquals=function(e,t){if(e=e||[],t=t||[],e.length!==t.length)return!1;for(var n=0;n-w&&ew||e<-w}function r(e,t,n,o,i){var r=1-i;return r*r*(r*e+3*i*t)+i*i*(i*o+3*r*n)}function a(e,t,n,o,i){var r=1-i;return 3*(((t-e)*r+2*(n-t)*i)*r+(o-n)*i*i)}function l(e,t,n,i,r,a){var l=i+3*(t-n)-e,s=3*(n-2*t+e),c=3*(t-e),u=e-r,d=s*s-3*l*c,f=s*c-9*l*u,p=c*c-3*s*u,h=0;if(o(d)&&o(f))if(o(s))a[0]=0;else{var g=-c/s;g>=0&&g<=1&&(a[h++]=g)}else{var m=f*f-4*d*p;if(o(m)){var v=f/d,g=-s/l+v,b=-v/2;g>=0&&g<=1&&(a[h++]=g),b>=0&&b<=1&&(a[h++]=b)}else if(m>0){var x=_(m),w=d*s+1.5*l*(-f+x),k=d*s+1.5*l*(-f-x);w=w<0?-y(-w,M):y(w,M),k=k<0?-y(-k,M):y(k,M);var g=(-s-(w+k))/(3*l);g>=0&&g<=1&&(a[h++]=g)}else{var C=(2*d*s-3*l*f)/(2*_(d*d*d)),A=Math.acos(C)/3,T=_(d),E=Math.cos(A),g=(-s-2*T*E)/(3*l),b=(-s+T*(E+S*Math.sin(A)))/(3*l),I=(-s+T*(E-S*Math.sin(A)))/(3*l);g>=0&&g<=1&&(a[h++]=g),b>=0&&b<=1&&(a[h++]=b),I>=0&&I<=1&&(a[h++]=I)}}return h}function s(e,t,n,r,a){var l=6*n-12*t+6*e,s=9*t+3*r-3*e-9*n,c=3*t-3*e,u=0;if(o(s)){if(i(l)){var d=-c/l;d>=0&&d<=1&&(a[u++]=d)}}else{var f=l*l-4*s*c;if(o(f))a[0]=-l/(2*s);else if(f>0){var p=_(f),d=(-l+p)/(2*s),h=(-l-p)/(2*s);d>=0&&d<=1&&(a[u++]=d),h>=0&&h<=1&&(a[u++]=h)}}return u}function c(e,t,n,o,i,r){var a=(t-e)*i+e,l=(n-t)*i+t,s=(o-n)*i+n,c=(l-a)*i+a,u=(s-l)*i+l,d=(u-c)*i+c;r[0]=e,r[1]=a,r[2]=c,r[3]=d,r[4]=d,r[5]=u,r[6]=s,r[7]=o}function u(e,t,n,o,i,a,l,s,c,u,d){var f,p,h,g,m,v=.005,b=1/0;C[0]=c,C[1]=u;for(var y=0;y<1;y+=.05)A[0]=r(e,n,i,l,y),A[1]=r(t,o,a,s,y),(g=x(C,A))=0&&g=0&&d<=1&&(a[u++]=d)}}else{var f=s*s-4*l*c;if(o(f)){var d=-s/(2*l);d>=0&&d<=1&&(a[u++]=d)}else if(f>0){var p=_(f),d=(-s+p)/(2*l),h=(-s-p)/(2*l);d>=0&&d<=1&&(a[u++]=d),h>=0&&h<=1&&(a[u++]=h)}}return u}function h(e,t,n){var o=e+n-2*t;return 0===o?.5:(e-t)/o}function g(e,t,n,o,i){var r=(t-e)*o+e,a=(n-t)*o+t,l=(a-r)*o+r;i[0]=e,i[1]=r,i[2]=l,i[3]=l,i[4]=a,i[5]=n}function m(e,t,n,o,i,r,a,l,s){var c,u=.005,f=1/0;C[0]=a,C[1]=l;for(var p=0;p<1;p+=.05){A[0]=d(e,n,i,p),A[1]=d(t,o,r,p);var h=x(C,A);h=0&&ho[1],s="start"===t&&!l||"start"!==t&&l;return _(a-T/2)?(r=s?"bottom":"top",i="center"):_(a-1.5*T)?(r=s?"top":"bottom",i="center"):(r="middle",i=a<1.5*T&&a>T/2?s?"left":"right":s?"right":"left"),{rotation:a,textAlign:i,textVerticalAlign:r}}function r(e){var t=e.get("tooltip");return e.get("silent")||!(e.get("triggerEvent")||t&&t.show)}function a(e,t,n){var o=e.get("axisLabel.showMinLabel"),i=e.get("axisLabel.showMaxLabel");t=t||[],n=n||[];var r=t[0],a=t[1],c=t[t.length-1],u=t[t.length-2],d=n[0],f=n[1],p=n[n.length-1],h=n[n.length-2];!1===o?(l(r),l(d)):s(r,a)&&(o?(l(a),l(f)):(l(r),l(d))),!1===i?(l(c),l(p)):s(u,c)&&(i?(l(u),l(h)):(l(c),l(p)))}function l(e){e&&(e.ignore=!0)}function s(e,t,n){var o=e&&e.getBoundingRect().clone(),i=t&&t.getBoundingRect().clone();if(o&&i){var r=M.identity([]);return M.rotate(r,r,-e.rotation),o.applyTransform(M.mul([],r,e.getLocalTransform())),i.applyTransform(M.mul([],r,t.getLocalTransform())),o.intersect(i)}}function c(e){return"middle"===e||"center"===e}function u(e,t,n){var o=t.axis;if(t.get("axisTick.show")&&!o.scale.isBlank()){for(var i=t.getModel("axisTick"),r=i.getModel("lineStyle"),a=i.get("length"),l=P(i,n.labelInterval),s=o.getTicksCoords(i.get("alignWithLabel")),c=o.scale.getTicks(),u=t.get("axisLabel.showMinLabel"),d=t.get("axisLabel.showMaxLabel"),f=[],p=[],g=e._transform,m=[],v=s.length,x=0;xf[1]?-1:1,m=["start"===l?f[0]-h*d:"end"===l?f[1]+h*d:(f[0]+f[1])/2,c(l)?e.labelOffset+s*d:0],x=t.get("nameRotate");null!=x&&(x=x*T/180);var y;c(l)?a=O(e.rotation,null!=x?x:e.rotation,s):(a=i(e,l,x||0,f),null!=(y=e.axisNameAvailableWidth)&&(y=Math.abs(y/Math.sin(a.rotation)),!isFinite(y)&&(y=null)));var _=u.getFont(),w=t.get("nameTruncate",!0)||{},k=w.ellipsis,S=p(e.nameTruncateMaxWidth,w.maxWidth,y),M=null!=k&&null!=S?v.truncateText(n,S,_,k,{minChar:2,placeholder:w.placeholder}):n,C=t.get("tooltip",!0),A=t.mainType,E={componentType:A,name:n,$vars:["name"]};E[A+"Index"]=t.componentIndex;var I=new b.Text({anid:"name",__fullText:n,__truncatedText:M,position:m,rotation:a.rotation,silent:r(t),z2:1,tooltip:C&&C.show?g({content:n,formatter:function(){return n},formatterParams:E},C):null});b.setTextStyle(I.style,u,{text:M,textFont:_,textFill:u.getTextColor()||t.get("axisLine.lineStyle.color"),textAlign:a.textAlign,textVerticalAlign:a.textVerticalAlign}),t.get("triggerEvent")&&(I.eventData=o(t),I.eventData.targetType="axisName",I.eventData.name=n),this._dumbGroup.add(I),I.updateTransform(),this.group.add(I),I.decomposeTransform()}}},O=E.innerTextLayout=function(e,t,n){var o,i,r=w(t-e);return _(r)?(i=n>0?"top":"bottom",o="center"):_(r-T)?(i=n>0?"bottom":"top",o="center"):(i="middle",o=r>0&&r0?"right":"left":n>0?"left":"right"),{rotation:r,textAlign:o,textVerticalAlign:i}},L=E.ifIgnoreOnTick=function(e,t,n,o,i,r){if(0===t&&i||t===o-1&&r)return!1;var a,l=e.scale;return"ordinal"===l.type&&("function"==typeof n?(a=l.getTicks()[t],!n(a,l.getLabel(a))):t%(n+1))},P=E.getInterval=function(e,t){var n=e.get("interval");return null!=n&&"auto"!=n||(n=t),n},D=E;e.exports=D},function(e,t,n){function o(e,t,n,o,r,a){var c=s.getAxisPointerClass(e.axisPointerClass);if(c){var u=l.getAxisPointerModel(t);u?(e._axisPointer||(e._axisPointer=new c)).render(t,u,o,a):i(e,o)}}function i(e,t,n){var o=e._axisPointer;o&&o.dispose(t,n),e._axisPointer=null}var r=n(4),a=(r.__DEV__,n(1)),l=n(84),s=a.extendComponentView({type:"axis",_axisPointer:null,axisPointerClass:null,render:function(e,t,n,i){this.axisPointerClass&&l.fixValue(e),s.superApply(this,"render",arguments),o(this,e,t,n,i,!0)},updateAxisPointer:function(e,t,n,i,r){o(this,e,t,n,i,!1)},remove:function(e,t){var n=this._axisPointer;n&&n.remove(t),s.superApply(this,"remove",arguments)},dispose:function(e,t){i(this,t),s.superApply(this,"dispose",arguments)}}),c=[];s.registerAxisPointerClass=function(e,t){c[e]=t},s.getAxisPointerClass=function(e){return e&&c[e]};var u=s;e.exports=u},function(e,t){function n(e,t,n){function o(){u=(new Date).getTime(),d=null,e.apply(a,l||[])}var i,r,a,l,s,c=0,u=0,d=null;t=t||0;var f=function(){i=(new Date).getTime(),a=this,l=arguments;var e=s||t,f=s||n;s=null,r=i-(f?c:u)-e,clearTimeout(d),f?d=setTimeout(o,e):r>=0?o():d=setTimeout(o,-r),c=i};return f.clear=function(){d&&(clearTimeout(d),d=null)},f.debounceNextCall=function(e){s=e},f}function o(e,t,o,i){var s=e[t];if(s){var c=s[r]||s,u=s[l];if(s[a]!==o||u!==i){if(null==o||!i)return e[t]=c;s=e[t]=n(c,o,"debounce"===i),s[r]=c,s[l]=i,s[a]=o}return s}}function i(e,t){var n=e[t];n&&n[r]&&(e[t]=n[r])}var r="\0__throttleOriginMethod",a="\0__throttleRate",l="\0__throttleType";t.throttle=n,t.createOrUpdate=o,t.clear=i},function(e,t,n){function o(e){var t=e.pieceList;e.hasSpecialVisual=!1,g.each(t,function(t,n){t.originIndex=n,null!=t.visual&&(e.hasSpecialVisual=!0)})}function i(e){var t=e.categories,n=e.visual,o=e.categoryMap={};if(x(t,function(e,t){o[e]=t}),!g.isArray(n)){var i=[];g.isObject(n)?x(n,function(e,t){var n=o[t];i[null!=n?n:_]=e}):i[_]=n,n=p(e,i)}for(var r=t.length-1;r>=0;r--)null==n[r]&&(delete o[t[r]],t.pop())}function r(e,t){var n=e.visual,o=[];g.isObject(n)?x(n,function(e){o.push(e)}):null!=n&&o.push(n);var i={color:1,symbol:1};t||1!==o.length||i.hasOwnProperty(e.type)||(o[1]=o[0]),p(e,o)}function a(e){return{applyVisual:function(t,n,o){t=this.mapValueToVisual(t),o("color",e(n("color"),t))},_doMap:d([0,1])}}function l(e){var t=this.option.visual;return t[Math.round(b(e,[0,1],[0,t.length-1],!0))]||{}}function s(e){return function(t,n,o){o(e,this.mapValueToVisual(t))}}function c(e){var t=this.option.visual;return t[this.option.loop&&e!==_?e%t.length:e]}function u(){return this.option.visual[0]}function d(e){return{linear:function(t){return b(t,e,this.option.visual,!0)},category:c,piecewise:function(t,n){var o=f.call(this,n);return null==o&&(o=b(t,e,this.option.visual,!0)),o},fixed:u}}function f(e){var t=this.option,n=t.pieceList;if(t.hasSpecialVisual){var o=w.findPieceIndex(e,n),i=n[o];if(i&&i.visual)return i.visual[this.type]}}function p(e,t){return e.visual=t,"color"===e.type&&(e.parsedVisual=g.map(t,function(e){return m.parse(e)})),t}function h(e,t,n){return e?t<=n:tl[0]+3)return n.toExponential(t).replace("e+","x10^");for(var c=void 0,u=0;u=d){c=d;break}}var f=a-c+1,p=r.split(""),h=p.slice(0,f),g=p.slice(f,f+t+1),m=h.join(""),v=g.join("");v.length=i)return l.formatNumber(e/i,n,"")+" "+o+"B"}return e>=1024?l.formatNumber(e/1024,0)+" KB":l.formatNumber(e,0)+l.pluralize(e," byte")},filesize:function(){return l.fileSize.apply(l,arguments)},formatNumber:function(e){var t=arguments.length<=1||void 0===arguments[1]?0:arguments[1],n=arguments.length<=2||void 0===arguments[2]?",":arguments[2],o=arguments.length<=3||void 0===arguments[3]?".":arguments[3],i=l.normalizePrecision(t),r=e<0&&"-"||"",a=String(parseInt(l.toFixed(Math.abs(e||0),i),10)),s=a.length>3?a.length%3:0;return r+function(e,t,n){return n?e.substr(0,n)+t:""}(a,n,s)+function(e,t,n){return e.substr(n).replace(/(\d{3})(?=\d)/g,"$1"+t)}(a,n,s)+function(e,t,n){return n?t+l.toFixed(Math.abs(e),n).split(".")[1]:""}(e,o,i)},toFixed:function(e,t){t=n(t)?t:l.normalizePrecision(t,0);var o=Math.pow(10,t);return(Math.round(e*o)/o).toFixed(t)},normalizePrecision:function(e,t){return e=Math.round(Math.abs(e)),o(e)?t:e},ordinal:function(e){var t=parseInt(e,10);if(0===t)return e;if([11,12,13].indexOf(t%100)>=0)return t+"th";var n=t%10,o=void 0;switch(n){case 1:o="st";break;case 2:o="nd";break;case 3:o="rd";break;default:o="th"}return""+t+o},times:function(e){var t=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];if(i(e)&&e>=0){var o=parseFloat(e),r=["never","once","twice"];return n(t[o])?String(t[o]):n(r[o])&&r[o].toString()||o.toString()+" times"}return null},pluralize:function(e,t,o){return n(e)&&n(t)?(o=n(o)?o:t+"s",1===parseInt(e,10)?t:o):null},truncate:function(e){var t=arguments.length<=1||void 0===arguments[1]?100:arguments[1],n=arguments.length<=2||void 0===arguments[2]?"...":arguments[2];return e.length>t?e.substring(0,t-n.length)+n:e},truncateWords:function(e,t){for(var o=e.split(" "),i="",r=0;rt?i+"...":null},truncatewords:function(){return l.truncateWords.apply(l,arguments)},boundedNumber:function(e){var t=arguments.length<=1||void 0===arguments[1]?100:arguments[1],n=arguments.length<=2||void 0===arguments[2]?"+":arguments[2],o=void 0;return i(e)&&i(t)&&e>t&&(o=t+n),(o||e).toString()},truncatenumber:function(){return l.boundedNumber.apply(l,arguments)},oxford:function(e,t,o){var i=e.length,r=void 0;if(i<2)return String(e);if(2===i)return e.join(" and ");if(n(t)&&i>t){var a=i-t;r=t,o=n(o)?o:", and "+a+" "+l.pluralize(a,"other")}else r=-1,o=", and "+e[i-1];return e.slice(0,r).join(", ")+o},dictionary:function(e){var t=arguments.length<=1||void 0===arguments[1]?" is ":arguments[1],o=arguments.length<=2||void 0===arguments[2]?", ":arguments[2];if(n(e)&&"object"===(void 0===e?"undefined":r(e))&&!a(e)){var i=[];for(var l in e)if(e.hasOwnProperty(l)){var s=e[l];i.push(""+l+t+s)}return i.join(o)}return""},frequency:function(e,t){if(!a(e))return null;var n=e.length,o=l.times(n);return 0===n?o+" "+t:t+" "+o},pace:function(t,n){var o=arguments.length<=2||void 0===arguments[2]?"time":arguments[2];if(0===t||0===n)return"No "+l.pluralize(0,o);for(var i="Approximately",r=void 0,a=void 0,s=t/n,c=0;c1){r=u.name;break}}r||(i="Less than",a=1,r=e[e.length-1].name);var d=Math.round(a);return o=l.pluralize(d,o),i+" "+d+" "+o+" per "+r},nl2br:function(e){var t=arguments.length<=1||void 0===arguments[1]?"
":arguments[1];return e.replace(/\n/g,t)},br2nl:function(e){var t=arguments.length<=1||void 0===arguments[1]?"\r\n":arguments[1];return e.replace(/\/g,t)},capitalize:function(e){var t=!(arguments.length<=1||void 0===arguments[1])&&arguments[1];return""+e.charAt(0).toUpperCase()+(t?e.slice(1).toLowerCase():e.slice(1))},capitalizeAll:function(e){return e.replace(/(?:^|\s)\S/g,function(e){return e.toUpperCase()})},titleCase:function(e){var t=/\b(a|an|and|at|but|by|de|en|for|if|in|of|on|or|the|to|via|vs?\.?)\b/i,n=/\S+[A-Z]+\S*/,o=/\s+/,i=/-/,r=void 0;return(r=function(e){for(var a=!(arguments.length<=1||void 0===arguments[1])&&arguments[1],s=arguments.length<=2||void 0===arguments[2]||arguments[2],c=[],u=e.split(a?i:o),d=0;d3&&(t=n.call(t,1));for(var i=this._$handlers[e],r=i.length,a=0;a4&&(t=n.call(t,1,t.length-1));for(var i=t[t.length-1],r=this._$handlers[e],a=r.length,l=0;l=0||o&&i.indexOf(o,l)<0)){var s=t.getShallow(l);null!=s&&(r[e[a][0]]=s)}}return r}}var i=n(0);e.exports=o},function(e,t,n){"use strict";function o(e,t,n){this.$children.forEach(function(i){i.$options.componentName===e?i.$emit.apply(i,[t].concat(n)):o.apply(i,[e,t].concat([n]))})}t.__esModule=!0,t.default={methods:{dispatch:function(e,t,n){for(var o=this.$parent||this.$root,i=o.$options.componentName;o&&(!i||i!==e);)(o=o.$parent)&&(i=o.$options.componentName);o&&o.$emit.apply(o,[t].concat(n))},broadcast:function(e,t,n){o.call(this,e,t,n)}}}},function(e,t,n){"use strict";var o=n(649),i=n(235),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){var o=n(39),i=n(7),r=n(133),a=n(10),l=n(95),s=l.devicePixelRatio,c={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},u=[],d=[],f=[],p=[],h=Math.min,g=Math.max,m=Math.cos,v=Math.sin,b=Math.sqrt,x=Math.abs,y="undefined"!=typeof Float32Array,_=function(e){this._saveData=!e,this._saveData&&(this.data=[]),this._ctx=null};_.prototype={constructor:_,_xi:0,_yi:0,_x0:0,_y0:0,_ux:0,_uy:0,_len:0,_lineDash:null,_dashOffset:0,_dashIdx:0,_dashSum:0,setScale:function(e,t){this._ux=x(1/s/e)||0,this._uy=x(1/s/t)||0},getContext:function(){return this._ctx},beginPath:function(e){return this._ctx=e,e&&e.beginPath(),e&&(this.dpr=e.dpr),this._saveData&&(this._len=0),this._lineDash&&(this._lineDash=null,this._dashOffset=0),this},moveTo:function(e,t){return this.addData(c.M,e,t),this._ctx&&this._ctx.moveTo(e,t),this._x0=e,this._y0=t,this._xi=e,this._yi=t,this},lineTo:function(e,t){var n=x(e-this._xi)>this._ux||x(t-this._yi)>this._uy||this._len<5;return this.addData(c.L,e,t),this._ctx&&n&&(this._needsDash()?this._dashedLineTo(e,t):this._ctx.lineTo(e,t)),n&&(this._xi=e,this._yi=t),this},bezierCurveTo:function(e,t,n,o,i,r){return this.addData(c.C,e,t,n,o,i,r),this._ctx&&(this._needsDash()?this._dashedBezierTo(e,t,n,o,i,r):this._ctx.bezierCurveTo(e,t,n,o,i,r)),this._xi=i,this._yi=r,this},quadraticCurveTo:function(e,t,n,o){return this.addData(c.Q,e,t,n,o),this._ctx&&(this._needsDash()?this._dashedQuadraticTo(e,t,n,o):this._ctx.quadraticCurveTo(e,t,n,o)),this._xi=n,this._yi=o,this},arc:function(e,t,n,o,i,r){return this.addData(c.A,e,t,n,n,o,i-o,0,r?0:1),this._ctx&&this._ctx.arc(e,t,n,o,i,r),this._xi=m(i)*n+e,this._yi=v(i)*n+e,this},arcTo:function(e,t,n,o,i){return this._ctx&&this._ctx.arcTo(e,t,n,o,i),this},rect:function(e,t,n,o){return this._ctx&&this._ctx.rect(e,t,n,o),this.addData(c.R,e,t,n,o),this},closePath:function(){this.addData(c.Z);var e=this._ctx,t=this._x0,n=this._y0;return e&&(this._needsDash()&&this._dashedLineTo(t,n),e.closePath()),this._xi=t,this._yi=n,this},fill:function(e){e&&e.fill(),this.toStatic()},stroke:function(e){e&&e.stroke(),this.toStatic()},setLineDash:function(e){if(e instanceof Array){this._lineDash=e,this._dashIdx=0;for(var t=0,n=0;nt.length&&(this._expandData(),t=this.data);for(var n=0;n0&&p<=e||u<0&&p>=e||0==u&&(d>0&&m<=t||d<0&&m>=t);)o=this._dashIdx,n=a[o],p+=u*n,m+=d*n,this._dashIdx=(o+1)%v,u>0&&ps||d>0&&mc||l[o%2?"moveTo":"lineTo"](u>=0?h(p,e):g(p,e),d>=0?h(m,t):g(m,t));u=p-e,d=m-t,this._dashOffset=-b(u*u+d*d)},_dashedBezierTo:function(e,t,n,i,r,a){var l,s,c,u,d,f=this._dashSum,p=this._dashOffset,h=this._lineDash,g=this._ctx,m=this._xi,v=this._yi,x=o.cubicAt,y=0,_=this._dashIdx,w=h.length,k=0;for(p<0&&(p=f+p),p%=f,l=0;l<1;l+=.1)s=x(m,e,n,r,l+.1)-x(m,e,n,r,l),c=x(v,t,i,a,l+.1)-x(v,t,i,a,l),y+=b(s*s+c*c);for(;_p);_++);for(l=(k-p)/y;l<=1;)u=x(m,e,n,r,l),d=x(v,t,i,a,l),_%2?g.moveTo(u,d):g.lineTo(u,d),l+=h[_]/y,_=(_+1)%w;_%2!=0&&g.lineTo(r,a),s=r-u,c=a-d,this._dashOffset=-b(s*s+c*c)},_dashedQuadraticTo:function(e,t,n,o){var i=n,r=o;n=(n+2*e)/3,o=(o+2*t)/3,e=(this._xi+2*e)/3,t=(this._yi+2*t)/3,this._dashedBezierTo(e,t,n,o,i,r)},toStatic:function(){var e=this.data;e instanceof Array&&(e.length=this._len,y&&(this.data=new Float32Array(e)))},getBoundingRect:function(){u[0]=u[1]=f[0]=f[1]=Number.MAX_VALUE,d[0]=d[1]=p[0]=p[1]=-Number.MAX_VALUE;for(var e=this.data,t=0,n=0,o=0,l=0,s=0;ss||x(a-i)>u||f===d-1)&&(e.lineTo(r,a),o=r,i=a);break;case c.C:e.bezierCurveTo(l[f++],l[f++],l[f++],l[f++],l[f++],l[f++]),o=l[f-2],i=l[f-1];break;case c.Q:e.quadraticCurveTo(l[f++],l[f++],l[f++],l[f++]),o=l[f-2],i=l[f-1];break;case c.A:var h=l[f++],g=l[f++],b=l[f++],y=l[f++],_=l[f++],w=l[f++],k=l[f++],S=l[f++],M=b>y?b:y,C=b>y?1:b/y,A=b>y?y/b:1,T=Math.abs(b-y)>.001,E=_+w;T?(e.translate(h,g),e.rotate(k),e.scale(C,A),e.arc(0,0,M,_,E,1-S),e.scale(1/C,1/A),e.rotate(-k),e.translate(-h,-g)):e.arc(h,g,M,_,E,1-S),1==f&&(t=m(_)*b+h,n=v(_)*y+g),o=m(E)*b+h,i=v(E)*y+g;break;case c.R:t=o=l[f],n=i=l[f+1],e.rect(l[f++],l[f++],l[f++],l[f++]);break;case c.Z:e.closePath(),o=t,i=n}}}},_.CMD=c;var w=_;e.exports=w},function(e,t){var n=e.exports={version:"2.6.12"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){function o(e){this.group=new r.Group,this._symbolCtor=e||a}function i(e,t,n){var o=e.getItemLayout(t);return o&&!isNaN(o[0])&&!isNaN(o[1])&&!(n&&n(t))&&"none"!==e.getItemVisual(t,"symbol")}var r=n(2),a=n(82),l=o.prototype;l.updateData=function(e,t){var n=this.group,o=e.hostModel,a=this._data,l=this._symbolCtor,s={itemStyle:o.getModel("itemStyle.normal").getItemStyle(["color"]),hoverItemStyle:o.getModel("itemStyle.emphasis").getItemStyle(),symbolRotate:o.get("symbolRotate"),symbolOffset:o.get("symbolOffset"),hoverAnimation:o.get("hoverAnimation"),labelModel:o.getModel("label.normal"),hoverLabelModel:o.getModel("label.emphasis"),cursorStyle:o.get("cursor")};e.diff(a).add(function(o){var r=e.getItemLayout(o);if(i(e,o,t)){var a=new l(e,o,s);a.attr("position",r),e.setItemGraphicEl(o,a),n.add(a)}}).update(function(c,u){var d=a.getItemGraphicEl(u),f=e.getItemLayout(c);if(!i(e,c,t))return void n.remove(d);d?(d.updateData(e,c,s),r.updateProps(d,{position:f},o)):(d=new l(e,c),d.attr("position",f)),n.add(d),e.setItemGraphicEl(c,d)}).remove(function(e){var t=a.getItemGraphicEl(e);t&&t.fadeOut(function(){n.remove(t)})}).execute(),this._data=e},l.updateLayout=function(){var e=this._data;e&&e.eachItemGraphicEl(function(t,n){var o=e.getItemLayout(n);t.attr("position",o)})},l.remove=function(e){var t=this.group,n=this._data;n&&(e?n.eachItemGraphicEl(function(e){e.fadeOut(function(){t.remove(e)})}):t.removeAll())};var s=o;e.exports=s},function(e,t,n){function o(e,t){if(e&&("treemapZoomToNode"===e.type||"treemapRootToNode"===e.type)){var n=t.getData().tree.root,o=e.targetNode;if(o&&n.contains(o))return{node:o};var i=e.targetNodeId;if(null!=i&&(o=n.getNodeById(i)))return{node:o}}}function i(e){for(var t=[];e;)(e=e.parentNode)&&t.push(e);return t.reverse()}function r(e,t){var n=i(e);return l.indexOf(n,t)>=0}function a(e,t){for(var n=[];e;){var o=e.dataIndex;n.push({name:e.name,dataIndex:o,value:t.getRawValue(o)}),e=e.parentNode}return n.reverse(),n}var l=n(0);t.retrieveTargetInfo=o,t.getPathToRoot=i,t.aboveViewRoot=r,t.wrapTreePathInfo=a},function(e,t,n){var o=n(1),i=n(0),r=n(84),a=n(481);n(477),n(478),n(180),o.registerPreprocessor(function(e){if(e){(!e.axisPointer||0===e.axisPointer.length)&&(e.axisPointer={});var t=e.axisPointer.link;t&&!i.isArray(t)&&(e.axisPointer.link=[t])}}),o.registerProcessor(o.PRIORITY.PROCESSOR.STATISTIC,function(e,t){e.getComponent("axisPointer").coordSysAxesInfo=r.collect(e,t)}),o.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},a)},function(e,t,n){function o(e){var t={};return f(["start","end","startValue","endValue","throttle"],function(n){e.hasOwnProperty(n)&&(t[n]=e[n])}),t}function i(e,t){var n=e._rangePropMode,o=e.get("rangeMode");f([["start","startValue"],["end","endValue"]],function(e,i){var r=null!=t[e[0]],a=null!=t[e[1]];r&&!a?n[i]="percent":!r&&a?n[i]="value":o?n[i]=o[i]:r&&(n[i]="percent")})}var r=n(4),a=(r.__DEV__,n(1)),l=n(0),s=n(15),c=n(5),u=n(117),d=n(492),f=l.each,p=u.eachAxisDim,h=a.extendComponentModel({type:"dataZoom",dependencies:["xAxis","yAxis","zAxis","radiusAxis","angleAxis","singleAxis","series"],defaultOption:{zlevel:0,z:4,orient:null,xAxisIndex:null,yAxisIndex:null,filterMode:"filter",throttle:null,start:0,end:100,startValue:null,endValue:null,minSpan:null,maxSpan:null,minValueSpan:null,maxValueSpan:null,rangeMode:null},init:function(e,t,n){this._dataIntervalByAxis={},this._dataInfo={},this._axisProxies={},this.textStyleModel,this._autoThrottle=!0,this._rangePropMode=["percent","percent"];var i=o(e);this.mergeDefaultAndTheme(e,n),this.doInit(i)},mergeOption:function(e){var t=o(e);l.merge(this.option,e,!0),this.doInit(t)},doInit:function(e){var t=this.option;s.canvasSupported||(t.realtime=!1),this._setDefaultThrottle(e),i(this,e),f([["start","startValue"],["end","endValue"]],function(e,n){"value"===this._rangePropMode[n]&&(t[e[0]]=null)},this),this.textStyleModel=this.getModel("textStyle"),this._resetTarget(),this._giveAxisProxies()},_giveAxisProxies:function(){var e=this._axisProxies;this.eachTargetAxis(function(t,n,o,i){var r=this.dependentModels[t.axis][n],a=r.__dzAxisProxy||(r.__dzAxisProxy=new d(t.name,n,this,i));e[t.name+"_"+n]=a},this)},_resetTarget:function(){var e=this.option,t=this._judgeAutoMode();p(function(t){var n=t.axisIndex;e[n]=c.normalizeToArray(e[n])},this),"axisIndex"===t?this._autoSetAxisIndex():"orient"===t&&this._autoSetOrient()},_judgeAutoMode:function(){var e=this.option,t=!1;p(function(n){null!=e[n.axisIndex]&&(t=!0)},this);var n=e.orient;return null==n&&t?"orient":t?void 0:(null==n&&(e.orient="horizontal"),"axisIndex")},_autoSetAxisIndex:function(){var e=!0,t=this.get("orient",!0),n=this.option,o=this.dependentModels;if(e){var i="vertical"===t?"y":"x";o[i+"Axis"].length?(n[i+"AxisIndex"]=[0],e=!1):f(o.singleAxis,function(o){e&&o.get("orient",!0)===t&&(n.singleAxisIndex=[o.componentIndex],e=!1)})}e&&p(function(t){if(e){var o=[],i=this.dependentModels[t.axis];if(i.length&&!o.length)for(var r=0,a=i.length;r0?100:20}},getFirstTargetAxisModel:function(){var e;return p(function(t){if(null==e){var n=this.get(t.axisIndex);n.length&&(e=this.dependentModels[t.axis][n[0]])}},this),e},eachTargetAxis:function(e,t){var n=this.ecModel;p(function(o){f(this.get(o.axisIndex),function(i){e.call(t,o,i,this,n)},this)},this)},getAxisProxy:function(e,t){return this._axisProxies[e+"_"+t]},getAxisModel:function(e,t){var n=this.getAxisProxy(e,t);return n&&n.getAxisModel()},setRawRange:function(e,t){var n=this.option;f([["start","startValue"],["end","endValue"]],function(t){null==e[t[0]]&&null==e[t[1]]||(n[t[0]]=e[t[0]],n[t[1]]=e[t[1]])},this),!t&&i(this,e)},getPercentRange:function(){var e=this.findRepresentativeAxisProxy();if(e)return e.getDataPercentWindow()},getValueRange:function(e,t){if(null!=e||null!=t)return this.getAxisProxy(e,t).getDataValueWindow();var n=this.findRepresentativeAxisProxy();return n?n.getDataValueWindow():void 0},findRepresentativeAxisProxy:function(e){if(e)return e.__dzAxisProxy;var t=this._axisProxies;for(var n in t)if(t.hasOwnProperty(n)&&t[n].hostedBy(this))return t[n];for(var n in t)if(t.hasOwnProperty(n)&&!t[n].hostedBy(this))return t[n]},getRangePropMode:function(){return this._rangePropMode.slice()}}),g=h;e.exports=g},function(e,t,n){var o=n(129),i=o.extend({type:"dataZoom",render:function(e,t,n,o){this.dataZoomModel=e,this.ecModel=t,this.api=n},getTargetCoordInfo:function(){function e(e,t,n,o){for(var i,r=0;rl&&(t[1-r]=t[r]+f.sign*l),t}function o(e,t){var n=e[t]-e[1-t];return{span:Math.abs(n),sign:n>0?-1:n<0?1:t?-1:1}}function i(e,t){return Math.min(t[1],Math.max(t[0],e))}e.exports=n},function(e,t,n){var o=n(95),i=o.debugMode,r=function(){};1===i?r=function(){for(var e in arguments)throw new Error(arguments[e])}:i>1&&(r=function(){for(var e in arguments)console.log(arguments[e])});var a=r;e.exports=a},function(e,t,n){function o(e){i.call(this,e)}var i=n(97),r=n(10),a=n(0),l=n(135);o.prototype={constructor:o,type:"image",brush:function(e,t){var n=this.style,o=n.image;n.bind(e,this,t);var i=this._image=l.createOrUpdateImage(o,this._image,this,this.onload);if(i&&l.isImageReady(i)){var r=n.x||0,a=n.y||0,s=n.width,c=n.height,u=i.width/i.height;if(null==s&&null!=c?s=c*u:null==c&&null!=s?c=s/u:null==s&&null==c&&(s=i.width,c=i.height),this.setTransform(e),n.sWidth&&n.sHeight){var d=n.sx||0,f=n.sy||0;e.drawImage(i,d,f,n.sWidth,n.sHeight,r,a,s,c)}else if(n.sx&&n.sy){var d=n.sx,f=n.sy,p=s-d,h=c-f;e.drawImage(i,d,f,p,h,r,a,s,c)}else e.drawImage(i,r,a,s,c);this.restoreTransform(e),null!=n.text&&this.drawRectText(e,this.getBoundingRect())}},getBoundingRect:function(){var e=this.style;return this._rect||(this._rect=new r(e.x||0,e.y||0,e.width||0,e.height||0)),this._rect}},a.inherits(o,i);var s=o;e.exports=s},function(e,t,n){var o=n(97),i=n(0),r=n(27),a=n(99),l=function(e){o.call(this,e)};l.prototype={constructor:l,type:"text",brush:function(e,t){var n=this.style;this.__dirty&&a.normalizeTextStyle(n,!0),n.fill=n.stroke=n.shadowBlur=n.shadowColor=n.shadowOffsetX=n.shadowOffsetY=null;var o=n.text;null!=o&&(o+=""),n.bind(e,this,t),a.needDrawText(o,n)&&(this.setTransform(e),a.renderText(this,e,o,n),this.restoreTransform(e))},getBoundingRect:function(){var e=this.style;if(this.__dirty&&a.normalizeTextStyle(e,!0),!this._rect){var t=e.text;null!=t?t+="":t="";var n=r.getBoundingRect(e.text+"",e.font,e.textAlign,e.textVerticalAlign,e.textPadding,e.rich);if(n.x+=e.x||0,n.y+=e.y||0,a.getStroke(e.textStroke,e.textStrokeWidth)){var o=e.textStrokeWidth;n.x-=o/2,n.y-=o/2,n.width+=o,n.height+=o}this._rect=n}return this._rect}},i.inherits(l,o);var s=l;e.exports=s},function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(309),r=o(i),a=n(308),l=o(a),s="function"==typeof l.default&&"symbol"==typeof r.default?function(e){return typeof e}:function(e){return e&&"function"==typeof l.default&&e.constructor===l.default&&e!==l.default.prototype?"symbol":typeof e};t.default="function"==typeof l.default&&"symbol"===s(r.default)?function(e){return void 0===e?"undefined":s(e)}:function(e){return e&&"function"==typeof l.default&&e.constructor===l.default&&e!==l.default.prototype?"symbol":void 0===e?"undefined":s(e)}},function(e,t,n){var o=n(64);e.exports=function(e){if(!o(e))throw TypeError(e+" is not an object!");return e}},function(e,t){e.exports=!0},function(e,t,n){var o=n(160),i=n(102);e.exports=Object.keys||function(e){return o(e,i)}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){var n=0,o=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+o).toString(36))}},function(e,t,n){function o(e,t){var n=e.getItemVisual(t,"symbolSize");return n instanceof Array?n.slice():[+n,+n]}function i(e){return[e[0]/2,e[1]/2]}function r(e,t,n){u.Group.call(this),this.updateData(e,t,n)}function a(e,t){this.parent.drift(e,t)}var l=n(0),s=n(23),c=s.createSymbol,u=n(2),d=n(3),f=d.parsePercent,p=n(173),h=p.findLabelValueDim,g=r.prototype;g._createSymbol=function(e,t,n,o){this.removeAll();var r=t.getItemVisual(n,"color"),l=c(e,-1,-1,2,2,r);l.attr({z2:100,culling:!0,scale:i(o)}),l.drift=a,this._symbolType=e,this.add(l)},g.stopSymbolAnimation=function(e){this.childAt(0).stopAnimation(e)},g.getSymbolPath=function(){return this.childAt(0)},g.getScale=function(){return this.childAt(0).scale},g.highlight=function(){this.childAt(0).trigger("emphasis")},g.downplay=function(){this.childAt(0).trigger("normal")},g.setZ=function(e,t){var n=this.childAt(0);n.zlevel=e,n.z=t},g.setDraggable=function(e){var t=this.childAt(0);t.draggable=e,t.cursor=e?"move":"pointer"},g.updateData=function(e,t,n){this.silent=!1;var r=e.getItemVisual(t,"symbol")||"circle",a=e.hostModel,l=o(e,t),s=r!==this._symbolType;if(s)this._createSymbol(r,e,t,l);else{var c=this.childAt(0);c.silent=!1,u.updateProps(c,{scale:i(l)},a,t)}if(this._updateCommon(e,t,l,n),s){var c=this.childAt(0),d=n&&n.fadeIn,f={scale:c.scale.slice()};d&&(f.style={opacity:c.style.opacity}),c.scale=[0,0],d&&(c.style.opacity=0),u.initProps(c,f,a,t)}this._seriesModel=a};var m=["itemStyle","normal"],v=["itemStyle","emphasis"],b=["label","normal"],x=["label","emphasis"];g._updateCommon=function(e,t,n,o){var r=this.childAt(0),a=e.hostModel,s=e.getItemVisual(t,"color");"image"!==r.type&&r.useStyle({strokeNoScale:!0});var c=o&&o.itemStyle,d=o&&o.hoverItemStyle,p=o&&o.symbolRotate,g=o&&o.symbolOffset,y=o&&o.labelModel,_=o&&o.hoverLabelModel,w=o&&o.hoverAnimation,k=o&&o.cursorStyle;if(!o||e.hasItemOption){var S=o&&o.itemModel?o.itemModel:e.getItemModel(t);c=S.getModel(m).getItemStyle(["color"]),d=S.getModel(v).getItemStyle(),p=S.getShallow("symbolRotate"),g=S.getShallow("symbolOffset"),y=S.getModel(b),_=S.getModel(x),w=S.getShallow("hoverAnimation"),k=S.getShallow("cursor")}else d=l.extend({},d);var M=r.style;r.attr("rotation",(p||0)*Math.PI/180||0),g&&r.attr("position",[f(g[0],n[0]),f(g[1],n[1])]),k&&r.attr("cursor",k),r.setColor(s,o&&o.symbolInnerColor),r.setStyle(c);var C=e.getItemVisual(t,"opacity");null!=C&&(M.opacity=C);var A=o&&o.useNameLabel,T=!A&&h(e);(A||null!=T)&&u.setLabelStyle(M,d,y,_,{labelFetcher:a,labelDataIndex:t,defaultText:A?e.getName(t):e.get(T,t),isRectText:!0,autoColor:s}),r.off("mouseover").off("mouseout").off("emphasis").off("normal"),r.hoverStyle=d,u.setHoverStyle(r);var E=i(n);if(w&&a.isAnimationEnabled()){var I=function(){var e=E[1]/E[0];this.animateTo({scale:[Math.max(1.1*E[0],E[0]+3),Math.max(1.1*E[1],E[1]+3*e)]},400,"elasticOut")},O=function(){this.animateTo({scale:E},400,"elasticOut")};r.on("mouseover",I).on("mouseout",O).on("emphasis",I).on("normal",O)}},g.fadeOut=function(e,t){var n=this.childAt(0);this.silent=n.silent=!0,!(t&&t.keepLabel)&&(n.style.text=null),u.updateProps(n,{style:{opacity:0},scale:[0,0]},this._seriesModel,this.dataIndex,e)},l.inherits(r,u.Group);var y=r;e.exports=y},function(e,t,n){var o=n(13),i=n(25),r=n(411),a=n(0),l={_baseAxisDim:null,getInitialData:function(e,t){var n,r,l=t.getComponent("xAxis",this.get("xAxisIndex")),s=t.getComponent("yAxis",this.get("yAxisIndex")),c=l.get("type"),u=s.get("type");"category"===c?(e.layout="horizontal",n=l.getCategories(),r=!0):"category"===u?(e.layout="vertical",n=s.getCategories(),r=!0):e.layout=e.layout||"horizontal";var d=["x","y"],f="horizontal"===e.layout?0:1,p=this._baseAxisDim=d[f],h=d[1-f],g=e.data;r&&a.each(g,function(e,t){e.value&&a.isArray(e.value)?e.value.unshift(t):a.isArray(e)&&e.unshift(t)});var m=this.defaultValueDimensions,v=[{name:p,otherDims:{tooltip:!1},dimsDef:["base"]},{name:h,dimsDef:m.slice()}];v=i(v,g,{encodeDef:this.get("encode"),dimsDef:this.get("dimensions"),dimCount:m.length+1});var b=new o(v,this);return b.initData(g,n?n.slice():null),b},getBaseAxis:function(){var e=this._baseAxisDim;return this.ecModel.getComponent(e+"Axis",this.get(e+"AxisIndex")).axis}},s={init:function(){var e=this._whiskerBoxDraw=new r(this.getStyleUpdater());this.group.add(e.group)},render:function(e,t,n){this._whiskerBoxDraw.updateData(e.getData())},remove:function(e){this._whiskerBoxDraw.remove()}};t.seriesModelMixin=l,t.viewMixin=s},function(e,t,n){function o(e,t){var n={axesInfo:{},seriesInvolved:!1,coordSysAxesInfo:{},coordSysMap:{}};return i(n,e,t),n.seriesInvolved&&a(n,e),n}function i(e,t,n){var o=t.getComponent("tooltip"),i=t.getComponent("axisPointer"),a=i.get("link",!0)||[],s=[];m(n.getCoordinateSystems(),function(n){function c(o,c,u){var h=u.model.getModel("axisPointer",i),m=h.get("show");if(m&&("auto"!==m||o||f(h))){null==c&&(c=h.get("triggerTooltip")),h=o?r(u,g,i,t,o,c):h;var v=h.get("snap"),b=p(u.model),x=c||v||"category"===u.type,y=e.axesInfo[b]={key:b,axis:u,coordSys:n,axisPointerModel:h,triggerTooltip:c,involveSeries:x,snap:v,useHandle:f(h),seriesModels:[]};d[b]=y,e.seriesInvolved|=x;var _=l(a,u);if(null!=_){var w=s[_]||(s[_]={axesInfo:{}});w.axesInfo[b]=y,w.mapper=a[_].mapper,y.linkGroup=w}}}if(n.axisPointerEnabled){var u=p(n.model),d=e.coordSysAxesInfo[u]={};e.coordSysMap[u]=n;var h=n.model,g=h.getModel("tooltip",o);if(m(n.getAxes(),v(c,!1,null)),n.getTooltipAxes&&o&&g.get("show")){var b="axis"===g.get("trigger"),x="cross"===g.get("axisPointer.type"),y=n.getTooltipAxes(g.get("axisPointer.axis"));(b||x)&&m(y.baseAxes,v(c,!x||"cross",b)),x&&m(y.otherAxes,v(c,"cross",!1))}}})}function r(e,t,n,o,i,r){var a=t.getModel("axisPointer"),l={};m(["type","snap","lineStyle","shadowStyle","label","animation","animationDurationUpdate","animationEasingUpdate","z"],function(e){l[e]=h.clone(a.get(e))}),l.snap="category"!==e.type&&!!r,"cross"===a.get("type")&&(l.type="line");var s=l.label||(l.label={});if(null==s.show&&(s.show=!1),"cross"===i&&(s.show=!0,!r)){var c=l.lineStyle=a.get("crossStyle");c&&h.defaults(s,c.textStyle)}return e.model.getModel("axisPointer",new g(l,n,o))}function a(e,t){t.eachSeries(function(t){var n=t.coordinateSystem,o=t.get("tooltip.trigger",!0),i=t.get("tooltip.show",!0);n&&"none"!==o&&!1!==o&&"item"!==o&&!1!==i&&!1!==t.get("axisPointer.show",!0)&&m(e.coordSysAxesInfo[p(n.model)],function(e){var o=e.axis;n.getAxis(o.dim)===o&&(e.seriesModels.push(t),null==e.seriesDataCount&&(e.seriesDataCount=0),e.seriesDataCount+=t.getData().count())})},this)}function l(e,t){for(var n=t.model,o=t.dim,i=0;i=0||e===t}function c(e){var t=u(e);if(t){var n=t.axisPointerModel,o=t.axis.scale,i=n.option,r=n.get("status"),a=n.get("value");null!=a&&(a=o.parse(a));var l=f(n);null==r&&(i.status=l?"show":"hide");var s=o.getExtent().slice();s[0]>s[1]&&s.reverse(),(null==a||a>s[1])&&(a=s[1]),a0?1.1:1/1.1;c.call(this,e,t,e.offsetX,e.offsetY)}}function s(e){if(!h.isTaken(this._zr,"globalPan")){var t=e.pinchScale>1?1.1:1/1.1;c.call(this,e,t,e.pinchX,e.pinchY)}}function c(e,t,n,o){this.pointerChecker&&this.pointerChecker(e,n,o)&&(p.stop(e.event),this.trigger("zoom",t,n,o))}function u(e,t,n){var o=e._opt[t];return o&&(!d.isString(o)||n.event[o+"Key"])}var d=n(0),f=n(49),p=n(31),h=n(190);d.mixin(o,f);var g=o;e.exports=g},function(e,t,n){function o(e,t,n,o){i.each(u,function(a){t.extend({type:e+"Axis."+a,mergeDefaultAndTheme:function(t,o){var r=this.layoutMode,l=r?s(t):{},u=o.getTheme();i.merge(t,u.get(a+"Axis")),i.merge(t,this.getDefaultOption()),t.type=n(e,t),r&&c(t,l,r)},defaultOption:i.mergeAll([{},r[a+"Axis"],o],!0)})}),a.registerSubTypeDefaulter(e+"Axis",i.curry(n,e))}var i=n(0),r=n(207),a=n(14),l=n(6),s=l.getLayoutParams,c=l.mergeLayoutParam,u=["value","category","time","log"];e.exports=o},function(e,t,n){function o(e,t){var n=e.get("boundingCoords");if(null!=n){var o=n[0],i=n[1];isNaN(o[0])||isNaN(o[1])||isNaN(i[0])||isNaN(i[1])||this.setBoundingRect(o[0],o[1],i[0]-o[0],i[1]-o[1])}var r,a=this.getBoundingRect(),l=e.get("layoutCenter"),s=e.get("layoutSize"),d=t.getWidth(),f=t.getHeight(),p=e.get("aspectScale")||.75,h=a.width/a.height*p,g=!1;l&&s&&(l=[u.parsePercent(l[0],d),u.parsePercent(l[1],f)],s=u.parsePercent(s,Math.min(d,f)),isNaN(l[0])||isNaN(l[1])||isNaN(s)||(g=!0));var m;if(g){var m={};h>1?(m.width=s,m.height=s/h):(m.height=s,m.width=s*h),m.y=l[1]-m.height/2,m.x=l[0]-m.width/2}else r=e.getBoxLayoutParams(),r.aspect=h,m=c.getLayoutRect(r,{width:d,height:f});this.setViewRect(m.x,m.y,m.width,m.height),this.setCenter(e.get("center")),this.setZoom(e.get("zoom"))}function i(e,t){l.each(t.get("geoCoord"),function(t,n){e.addGeoCoord(n,t)})}var r=n(4),a=(r.__DEV__,n(1)),l=n(0),s=n(562),c=n(6),u=n(3),d={dimensions:s.prototype.dimensions,create:function(e,t){var n=[];e.eachComponent("geo",function(e,r){var l=e.get("map"),c=a.getMap(l),u=new s(l+r,l,c&&c.geoJson,c&&c.specialAreas,e.get("nameMap"));u.zoomLimit=e.get("scaleLimit"),n.push(u),i(u,e),e.coordinateSystem=u,u.model=e,u.resize=o,u.resize(e,t)}),e.eachSeries(function(e){if("geo"===e.get("coordinateSystem")){var t=e.get("geoIndex")||0;e.coordinateSystem=n[t]}});var r={};return e.eachSeriesByType("map",function(e){if(!e.getHostGeoModel()){var t=e.getMapType();r[t]=r[t]||[],r[t].push(e)}}),l.each(r,function(e,r){var c=a.getMap(r),u=l.map(e,function(e){return e.get("nameMap")}),d=new s(r,r,c&&c.geoJson,c&&c.specialAreas,l.mergeAll(u));d.zoomLimit=l.retrieve.apply(null,l.map(e,function(e){return e.get("scaleLimit")})),n.push(d),d.resize=o,d.resize(e[0],t),l.each(e,function(e){e.coordinateSystem=d,i(d,e)})}),n},getFilledRegions:function(e,t,n){var o=(e||[]).slice();n=n||{};var i=a.getMap(t),r=i&&i.geoJson;if(!r)return e;for(var s=l.createHashMap(),c=r.features,u=0;ut[1]&&(t[1]=e[1]),s.prototype.setExtent.call(this,t[0],t[1])},getInterval:function(){return this._interval},setInterval:function(e){this._interval=e,this._niceExtent=this._extent.slice(),this._intervalPrecision=a.getIntervalPrecision(e)},getTicks:function(){return a.intervalScaleGetTicks(this._interval,this._extent,this._niceExtent,this._intervalPrecision)},getTicksLabels:function(){for(var e=[],t=this.getTicks(),n=0;n=t[0]&&e<=t[1]},o.prototype.normalize=function(e){var t=this._extent;return t[1]===t[0]?.5:(e-t[0])/(t[1]-t[0])},o.prototype.scale=function(e){var t=this._extent;return e*(t[1]-t[0])+t[0]},o.prototype.unionExtent=function(e){var t=this._extent;e[0]t[1]&&(t[1]=e[1])},o.prototype.unionExtentFromData=function(e,t){this.unionExtent(e.getDataExtent(t,!0))},o.prototype.getExtent=function(){return this._extent.slice()},o.prototype.setExtent=function(e,t){var n=this._extent;isNaN(e)||(n[0]=e),isNaN(t)||(n[1]=t)},o.prototype.getTicksLabels=function(){for(var e=[],t=this.getTicks(),n=0;n-1?"center "+n:n+" center"}},appendArrow:function(e){var t=void 0;if(!this.appended){this.appended=!0;for(var n in e.attributes)if(/^_v-/.test(e.attributes[n].name)){t=e.attributes[n].name;break}var o=document.createElement("div");t&&o.setAttribute(t,""),o.setAttribute("x-arrow",""),o.className="popper__arrow",e.appendChild(o)}}},beforeDestroy:function(){this.doDestroy(!0),this.popperElm&&this.popperElm.parentNode===document.body&&(this.popperElm.removeEventListener("click",l),document.body.removeChild(this.popperElm))},deactivated:function(){this.$options.beforeDestroy[0].call(this)}}},function(e,t){var n=1;"undefined"!=typeof window&&(n=Math.max(window.devicePixelRatio||1,1));var o=n;t.debugMode=0,t.devicePixelRatio=o},function(e,t,n){var o=n(0),i=n(236),r=n(10),a=function(e){e=e||{},i.call(this,e);for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);this._children=[],this.__storage=null,this.__dirty=!0};a.prototype={constructor:a,isGroup:!0,type:"group",silent:!1,children:function(){return this._children.slice()},childAt:function(e){return this._children[e]},childOfName:function(e){for(var t=this._children,n=0;n=0&&(n.splice(o,0,e),this._doAdd(e))}return this},_doAdd:function(e){e.parent&&e.parent.remove(e),e.parent=this;var t=this.__storage,n=this.__zr;t&&t!==e.__storage&&(t.addToStorage(e),e instanceof a&&e.addChildrenToStorage(t)),n&&n.refresh()},remove:function(e){var t=this.__zr,n=this.__storage,i=this._children,r=o.indexOf(i,e);return r<0?this:(i.splice(r,1),e.parent=null,n&&(n.delFromStorage(e),e instanceof a&&e.delChildrenFromStorage(n)),t&&t.refresh(),this)},removeAll:function(){var e,t,n=this._children,o=this.__storage;for(t=0;t=0&&(M=A[z],"right"===M.textAlign);)u(e,t,M,o,I,w,D,"right"),O-=M.width,D-=M.width,z--;for(P+=(r-(P-_)-(k-D)-O)/2;L<=z;)M=A[L],u(e,t,M,o,I,w,P+M.width/2,"center"),P+=M.width,L++;w+=I}}function c(e,t,n,o,i){if(n&&t.textRotation){var r=t.textOrigin;"center"===r?(o=n.width/2+n.x,i=n.height/2+n.y):r&&(o=r[0]+n.x,i=r[1]+n.y),e.translate(o,i),e.rotate(-t.textRotation),e.translate(-o,-i)}}function u(e,t,n,o,i,r,a,l){var s=o.rich[n.styleName]||{},c=n.textVerticalAlign,u=r+i/2;"top"===c?u=r+n.height/2:"bottom"===c&&(u=r+i-n.height/2),!n.isLineHolder&&d(s)&&f(e,t,s,"right"===l?a-n.width:"center"===l?a-n.width/2:a,u-n.height/2,n.width,n.height);var p=n.textPadding;p&&(a=x(a,l,p),u-=n.height/2-p[2]-n.textHeight/2),g(t,"shadowBlur",k(s.textShadowBlur,o.textShadowBlur,0)),g(t,"shadowColor",s.textShadowColor||o.textShadowColor||"transparent"),g(t,"shadowOffsetX",k(s.textShadowOffsetX,o.textShadowOffsetX,0)),g(t,"shadowOffsetY",k(s.textShadowOffsetY,o.textShadowOffsetY,0)),g(t,"textAlign",l),g(t,"textBaseline","middle"),g(t,"font",n.font||T.DEFAULT_FONT);var h=m(s.textStroke||o.textStroke,y),b=v(s.textFill||o.textFill),y=w(s.textStrokeWidth,o.textStrokeWidth);h&&(g(t,"lineWidth",y),g(t,"strokeStyle",h),t.strokeText(n.text,a,u)),b&&(g(t,"fillStyle",b),t.fillText(n.text,a,u))}function d(e){return e.textBackgroundColor||e.textBorderWidth&&e.textBorderColor}function f(e,t,n,o,i,r,a){var l=n.textBackgroundColor,s=n.textBorderWidth,c=n.textBorderColor,u=C(l);if(g(t,"shadowBlur",n.textBoxShadowBlur||0),g(t,"shadowColor",n.textBoxShadowColor||"transparent"),g(t,"shadowOffsetX",n.textBoxShadowOffsetX||0),g(t,"shadowOffsetY",n.textBoxShadowOffsetY||0),u||s&&c){t.beginPath();var d=n.textBorderRadius;d?E.buildPath(t,{x:o,y:i,width:r,height:a,r:d}):t.rect(o,i,r,a),t.closePath()}if(u)g(t,"fillStyle",l),t.fill();else if(A(l)){var f=l.image;(f=I.createOrUpdateImage(f,null,e,p,l))&&I.isImageReady(f)&&t.drawImage(f,o,i,r,a)}s&&c&&(g(t,"lineWidth",s),g(t,"strokeStyle",c),t.stroke())}function p(e,t){t.image=e}function h(e,t,n){var o=t.x||0,i=t.y||0,r=t.textAlign,a=t.textVerticalAlign;if(n){var l=t.textPosition;if(l instanceof Array)o=n.x+b(l[0],n.width),i=n.y+b(l[1],n.height);else{var s=T.adjustTextPositionOnRect(l,n,t.textDistance);o=s.x,i=s.y,r=r||s.textAlign,a=a||s.textVerticalAlign}var c=t.textOffset;c&&(o+=c[0],i+=c[1])}return{baseX:o,baseY:i,textAlign:r,textVerticalAlign:a}}function g(e,t,n){return e[t]=n,e[t]}function m(e,t){return null==e||t<=0||"transparent"===e||"none"===e?null:e.image||e.colorStops?"#000":e}function v(e){return null==e||"none"===e?null:e.image||e.colorStops?"#000":e}function b(e,t){return"string"==typeof e?e.lastIndexOf("%")>=0?parseFloat(e)/100*t:parseFloat(e):e}function x(e,t,n){return"right"===t?e-n[1]:"center"===t?e+n[3]/2-n[1]/2:e+n[3]}function y(e,t){return null!=e&&(e||t.textBackgroundColor||t.textBorderWidth&&t.textBorderColor||t.textPadding)}var _=n(0),w=_.retrieve2,k=_.retrieve3,S=_.each,M=_.normalizeCssArray,C=_.isString,A=_.isObject,T=n(27),E=n(251),I=n(135),O={left:1,right:1,center:1},L={top:1,bottom:1,middle:1};t.normalizeTextStyle=o,t.renderText=r,t.getStroke=m,t.getFill=v,t.needDrawText=y},function(e,t,n){function o(e,t){var n=new x(s(),e,t);return b[n.id]=n,n}function i(e){if(e)e.dispose();else{for(var t in b)b.hasOwnProperty(t)&&b[t].dispose();b={}}return this}function r(e){return b[e]}function a(e,t){v[e]=t}function l(e){delete b[e]}var s=n(245),c=n(15),u=n(0),d=n(660),f=n(663),p=n(662),h=n(664),g=n(672),m=!c.canvasSupported,v={canvas:p},b={},x=function(e,t,n){n=n||{},this.dom=t,this.id=e;var o=this,i=new f,r=n.renderer;if(m){if(!v.vml)throw new Error("You need to require 'zrender/vml/vml' to support IE8");r="vml"}else r&&v[r]||(r="canvas");var a=new v[r](t,i,n);this.storage=i,this.painter=a;var l=c.node?null:new g(a.getViewportRoot());this.handler=new d(i,a,l,a.root),this.animation=new h({stage:{update:u.bind(this.flush,this)}}),this.animation.start(),this._needsRefresh;var s=i.delFromStorage,p=i.addToStorage;i.delFromStorage=function(e){s.call(i,e),e&&e.removeSelfFromZr(o)},i.addToStorage=function(e){p.call(i,e),e.addSelfToZr(o)}};x.prototype={constructor:x,getId:function(){return this.id},add:function(e){this.storage.addRoot(e),this._needsRefresh=!0},remove:function(e){this.storage.delRoot(e),this._needsRefresh=!0},configLayer:function(e,t){this.painter.configLayer(e,t),this._needsRefresh=!0},refreshImmediately:function(){this._needsRefresh=!1,this.painter.refresh(),this._needsRefresh=!1},refresh:function(){this._needsRefresh=!0},flush:function(){this._needsRefresh&&this.refreshImmediately(),this._needsRefreshHover&&this.refreshHoverImmediately()},addHover:function(e,t){this.painter.addHover&&(this.painter.addHover(e,t),this.refreshHover())},removeHover:function(e){this.painter.removeHover&&(this.painter.removeHover(e),this.refreshHover())},clearHover:function(){this.painter.clearHover&&(this.painter.clearHover(),this.refreshHover())},refreshHover:function(){this._needsRefreshHover=!0},refreshHoverImmediately:function(){this._needsRefreshHover=!1,this.painter.refreshHover&&this.painter.refreshHover()},resize:function(e){e=e||{},this.painter.resize(e.width,e.height),this.handler.resize()},clearAnimation:function(){this.animation.clear()},getWidth:function(){return this.painter.getWidth()},getHeight:function(){return this.painter.getHeight()},pathToImage:function(e,t){return this.painter.pathToImage(e,t)},setCursorStyle:function(e){this.handler.setCursorStyle(e)},findHover:function(e,t){return this.handler.findHover(e,t)},on:function(e,t,n){this.handler.on(e,t,n)},off:function(e,t){this.handler.off(e,t)},trigger:function(e,t){this.handler.trigger(e,t)},clear:function(){this.storage.delRoot(),this.painter.clear()},dispose:function(){this.animation.stop(),this.clear(),this.storage.dispose(),this.painter.dispose(),this.handler.dispose(),this.animation=this.storage=this.painter=this.handler=null,l(this.id)}},t.version="3.7.4",t.init=o,t.dispose=i,t.getInstance=r,t.registerPainter=a},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,n){var o=n(33),i=n(62),r=n(318),a=n(51),l=n(41),s=function(e,t,n){var c,u,d,f=e&s.F,p=e&s.G,h=e&s.S,g=e&s.P,m=e&s.B,v=e&s.W,b=p?i:i[t]||(i[t]={}),x=b.prototype,y=p?o:h?o[t]:(o[t]||{}).prototype;p&&(n=t);for(c in n)(u=!f&&y&&void 0!==y[c])&&l(b,c)||(d=u?y[c]:n[c],b[c]=p&&"function"!=typeof y[c]?n[c]:m&&u?r(d,o):v&&y[c]==d?function(e){var t=function(t,n,o){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,o)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(d):g&&"function"==typeof d?r(Function.call,d):d,g&&((b.virtual||(b.virtual={}))[c]=d,e&s.R&&x&&!x[c]&&a(x,c,d)))};s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,e.exports=s},function(e,t){e.exports={}},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var o=n(52).f,i=n(41),r=n(54)("toStringTag");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,r)&&o(e,r,{configurable:!0,value:t})}},function(e,t,n){var o=n(108)("keys"),i=n(81);e.exports=function(e){return o[e]||(o[e]=i(e))}},function(e,t,n){var o=n(62),i=n(33),r=i["__core-js_shared__"]||(i["__core-js_shared__"]={});(e.exports=function(e,t){return r[e]||(r[e]=void 0!==t?t:{})})("versions",[]).push({version:o.version,mode:n(77)?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},function(e,t){var n=Math.ceil,o=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?o:n)(e)}},function(e,t,n){var o=n(101);e.exports=function(e){return Object(o(e))}},function(e,t,n){var o=n(64);e.exports=function(e,t){if(!o(e))return e;var n,i;if(t&&"function"==typeof(n=e.toString)&&!o(i=n.call(e)))return i;if("function"==typeof(n=e.valueOf)&&!o(i=n.call(e)))return i;if(!t&&"function"==typeof(n=e.toString)&&!o(i=n.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},function(e,t,n){var o=n(33),i=n(62),r=n(77),a=n(113),l=n(52).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=r?{}:o.Symbol||{});"_"==e.charAt(0)||e in t||l(t,e,{value:a.f(e)})}},function(e,t,n){t.f=n(54)},function(e,t,n){function o(e){return"_"+e+"Type"}function i(e,t,n){var o=t.getItemVisual(n,"color"),i=t.getItemVisual(n,e),r=t.getItemVisual(n,e+"Size");if(i&&"none"!==i){c.isArray(r)||(r=[r,r]);var a=d.createSymbol(i,-r[0]/2,-r[1]/2,r[0],r[1],o);return a.name=e,a}}function r(e){var t=new f({name:"line"});return a(t.shape,e),t}function a(e,t){var n=t[0],o=t[1],i=t[2];e.x1=n[0],e.y1=n[1],e.x2=o[0],e.y2=o[1],e.percent=1,i?(e.cpx1=i[0],e.cpy1=i[1]):(e.cpx1=NaN,e.cpy1=NaN)}function l(){var e=this,t=e.childOfName("fromSymbol"),n=e.childOfName("toSymbol"),o=e.childOfName("label");if(t||n||!o.ignore){for(var i=1,r=this.parent;r;)r.scale&&(i/=r.scale[0]),r=r.parent;var a=e.childOfName("line");if(this.__dirty||a.__dirty){var l=a.shape.percent,s=a.pointAt(0),c=a.pointAt(l),d=u.sub([],c,s);if(u.normalize(d,d),t){t.attr("position",s);var f=a.tangentAt(0);t.attr("rotation",Math.PI/2-Math.atan2(f[1],f[0])),t.attr("scale",[i*l,i*l])}if(n){n.attr("position",c);var f=a.tangentAt(1);n.attr("rotation",-Math.PI/2-Math.atan2(f[1],f[0])),n.attr("scale",[i*l,i*l])}if(!o.ignore){o.attr("position",c);var p,h,g,m=5*i;if("end"===o.__position)p=[d[0]*m+c[0],d[1]*m+c[1]],h=d[0]>.8?"left":d[0]<-.8?"right":"center",g=d[1]>.8?"top":d[1]<-.8?"bottom":"middle";else if("middle"===o.__position){var v=l/2,f=a.tangentAt(v),b=[f[1],-f[0]],x=a.pointAt(v);b[1]>0&&(b[0]=-b[0],b[1]=-b[1]),p=[x[0]+b[0]*m,x[1]+b[1]*m],h="center",g="bottom";var y=-Math.atan2(f[1],f[0]);c[0].8?"right":d[0]<-.8?"left":"center",g=d[1]>.8?"bottom":d[1]<-.8?"top":"middle";o.attr({style:{textVerticalAlign:o.__verticalAlign||g,textAlign:o.__textAlign||h},position:p,scale:[i,i]})}}}}function s(e,t,n){p.Group.call(this),this._createLine(e,t,n)}var c=n(0),u=n(7),d=n(23),f=n(410),p=n(2),h=n(3),g=h.round,m=["fromSymbol","toSymbol"],v=s.prototype;v.beforeUpdate=l,v._createLine=function(e,t,n){var a=e.hostModel,l=e.getItemLayout(t),s=r(l);s.shape.percent=0,p.initProps(s,{shape:{percent:1}},a,t),this.add(s);var u=new p.Text({name:"label"});this.add(u),c.each(m,function(n){var r=i(n,e,t);this.add(r),this[o(n)]=e.getItemVisual(t,n)},this),this._updateCommonStl(e,t,n)},v.updateData=function(e,t,n){var r=e.hostModel,l=this.childOfName("line"),s=e.getItemLayout(t),u={shape:{}};a(u.shape,s),p.updateProps(l,u,r,t),c.each(m,function(n){var r=e.getItemVisual(t,n),a=o(n);if(this[a]!==r){this.remove(this.childOfName(n));var l=i(n,e,t);this.add(l)}this[a]=r},this),this._updateCommonStl(e,t,n)},v._updateCommonStl=function(e,t,n){var o=e.hostModel,i=this.childOfName("line"),r=n&&n.lineStyle,a=n&&n.hoverLineStyle,l=n&&n.labelModel,s=n&&n.hoverLabelModel;if(!n||e.hasItemOption){var u=e.getItemModel(t);r=u.getModel("lineStyle.normal").getLineStyle(),a=u.getModel("lineStyle.emphasis").getLineStyle(),l=u.getModel("label.normal"),s=u.getModel("label.emphasis")}var d=e.getItemVisual(t,"color"),f=c.retrieve3(e.getItemVisual(t,"opacity"),r.opacity,1);i.useStyle(c.defaults({strokeNoScale:!0,fill:"none",stroke:d,opacity:f},r)),i.hoverStyle=a,c.each(m,function(e){var t=this.childOfName(e);t&&(t.setColor(d),t.setStyle({opacity:f}))},this);var h,v,b,x,y=l.getShallow("show"),_=s.getShallow("show"),w=this.childOfName("label");if(y||_){var k=o.getRawValue(t);v=null==k?v=e.getName(t):isFinite(k)?g(k):k,h=d||"#000",b=c.retrieve2(o.getFormattedLabel(t,"normal",e.dataType),v),x=c.retrieve2(o.getFormattedLabel(t,"emphasis",e.dataType),b)}if(y){var S=p.setTextStyle(w.style,l,{text:b},{autoColor:h});w.__textAlign=S.textAlign,w.__verticalAlign=S.textVerticalAlign,w.__position=l.get("position")||"middle"}else w.setStyle("text",null);w.hoverStyle=_?{text:x,textFill:s.getTextColor(!0),fontStyle:s.getShallow("fontStyle"),fontWeight:s.getShallow("fontWeight"),fontSize:s.getShallow("fontSize"),fontFamily:s.getShallow("fontFamily")}:{text:null},w.ignore=!y&&!_,p.setHoverStyle(this)},v.highlight=function(){this.trigger("emphasis")},v.downplay=function(){this.trigger("normal")},v.updateLayout=function(e,t){this.setLinePoints(e.getItemLayout(t))},v.setLinePoints=function(e){var t=this.childOfName("line");a(t.shape,e),t.dirty()},c.inherits(s,p.Group);var b=s;e.exports=b},function(e,t,n){function o(e){return isNaN(e[0])||isNaN(e[1])}function i(e){return!o(e[0])&&!o(e[1])}function r(e){this._ctor=e||l,this.group=new a.Group}var a=n(2),l=n(114),s=r.prototype;s.updateData=function(e){var t=this._lineData,n=this.group,o=this._ctor,r=e.hostModel,a={lineStyle:r.getModel("lineStyle.normal").getLineStyle(),hoverLineStyle:r.getModel("lineStyle.emphasis").getLineStyle(),labelModel:r.getModel("label.normal"),hoverLabelModel:r.getModel("label.emphasis")};e.diff(t).add(function(t){if(i(e.getItemLayout(t))){var r=new o(e,t,a);e.setItemGraphicEl(t,r),n.add(r)}}).update(function(r,l){var s=t.getItemGraphicEl(l);if(!i(e.getItemLayout(r)))return void n.remove(s);s?s.updateData(e,r,a):s=new o(e,r,a),e.setItemGraphicEl(r,s),n.add(s)}).remove(function(e){n.remove(t.getItemGraphicEl(e))}).execute(),this._lineData=e},s.updateLayout=function(){var e=this._lineData;e.eachItemGraphicEl(function(t,n){t.updateLayout(e,n)},this)},s.remove=function(){this.group.removeAll()};var c=r;e.exports=c},function(e,t,n){function o(){}function i(e,t,n,o){r(m(n).lastProp,o)||(m(n).lastProp=o,t?d.updateProps(n,o,e):(n.stopAnimation(),n.attr(o)))}function r(e,t){if(c.isObject(e)&&c.isObject(t)){var n=!0;return c.each(t,function(t,o){n=n&&r(e[o],t)}),!!n}return e===t}function a(e,t){e[t.get("label.show")?"show":"hide"]()}function l(e){return{position:e.position.slice(),rotation:e.rotation||0}}function s(e,t,n){var o=t.get("z"),i=t.get("zlevel");e&&e.traverse(function(e){"group"!==e.type&&(null!=o&&(e.z=o),null!=i&&(e.zlevel=i),e.silent=n)})}var c=n(0),u=n(28),d=n(2),f=n(84),p=n(31),h=n(44),g=n(5),m=g.makeGetter(),v=c.clone,b=c.bind;o.prototype={_group:null,_lastGraphicKey:null,_handle:null,_dragging:!1,_lastValue:null,_lastStatus:null,_payloadInfo:null,animationThreshold:15,render:function(e,t,n,o){var r=t.get("value"),a=t.get("status");if(this._axisModel=e,this._axisPointerModel=t,this._api=n,o||this._lastValue!==r||this._lastStatus!==a){this._lastValue=r,this._lastStatus=a;var l=this._group,u=this._handle;if(!a||"hide"===a)return l&&l.hide(),void(u&&u.hide());l&&l.show(),u&&u.show();var f={};this.makeElOption(f,r,e,t,n);var p=f.graphicKey;p!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=p;var h=this._moveAnimation=this.determineAnimation(e,t);if(l){var g=c.curry(i,t,h);this.updatePointerEl(l,f,g,t),this.updateLabelEl(l,f,g,t)}else l=this._group=new d.Group,this.createPointerEl(l,f,e,t),this.createLabelEl(l,f,e,t),n.getZr().add(l);s(l,t,!0),this._renderHandle(r)}},remove:function(e){this.clear(e)},dispose:function(e){this.clear(e)},determineAnimation:function(e,t){var n=t.get("animation"),o=e.axis,i="category"===o.type,r=t.get("snap");if(!r&&!i)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(i&&o.getBandWidth()>a)return!0;if(r){var l=f.getAxisInfo(e).seriesDataCount,s=o.getExtent();return Math.abs(s[0]-s[1])/l>a}return!1}return!0===n},makeElOption:function(e,t,n,o,i){},createPointerEl:function(e,t,n,o){var i=t.pointer;if(i){var r=m(e).pointerEl=new d[i.type](v(t.pointer));e.add(r)}},createLabelEl:function(e,t,n,o){if(t.label){var i=m(e).labelEl=new d.Rect(v(t.label));e.add(i),a(i,o)}},updatePointerEl:function(e,t,n){var o=m(e).pointerEl;o&&(o.setStyle(t.pointer.style),n(o,{shape:t.pointer.shape}))},updateLabelEl:function(e,t,n,o){var i=m(e).labelEl;i&&(i.setStyle(t.label.style),n(i,{shape:t.label.shape,position:t.label.position}),a(i,o))},_renderHandle:function(e){if(!this._dragging&&this.updateHandleTransform){var t=this._axisPointerModel,n=this._api.getZr(),o=this._handle,i=t.getModel("handle"),r=t.get("status");if(!i.get("show")||!r||"hide"===r)return o&&n.remove(o),void(this._handle=null);var a;this._handle||(a=!0,o=this._handle=d.createIcon(i.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(e){p.stop(e.event)},onmousedown:b(this._onHandleDragMove,this,0,0),drift:b(this._onHandleDragMove,this),ondragend:b(this._onHandleDragEnd,this)}),n.add(o)),s(o,t,!1);var l=["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"];o.setStyle(i.getItemStyle(null,l));var u=i.get("size");c.isArray(u)||(u=[u,u]),o.attr("scale",[u[0]/2,u[1]/2]),h.createOrUpdate(this,"_doDispatchAxisPointer",i.get("throttle")||0,"fixRate"),this._moveHandleToValue(e,a)}},_moveHandleToValue:function(e,t){i(this._axisPointerModel,!t&&this._moveAnimation,this._handle,l(this.getHandleTransform(e,this._axisModel,this._axisPointerModel)))},_onHandleDragMove:function(e,t){var n=this._handle;if(n){this._dragging=!0;var o=this.updateHandleTransform(l(n),[e,t],this._axisModel,this._axisPointerModel);this._payloadInfo=o,n.stopAnimation(),n.attr(l(o)),m(n).lastProp=null,this._doDispatchAxisPointer()}},_doDispatchAxisPointer:function(){if(this._handle){var e=this._payloadInfo,t=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:e.cursorPoint[0],y:e.cursorPoint[1],tooltipOption:e.tooltipOption,axesInfo:[{axisDim:t.axis.dim,axisIndex:t.componentIndex}]})}},_onHandleDragEnd:function(e){if(this._dragging=!1,this._handle){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},getHandleTransform:null,updateHandleTransform:null,clear:function(e){this._lastValue=null,this._lastStatus=null;var t=e.getZr(),n=this._group,o=this._handle;t&&n&&(this._lastGraphicKey=null,n&&t.remove(n),o&&t.remove(o),this._group=null,this._handle=null,this._payloadInfo=null)},doClear:function(){},buildLabel:function(e,t,n){return n=n||0,{x:e[n],y:e[1-n],width:t[n],height:t[1-n]}}},o.prototype.constructor=o,u.enableClassExtend(o);var x=o;e.exports=x},function(e,t,n){function o(e){return a.indexOf(c,e)>=0}function i(e,t){e=e.slice();var n=a.map(e,l.capitalFirst);t=(t||[]).slice();var o=a.map(t,l.capitalFirst);return function(i,r){a.each(e,function(e,a){for(var l={name:e,capital:n[a]},s=0;s=0}function i(e,o){var i=!1;return t(function(t){a.each(n(e,t)||[],function(e){o.records[t.name][e]&&(i=!0)})}),i}function r(e,o){o.nodes.push(e),t(function(t){a.each(n(e,t)||[],function(e){o.records[t.name][e]=!0})})}return function(n){function a(e){!o(e,l)&&i(e,l)&&(r(e,l),s=!0)}var l={nodes:[],records:{}};if(t(function(e){l.records[e.name]={}}),!n)return l;r(n,l);var s;do{s=!1,e(a)}while(s);return l}}var a=n(0),l=n(8),s=["x","y","z","radius","angle","single"],c=["cartesian2d","polar","singleAxis"],u=i(s,["axisIndex","axis","index","id"]);t.isCoordSupported=o,t.createNameEach=i,t.eachAxisDim=u,t.createLinkedNodesFinder=r},function(e,t,n){function o(e){V.call(this),this._zr=e,this.group=new j.Group,this._brushType,this._brushOption,this._panels,this._track=[],this._dragging,this._covers=[],this._creatingCover,this._creatingPanel,this._enableGlobalPan,this._uid="brushController_"+oe++,this._handlers={},G(ie,function(e,t){this._handlers[t]=F.bind(e,this)},this)}function i(e,t){var n=e._zr;e._enableGlobalPan||$.take(n,Q,e._uid),G(e._handlers,function(e,t){n.on(t,e)}),e._brushType=t.brushType,e._brushOption=F.merge(F.clone(ne),t,!0)}function r(e){var t=e._zr;$.release(t,Q,e._uid),G(e._handlers,function(e,n){t.off(n,e)}),e._brushType=e._brushOption=null}function a(e,t){var n=re[t.brushType].createCover(e,t);return n.__brushOption=t,c(n,t),e.group.add(n),n}function l(e,t){var n=d(t);return n.endCreating&&(n.endCreating(e,t),c(t,t.__brushOption)),t}function s(e,t){var n=t.__brushOption;d(t).updateCoverShape(e,t,n.range,n)}function c(e,t){var n=t.z;null==n&&(n=X),e.traverse(function(e){e.z=n,e.z2=n})}function u(e,t){d(t).updateCommon(e,t),s(e,t)}function d(e){return re[e.__brushOption.brushType]}function f(e,t,n){var o=e._panels;if(!o)return!0;var i,r=e._transform;return G(o,function(e){e.isTargetByCursor(t,n,r)&&(i=e)}),i}function p(e,t){var n=e._panels;if(!n)return!0;var o=t.__brushOption.panelId;return null==o||n[o]}function h(e){var t=e._covers,n=t.length;return G(t,function(t){e.group.remove(t)},e),t.length=0,!!n}function g(e,t){var n=U(e._covers,function(e){var t=e.__brushOption,n=F.clone(t.range);return{brushType:t.brushType,panelId:t.panelId,range:n}});e.trigger("brush",n,{isEnd:!!t.isEnd,removeOnClick:!!t.removeOnClick})}function m(e){var t=e._track;if(!t.length)return!1;var n=t[t.length-1],o=t[0],i=n[0]-o[0],r=n[1]-o[1];return Z(i*i+r*r,.5)>K}function v(e){var t=e.length-1;return t<0&&(t=0),[e[0],e[t]]}function b(e,t,n,o){var i=new j.Group;return i.add(new j.Rect({name:"main",style:w(n),silent:!0,draggable:!0,cursor:"move",drift:W(e,t,i,"nswe"),ondragend:W(g,t,{isEnd:!0})})),G(o,function(n){i.add(new j.Rect({name:n,style:{opacity:0},draggable:!0,silent:!0,invisible:!0,drift:W(e,t,i,n),ondragend:W(g,t,{isEnd:!0})}))}),i}function x(e,t,n,o){var i=o.brushStyle.lineWidth||0,r=Y(i,J),a=n[0][0],l=n[1][0],s=a-i/2,c=l-i/2,u=n[0][1],d=n[1][1],f=u-r+i/2,p=d-r+i/2,h=u-a,g=d-l,m=h+i,v=g+i;_(e,t,"main",a,l,h,g),o.transformable&&(_(e,t,"w",s,c,r,v),_(e,t,"e",f,c,r,v),_(e,t,"n",s,c,m,r),_(e,t,"s",s,p,m,r),_(e,t,"nw",s,c,r,r),_(e,t,"ne",f,c,r,r),_(e,t,"sw",s,p,r,r),_(e,t,"se",f,p,r,r))}function y(e,t){var n=t.__brushOption,o=n.transformable,i=t.childAt(0);i.useStyle(w(n)),i.attr({silent:!o,cursor:o?"move":"default"}),G(["w","e","n","s","se","sw","ne","nw"],function(n){var i=t.childOfName(n),r=M(e,n);i&&i.attr({silent:!o,invisible:!o,cursor:o?te[r]+"-resize":null})})}function _(e,t,n,o,i,r,a){var l=t.childOfName(n);l&&l.setShape(I(E(e,t,[[o,i],[o+r,i+a]])))}function w(e){return F.defaults({strokeNoScale:!0},e.brushStyle)}function k(e,t,n,o){var i=[q(e,n),q(t,o)],r=[Y(e,n),Y(t,o)];return[[i[0],r[0]],[i[1],r[1]]]}function S(e){return j.getTransform(e.group)}function M(e,t){if(t.length>1){t=t.split("");var n=[M(e,t[0]),M(e,t[1])];return("e"===n[0]||"w"===n[0])&&n.reverse(),n.join("")}var o={w:"left",e:"right",n:"top",s:"bottom"},i={left:"w",right:"e",top:"n",bottom:"s"},n=j.transformDirection(o[t],S(e));return i[n]}function C(e,t,n,o,i,r,a,l){var s=o.__brushOption,c=e(s.range),d=T(n,r,a);G(i.split(""),function(e){var t=ee[e];c[t[0]][t[1]]+=d[t[0]]}),s.range=t(k(c[0][0],c[1][0],c[0][1],c[1][1])),u(n,o),g(n,{isEnd:!1})}function A(e,t,n,o,i){var r=t.__brushOption.range,a=T(e,n,o);G(r,function(e){e[0]+=a[0],e[1]+=a[1]}),u(e,t),g(e,{isEnd:!1})}function T(e,t,n){var o=e.group,i=o.transformCoordToLocal(t,n),r=o.transformCoordToLocal(0,0);return[i[0]-r[0],i[1]-r[1]]}function E(e,t,n){var o=p(e,t);return o&&!0!==o?o.clipPath(n,e._transform):F.clone(n)}function I(e){var t=q(e[0][0],e[1][0]),n=q(e[0][1],e[1][1]);return{x:t,y:n,width:Y(e[0][0],e[1][0])-t,height:Y(e[0][1],e[1][1])-n}}function O(e,t,n){if(e._brushType){var o=e._zr,i=e._covers,r=f(e,t,n);if(!e._dragging)for(var a=0;a"),i&&(r+=d(i),null!=n&&(r+=" : ")),null!=n&&(r+=d(o)),r},getData:function(){return this._data},setData:function(e){this._data=e}});a.mixin(f,s.dataFormatMixin);var p=f;e.exports=p},function(e,t,n){var o=n(1),i=n(0),r=o.extendComponentView({type:"marker",init:function(){this.markerGroupMap=i.createHashMap()},render:function(e,t,n){var o=this.markerGroupMap;o.each(function(e){e.__keep=!1});var i=this.type+"Model";t.eachSeries(function(e){var o=e[i];o&&this.renderSeries(e,o,t,n)},this),o.each(function(e){!e.__keep&&this.group.remove(e.group)},this)},renderSeries:function(){}});e.exports=r},function(e,t,n){function o(e){return!(isNaN(parseFloat(e.x))&&isNaN(parseFloat(e.y)))}function i(e){return!isNaN(parseFloat(e.x))&&!isNaN(parseFloat(e.y))}function r(e,t,n){var o=-1;do{o=Math.max(p.getPrecision(e.get(t,n)),o),e=e.stackedOn}while(e);return o}function a(e,t,n,o,i,a){var l=[],s=d(t,o,e),c=t.indicesOfNearest(o,s,!0)[0];l[i]=t.get(n,c,!0),l[a]=t.get(o,c,!0);var u=r(t,o,c);return u=Math.min(u,20),u>=0&&(l[a]=+l[a].toFixed(u)),l}function l(e,t){var n=e.getData(),o=e.coordinateSystem;if(t&&!i(t)&&!f.isArray(t.coord)&&o){var r=o.dimensions,a=s(t,n,o,e);if(t=f.clone(t),t.type&&m[t.type]&&a.baseAxis&&a.valueAxis){var l=h(r,a.baseAxis.dim),c=h(r,a.valueAxis.dim);t.coord=m[t.type](n,a.baseDataDim,a.valueDataDim,l,c),t.value=t.coord[c]}else{for(var u=[null!=t.xAxis?t.xAxis:t.radiusAxis,null!=t.yAxis?t.yAxis:t.angleAxis],p=0;p<2;p++)if(m[u[p]]){var g=e.coordDimToDataDim(r[p])[0];u[p]=d(n,g,u[p])}t.coord=u}}return t}function s(e,t,n,o){var i={};return null!=e.valueIndex||null!=e.valueDim?(i.valueDataDim=null!=e.valueIndex?t.getDimension(e.valueIndex):e.valueDim,i.valueAxis=n.getAxis(o.dataDimToCoordDim(i.valueDataDim)),i.baseAxis=n.getOtherAxis(i.valueAxis),i.baseDataDim=o.coordDimToDataDim(i.baseAxis.dim)[0]):(i.baseAxis=o.getBaseAxis(),i.valueAxis=n.getOtherAxis(i.baseAxis),i.baseDataDim=o.coordDimToDataDim(i.baseAxis.dim)[0],i.valueDataDim=o.coordDimToDataDim(i.valueAxis.dim)[0]),i}function c(e,t){return!(e&&e.containData&&t.coord&&!o(t))||e.containData(t.coord)}function u(e,t,n,o){return o<2?e.coord&&e.coord[o]:e.value}function d(e,t,n){if("average"===n){var o=0,i=0;return e.each(t,function(e,t){isNaN(e)||(o+=e,i++)},!0),o/i}return e.getDataExtent(t,!0)["max"===n?1:0]}var f=n(0),p=n(3),h=f.indexOf,g=f.curry,m={min:g(a,"min"),max:g(a,"max"),average:g(a,"average")};t.dataTransform=l,t.getAxisInfo=s,t.dataFilter=c,t.dimValueGetter=u,t.numCalculate=d},function(e,t,n){function o(e,t,n){return e.getCoordSysModel()===t}function i(e,t){var n=t*Math.PI/180,o=e.plain(),i=o.width,r=o.height,a=i*Math.cos(n)+r*Math.sin(n),l=i*Math.sin(n)+r*Math.cos(n);return new h(o.x,o.y,a,l)}function r(e){var t,n=e.model,o=n.getFormattedLabels(),r=n.getModel("axisLabel"),a=1,l=o.length;l>40&&(a=Math.ceil(l/40));for(var s=0;s=0?"p":"n",m=v[n],b=l[c][n][u],x=s[c][n][u];p.isHorizontal()?(o=b,i=m[1]+d,r=m[0]-x,a=f,s[c][n][u]+=r,Math.abs(r)=0&&n.push(e)}),n}e.topologicalTravel=function(e,t,o,i){function r(e){0==--c[e].entryCount&&u.push(e)}function l(e){d[e]=!0,r(e)}if(e.length){var s=n(t),c=s.graph,u=s.noEntryList,d={};for(a.each(e,function(e){d[e]=!0});u.length;){var f=u.pop(),p=c[f],h=!!d[f];h&&(o.call(i,f,p.originalDeps.slice()),delete d[f]),a.each(p.successor,h?l:r)}a.each(d,function(){throw new Error("Circle dependency may exists")})}}}var a=n(0),l=n(28),s=l.parseClassType,c=0,u="_";t.getUID=o,t.enableSubTypeDefaulter=i,t.enableTopologicalTravel=r},function(e,t,n){var o=n(96),i=n(128),r=n(28),a=function(){this.group=new o,this.uid=i.getUID("viewComponent")};a.prototype={constructor:a,init:function(e,t){},render:function(e,t,n,o){},dispose:function(){}};var l=a.prototype;l.updateView=l.updateLayout=l.updateVisual=function(e,t,n,o){},r.enableClassExtend(a),r.enableClassManagement(a,{registerWhenExtend:!0});var s=a;e.exports=s},function(e,t){function n(e,t){var n={};t.eachRawSeriesByType(e,function(e){var o=e.getRawData(),i={};if(!t.isSeriesFiltered(e)){var r=e.getData();r.each(function(e){var t=r.getRawIndex(e);i[t]=e}),o.each(function(t){var a=i[t],l=null!=a&&r.getItemVisual(a,"color",!0);if(l)o.setItemVisual(t,"color",l);else{var s=o.getItemModel(t),c=s.get("itemStyle.normal.color")||e.getColorFromPalette(o.getName(t),n);o.setItemVisual(t,"color",c),null!=a&&r.setItemVisual(a,"color",c)}})}})}e.exports=n},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(){if(i.default.prototype.$isServer)return 0;if(void 0!==r)return r;var e=document.createElement("div");e.className="el-scrollbar__wrap",e.style.visibility="hidden",e.style.width="100px",e.style.position="absolute",e.style.top="-9999px",document.body.appendChild(e);var t=e.offsetWidth;e.style.overflow="scroll";var n=document.createElement("div");n.style.width="100%",e.appendChild(n);var o=n.offsetWidth;return e.parentNode.removeChild(e),r=t-o};var o=n(21),i=function(e){return e&&e.__esModule?e:{default:e}}(o),r=void 0},function(e,t,n){var o=n(224);e.exports=function(e,t,n){return void 0===n?o(e,t,!1):o(e,n,!1!==t)}},function(e,t,n){function o(e,t,n){if(0!==e.length){var o,i=e[0],r=i[0],a=i[0],l=i[1],s=i[1];for(o=1;o1e-4)return l[0]=e-n,l[1]=t-o,c[0]=e+n,void(c[1]=t+o);if(g[0]=p(i)*n+e,g[1]=f(i)*o+t,m[0]=p(r)*n+e,m[1]=f(r)*o+t,u(l,g,m),d(c,g,m),i%=h,i<0&&(i+=h),r%=h,r<0&&(r+=h),i>r&&!a?r+=h:ii&&(v[0]=p(y)*n+e,v[1]=f(y)*o+t,u(l,v,l),d(c,v,c))}var s=n(7),c=n(39),u=Math.min,d=Math.max,f=Math.sin,p=Math.cos,h=2*Math.PI,g=s.create(),m=s.create(),v=s.create(),b=[],x=[];t.fromPoints=o,t.fromLine=i,t.fromCubic=r,t.fromQuadratic=a,t.fromArc=l},function(e,t){function n(e){for(var t=0;e>=u;)t|=1&e,e>>=1;return e+t}function o(e,t,n,o){var r=t+1;if(r===n)return 1;if(o(e[r++],e[t])<0){for(;r=0;)r++;return r-t}function i(e,t,n){for(n--;t>>1,i(a,e[r])<0?s=r:l=r+1;var c=o-l;switch(c){case 3:e[l+3]=e[l+2];case 2:e[l+2]=e[l+1];case 1:e[l+1]=e[l];break;default:for(;c>0;)e[l+c]=e[l+c-1],c--}e[l]=a}}function a(e,t,n,o,i,r){var a=0,l=0,s=1;if(r(e,t[n+i])>0){for(l=o-i;s0;)a=s,(s=1+(s<<1))<=0&&(s=l);s>l&&(s=l),a+=i,s+=i}else{for(l=i+1;sl&&(s=l);var c=a;a=i-s,s=i-c}for(a++;a>>1);r(e,t[n+u])>0?a=u+1:s=u}return s}function l(e,t,n,o,i,r){var a=0,l=0,s=1;if(r(e,t[n+i])<0){for(l=i+1;sl&&(s=l);var c=a;a=i-s,s=i-c}else{for(l=o-i;s=0;)a=s,(s=1+(s<<1))<=0&&(s=l);s>l&&(s=l),a+=i,s+=i}for(a++;a>>1);r(e,t[n+u])<0?s=u:a=u+1}return s}function s(e,t){function n(e,t){u[h]=e,f[h]=t,h+=1}function o(){for(;h>1;){var e=h-2;if(e>=1&&f[e-1]<=f[e]+f[e+1]||e>=2&&f[e-2]<=f[e]+f[e-1])f[e-1]f[e+1])break;r(e)}}function i(){for(;h>1;){var e=h-2;e>0&&f[e-1]=d||m>=d);if(v)break;b<0&&(b=0),b+=2}if(p=b,p<1&&(p=1),1===o){for(s=0;s=0;s--)e[m+s]=e[h+s];return void(e[f]=g[u])}for(var v=p;;){var b=0,x=0,y=!1;do{if(t(g[u],e[c])<0){if(e[f--]=e[c--],b++,x=0,0==--o){y=!0;break}}else if(e[f--]=g[u--],x++,b=0,1==--r){y=!0;break}}while((b|x)=0;s--)e[m+s]=e[h+s];if(0===o){y=!0;break}}if(e[f--]=g[u--],1==--r){y=!0;break}if(0!=(x=r-a(e[c],g,0,r,r-1,t))){for(f-=x,u-=x,r-=x,m=f+1,h=u+1,s=0;s=d||x>=d);if(y)break;v<0&&(v=0),v+=2}if(p=v,p<1&&(p=1),1===r){for(f-=o,c-=o,m=f+1,h=c+1,s=o-1;s>=0;s--)e[m+s]=e[h+s];e[f]=g[u]}else{if(0===r)throw new Error;for(h=f-(r-1),s=0;sf&&(p=f),r(e,i,i+p,i+c,t),c=p}d.pushRun(i,c),d.mergeRuns(),l-=c,i+=c}while(0!==l);d.forceMergeRuns()}}var u=32,d=7;e.exports=c},function(e,t,n){function o(e){if("string"==typeof e){var t=s.get(e);return t&&t.image}return e}function i(e,t,n,o,i){if(e){if("string"==typeof e){if(t&&t.__zrImageSrc===e||!n)return t;var l=s.get(e),c={hostEl:n,cb:o,cbPayload:i};return l?(t=l.image,!a(t)&&l.pending.push(c)):(!t&&(t=new Image),t.onload=r,s.put(e,t.__cachedImgObj={image:t,pending:[c]}),t.src=t.__zrImageSrc=e),t}return e}return t}function r(){var e=this.__cachedImgObj;this.onload=this.__cachedImgObj=null;for(var t=0;tl||e<-l}var i=n(24),r=n(7),a=i.identity,l=5e-5,s=function(e){e=e||{},e.position||(this.position=[0,0]),null==e.rotation&&(this.rotation=0),e.scale||(this.scale=[1,1]),this.origin=this.origin||null},c=s.prototype;c.transform=null,c.needLocalTransform=function(){return o(this.rotation)||o(this.position[0])||o(this.position[1])||o(this.scale[0]-1)||o(this.scale[1]-1)},c.updateTransform=function(){var e=this.parent,t=e&&e.transform,n=this.needLocalTransform(),o=this.transform;if(!n&&!t)return void(o&&a(o));o=o||i.create(),n?this.getLocalTransform(o):a(o),t&&(n?i.mul(o,e.transform,o):i.copy(o,e.transform)),this.transform=o,this.invTransform=this.invTransform||i.create(),i.invert(this.invTransform,o)},c.getLocalTransform=function(e){return s.getLocalTransform(this,e)},c.setTransform=function(e){var t=this.transform,n=e.dpr||1;t?e.setTransform(n*t[0],n*t[1],n*t[2],n*t[3],n*t[4],n*t[5]):e.setTransform(n,0,0,n,0,0)},c.restoreTransform=function(e){var t=e.dpr||1;e.setTransform(t,0,0,t,0,0)};var u=[];c.decomposeTransform=function(){if(this.transform){var e=this.parent,t=this.transform;e&&e.transform&&(i.mul(u,e.invTransform,t),t=u);var n=t[0]*t[0]+t[1]*t[1],r=t[2]*t[2]+t[3]*t[3],a=this.position,l=this.scale;o(n-1)&&(n=Math.sqrt(n)),o(r-1)&&(r=Math.sqrt(r)),t[0]<0&&(n=-n),t[3]<0&&(r=-r),a[0]=t[4],a[1]=t[5],l[0]=n,l[1]=r,this.rotation=Math.atan2(-t[1]/r,t[0]/n)}},c.getGlobalScale=function(){var e=this.transform;if(!e)return[1,1];var t=Math.sqrt(e[0]*e[0]+e[1]*e[1]),n=Math.sqrt(e[2]*e[2]+e[3]*e[3]);return e[0]<0&&(t=-t),e[3]<0&&(n=-n),[t,n]},c.transformCoordToLocal=function(e,t){var n=[e,t],o=this.invTransform;return o&&r.applyTransform(n,n,o),n},c.transformCoordToGlobal=function(e,t){var n=[e,t],o=this.transform;return o&&r.applyTransform(n,n,o),n},s.getLocalTransform=function(e,t){t=t||[],a(t);var n=e.origin,o=e.scale||[1,1],r=e.rotation||0,l=e.position||[0,0];return n&&(t[4]-=n[0],t[5]-=n[1]),i.scale(t,t,o),r&&i.rotate(t,t,r),n&&(t[4]+=n[0],t[5]+=n[1]),t[4]+=l[0],t[5]+=l[1],t};var d=s;e.exports=d},function(e,t){function n(e){return document.createElementNS(o,e)}var o="http://www.w3.org/2000/svg";t.createElement=n},function(e,t,n){function o(e){return k(1e4*e)/1e4}function i(e){return e-E}function r(e,t){var n=t?e.textFill:e.fill;return null!=n&&n!==w}function a(e,t){var n=t?e.textStroke:e.stroke;return null!=n&&n!==w}function l(e,t){t&&s(e,"transform","matrix("+_.call(t,",")+")")}function s(e,t,n){(!n||"linear"!==n.type&&"radial"!==n.type)&&e.setAttribute(t,n)}function c(e,t,n){e.setAttributeNS("http://www.w3.org/1999/xlink",t,n)}function u(e,t,n){if(r(t,n)){var o=n?t.textFill:t.fill;o="transparent"===o?w:o,"none"!==e.getAttribute("clip-path")&&o===w&&(o="rgba(0, 0, 0, 0.002)"),s(e,"fill",o),s(e,"fill-opacity",t.opacity)}else s(e,"fill",w);if(a(t,n)){var i=n?t.textStroke:t.stroke;i="transparent"===i?w:i,s(e,"stroke",i),s(e,"stroke-width",(n?t.textStrokeWidth:t.lineWidth)/(t.strokeNoScale?t.host.getLineScale():1)),s(e,"paint-order","stroke"),s(e,"stroke-opacity",t.opacity),t.lineDash?(s(e,"stroke-dasharray",t.lineDash.join(",")),s(e,"stroke-dashoffset",k(t.lineDashOffset||0))):s(e,"stroke-dasharray",""),t.lineCap&&s(e,"stroke-linecap",t.lineCap),t.lineJoin&&s(e,"stroke-linejoin",t.lineJoin),t.miterLimit&&s(e,"stroke-miterlimit",t.miterLimit)}else s(e,"stroke",w)}function d(e){for(var t=[],n=e.data,r=e.len(),a=0;a=A||!i(b)&&(g>-C&&g<0||g>C)==!!v;var w=o(u+f*M(h)),E=o(d+p*S(h));x&&(g=v?A-1e-4:1e-4-A,_=!0,9===a&&t.push("M",w,E));var I=o(u+f*M(h+g)),O=o(d+p*S(h+g));t.push("A",o(f),o(p),k(m*T),+_,+v,I,O);break;case y.Z:s="Z";break;case y.R:var I=o(n[a++]),O=o(n[a++]),L=o(n[a++]),P=o(n[a++]);t.push("M",I,O,"L",I+L,O,"L",I+L,O+P,"L",I,O+P,"L",I,O)}s&&t.push(s);for(var D=0;D0&&(t+=e[0].name+"
");var n=!0,o=!1,i=void 0;try{for(var r,a=e[Symbol.iterator]();!(n=(r=a.next()).done);n=!0){var s=r.value;t+=''+s.seriesName+": "+l.a.fileSize(s.value)+"
"}}catch(e){o=!0,i=e}finally{try{!n&&a.return&&a.return()}finally{if(o)throw i}}return t}},legend:{data:["Traffic In","Traffic Out"]},grid:{left:"3%",right:"4%",bottom:"3%",containLabel:!0},xAxis:[{type:"category",data:a}],yAxis:[{type:"value",axisLabel:{formatter:function(e){return l.a.fileSize(e)}}}],series:[{name:"Traffic In",type:"bar",data:t},{name:"Traffic Out",type:"bar",data:n}]};i.setOption(u),i.hideLoading()}n.d(t,"b",function(){return o}),n.d(t,"c",function(){return i}),n.d(t,"a",function(){return r});var a=n(47),l=n.n(a),s=n(1),c=n.n(s),u=n(608),d=(n.n(u),n(165)),f=(n.n(d),n(175)),p=(n.n(f),n(198)),h=(n.n(p),n(197));n.n(h)},function(e,t,n){"use strict";t.__esModule=!0;var o=n(307),i=function(e){return e&&e.__esModule?e:{default:e}}(o);t.default=i.default||function(e){for(var t=1;tdocument.F=Object<\/script>"),e.close(),s=e.F;o--;)delete s.prototype[r[o]];return s()};e.exports=Object.create||function(e,t){var n;return null!==e?(l.prototype=o(e),n=new l,l.prototype=null,n[a]=e):n=s(),void 0===t?n:i(n,t)}},function(e,t,n){var o=n(160),i=n(102).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return o(e,i)}},function(e,t,n){var o=n(41),i=n(53),r=n(317)(!1),a=n(107)("IE_PROTO");e.exports=function(e,t){var n,l=i(e),s=0,c=[];for(n in l)n!=a&&o(l,n)&&c.push(n);for(;t.length>s;)o(l,n=t[s++])&&(~r(c,n)||c.push(n));return c}},function(e,t,n){e.exports=n(51)},function(e,t,n){function o(e,t){r.each(t,function(t){t.update="updateView",i.registerAction(t,function(n,o){var i={};return o.eachComponent({mainType:"series",subType:e,query:n},function(e){e[t.method]&&e[t.method](n.name,n.dataIndex);var o=e.getData();o.each(function(t){var n=o.getName(t);i[n]=e.isSelected(n)||!1})}),{name:n.name,selected:i}})})}var i=n(1),r=n(0);e.exports=o},function(e,t,n){var o=n(1),i=n(0),r=n(164),a=r.updateCenterAndZoom;o.registerAction({type:"geoRoam",event:"geoRoam",update:"updateLayout"},function(e,t){var n=e.componentType||"series";t.eachComponent({mainType:n,query:e},function(t){var o=t.coordinateSystem;if("geo"===o.type){var r=a(o,e,t.get("scaleLimit"));t.setCenter&&t.setCenter(r.center),t.setZoom&&t.setZoom(r.zoom),"series"===n&&i.each(t.seriesGroup,function(e){e.setCenter(r.center),e.setZoom(r.zoom)})}})})},function(e,t){function n(e,t,n){var o=e.getZoom(),i=e.getCenter(),r=t.zoom,a=e.dataToPoint(i);if(null!=t.dx&&null!=t.dy){a[0]-=t.dx,a[1]-=t.dy;var i=e.pointToData(a);e.setCenter(i)}if(null!=r){if(n){var l=n.min||0,s=n.max||1/0;r=Math.max(Math.min(o*r,s),l)/o}e.scale[0]*=r,e.scale[1]*=r;var c=e.position,u=(t.originX-c[0])*(r-1),d=(t.originY-c[1])*(r-1);c[0]-=u,c[1]-=d,e.updateTransform();var i=e.pointToData(a);e.setCenter(i),e.setZoom(r*o)}return{center:e.getCenter(),zoom:e.getZoom()}}t.updateCenterAndZoom=n},function(e,t,n){var o=n(1),i=n(0),r=n(126);n(124),n(361),n(362),n(70),o.registerLayout(i.curry(r,"bar")),o.registerVisual(function(e){e.eachSeriesByType("bar",function(e){e.getData().setVisual("legendSymbol","roundRect")})})},function(e,t,n){var o=n(18),i=n(34),r=o.extend({type:"series.__base_bar__",getInitialData:function(e,t){return i(e.data,this,t)},getMarkerPosition:function(e){var t=this.coordinateSystem;if(t){var n=t.dataToPoint(e,!0),o=this.getData(),i=o.getLayout("offset"),r=o.getLayout("size");return n[t.getBaseAxis().isHorizontal()?0:1]+=i+r/2,n}return[NaN,NaN]},defaultOption:{zlevel:0,z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,itemStyle:{}}});e.exports=r},function(e,t,n){function o(e,t,n,o,a,l,s){var c=n.getModel("label.normal"),u=n.getModel("label.emphasis");r.setLabelStyle(e,t,c,u,{labelFetcher:a,labelDataIndex:l,defaultText:a.getRawValue(l),isRectText:!0,autoColor:o}),i(e),i(t)}function i(e,t){"outside"===e.textPosition&&(e.textPosition=t)}var r=n(2);t.setLabel=o},function(e,t,n){function o(e){var t=e.coordinateSystem;if(!t||"view"===t.type){var n=t.getBoundingRect(),o=e.getData(),r=o.graph,a=0,l=o.getSum("value"),s=2*Math.PI/(l||o.count()),c=n.width/2+n.x,u=n.height/2+n.y,d=Math.min(n.width,n.height)/2;r.eachNode(function(e){var t=e.getValue("value");a+=s*(l?t:1)/2,e.setLayout([d*Math.cos(a)+c,d*Math.sin(a)+u]),a+=s*(l?t:1)/2}),o.setLayout({cx:c,cy:u}),r.eachEdge(function(e){var t,n=e.getModel().get("lineStyle.normal.curveness")||0,o=i.clone(e.node1.getLayout()),r=i.clone(e.node2.getLayout()),a=(o[0]+r[0])/2,l=(o[1]+r[1])/2;+n&&(n*=3,t=[c*n+a*(1-n),u*n+l*(1-n)]),e.setLayout([o,r,t])})}}var i=n(7);t.circularLayout=o},function(e,t,n){function o(e){var t=e.coordinateSystem;if(!t||"view"===t.type){var n=e.getGraph();n.eachNode(function(e){var t=e.getModel();e.setLayout([+t.get("x"),+t.get("y")])}),i(n)}}function i(e){e.eachEdge(function(e){var t=e.getModel().get("lineStyle.normal.curveness")||0,n=r.clone(e.node1.getLayout()),o=r.clone(e.node2.getLayout()),i=[n,o];+t&&i.push([(n[0]+o[0])/2-(n[1]-o[1])*t,(n[1]+o[1])/2-(o[0]-n[0])*t]),e.setLayout(i)})}var r=n(7);t.simpleLayout=o,t.simpleLayoutEdge=i},function(e,t,n){function o(e,t,n){i.Group.call(this),this.add(this.createLine(e,t,n)),this._updateEffectSymbol(e,t)}var i=n(2),r=n(114),a=n(0),l=n(23),s=l.createSymbol,c=n(7),u=n(39),d=o.prototype;d.createLine=function(e,t,n){return new r(e,t,n)},d._updateEffectSymbol=function(e,t){var n=e.getItemModel(t),o=n.getModel("effect"),i=o.get("symbolSize"),r=o.get("symbol");a.isArray(i)||(i=[i,i]);var l=o.get("color")||e.getItemVisual(t,"color"),c=this.childAt(1);this._symbolType!==r&&(this.remove(c),c=s(r,-.5,-.5,1,1,l),c.z2=100,c.culling=!0,this.add(c)),c&&(c.setStyle("shadowColor",l),c.setStyle(o.getItemStyle(["color"])),c.attr("scale",i),c.setColor(l),c.attr("scale",i),this._symbolType=r,this._updateEffectAnimation(e,o,t))},d._updateEffectAnimation=function(e,t,n){var o=this.childAt(1);if(o){var i=this,r=e.getItemLayout(n),l=1e3*t.get("period"),s=t.get("loop"),c=t.get("constantSpeed"),u=a.retrieve(t.get("delay"),function(t){return t/e.count()*l/3}),d="function"==typeof u;if(o.ignore=!0,this.updateAnimationPoints(o,r),c>0&&(l=this.getLineLength(o)/c*1e3),l!==this._period||s!==this._loop){o.stopAnimation();var f=u;d&&(f=u(n)),o.__t>0&&(f=-l*o.__t),o.__t=0;var p=o.animate("",s).when(l,{__t:1}).delay(f).during(function(){i.updateSymbolPosition(o)});s||p.done(function(){i.remove(o)}),p.start()}this._period=l,this._loop=s}},d.getLineLength=function(e){return c.dist(e.__p1,e.__cp1)+c.dist(e.__cp1,e.__p2)},d.updateAnimationPoints=function(e,t){e.__p1=t[0],e.__p2=t[1],e.__cp1=t[2]||[(t[0][0]+t[1][0])/2,(t[0][1]+t[1][1])/2]},d.updateData=function(e,t,n){this.childAt(0).updateData(e,t,n),this._updateEffectSymbol(e,t)},d.updateSymbolPosition=function(e){var t=e.__p1,n=e.__p2,o=e.__cp1,i=e.__t,r=e.position,a=u.quadraticAt,l=u.quadraticDerivativeAt;r[0]=a(t[0],o[0],n[0],i),r[1]=a(t[1],o[1],n[1],i);var s=l(t[0],o[0],n[0],i),c=l(t[1],o[1],n[1],i);e.rotation=-Math.atan2(c,s)-Math.PI/2,e.ignore=!1},d.updateLayout=function(e,t){this.childAt(0).updateLayout(e,t);var n=e.getItemModel(t).getModel("effect");this._updateEffectAnimation(e,n,t)},a.inherits(o,i.Group);var f=o;e.exports=f},function(e,t,n){function o(e,t,n){i.Group.call(this),this._createPolyline(e,t,n)}var i=n(2),r=n(0),a=o.prototype;a._createPolyline=function(e,t,n){var o=e.getItemLayout(t),r=new i.Polyline({shape:{points:o}});this.add(r),this._updateCommonStl(e,t,n)},a.updateData=function(e,t,n){var o=e.hostModel,r=this.childAt(0),a={shape:{points:e.getItemLayout(t)}};i.updateProps(r,a,o,t),this._updateCommonStl(e,t,n)},a._updateCommonStl=function(e,t,n){var o=this.childAt(0),a=e.getItemModel(t),l=e.getItemVisual(t,"color"),s=n&&n.lineStyle,c=n&&n.hoverLineStyle;n&&!e.hasItemOption||(s=a.getModel("lineStyle.normal").getLineStyle(),c=a.getModel("lineStyle.emphasis").getLineStyle()),o.useStyle(r.defaults({strokeNoScale:!0,fill:"none",stroke:l},s)),o.hoverStyle=c,i.setHoverStyle(this)},a.updateLayout=function(e,t){this.childAt(0).setShape("points",e.getItemLayout(t))},r.inherits(o,i.Group);var l=o;e.exports=l},function(e,t,n){function o(e,t,n,o,d){for(var f=new a(o),p=0;p "+x)),m++)}var y,_=n.get("coordinateSystem");if("cartesian2d"===_||"polar"===_)y=u(e,n,n.ecModel);else{var w=c.get(_),k=s((w&&"view"!==w.type?w.dimensions||[]:[]).concat(["value"]),e);y=new r(k,n),y.initData(e)}var S=new r(["value"],n);return S.initData(g,h),d&&d(y,S),l({mainData:y,struct:f,structAttr:"graph",datas:{node:y,edge:S},datasAttr:{node:"data",edge:"edgeData"}}),f.update(),f}var i=n(0),r=n(13),a=n(588),l=n(213),s=n(25),c=n(26),u=n(34);e.exports=o},function(e,t,n){function o(e){var t,n=r(e,"label");if(n.length)t=n[0];else for(var o,i=e.dimensions.slice();i.length&&(t=i.pop(),"ordinal"===(o=e.getDimensionInfo(t).type)||"time"===o););return t}var i=n(5),r=i.otherDimToDataDim;t.findLabelValueDim=o},function(e,t,n){function o(e){return isNaN(e[0])||isNaN(e[1])}function i(e,t,n,i,r,a,s,m,v,b,x){for(var y=0,_=n,w=0;w=r||_<0)break;if(o(k)){if(x){_+=a;continue}break}if(_===n)e[a>0?"moveTo":"lineTo"](k[0],k[1]),f(h,k);else if(v>0){var S=_+a,M=t[S];if(x)for(;M&&o(t[S]);)S+=a,M=t[S];var C=.5,A=t[y],M=t[S];if(!M||o(M))f(g,k);else{o(M)&&!x&&(M=k),l.sub(p,M,A);var T,E;if("x"===b||"y"===b){var I="x"===b?0:1;T=Math.abs(k[I]-A[I]),E=Math.abs(k[I]-M[I])}else T=l.dist(k,A),E=l.dist(k,M);C=E/(E+T),d(g,k,p,-v*(1-C))}c(h,h,m),u(h,h,s),c(g,g,m),u(g,g,s),e.bezierCurveTo(h[0],h[1],g[0],g[1],k[0],k[1]),d(h,k,p,v*C)}else e.lineTo(k[0],k[1]);y=_,_+=a}return w}function r(e,t){var n=[1/0,1/0],o=[-1/0,-1/0];if(t)for(var i=0;io[0]&&(o[0]=r[0]),r[1]>o[1]&&(o[1]=r[1])}return{min:t?n:o,max:t?o:n}}var a=n(16),l=n(7),s=n(249),c=l.min,u=l.max,d=l.scaleAndAdd,f=l.copy,p=[],h=[],g=[],m=a.extend({type:"ec-polyline",shape:{points:[],smooth:0,smoothConstraint:!0,smoothMonotone:null,connectNulls:!1},style:{fill:null,stroke:"#000"},brush:s(a.prototype.brush),buildPath:function(e,t){var n=t.points,a=0,l=n.length,s=r(n,t.smoothConstraint);if(t.connectNulls){for(;l>0&&o(n[l-1]);l--);for(;a0&&o(n[s-1]);s--);for(;lb.getLayout().x&&(b=e),e.depth>x.depth&&(x=e)});var y=v===b?1:h(v,b)/2,_=y-v.getLayout().x,w=0,k=0,S=0,M=0;"radial"===o?(w=i/(b.getLayout().x+y+_),k=l/(x.depth-1||1),a(m,function(e){S=(e.getLayout().x+_)*w,M=(e.depth-1)*k;var t=f(S,M);e.setLayout({x:t.x,y:t.y,rawX:S,rawY:M},!0)})):"horizontal"===e.get("orient")?(k=l/(b.getLayout().x+y+_),w=i/(x.depth-1||1),a(m,function(e){M=(e.getLayout().x+_)*k,S=(e.depth-1)*w,e.setLayout({x:S,y:M},!0)})):(w=i/(b.getLayout().x+y+_),k=l/(x.depth-1||1),a(m,function(e){S=(e.getLayout().x+_)*w,M=(e.depth-1)*k,e.setLayout({x:S,y:M},!0)}))}var i=n(460),r=i.eachAfter,a=i.eachBefore,l=n(177),s=l.init,c=l.firstWalk,u=l.secondWalk,d=l.separation,f=l.radialCoordinate,p=l.getViewRect;e.exports=o},function(e,t,n){function o(e){e.hierNode={defaultAncestor:null,ancestor:e,prelim:0,modifier:0,change:0,shift:0,i:0,thread:null};for(var t,n,o=[e];t=o.pop();)if(n=t.children,t.isExpand&&n.length)for(var i=n.length,r=i-1;r>=0;r--){var a=n[r];a.hierNode={defaultAncestor:null,ancestor:a,prelim:0,modifier:0,change:0,shift:0,i:r,thread:null},o.push(a)}}function i(e,t){var n=e.isExpand?e.children:[],o=e.parentNode.children,i=e.hierNode.i?o[e.hierNode.i-1]:null;if(n.length){c(e);var r=(n[0].hierNode.prelim+n[n.length-1].hierNode.prelim)/2;i?(e.hierNode.prelim=i.hierNode.prelim+t(e,i),e.hierNode.modifier=e.hierNode.prelim-r):e.hierNode.prelim=r}else i&&(e.hierNode.prelim=i.hierNode.prelim+t(e,i));e.parentNode.hierNode.defaultAncestor=u(e,i,e.parentNode.hierNode.defaultAncestor||o[0],t)}function r(e){var t=e.hierNode.prelim+e.parentNode.hierNode.modifier;e.setLayout({x:t},!0),e.hierNode.modifier+=e.parentNode.hierNode.modifier}function a(e){return arguments.length?e:g}function l(e,t){var n={};return e-=Math.PI/2,n.x=t*Math.cos(e),n.y=t*Math.sin(e),n}function s(e,t){return m.getLayoutRect(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()})}function c(e){for(var t=e.children,n=t.length,o=0,i=0;--n>=0;){var r=t[n];r.hierNode.prelim+=o,r.hierNode.modifier+=o,i+=r.hierNode.change,o+=r.hierNode.shift+i}}function u(e,t,n,o){if(t){for(var i=e,r=e,a=r.parentNode.children[0],l=t,s=i.hierNode.modifier,c=r.hierNode.modifier,u=a.hierNode.modifier,g=l.hierNode.modifier;l=d(l),r=f(r),l&&r;){i=d(i),a=f(a),i.hierNode.ancestor=e;var m=l.hierNode.prelim+g-r.hierNode.prelim-c+o(l,r);m>0&&(h(p(l,e,n),e,m),c+=m,s+=m),g+=l.hierNode.modifier,c+=r.hierNode.modifier,s+=i.hierNode.modifier,u+=a.hierNode.modifier}l&&!d(i)&&(i.hierNode.thread=l,i.hierNode.modifier+=g-s),r&&!f(a)&&(a.hierNode.thread=r,a.hierNode.modifier+=c-u,n=e)}return n}function d(e){var t=e.children;return t.length&&e.isExpand?t[t.length-1]:e.hierNode.thread}function f(e){var t=e.children;return t.length&&e.isExpand?t[0]:e.hierNode.thread}function p(e,t,n){return e.hierNode.ancestor.parentNode===t.parentNode?e.hierNode.ancestor:n}function h(e,t,n){var o=n/(t.hierNode.i-e.hierNode.i);t.hierNode.change-=o,t.hierNode.shift+=n,t.hierNode.modifier+=n,t.hierNode.prelim+=n,e.hierNode.change+=o}function g(e,t){return e.parentNode===t.parentNode?1:2}var m=n(6);t.init=o,t.firstWalk=i,t.secondWalk=r,t.separation=a,t.radialCoordinate=l,t.getViewRect=s},function(e,t,n){function o(e,t,n){n=n||{};var o=e.coordinateSystem,r=t.axis,a={},l=r.position,s=r.onZero?"onZero":l,c=r.dim,u=o.getRect(),d=[u.x,u.x+u.width,u.y,u.y+u.height],f={left:0,right:1,top:0,bottom:1,onZero:2},p=t.get("offset")||0,h="x"===c?[d[2]-p,d[3]+p]:[d[0]-p,d[1]+p];if(r.onZero){var g=o.getAxis("x"===c?"y":"x",r.onZeroAxisIndex),m=g.toGlobalCoord(g.dataToCoord(0));h[f.onZero]=Math.max(Math.min(m,h[1]),h[0])}a.position=["y"===c?h[f[s]]:d[0],"x"===c?h[f[s]]:d[3]],a.rotation=Math.PI/2*("x"===c?0:1);var v={top:-1,bottom:1,left:-1,right:1};a.labelDirection=a.tickDirection=a.nameDirection=v[l],a.labelOffset=r.onZero?h[f[l]]-h[f.onZero]:0,t.get("axisTick.inside")&&(a.tickDirection=-a.tickDirection),i.retrieve(n.labelInside,t.get("axisLabel.inside"))&&(a.labelDirection=-a.labelDirection);var b=t.get("axisLabel.rotate");return a.labelRotate="top"===s?-b:b,a.labelInterval=r.getLabelInterval(),a.z2=1,a}var i=n(0);t.layout=o},function(e,t,n){function o(e,t){t=t||{};var n=e.coordinateSystem,o=e.axis,r={},a=o.position,l=o.orient,s=n.getRect(),c=[s.x,s.x+s.width,s.y,s.y+s.height],u={horizontal:{top:c[2],bottom:c[3]},vertical:{left:c[0],right:c[1]}};r.position=["vertical"===l?u.vertical[a]:c[0],"horizontal"===l?u.horizontal[a]:c[3]];var d={horizontal:0,vertical:1};r.rotation=Math.PI/2*d[l];var f={top:-1,bottom:1,right:1,left:-1};r.labelDirection=r.tickDirection=r.nameDirection=f[a],e.get("axisTick.inside")&&(r.tickDirection=-r.tickDirection),i.retrieve(t.labelInside,e.get("axisLabel.inside"))&&(r.labelDirection=-r.labelDirection);var p=t.rotate;return null==p&&(p=e.get("axisLabel.rotate")),r.labelRotation="top"===a?-p:p,r.labelInterval=o.getLabelInterval(),r.z2=1,r}var i=n(0);t.layout=o},function(e,t,n){function o(e,t){var n={};return n[t.dim+"AxisIndex"]=t.index,e.getCartesian(n)}function i(e){return"x"===e.dim?0:1}var r=n(2),a=n(116),l=n(85),s=n(178),c=n(43),u=a.extend({makeElOption:function(e,t,n,i,r){var a=n.axis,c=a.grid,u=i.get("type"),f=o(c,a).getOtherAxis(a).getGlobalExtent(),p=a.toGlobalCoord(a.dataToCoord(t,!0));if(u&&"none"!==u){var h=l.buildElStyle(i),g=d[u](a,p,f,h);g.style=h,e.graphicKey=g.type,e.pointer=g}var m=s.layout(c.model,n);l.buildCartesianSingleLabelElOption(t,e,m,n,i,r)},getHandleTransform:function(e,t,n){var o=s.layout(t.axis.grid.model,t,{labelInside:!1});return o.labelMargin=n.get("handle.margin"),{position:l.getTransformedPosition(t.axis,e,o),rotation:o.rotation+(o.labelDirection<0?Math.PI:0)}},updateHandleTransform:function(e,t,n,i){var r=n.axis,a=r.grid,l=r.getGlobalExtent(!0),s=o(a,r).getOtherAxis(r).getGlobalExtent(),c="x"===r.dim?0:1,u=e.position;u[c]+=t[c],u[c]=Math.min(l[1],u[c]),u[c]=Math.max(l[0],u[c]);var d=(s[1]+s[0])/2,f=[d,d];f[c]=u[c];var p=[{verticalAlign:"middle"},{align:"center"}];return{position:u,rotation:e.rotation,cursorPoint:f,tooltipOption:p[c]}}}),d={line:function(e,t,n,o){var a=l.makeLineShape([t,n[0]],[t,n[1]],i(e));return r.subPixelOptimizeLine({shape:a,style:o}),{type:"Line",shape:a}},shadow:function(e,t,n,o){var r=e.getBandWidth(),a=n[1]-n[0];return{type:"Rect",shape:l.makeRectShape([t-r/2,n[0]],[r,a],i(e))}}};c.registerAxisPointerClass("CartesianAxisPointer",u);var f=u;e.exports=f},function(e,t,n){function o(e,t){var n,o=[],a=e.seriesIndex;if(null==a||!(n=t.getSeriesByIndex(a)))return{point:[]};var l=n.getData(),s=r.queryDataIndex(l,e);if(null==s||i.isArray(s))return{point:[]};var c=l.getItemGraphicEl(s),u=n.coordinateSystem;if(n.getTooltipPosition)o=n.getTooltipPosition(s)||[];else if(u&&u.dataToPoint)o=u.dataToPoint(l.getValues(i.map(u.dimensions,function(e){return n.coordDimToDataDim(e)[0]}),s,!0))||[];else if(c){var d=c.getBoundingRect().clone();d.applyTransform(c.transform),o=[d.x+d.width/2,d.y+d.height/2]}return{point:o,el:c}}var i=n(0),r=n(5);e.exports=o},function(e,t,n){function o(e,t,n){if(!d.node){var o=t.getZr();p(o).records||(p(o).records={}),i(o,t),(p(o).records[e]||(p(o).records[e]={})).handler=n}}function i(e,t){function n(n,o){e.on(n,function(n){var i=s(t);h(p(e).records,function(e){e&&o(e,n,i.dispatchAction)}),r(i.pendings,t)})}p(e).initialized||(p(e).initialized=!0,n("click",u.curry(l,"click")),n("mousemove",u.curry(l,"mousemove")),n("globalout",a))}function r(e,t){var n,o=e.showTip.length,i=e.hideTip.length;o?n=e.showTip[o-1]:i&&(n=e.hideTip[i-1]),n&&(n.dispatchAction=null,t.dispatchAction(n))}function a(e,t,n){e.handler("leave",null,n)}function l(e,t,n,o){t.handler(e,n,o)}function s(e){var t={showTip:[],hideTip:[]},n=function(o){var i=t[o.type];i?i.push(o):(o.dispatchAction=n,e.dispatchAction(o))};return{dispatchAction:n,pendings:t}}function c(e,t){if(!d.node){var n=t.getZr();(p(n).records||{})[e]&&(p(n).records[e]=null)}}var u=n(0),d=n(15),f=n(5),p=f.makeGetter(),h=u.each;t.register=o,t.unregister=c},function(e,t,n){var o=n(1),i=n(0),r=n(117);o.registerAction("dataZoom",function(e,t){var n=r.createLinkedNodesFinder(i.bind(t.eachComponent,t,"dataZoom"),r.eachAxisDim,function(e,t){return e.get(t.axisIndex)}),o=[];t.eachComponent({mainType:"dataZoom",query:e},function(e,t){o.push.apply(o,n(e).nodes)}),i.each(o,function(t,n){t.setRawRange({start:e.start,end:e.end,startValue:e.startValue,endValue:e.endValue})})})},function(e,t,n){function o(e,t,n){n.getAxisProxy(e.name,t).reset(n)}function i(e,t,n){n.getAxisProxy(e.name,t).filterData(n)}n(1).registerProcessor(function(e,t){e.eachComponent("dataZoom",function(e){e.eachTargetAxis(o),e.eachTargetAxis(i)}),e.eachComponent("dataZoom",function(e){var t=e.findRepresentativeAxisProxy(),n=t.getDataPercentWindow(),o=t.getDataValueWindow();e.setRawRange({start:n[0],end:n[1],startValue:o[0],endValue:o[1]},!0)})})},function(e,t,n){function o(e,t){var n=l(e);c(t,function(t,o){for(var i=n.length-1;i>=0&&!n[i][o];i--);if(i<0){var r=e.queryComponents({mainType:"dataZoom",subType:"select",id:o})[0];if(r){var a=r.getPercentRange();n[0][o]={dataZoomId:o,start:a[0],end:a[1]}}}}),n.push(t)}function i(e){var t=l(e),n=t[t.length-1];t.length>1&&t.pop();var o={};return c(n,function(e,n){for(var i=t.length-1;i>=0;i--){var e=t[i][n];if(e){o[n]=e;break}}}),o}function r(e){e[u]=null}function a(e){return l(e).length}function l(e){var t=e[u];return t||(t=e[u]=[{}]),t}var s=n(0),c=s.each,u="\0_ec_hist_store";t.push=o,t.pop=i,t.clear=r,t.count=a},function(e,t,n){n(14).registerSubTypeDefaulter("dataZoom",function(){return"slider"})},function(e,t,n){function o(e,t,n){var o=this._targetInfoList=[],i={},a=r(t,e);g(_,function(e,t){(!n||!n.include||m(n.include,t)>=0)&&e(a,o,i)})}function i(e){return e[0]>e[1]&&e.reverse(),e}function r(e,t){return p.parseFinder(e,t,{includeMainTypes:x})}function a(e,t,n,o){var r=n.getAxis(["x","y"][e]),a=i(d.map([0,1],function(e){return t?r.coordToData(r.toLocalCoord(o[e])):r.toGlobalCoord(r.dataToCoord(o[e]))})),l=[];return l[e]=a,l[1-e]=[NaN,NaN],{values:a,xyMinMax:l}}function l(e,t,n,o){return[t[0]-o[e]*n[0],t[1]-o[e]*n[1]]}function s(e,t){var n=c(e),o=c(t),i=[n[0]/o[0],n[1]/o[1]];return isNaN(i[0])&&(i[0]=1),isNaN(i[1])&&(i[1]=1),i}function c(e){return e?[e[0][1]-e[0][0],e[1][1]-e[1][0]]:[NaN,NaN]}var u=n(4),d=(u.__DEV__,n(0)),f=n(2),p=n(5),h=n(189),g=d.each,m=d.indexOf,v=d.curry,b=["dataToPoint","pointToData"],x=["grid","xAxis","yAxis","geo","graph","polar","radiusAxis","angleAxis","bmap"],y=o.prototype;y.setOutputRanges=function(e,t){this.matchOutputRanges(e,t,function(e,t,n){if((e.coordRanges||(e.coordRanges=[])).push(t),!e.coordRange){e.coordRange=t;var o=S[e.brushType](0,n,t);e.__rangeOffset={offset:M[e.brushType](o.values,e.range,[1,1]),xyMinMax:o.xyMinMax}}})},y.matchOutputRanges=function(e,t,n){g(e,function(e){var o=this.findTargetInfo(e,t);o&&!0!==o&&d.each(o.coordSyses,function(o){var i=S[e.brushType](1,o,e.range);n(e,i.values,o,t)})},this)},y.setInputRanges=function(e,t){g(e,function(e){var n=this.findTargetInfo(e,t);if(e.range=e.range||[],n&&!0!==n){e.panelId=n.panelId;var o=S[e.brushType](0,n.coordSys,e.coordRange),i=e.__rangeOffset;e.range=i?M[e.brushType](o.values,i.offset,s(o.xyMinMax,i.xyMinMax)):o.values}},this)},y.makePanelOpts=function(e,t){return d.map(this._targetInfoList,function(n){var o=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:t&&t(n),clipPath:h.makeRectPanelClipPath(o),isTargetByCursor:h.makeRectIsTargetByCursor(o,e,n.coordSysModel),getLinearBrushOtherExtent:h.makeLinearBrushOtherExtent(o)}})},y.controlSeries=function(e,t,n){var o=this.findTargetInfo(e,n);return!0===o||o&&m(o.coordSyses,t.coordinateSystem)>=0},y.findTargetInfo=function(e,t){for(var n=this._targetInfoList,o=r(t,e),i=0;i=0||m(o,e.getAxis("y").model)>=0)&&r.push(e)}),t.push({panelId:"grid--"+e.id,gridModel:e,coordSysModel:e,coordSys:r[0],coordSyses:r,getPanelRect:k.grid,xAxisDeclared:a[e.id],yAxisDeclared:l[e.id]})}))},geo:function(e,t){g(e.geoModels,function(e){var n=e.coordinateSystem;t.push({panelId:"geo--"+e.id,geoModel:e,coordSysModel:e,coordSys:n,coordSyses:[n],getPanelRect:k.geo})})}},w=[function(e,t){var n=e.xAxisModel,o=e.yAxisModel,i=e.gridModel;return!i&&n&&(i=n.axis.grid.model),!i&&o&&(i=o.axis.grid.model),i&&i===t.gridModel},function(e,t){var n=e.geoModel;return n&&n===t.geoModel}],k={grid:function(){return this.coordSys.grid.getRect().clone()},geo:function(){var e=this.coordSys,t=e.getBoundingRect().clone();return t.applyTransform(f.getTransform(e)),t}},S={lineX:v(a,0),lineY:v(a,1),rect:function(e,t,n){var o=t[b[e]]([n[0][0],n[1][0]]),r=t[b[e]]([n[0][1],n[1][1]]),a=[i([o[0],r[0]]),i([o[1],r[1]])];return{values:a,xyMinMax:a}},polygon:function(e,t,n){var o=[[1/0,-1/0],[1/0,-1/0]];return{values:d.map(n,function(n){var i=t[b[e]](n);return o[0][0]=Math.min(o[0][0],i[0]),o[1][0]=Math.min(o[1][0],i[1]),o[0][1]=Math.max(o[0][1],i[0]),o[1][1]=Math.max(o[1][1],i[1]),i}),xyMinMax:o}}},M={lineX:v(l,0),lineY:v(l,1),rect:function(e,t,n){return[[e[0][0]-n[0]*t[0][0],e[0][1]-n[0]*t[0][1]],[e[1][0]-n[1]*t[1][0],e[1][1]-n[1]*t[1][1]]]},polygon:function(e,t,n){return d.map(e,function(e,o){return[e[0]-n[0]*t[o][0],e[1]-n[1]*t[o][1]]})}},C=o;e.exports=C},function(e,t,n){function o(e,t){var n=e.getItemStyle(),o=e.get("areaColor");return null!=o&&(n.fill=o),n}function i(e,t,n,o,i){n.off("click"),n.off("mousedown"),t.get("selectedMode")&&(n.on("mousedown",function(){e._mouseDownFlag=!0}),n.on("click",function(a){if(e._mouseDownFlag){e._mouseDownFlag=!1;for(var s=a.target;!s.__regions;)s=s.parent;if(s){var c={type:("geo"===t.mainType?"geo":"map")+"ToggleSelect",batch:l.map(s.__regions,function(e){return{name:e.name,from:i.uid}})};c[t.mainType+"Id"]=t.id,o.dispatchAction(c),r(t,n)}}}))}function r(e,t){t.eachChild(function(t){l.each(t.__regions,function(n){t.trigger(e.isSelected(n.name)?"emphasis":"normal")})})}function a(e,t){var n=new f.Group;this._controller=new s(e.getZr()),this._controllerHost={target:t?n:null},this.group=n,this._updateGroup=t,this._mouseDownFlag}var l=n(0),s=n(86),c=n(192),u=n(119),d=u.onIrrelevantElement,f=n(2);a.prototype={constructor:a,draw:function(e,t,n,a,s){var c="geo"===e.mainType,u=e.getData&&e.getData();c&&t.eachComponent({mainType:"series",subType:"map"},function(t){u||t.getHostGeoModel()!==e||(u=t.getData())});var d=e.coordinateSystem,p=this.group,h=d.scale,g={position:d.position,scale:h};!p.childAt(0)||s?p.attr(g):f.updateProps(p,g,e),p.removeAll();var m=["itemStyle","normal"],v=["itemStyle","emphasis"],b=["label","normal"],x=["label","emphasis"],y=l.createHashMap();l.each(d.regions,function(t){var n=y.get(t.name)||y.set(t.name,new f.Group),i=new f.CompoundPath({shape:{paths:[]}});n.add(i);var r,a=e.getRegionModel(t.name)||e,s=a.getModel(m),d=a.getModel(v),g=o(s,h),_=o(d,h),w=a.getModel(b),k=a.getModel(x);if(u){r=u.indexOfName(t.name);var S=u.getItemVisual(r,"color",!0);S&&(g.fill=S)}l.each(t.geometries,function(e){if("polygon"===e.type){i.shape.paths.push(new f.Polygon({shape:{points:e.exterior}}));for(var t=0;t<(e.interiors?e.interiors.length:0);t++)i.shape.paths.push(new f.Polygon({shape:{points:e.interiors[t]}}))}}),i.setStyle(g),i.style.strokeNoScale=!0,i.culling=!0;var M=w.get("show"),C=k.get("show"),A=u&&isNaN(u.get("value",r)),T=u&&u.getItemLayout(r);if(c||A&&(M||C)||T&&T.showLabel){var E,I=c?t.name:r;(!u||r>=0)&&(E=e);var O=new f.Text({position:t.center.slice(),scale:[1/h[0],1/h[1]],z2:10,silent:!0});f.setLabelStyle(O.style,O.hoverStyle={},w,k,{labelFetcher:E,labelDataIndex:I,defaultText:t.name,useInsideStyle:!1},{textAlign:"center",textVerticalAlign:"middle"}),n.add(O)}if(u)u.setItemGraphicEl(r,n);else{var a=e.getRegionModel(t.name);i.eventData={componentType:"geo",geoIndex:e.componentIndex,name:t.name,region:a&&a.option||{}}}(n.__regions||(n.__regions=[])).push(t),f.setHoverStyle(n,_,{hoverSilentOnTouch:!!e.get("selectedMode")}),p.add(n)}),this._updateController(e,t,n),i(this,e,p,n,a),r(e,p)},remove:function(){this.group.removeAll(),this._controller.dispose(),this._controllerHost={}},_updateController:function(e,t,n){function o(){var t={type:"geoRoam",componentType:s};return t[s+"Id"]=e.id,t}var i=e.coordinateSystem,r=this._controller,a=this._controllerHost;a.zoomLimit=e.get("scaleLimit"),a.zoom=i.getZoom(),r.enable(e.get("roam")||!1);var s=e.mainType;r.off("pan").on("pan",function(e,t){this._mouseDownFlag=!1,c.updateViewOnPan(a,e,t),n.dispatchAction(l.extend(o(),{dx:e,dy:t}))},this),r.off("zoom").on("zoom",function(e,t,i){if(this._mouseDownFlag=!1,c.updateViewOnZoom(a,e,t,i),n.dispatchAction(l.extend(o(),{zoom:e,originX:t,originY:i})),this._updateGroup){var r=this.group,s=r.scale;r.traverse(function(e){"text"===e.type&&e.attr("scale",[1/s[0],1/s[1]])})}},this),r.setPointerChecker(function(t,o,r){return i.getViewRectAfterRoam().contain(o,r)&&!d(t,n,e)})}};var p=a;e.exports=p},function(e,t,n){function o(e){return e=a(e),function(t,n){return u.clipPointsByRect(t,e)}}function i(e,t){return e=a(e),function(n){var o=null!=t?t:n,i=o?e.width:e.height,r=o?e.x:e.y;return[r,r+(i||0)]}}function r(e,t,n){return e=a(e),function(o,i,r){return e.contain(i[0],i[1])&&!c(o,t,n)}}function a(e){return l.create(e)}var l=n(10),s=n(119),c=s.onIrrelevantElement,u=n(2);t.makeRectPanelClipPath=o,t.makeLinearBrushOtherExtent=i,t.makeRectIsTargetByCursor=r},function(e,t,n){function o(e,t,n){a(e)[t]=n}function i(e,t,n){var o=a(e);o[t]===n&&(o[t]=null)}function r(e,t){return!!a(e)[t]}function a(e){return e[s]||(e[s]={})}var l=n(1),s="\0_ec_interaction_mutex";l.registerAction({type:"takeGlobalCursor",event:"globalCursorTaken",update:"update"},function(){}),t.take=o,t.release=i,t.isTaken=r},function(e,t,n){function o(e,t,n){var o=t.getBoxLayoutParams(),i=t.get("padding"),r={width:n.getWidth(),height:n.getHeight()},c=a(o,r,i);l(t.get("orient"),e,t.get("itemGap"),c.width,c.height),s(e,o,r,i)}function i(e,t){var n=c.normalizeCssArray(t.get("padding")),o=t.getItemStyle(["color","opacity"]);o.fill=t.get("backgroundColor");var e=new u.Rect({shape:{x:e.x-n[3],y:e.y-n[0],width:e.width+n[1]+n[3],height:e.height+n[0]+n[2],r:t.get("borderRadius")},style:o,silent:!0,z2:-1});return e}var r=n(6),a=r.getLayoutRect,l=r.box,s=r.positionElement,c=n(8),u=n(2);t.layout=o,t.makeBackground=i},function(e,t){function n(e,t,n){var o=e.target,i=o.position;i[0]+=t,i[1]+=n,o.dirty()}function o(e,t,n,o){var i=e.target,r=e.zoomLimit,a=i.position,l=i.scale,s=e.zoom=e.zoom||1;if(s*=t,r){var c=r.min||0,u=r.max||1/0;s=Math.max(Math.min(u,s),c)}var d=s/e.zoom;e.zoom=s,a[0]-=(n-a[0])*(d-1),a[1]-=(o-a[1])*(d-1),l[0]*=d,l[1]*=d,i.dirty()}t.updateViewOnPan=n,t.updateViewOnZoom=o},function(e,t,n){var o=n(1),i=n(0),r=n(12),a=o.extendComponentModel({type:"legend.plain",dependencies:["series"],layoutMode:{type:"box",ignoreSize:!0},init:function(e,t,n){this.mergeDefaultAndTheme(e,n),e.selected=e.selected||{}},mergeOption:function(e){a.superCall(this,"mergeOption",e)},optionUpdated:function(){this._updateData(this.ecModel);var e=this._data;if(e[0]&&"single"===this.get("selectedMode")){for(var t=!1,n=0;n=0},defaultOption:{zlevel:0,z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,inactiveColor:"#ccc",textStyle:{color:"#333"},selectedMode:!0,tooltip:{show:!1}}}),l=a;e.exports=l},function(e,t,n){function o(e,t){t.dispatchAction({type:"legendToggleSelect",name:e})}function i(e,t,n){var o=n.getZr().storage.getDisplayList()[0];o&&o.useHoverLayer||e.get("legendHoverLink")&&n.dispatchAction({type:"highlight",seriesName:e.name,name:t})}function r(e,t,n){var o=n.getZr().storage.getDisplayList()[0];o&&o.useHoverLayer||e.get("legendHoverLink")&&n.dispatchAction({type:"downplay",seriesName:e.name,name:t})}var a=n(4),l=(a.__DEV__,n(1)),s=n(0),c=n(23),u=c.createSymbol,d=n(2),f=n(191),p=f.makeBackground,h=n(6),g=s.curry,m=s.each,v=d.Group,b=l.extendComponentView({type:"legend.plain",newlineDisabled:!1,init:function(){this.group.add(this._contentGroup=new v),this._backgroundEl},getContentGroup:function(){return this._contentGroup},render:function(e,t,n){if(this.resetInner(),e.get("show",!0)){var o=e.get("align");o&&"auto"!==o||(o="right"===e.get("left")&&"vertical"===e.get("orient")?"right":"left"),this.renderInner(o,e,t,n);var i=e.getBoxLayoutParams(),r={width:n.getWidth(),height:n.getHeight()},a=e.get("padding"),l=h.getLayoutRect(i,r,a),c=this.layoutInner(e,o,l),u=h.getLayoutRect(s.defaults({width:c.width,height:c.height},i),r,a);this.group.attr("position",[u.x-c.x,u.y-c.y]),this.group.add(this._backgroundEl=p(c,e))}},resetInner:function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl)},renderInner:function(e,t,n,a){var l=this.getContentGroup(),c=s.createHashMap(),u=t.get("selectedMode");m(t.getData(),function(s,d){var f=s.get("name");if(!this.newlineDisabled&&(""===f||"\n"===f))return void l.add(new v({newline:!0}));var p=n.getSeriesByName(f)[0];if(!c.get(f))if(p){var h=p.getData(),m=h.getVisual("color");"function"==typeof m&&(m=m(p.getDataParams(0)));var b=h.getVisual("legendSymbol")||"roundRect",x=h.getVisual("symbol"),y=this._createItem(f,d,s,t,b,x,e,m,u);y.on("click",g(o,f,a)).on("mouseover",g(i,p,null,a)).on("mouseout",g(r,p,null,a)),c.set(f,!0)}else n.eachRawSeries(function(n){if(!c.get(f)&&n.legendDataProvider){var l=n.legendDataProvider(),p=l.indexOfName(f);if(p<0)return;var h=l.getItemVisual(p,"color");this._createItem(f,d,s,t,"roundRect",null,e,h,u).on("click",g(o,f,a)).on("mouseover",g(i,n,f,a)).on("mouseout",g(r,n,f,a)),c.set(f,!0)}},this)},this)},_createItem:function(e,t,n,o,i,r,a,l,c){var f=o.get("itemWidth"),p=o.get("itemHeight"),h=o.get("inactiveColor"),g=o.isSelected(e),m=new v,b=n.getModel("textStyle"),x=n.get("icon"),y=n.getModel("tooltip"),_=y.parentModel;if(i=x||i,m.add(u(i,0,0,f,p,g?l:h,!0)),!x&&r&&(r!==i||"none"==r)){var w=.8*p;"none"===r&&(r="circle"),m.add(u(r,(f-w)/2,(p-w)/2,w,w,g?l:h))}var k="left"===a?f+5:-5,S=a,M=o.get("formatter"),C=e;"string"==typeof M&&M?C=M.replace("{name}",null!=e?e:""):"function"==typeof M&&(C=M(e)),m.add(new d.Text({style:d.setTextStyle({},b,{text:C,x:k,y:p/2,textFill:g?b.getTextColor():h,textAlign:S,textVerticalAlign:"middle"})}));var A=new d.Rect({shape:m.getBoundingRect(),invisible:!0,tooltip:y.get("show")?s.extend({content:e,formatter:_.get("formatter",!0)||function(){return e},formatterParams:{componentType:"legend",legendIndex:o.componentIndex,name:e,$vars:["name"]}},y.option):null});return m.add(A),m.eachChild(function(e){e.silent=!0}),A.silent=!c,this.getContentGroup().add(m),d.setHoverStyle(m),m.__legendDataIndex=t,m},layoutInner:function(e,t,n){var o=this.getContentGroup();h.box(e.get("orient"),o,e.get("itemGap"),n.width,n.height);var i=o.getBoundingRect();return o.attr("position",[-i.x,-i.y]),this.group.getBoundingRect()}});e.exports=b},function(e,t,n){function o(e,t){var n=e._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===t}var i=n(1),r=n(0),a=n(44),l=n(573);n(211),n(572),n(521),i.extendComponentView({type:"parallel",render:function(e,t,n){this._model=e,this._api=n,this._handlers||(this._handlers={},r.each(s,function(e,t){n.getZr().on(t,this._handlers[t]=r.bind(e,this))},this)),a.createOrUpdate(this,"_throttledDispatchExpand",e.get("axisExpandRate"),"fixRate")},dispose:function(e,t){r.each(this._handlers,function(e,n){t.getZr().off(n,e)}),this._handlers=null},_throttledDispatchExpand:function(e){this._dispatchExpand(e)},_dispatchExpand:function(e){e&&this._api.dispatchAction(r.extend({type:"parallelAxisExpand"},e))}});var s={mousedown:function(e){o(this,"click")&&(this._mouseDownPoint=[e.offsetX,e.offsetY])},mouseup:function(e){var t=this._mouseDownPoint;if(o(this,"click")&&t){var n=[e.offsetX,e.offsetY];if(Math.pow(t[0]-n[0],2)+Math.pow(t[1]-n[1],2)>5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([e.offsetX,e.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(e){if(!this._mouseDownPoint&&o(this,"mousemove")){var t=this._model,n=t.coordinateSystem.getSlidedAxisExpandWindow([e.offsetX,e.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(t.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i&&null})}}};i.registerPreprocessor(l)},function(e,t,n){var o=n(1);n(587),n(475),n(583),n(67),n(480),o.extendComponentView({type:"single"})},function(e,t,n){var o=n(1),i=n(2),r=n(6),a=r.getLayoutRect;o.extendComponentModel({type:"title",layoutMode:{type:"box",ignoreSize:!0},defaultOption:{zlevel:0,z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bolder",color:"#333"},subtextStyle:{color:"#aaa"}}}),o.extendComponentView({type:"title",render:function(e,t,n){if(this.group.removeAll(),e.get("show")){var o=this.group,r=e.getModel("textStyle"),l=e.getModel("subtextStyle"),s=e.get("textAlign"),c=e.get("textBaseline"),u=new i.Text({style:i.setTextStyle({},r,{text:e.get("text"),textFill:r.getTextColor()},{disableBox:!0}),z2:10}),d=u.getBoundingRect(),f=e.get("subtext"),p=new i.Text({style:i.setTextStyle({},l,{text:f,textFill:l.getTextColor(),y:d.height+e.get("itemGap"),textVerticalAlign:"top"},{disableBox:!0}),z2:10}),h=e.get("link"),g=e.get("sublink");u.silent=!h,p.silent=!g,h&&u.on("click",function(){window.open(h,"_"+e.get("target"))}),g&&p.on("click",function(){window.open(g,"_"+e.get("subtarget"))}),o.add(u),f&&o.add(p);var m=o.getBoundingRect(),v=e.getBoxLayoutParams();v.width=m.width,v.height=m.height;var b=a(v,{width:n.getWidth(),height:n.getHeight()},e.get("padding"));s||(s=e.get("left")||e.get("right"),"middle"===s&&(s="center"),"right"===s?b.x+=b.width:"center"===s&&(b.x+=b.width/2)),c||(c=e.get("top")||e.get("bottom"),"center"===c&&(c="middle"),"bottom"===c?b.y+=b.height:"middle"===c&&(b.y+=b.height/2),c=c||"top"),o.attr("position",[b.x,b.y]);var x={textAlign:s,textVerticalAlign:c};u.setStyle(x),p.setStyle(x),m=o.getBoundingRect();var y=b.margin,_=e.getItemStyle(["color","opacity"]);_.fill=e.get("backgroundColor");var w=new i.Rect({shape:{x:m.x-y[3],y:m.y-y[0],width:m.width+y[1]+y[3],height:m.height+y[0]+y[2],r:e.get("borderRadius")},style:_,silent:!0});i.subPixelOptimizeRect(w),o.add(w)}}})},function(e,t,n){var o=n(1);n(67),n(545),n(546),o.registerAction({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},function(){}),o.registerAction({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},function(){})},function(e,t,n){var o=n(1),i=n(0),r=n(15),a=n(218),l=n(45),s=n(92),c=n(5),u=n(3),d=l.mapVisual,f=l.eachVisual,p=i.isArray,h=i.each,g=u.asc,m=u.linearMap,v=i.noop,b=["#f6efa6","#d88273","#bf444c"],x=o.extendComponentModel({type:"visualMap",dependencies:["series"],stateList:["inRange","outOfRange"],replacableOptionKeys:["inRange","outOfRange","target","controller","color"],dataBound:[-1/0,1/0],layoutMode:{type:"box",ignoreSize:!0},defaultOption:{show:!0,zlevel:0,z:4,seriesIndex:"all",min:0,max:200,dimension:null,inRange:null,outOfRange:null,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,color:null,formatter:null,text:null,textStyle:{color:"#333"}},init:function(e,t,n){this._dataExtent,this.targetVisuals={},this.controllerVisuals={},this.textStyleModel,this.itemSize,this.mergeDefaultAndTheme(e,n)},optionUpdated:function(e,t){var n=this.option;r.canvasSupported||(n.realtime=!1),!t&&s.replaceVisualOption(n,e,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},resetVisual:function(e){var t=this.stateList;e=i.bind(e,this),this.controllerVisuals=s.createVisualMappings(this.option.controller,t,e),this.targetVisuals=s.createVisualMappings(this.option.target,t,e)},getTargetSeriesIndices:function(){var e=this.option.seriesIndex,t=[];return null==e||"all"===e?this.ecModel.eachSeries(function(e,n){t.push(n)}):t=c.normalizeToArray(e),t},eachTargetSeries:function(e,t){i.each(this.getTargetSeriesIndices(),function(n){e.call(t,this.ecModel.getSeriesByIndex(n))},this)},isTargetSeries:function(e){var t=!1;return this.eachTargetSeries(function(n){n===e&&(t=!0)}),t},formatValueText:function(e,t,n){function o(e){return e===c[0]?"min":e===c[1]?"max":(+e).toFixed(Math.min(s,20))}var r,a,l=this.option,s=l.precision,c=this.dataBound,u=l.formatter;return n=n||["<",">"],i.isArray(e)&&(e=e.slice(),r=!0),a=t?e:r?[o(e[0]),o(e[1])]:o(e),i.isString(u)?u.replace("{value}",r?a[0]:a).replace("{value2}",r?a[1]:a):i.isFunction(u)?r?u(e[0],e[1]):u(e):r?e[0]===c[0]?n[0]+" "+a[1]:e[1]===c[1]?n[1]+" "+a[0]:a[0]+" - "+a[1]:a},resetExtent:function(){var e=this.option,t=g([e.min,e.max]);this._dataExtent=t},getDataDimension:function(e){var t=this.option.dimension;return null!=t?t:e.dimensions.length-1},getExtent:function(){return this._dataExtent.slice()},completeVisualOption:function(){function e(e){p(o.color)&&!e.inRange&&(e.inRange={color:o.color.slice().reverse()}),e.inRange=e.inRange||{color:b},h(this.stateList,function(t){var n=e[t];if(i.isString(n)){var o=a.get(n,"active",u);o?(e[t]={},e[t][n]=o):delete e[t]}},this)}function t(e,t,n){var o=e[t],i=e[n];o&&!i&&(i=e[n]={},h(o,function(e,t){if(l.isValidType(t)){var n=a.get(t,"inactive",u);null!=n&&(i[t]=n,"color"!==t||i.hasOwnProperty("opacity")||i.hasOwnProperty("colorAlpha")||(i.opacity=[0,0]))}}))}function n(e){var t=(e.inRange||{}).symbol||(e.outOfRange||{}).symbol,n=(e.inRange||{}).symbolSize||(e.outOfRange||{}).symbolSize,o=this.get("inactiveColor");h(this.stateList,function(r){var a=this.itemSize,l=e[r];l||(l=e[r]={color:u?o:[o]}),null==l.symbol&&(l.symbol=t&&i.clone(t)||(u?"roundRect":["roundRect"])),null==l.symbolSize&&(l.symbolSize=n&&i.clone(n)||(u?a[0]:[a[0],a[0]])),l.symbol=d(l.symbol,function(e){return"none"===e||"square"===e?"roundRect":e});var s=l.symbolSize;if(null!=s){var c=-1/0;f(s,function(e){e>c&&(c=e)}),l.symbolSize=d(s,function(e){return m(e,[0,c],[0,a[0]],!0)})}},this)}var o=this.option,r={inRange:o.inRange,outOfRange:o.outOfRange},s=o.target||(o.target={}),c=o.controller||(o.controller={});i.merge(s,r),i.merge(c,r);var u=this.isCategory();e.call(this,s),e.call(this,c),t.call(this,s,"inRange","outOfRange"),n.call(this,c)},resetItemSize:function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},isCategory:function(){return!!this.option.categories},setSelected:v,getValueState:v,getVisualMeta:v}),y=x;e.exports=y},function(e,t,n){var o=n(1),i=n(0),r=n(2),a=n(8),l=n(6),s=n(45),c=o.extendComponentView({type:"visualMap",autoPositionValues:{left:1,right:1,top:1,bottom:1},init:function(e,t){this.ecModel=e,this.api=t,this.visualMapModel},render:function(e,t,n,o){if(this.visualMapModel=e,!1===e.get("show"))return void this.group.removeAll();this.doRender.apply(this,arguments)},renderBackground:function(e){var t=this.visualMapModel,n=a.normalizeCssArray(t.get("padding")||0),o=e.getBoundingRect();e.add(new r.Rect({z2:-1,silent:!0,shape:{x:o.x-n[3],y:o.y-n[0],width:o.width+n[3]+n[1],height:o.height+n[0]+n[2]},style:{fill:t.get("backgroundColor"),stroke:t.get("borderColor"),lineWidth:t.get("borderWidth")}}))},getControllerVisual:function(e,t,n){function o(e){return c[e]}function r(e,t){c[e]=t}n=n||{};var a=n.forceState,l=this.visualMapModel,c={};if("symbol"===t&&(c.symbol=l.get("itemSymbol")),"color"===t){var u=l.get("contentColor");c.color=u}var d=l.controllerVisuals[a||l.getValueState(e)],f=s.prepareVisualTypes(d);return i.each(f,function(i){var a=d[i];n.convertOpacityToAlpha&&"opacity"===i&&(i="colorAlpha",a=d.__alphaForOpacity),s.dependsOn(i,t)&&a&&a.applyVisual(e,o,r)}),c[t]},positionGroup:function(e){var t=this.visualMapModel,n=this.api;l.positionElement(e,t.getBoxLayoutParams(),{width:n.getWidth(),height:n.getHeight()})},doRender:i.noop});e.exports=c},function(e,t,n){function o(e,t,n){var o=e.option,i=o.align;if(null!=i&&"auto"!==i)return i;for(var r={width:t.getWidth(),height:t.getHeight()},a="horizontal"===o.orient?1:0,s=[["left","right","width"],["top","bottom","height"]],c=s[a],u=[0,null,10],d={},f=0;f<3;f++)d[s[1-a][f]]=u[f],d[c[f]]=2===f?n[0]:o[c[f]];var p=[["x","width",3],["y","height",0]][a],h=l(d,r,o.padding);return c[(h.margin[p[2]]||0)+h[p[0]]+.5*h[p[1]]<.5*r[p[1]]?0:1]}function i(e){return r.each(e||[],function(t){null!=e.dataIndex&&(e.dataIndexInside=e.dataIndex,e.dataIndex=null)}),e}var r=n(0),a=n(6),l=a.getLayoutRect;t.getItemAlign=o,t.convertDataIndex=i},function(e,t,n){function o(e){var t=e&&e.visualMap;r.isArray(t)||(t=t?[t]:[]),a(t,function(e){if(e){i(e,"splitList")&&!i(e,"pieces")&&(e.pieces=e.splitList,delete e.splitList);var t=e.pieces;t&&r.isArray(t)&&a(t,function(e){r.isObject(e)&&(i(e,"start")&&!i(e,"min")&&(e.min=e.start),i(e,"end")&&!i(e,"max")&&(e.max=e.end))})}})}function i(e,t){return e&&e.hasOwnProperty&&e.hasOwnProperty(t)}var r=n(0),a=r.each;e.exports=o},function(e,t,n){n(14).registerSubTypeDefaulter("visualMap",function(e){return e.categories||(e.pieces?e.pieces.length>0:e.splitNumber>0)&&!e.calculable?"piecewise":"continuous"})},function(e,t,n){function o(e,t){e.eachTargetSeries(function(t){var n=t.getData();s.applyVisual(e.stateList,e.targetVisuals,n,e.getValueState,e,e.getDataDimension(n))})}function i(e){e.eachSeries(function(t){var n=t.getData(),o=[];e.eachComponent("visualMap",function(e){if(e.isTargetSeries(t)){var i=e.getVisualMeta(l.bind(r,null,t,e))||{stops:[],outerColors:[]};i.dimension=e.getDataDimension(n),o.push(i)}}),t.getData().setVisual("visualMeta",o)})}function r(e,t,n,o){function i(e){return s[e]}function r(e,t){s[e]=t}for(var a=t.targetVisuals[o],l=c.prepareVisualTypes(a),s={color:e.getData().getVisual("color")},u=0,d=l.length;u>1^-(1&l),s=s>>1^-(1&s),l+=i,s+=r,i=l,r=s,o.push([l/n,s/n])}return o}function r(e){return o(e),a.map(a.filter(e.features,function(e){return e.geometry&&e.properties&&e.geometry.coordinates.length>0}),function(e){var t=e.properties,n=e.geometry,o=n.coordinates,i=[];"Polygon"===n.type&&i.push({type:"polygon",exterior:o[0],interiors:o.slice(1)}),"MultiPolygon"===n.type&&a.each(o,function(e){e[0]&&i.push({type:"polygon",exterior:e[0],interiors:e.slice(1)})});var r=new l(t.name,i,t.cp);return r.properties=t,r})}var a=n(0),l=n(209);e.exports=r},function(e,t,n){function o(e,t){var n=[];return e.eachComponent("parallel",function(o,r){var a=new i(o,e,t);a.name="parallel_"+r,a.resize(o,t),o.coordinateSystem=a,a.model=o,n.push(a)}),e.eachSeries(function(t){if("parallel"===t.get("coordinateSystem")){var n=e.queryComponents({mainType:"parallel",index:t.get("parallelIndex"),id:t.get("parallelId")})[0];t.coordinateSystem=n.coordinateSystem}}),n}var i=n(570);n(26).register("parallel",{create:o})},function(e,t,n){function o(e,t,n){this.root,this.data,this._nodes=[],this.hostModel=e,this.levelModels=r.map(t||[],function(t){return new a(t,e,e.ecModel)}),this.leavesModel=new a(n||{},e,e.ecModel)}function i(e,t){var n=t.children;e.parentNode!==t&&(n.push(e),e.parentNode=t)}var r=n(0),a=n(12),l=n(13),s=n(213),c=n(25),u=function(e,t){this.name=e||"",this.depth=0,this.height=0,this.parentNode=null,this.dataIndex=-1,this.children=[],this.viewChildren=[],this.hostTree=t};u.prototype={constructor:u,isRemoved:function(){return this.dataIndex<0},eachNode:function(e,t,n){"function"==typeof e&&(n=t,t=e,e=null),e=e||{},r.isString(e)&&(e={order:e});var o,i=e.order||"preorder",a=this[e.attr||"children"];"preorder"===i&&(o=t.call(n,this));for(var l=0;!o&&lt&&(t=o.height)}this.height=t+1},getNodeById:function(e){if(this.getId()===e)return this;for(var t=0,n=this.children,o=n.length;t=0&&this.hostTree.data.setItemLayout(this.dataIndex,e,t)},getLayout:function(){return this.hostTree.data.getItemLayout(this.dataIndex)},getModel:function(e){if(!(this.dataIndex<0)){var t,n=this.hostTree,o=n.data.getItemModel(this.dataIndex),i=this.getLevelModel();return i||0!==this.children.length&&(0===this.children.length||!1!==this.isExpand)||(t=this.getLeavesModel()),o.getModel(e,(i||t||n.hostModel).getModel(e))}},getLevelModel:function(){return(this.hostTree.levelModels||[])[this.depth]},getLeavesModel:function(){return this.hostTree.leavesModel},setVisual:function(e,t){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,e,t)},getVisual:function(e,t){return this.hostTree.data.getItemVisual(this.dataIndex,e,t)},getRawIndex:function(){return this.hostTree.data.getRawIndex(this.dataIndex)},getId:function(){return this.hostTree.data.getId(this.dataIndex)}},o.prototype={constructor:o,type:"tree",eachNode:function(e,t,n){this.root.eachNode(e,t,n)},getNodeByDataIndex:function(e){var t=this.data.getRawIndex(e);return this._nodes[t]},getNodeByName:function(e){return this.root.getNodeByName(e)},update:function(){for(var e=this.data,t=this._nodes,n=0,o=t.length;no&&(u=r.interval=o);var d=r.intervalPrecision=i(u);return a(r.niceTickExtent=[c(Math.ceil(e[0]/u)*u,d),c(Math.floor(e[1]/u)*u,d)],e),r}function i(e){return s.getPrecisionSafe(e)+2}function r(e,t,n){e[t]=Math.max(Math.min(e[t],n[1]),n[0])}function a(e,t){!isFinite(e[0])&&(e[0]=t[0]),!isFinite(e[1])&&(e[1]=t[1]),r(e,0,t),r(e,1,t),e[0]>e[1]&&(e[0]=e[1])}function l(e,t,n,o){var i=[];if(!e)return i;t[0]1e4)return[];return t[1]>(i.length?i[i.length-1]:n[1])&&i.push(t[1]),i}var s=n(3),c=s.round;t.intervalScaleNiceTicks=o,t.getIntervalPrecision=i,t.fixExtent=a,t.intervalScaleGetTicks=l},function(e,t,n){function o(){function e(t,o){if(o>=n.length)return t;for(var r=-1,a=t.length,l=n[o++],s={},c={};++r=n.length)return e;var a=[],l=o[r++];return i.each(e,function(e,n){a.push({key:n,values:t(e,r)})}),l?a.sort(function(e,t){return l(e.key,t.key)}):a}var n=[],o=[];return{key:function(e){return n.push(e),this},sortKeys:function(e){return o[n.length-1]=e,this},entries:function(n){return t(e(n,0),0)}}}var i=n(0);e.exports=o},function(e,t,n){var o=n(0),i={get:function(e,t,n){var i=o.clone((r[e]||{})[t]);return n&&o.isArray(i)?i[i.length-1]:i}},r={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},a=i;e.exports=a},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=83)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},4:function(e,t){e.exports=n(59)},83:function(e,t,n){"use strict";n.r(t);var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("label",{staticClass:"el-checkbox",class:[e.border&&e.checkboxSize?"el-checkbox--"+e.checkboxSize:"",{"is-disabled":e.isDisabled},{"is-bordered":e.border},{"is-checked":e.isChecked}],attrs:{id:e.id}},[n("span",{staticClass:"el-checkbox__input",class:{"is-disabled":e.isDisabled,"is-checked":e.isChecked,"is-indeterminate":e.indeterminate,"is-focus":e.focus},attrs:{tabindex:!!e.indeterminate&&0,role:!!e.indeterminate&&"checkbox","aria-checked":!!e.indeterminate&&"mixed"}},[n("span",{staticClass:"el-checkbox__inner"}),e.trueLabel||e.falseLabel?n("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",name:e.name,disabled:e.isDisabled,"true-value":e.trueLabel,"false-value":e.falseLabel},domProps:{checked:Array.isArray(e.model)?e._i(e.model,null)>-1:e._q(e.model,e.trueLabel)},on:{change:[function(t){var n=e.model,o=t.target,i=o.checked?e.trueLabel:e.falseLabel;if(Array.isArray(n)){var r=e._i(n,null);o.checked?r<0&&(e.model=n.concat([null])):r>-1&&(e.model=n.slice(0,r).concat(n.slice(r+1)))}else e.model=i},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}}):n("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",disabled:e.isDisabled,name:e.name},domProps:{value:e.label,checked:Array.isArray(e.model)?e._i(e.model,e.label)>-1:e.model},on:{change:[function(t){var n=e.model,o=t.target,i=!!o.checked;if(Array.isArray(n)){var r=e.label,a=e._i(n,r);o.checked?a<0&&(e.model=n.concat([r])):a>-1&&(e.model=n.slice(0,a).concat(n.slice(a+1)))}else e.model=i},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}})]),e.$slots.default||e.label?n("span",{staticClass:"el-checkbox__label"},[e._t("default"),e.$slots.default?e._e():[e._v(e._s(e.label))]],2):e._e()])},i=[];o._withStripped=!0;var r=n(4),a=n.n(r),l={name:"ElCheckbox",mixins:[a.a],inject:{elForm:{default:""},elFormItem:{default:""}},componentName:"ElCheckbox",data:function(){return{selfModel:!1,focus:!1,isLimitExceeded:!1}},computed:{model:{get:function(){return this.isGroup?this.store:void 0!==this.value?this.value:this.selfModel},set:function(e){this.isGroup?(this.isLimitExceeded=!1,void 0!==this._checkboxGroup.min&&e.lengththis._checkboxGroup.max&&(this.isLimitExceeded=!0),!1===this.isLimitExceeded&&this.dispatch("ElCheckboxGroup","input",[e])):(this.$emit("input",e),this.selfModel=e)}},isChecked:function(){return"[object Boolean]"==={}.toString.call(this.model)?this.model:Array.isArray(this.model)?this.model.indexOf(this.label)>-1:null!==this.model&&void 0!==this.model?this.model===this.trueLabel:void 0},isGroup:function(){for(var e=this.$parent;e;){if("ElCheckboxGroup"===e.$options.componentName)return this._checkboxGroup=e,!0;e=e.$parent}return!1},store:function(){return this._checkboxGroup?this._checkboxGroup.value:this.value},isLimitDisabled:function(){var e=this._checkboxGroup,t=e.max,n=e.min;return!(!t&&!n)&&this.model.length>=t&&!this.isChecked||this.model.length<=n&&this.isChecked},isDisabled:function(){return this.isGroup?this._checkboxGroup.disabled||this.disabled||(this.elForm||{}).disabled||this.isLimitDisabled:this.disabled||(this.elForm||{}).disabled},_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},checkboxSize:function(){var e=this.size||this._elFormItemSize||(this.$ELEMENT||{}).size;return this.isGroup?this._checkboxGroup.checkboxGroupSize||e:e}},props:{value:{},label:{},indeterminate:Boolean,disabled:Boolean,checked:Boolean,name:String,trueLabel:[String,Number],falseLabel:[String,Number],id:String,controls:String,border:Boolean,size:String},methods:{addToStore:function(){Array.isArray(this.model)&&-1===this.model.indexOf(this.label)?this.model.push(this.label):this.model=this.trueLabel||!0},handleChange:function(e){var t=this;if(!this.isLimitExceeded){var n=void 0;n=e.target.checked?void 0===this.trueLabel||this.trueLabel:void 0!==this.falseLabel&&this.falseLabel,this.$emit("change",n,e),this.$nextTick(function(){t.isGroup&&t.dispatch("ElCheckboxGroup","change",[t._checkboxGroup.value])})}}},created:function(){this.checked&&this.addToStore()},mounted:function(){this.indeterminate&&this.$el.setAttribute("aria-controls",this.controls)},watch:{value:function(e){this.dispatch("ElFormItem","el.form.change",e)}}},s=l,c=n(0),u=Object(c.a)(s,o,i,!1,null,null,null);u.options.__file="packages/checkbox/src/checkbox.vue";var d=u.exports;d.install=function(e){e.component(d.name,d)},t.default=d}})},function(e,t,n){"use strict";t.__esModule=!0,n(38),t.default={mounted:function(){},methods:{getMigratingConfig:function(){return{props:{},events:{}}}}}},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=131)}({131:function(e,t,n){"use strict";n.r(t);var o=n(5),i=n.n(o),r=n(17),a=n.n(r),l=n(2),s=n(3),c=n(7),u=n.n(c),d={name:"ElTooltip",mixins:[i.a],props:{openDelay:{type:Number,default:0},disabled:Boolean,manual:Boolean,effect:{type:String,default:"dark"},arrowOffset:{type:Number,default:0},popperClass:String,content:String,visibleArrow:{default:!0},transition:{type:String,default:"el-fade-in-linear"},popperOptions:{default:function(){return{boundariesPadding:10,gpuAcceleration:!1}}},enterable:{type:Boolean,default:!0},hideAfter:{type:Number,default:0},tabindex:{type:Number,default:0}},data:function(){return{tooltipId:"el-tooltip-"+Object(s.generateId)(),timeoutPending:null,focusing:!1}},beforeCreate:function(){var e=this;this.$isServer||(this.popperVM=new u.a({data:{node:""},render:function(e){return this.node}}).$mount(),this.debounceClose=a()(200,function(){return e.handleClosePopper()}))},render:function(e){var t=this;this.popperVM&&(this.popperVM.node=e("transition",{attrs:{name:this.transition},on:{afterLeave:this.doDestroy}},[e("div",{on:{mouseleave:function(){t.setExpectedState(!1),t.debounceClose()},mouseenter:function(){t.setExpectedState(!0)}},ref:"popper",attrs:{role:"tooltip",id:this.tooltipId,"aria-hidden":this.disabled||!this.showPopper?"true":"false"},directives:[{name:"show",value:!this.disabled&&this.showPopper}],class:["el-tooltip__popper","is-"+this.effect,this.popperClass]},[this.$slots.content||this.content])]));var n=this.getFirstElement();if(!n)return null;var o=n.data=n.data||{};return o.staticClass=this.addTooltipClass(o.staticClass),n},mounted:function(){var e=this;this.referenceElm=this.$el,1===this.$el.nodeType&&(this.$el.setAttribute("aria-describedby",this.tooltipId),this.$el.setAttribute("tabindex",this.tabindex),Object(l.on)(this.referenceElm,"mouseenter",this.show),Object(l.on)(this.referenceElm,"mouseleave",this.hide),Object(l.on)(this.referenceElm,"focus",function(){if(!e.$slots.default||!e.$slots.default.length)return void e.handleFocus();var t=e.$slots.default[0].componentInstance;t&&t.focus?t.focus():e.handleFocus()}),Object(l.on)(this.referenceElm,"blur",this.handleBlur),Object(l.on)(this.referenceElm,"click",this.removeFocusing)),this.value&&this.popperVM&&this.popperVM.$nextTick(function(){e.value&&e.updatePopper()})},watch:{focusing:function(e){e?Object(l.addClass)(this.referenceElm,"focusing"):Object(l.removeClass)(this.referenceElm,"focusing")}},methods:{show:function(){this.setExpectedState(!0),this.handleShowPopper()},hide:function(){this.setExpectedState(!1),this.debounceClose()},handleFocus:function(){this.focusing=!0,this.show()},handleBlur:function(){this.focusing=!1,this.hide()},removeFocusing:function(){this.focusing=!1},addTooltipClass:function(e){return e?"el-tooltip "+e.replace("el-tooltip",""):"el-tooltip"},handleShowPopper:function(){var e=this;this.expectedState&&!this.manual&&(clearTimeout(this.timeout),this.timeout=setTimeout(function(){e.showPopper=!0},this.openDelay),this.hideAfter>0&&(this.timeoutPending=setTimeout(function(){e.showPopper=!1},this.hideAfter)))},handleClosePopper:function(){this.enterable&&this.expectedState||this.manual||(clearTimeout(this.timeout),this.timeoutPending&&clearTimeout(this.timeoutPending),this.showPopper=!1,this.disabled&&this.doDestroy())},setExpectedState:function(e){!1===e&&clearTimeout(this.timeoutPending),this.expectedState=e},getFirstElement:function(){var e=this.$slots.default;if(!Array.isArray(e))return null;for(var t=null,n=0;n0?this._openTimer=setTimeout(function(){t._openTimer=null,t.doOpen(n)},o):this.doOpen(n)},doOpen:function(e){if(!this.$isServer&&(!this.willOpen||this.willOpen())&&!this.opened){this._opening=!0;var t=this.$el,n=e.modal,o=e.zIndex;if(o&&(c.default.zIndex=o),n&&(this._closing&&(c.default.closeModal(this._popupId),this._closing=!1),c.default.openModal(this._popupId,c.default.nextZIndex(),this.modalAppendToBody?void 0:t,e.modalClass,e.modalFade),e.lockScroll)){this.withoutHiddenClass=!(0,f.hasClass)(document.body,"el-popup-parent--hidden"),this.withoutHiddenClass&&(this.bodyPaddingRight=document.body.style.paddingRight,this.computedBodyPaddingRight=parseInt((0,f.getStyle)(document.body,"paddingRight"),10)),h=(0,d.default)();var i=document.documentElement.clientHeight0&&(i||"scroll"===r)&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.computedBodyPaddingRight+h+"px"),(0,f.addClass)(document.body,"el-popup-parent--hidden")}"static"===getComputedStyle(t).position&&(t.style.position="absolute"),t.style.zIndex=c.default.nextZIndex(),this.opened=!0,this.onOpen&&this.onOpen(),this.doAfterOpen()}},doAfterOpen:function(){this._opening=!1},close:function(){var e=this;if(!this.willClose||this.willClose()){null!==this._openTimer&&(clearTimeout(this._openTimer),this._openTimer=null),clearTimeout(this._closeTimer);var t=Number(this.closeDelay);t>0?this._closeTimer=setTimeout(function(){e._closeTimer=null,e.doClose()},t):this.doClose()}},doClose:function(){this._closing=!0,this.onClose&&this.onClose(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose()},doAfterClose:function(){c.default.closeModal(this._popupId),this._closing=!1},restoreBodyStyle:function(){this.modal&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.bodyPaddingRight,(0,f.removeClass)(document.body,"el-popup-parent--hidden")),this.withoutHiddenClass=!0}}},t.PopupManager=c.default},function(e,t,n){"use strict";t.__esModule=!0,t.removeResizeListener=t.addResizeListener=void 0;var o=n(626),i=function(e){return e&&e.__esModule?e:{default:e}}(o),r="undefined"==typeof window,a=function(e){for(var t=e,n=Array.isArray(t),o=0,t=n?t:t[Symbol.iterator]();;){var i;if(n){if(o>=t.length)break;i=t[o++]}else{if(o=t.next(),o.done)break;i=o.value}var r=i,a=r.target.__resizeListeners__||[];a.length&&a.forEach(function(e){e()})}};t.addResizeListener=function(e,t){r||(e.__resizeListeners__||(e.__resizeListeners__=[],e.__ro__=new i.default(a),e.__ro__.observe(e)),e.__resizeListeners__.push(t))},t.removeResizeListener=function(e,t){e&&e.__resizeListeners__&&(e.__resizeListeners__.splice(e.__resizeListeners__.indexOf(t),1),e.__resizeListeners__.length||e.__ro__.disconnect())}},function(e,t){e.exports=function(e,t,n,o){function i(){function i(){a=Number(new Date),n.apply(s,u)}function l(){r=void 0}var s=this,c=Number(new Date)-a,u=arguments;o&&!r&&i(),r&&clearTimeout(r),void 0===o&&c>e?i():!0!==t&&(r=setTimeout(o?l:i,void 0===o?e-c:e))}var r,a=0;return"boolean"!=typeof t&&(o=n,n=t,t=void 0),i}},function(e,t,n){e.exports=n.p+"732389ded34cb9c52dd88271f1345af9.ttf"},function(e,t,n){e.exports=n.p+"535877f50039c0cb49a6196a5b7517cd.woff"},function(e,t,n){"use strict";var o=n(142);t.a=o.a},function(e,t,n){"use strict";var o=n(143);t.a=o.a},function(e,t,n){"use strict";var o=n(144);t.a=o.a},function(e,t,n){"use strict";var o=n(145);t.a=o.a},function(e,t,n){"use strict";var o=n(146);t.a=o.a},function(e,t,n){"use strict";var o=n(147);t.a=o.a},function(e,t,n){"use strict";var o=n(148);t.a=o.a},function(e,t,n){"use strict";var o=n(149);t.a=o.a},function(e,t,n){"use strict";var o=n(150);t.a=o.a},function(e,t,n){var o=n(245),i=n(49),r=n(136),a=n(686),l=n(0),s=function(e){r.call(this,e),i.call(this,e),a.call(this,e),this.id=e.id||o()};s.prototype={type:"element",name:"",__zr:null,ignore:!1,clipPath:null,drift:function(e,t){switch(this.draggable){case"horizontal":t=0;break;case"vertical":e=0}var n=this.transform;n||(n=this.transform=[1,0,0,1,0,0]),n[4]+=e,n[5]+=t,this.decomposeTransform(),this.dirty(!1)},beforeUpdate:function(){},afterUpdate:function(){},update:function(){this.updateTransform()},traverse:function(e,t){},attrKV:function(e,t){if("position"===e||"scale"===e||"origin"===e){if(t){var n=this[e];n||(n=this[e]=[]),n[0]=t[0],n[1]=t[1]}}else this[e]=t},hide:function(){this.ignore=!0,this.__zr&&this.__zr.refresh()},show:function(){this.ignore=!1,this.__zr&&this.__zr.refresh()},attr:function(e,t){if("string"==typeof e)this.attrKV(e,t);else if(l.isObject(e))for(var n in e)e.hasOwnProperty(n)&&this.attrKV(n,e[n]);return this.dirty(!1),this},setClipPath:function(e){var t=this.__zr;t&&e.addSelfToZr(t),this.clipPath&&this.clipPath!==e&&this.removeClipPath(),this.clipPath=e,e.__zr=t,e.__clipTarget=this,this.dirty(!1)},removeClipPath:function(){var e=this.clipPath;e&&(e.__zr&&e.removeSelfFromZr(e.__zr),e.__zr=null,e.__clipTarget=null,this.clipPath=null,this.dirty(!1))},addSelfToZr:function(e){this.__zr=e;var t=this.animators;if(t)for(var n=0;n.5?t:e}function l(e,t,n,o,i){var a=e.length;if(1==i)for(var l=0;li)e.length=i;else for(var r=o;r=0&&!(T[n]<=t);n--);n=Math.min(n,_-2)}else{for(n=$;n<_&&!(T[n]>t);n++);n=Math.min(n-1,_-2)}$=n,H=t;var o=T[n+1]-T[n];if(0!==o)if(N=(t-T[n])/o,y)if(F=E[n],B=E[0===n?n:n-1],V=E[n>_-2?_-1:n+1],j=E[n>_-3?_-1:n+2],S)u(B,F,V,j,N,N*N,N*N*N,g(e,i),A);else{var s;if(M)s=u(B,F,V,j,N,N*N,N*N*N,W,1),s=p(W);else{if(C)return a(F,V,N);s=d(B,F,V,j,N,N*N,N*N*N)}b(e,i,s)}else if(S)l(E[n],E[n+1],N,g(e,i),A);else{var s;if(M)l(E[n],E[n+1],N,W,1),s=p(W);else{if(C)return a(E[n],E[n+1],N);s=r(E[n],E[n+1],N)}b(e,i,s)}},U=new m({target:e._target,life:w,loop:e._loop,delay:e._delay,onframe:G,ondestroy:n});return t&&"spline"!==t&&(U.easing=t),U}}}var m=n(665),v=n(32),b=n(0),x=b.isArrayLike,y=Array.prototype.slice,_=function(e,t,n,r){this._tracks={},this._target=e,this._loop=t||!1,this._getter=n||o,this._setter=r||i,this._clipCount=0,this._delay=0,this._doneList=[],this._onframeList=[],this._clipList=[]};_.prototype={when:function(e,t){var n=this._tracks;for(var o in t)if(t.hasOwnProperty(o)){if(!n[o]){n[o]=[];var i=this._getter(this._target,o);if(null==i)continue;0!==e&&n[o].push({time:0,value:f(i)})}n[o].push({time:e,value:t[o]})}return this},during:function(e){return this._onframeList.push(e),this},pause:function(){for(var e=0;et+l&&a>o+l||ae+l&&r>n+l||rt+u&&c>o+u&&c>a+u||ce+u&&s>n+u&&s>i+u||st&&r>o||ri?a:0}e.exports=n},function(e,t){var n=function(){this.head=null,this.tail=null,this._len=0},o=n.prototype;o.insert=function(e){var t=new i(e);return this.insertEntry(t),t},o.insertEntry=function(e){this.head?(this.tail.next=e,e.prev=this.tail,e.next=null,this.tail=e):this.head=this.tail=e,this._len++},o.remove=function(e){var t=e.prev,n=e.next;t?t.next=n:this.head=n,n?n.prev=t:this.tail=t,e.next=e.prev=null,this._len--},o.len=function(){return this._len},o.clear=function(){this.head=this.tail=null,this._len=0};var i=function(e){this.value=e,this.next,this.prev},r=function(e){this._list=new n,this._map={},this._maxSize=e||10,this._lastRemovedEntry=null},a=r.prototype;a.put=function(e,t){var n=this._list,o=this._map,r=null;if(null==o[e]){var a=n.len(),l=this._lastRemovedEntry;if(a>=this._maxSize&&a>0){var s=n.head;n.remove(s),delete o[s.key],r=s.value,this._lastRemovedEntry=s}l?l.value=t:l=new i(t),l.key=e,n.insertEntry(l),o[e]=l}return r},a.get=function(e){var t=this._map[e],n=this._list;if(null!=t)return t!==n.tail&&(n.remove(t),n.insertEntry(t)),t.value},a.clear=function(){this._list.clear(),this._map={}};var l=r;e.exports=l},function(e,t){function n(){return o++}var o=2311;e.exports=n},function(e,t,n){var o=n(0),i=n(98),r=function(e,t,n,o,r,a){this.x=null==e?0:e,this.y=null==t?0:t,this.x2=null==n?1:n,this.y2=null==o?0:o,this.type="linear",this.global=a||!1,i.call(this,r)};r.prototype={constructor:r},o.inherits(r,i);var a=r;e.exports=a},function(e,t){var n=function(e,t){this.image=e,this.repeat=t,this.type="pattern"};n.prototype.getCanvasPattern=function(e){return e.createPattern(this.image,this.repeat||"repeat")};var o=n;e.exports=o},function(e,t){function n(e,t,n){var o=null==t.x?0:t.x,i=null==t.x2?1:t.x2,r=null==t.y?0:t.y,a=null==t.y2?0:t.y2;return t.global||(o=o*n.width+n.x,i=i*n.width+n.x,r=r*n.height+n.y,a=a*n.height+n.y),e.createLinearGradient(o,r,i,a)}function o(e,t,n){var o=n.width,i=n.height,r=Math.min(o,i),a=null==t.x?.5:t.x,l=null==t.y?.5:t.y,s=null==t.r?.5:t.r;return t.global||(a=a*o+n.x,l=l*i+n.y,s*=r),e.createRadialGradient(a,l,0,a,l,s)}var i=[["shadowBlur",0],["shadowOffsetX",0],["shadowOffsetY",0],["shadowColor","#000"],["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]],r=function(e,t){this.extendFrom(e,!1),this.host=t};r.prototype={constructor:r,host:null,fill:"#000",stroke:null,opacity:1,lineDash:null,lineDashOffset:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,lineWidth:1,strokeNoScale:!1,text:null,font:null,textFont:null,fontStyle:null,fontWeight:null,fontSize:null,fontFamily:null,textTag:null,textFill:"#000",textStroke:null,textWidth:null,textHeight:null,textStrokeWidth:0,textLineHeight:null,textPosition:"inside",textRect:null,textOffset:null,textAlign:null,textVerticalAlign:null,textDistance:5,textShadowColor:"transparent",textShadowBlur:0,textShadowOffsetX:0,textShadowOffsetY:0,textBoxShadowColor:"transparent",textBoxShadowBlur:0,textBoxShadowOffsetX:0,textBoxShadowOffsetY:0,transformText:!1,textRotation:0,textOrigin:null,textBackgroundColor:null,textBorderColor:null,textBorderWidth:0,textBorderRadius:0,textPadding:null,rich:null,truncate:null,blend:null,bind:function(e,t,n){for(var o=this,r=n&&n.style,a=!r,l=0;l0},extendFrom:function(e,t){if(e)for(var n in e)!e.hasOwnProperty(n)||!0!==t&&(!1===t?this.hasOwnProperty(n):null==e[n])||(this[n]=e[n])},set:function(e,t){"string"==typeof e?this[e]=t:this.extendFrom(e,!0)},clone:function(){var e=new this.constructor;return e.extendFrom(this,!0),e},getGradient:function(e,t,i){for(var r="radial"===t.type?o:n,a=r(e,t,i),l=t.colorStops,s=0;s=11?function(){var t,n=this.__clipPaths,o=this.style;if(n)for(var i=0;i=2){if(a&&"spline"!==a){var l=r(o,a,n,t.smoothConstraint);e.moveTo(o[0][0],o[0][1]);for(var s=o.length,c=0;c<(n?s:s-1);c++){var u=l[2*c],d=l[2*c+1],f=o[(c+1)%s];e.bezierCurveTo(u[0],u[1],d[0],d[1],f[0],f[1])}}else{"spline"===a&&(o=i(o,n)),e.moveTo(o[0][0],o[0][1]);for(var c=1,p=o.length;cs&&(d=n+o,n*=s/d,o*=s/d),i+r>s&&(d=i+r,i*=s/d,r*=s/d),o+i>c&&(d=o+i,o*=c/d,i*=c/d),n+r>c&&(d=n+r,n*=c/d,r*=c/d),e.moveTo(a+n,l),e.lineTo(a+s-o,l),0!==o&&e.quadraticCurveTo(a+s,l,a+s,l+o),e.lineTo(a+s,l+c-i),0!==i&&e.quadraticCurveTo(a+s,l+c,a+s-i,l+c),e.lineTo(a+r,l+c),0!==r&&e.quadraticCurveTo(a,l+c,a,l+c-r),e.lineTo(a,l+n),0!==n&&e.quadraticCurveTo(a,l,a+n,l)}t.buildPath=n},function(e,t,n){var o=n(99),i=n(10),r=new i,a=function(){};a.prototype={constructor:a,drawRectText:function(e,t){var n=this.style;t=n.textRect||t,this.__dirty&&o.normalizeTextStyle(n,!0);var i=n.text;if(null!=i&&(i+=""),o.needDrawText(i,n)){e.save();var a=this.transform;n.transformText?this.setTransform(e):a&&(r.copy(t),r.applyTransform(a),t=r),o.renderText(this,e,i,n,t),e.restore()}}};var l=a;e.exports=l},function(e,t,n){function o(e,t,n){this._svgRoot=e,this._tagNames="string"==typeof t?[t]:t,this._markLabel=n,this.nextId=0}var i=n(137),r=i.createElement,a=n(0),l=n(16),s=n(73),c=n(74),u=n(138),d=u.path,f=u.image,p=u.text;o.prototype.createElement=r,o.prototype.getDefs=function(e){var t=this._svgRoot,n=this._svgRoot.getElementsByTagName("defs");return 0===n.length?e?(n=t.insertBefore(this.createElement("defs"),t.firstChild),n.contains||(n.contains=function(e){var t=n.children;if(!t)return!1;for(var o=t.length-1;o>=0;--o)if(t[o]===e)return!0;return!1}),n):null:n[0]},o.prototype.update=function(e,t){if(e){var n=this.getDefs(!1);if(e._dom&&n.contains(e._dom))"function"==typeof t&&t();else{var o=this.add(e);o&&(e._dom=o)}}},o.prototype.addDom=function(e){this.getDefs(!0).appendChild(e)},o.prototype.removeDom=function(e){this.getDefs(!1).removeChild(e._dom)},o.prototype.getDoms=function(){var e=this.getDefs(!1);if(!e)return[];var t=[];return a.each(this._tagNames,function(n){var o=e.getElementsByTagName(n);t=t.concat([].slice.call(o))}),t},o.prototype.markAllUnused=function(){var e=this.getDoms(),t=this;a.each(e,function(e){e[t._markLabel]="0"})},o.prototype.markUsed=function(e){e&&(e[this._markLabel]="1")},o.prototype.removeUnused=function(){var e=this.getDefs(!1);if(e){var t=this.getDoms(),n=this;a.each(t,function(t){"1"!==t[n._markLabel]&&e.removeChild(t)})}},o.prototype.getSvgProxy=function(e){return e instanceof l?d:e instanceof s?f:e instanceof c?p:d},o.prototype.getTextSvgElement=function(e){return e.__textSvgEl},o.prototype.getSvgElement=function(e){return e.__svgEl};var h=o;e.exports=h},function(e,t,n){function o(e){return r(e)}function i(){if(!c&&u){c=!0;var e=u.styleSheets;e.length<31?u.createStyleSheet().addRule(".zrvml","behavior:url(#default#VML)"):e[0].addRule(".zrvml","behavior:url(#default#VML)")}}var r,a=n(15),l="urn:schemas-microsoft-com:vml",s="undefined"==typeof window?null:window,c=!1,u=s&&s.document;if(u&&!a.canvasSupported)try{!u.namespaces.zrvml&&u.namespaces.add("zrvml",l),r=function(e){return u.createElement("')}}catch(e){r=function(e){return u.createElement("<"+e+' xmlns="'+l+'" class="zrvml">')}}t.doc=u,t.createNode=o,t.initVML=i},function(e,t,n){"use strict";var o=n(21),i=n(659),r=n(632),a=n(637),l=n(638),s=n(633),c=n(634),u=n(635),d=n(636);o.default.use(i.a),t.a=new i.a({routes:[{path:"/",name:"Overview",component:r.a},{path:"/proxies/tcp",name:"ProxiesTcp",component:a.a},{path:"/proxies/udp",name:"ProxiesUdp",component:l.a},{path:"/proxies/http",name:"ProxiesHttp",component:s.a},{path:"/proxies/https",name:"ProxiesHttps",component:c.a},{path:"/proxies/stcp",name:"ProxiesStcp",component:u.a},{path:"/proxies/sudp",name:"ProxiesSudp",component:d.a}]})},function(e,t){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=97)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},97:function(e,t,n){"use strict";n.r(t);var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("button",{staticClass:"el-button",class:[e.type?"el-button--"+e.type:"",e.buttonSize?"el-button--"+e.buttonSize:"",{"is-disabled":e.buttonDisabled,"is-loading":e.loading,"is-plain":e.plain,"is-round":e.round,"is-circle":e.circle}],attrs:{disabled:e.buttonDisabled||e.loading,autofocus:e.autofocus,type:e.nativeType},on:{click:e.handleClick}},[e.loading?n("i",{staticClass:"el-icon-loading"}):e._e(),e.icon&&!e.loading?n("i",{class:e.icon}):e._e(),e.$slots.default?n("span",[e._t("default")],2):e._e()])},i=[];o._withStripped=!0;var r={name:"ElButton",inject:{elForm:{default:""},elFormItem:{default:""}},props:{type:{type:String,default:"default"},size:String,icon:{type:String,default:""},nativeType:{type:String,default:"button"},loading:Boolean,disabled:Boolean,plain:Boolean,autofocus:Boolean,round:Boolean,circle:Boolean},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},buttonSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},buttonDisabled:function(){return this.disabled||(this.elForm||{}).disabled}},methods:{handleClick:function(e){this.$emit("click",e)}}},a=r,l=n(0),s=Object(l.a)(a,o,i,!1,null,null,null);s.options.__file="packages/button/src/button.vue";var c=s.exports;c.install=function(e){e.component(c.name,c)},t.default=c}})},function(e,t){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=134)}({134:function(e,t,n){"use strict";n.r(t);var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},i={name:"ElCol",props:{span:{type:Number,default:24},tag:{type:String,default:"div"},offset:Number,pull:Number,push:Number,xs:[Number,Object],sm:[Number,Object],md:[Number,Object],lg:[Number,Object],xl:[Number,Object]},computed:{gutter:function(){for(var e=this.$parent;e&&"ElRow"!==e.$options.componentName;)e=e.$parent;return e?e.gutter:0}},render:function(e){var t=this,n=[],i={};return this.gutter&&(i.paddingLeft=this.gutter/2+"px",i.paddingRight=i.paddingLeft),["span","offset","pull","push"].forEach(function(e){(t[e]||0===t[e])&&n.push("span"!==e?"el-col-"+e+"-"+t[e]:"el-col-"+t[e])}),["xs","sm","md","lg","xl"].forEach(function(e){if("number"==typeof t[e])n.push("el-col-"+e+"-"+t[e]);else if("object"===o(t[e])){var i=t[e];Object.keys(i).forEach(function(t){n.push("span"!==t?"el-col-"+e+"-"+t+"-"+i[t]:"el-col-"+e+"-"+i[t])})}}),e(this.tag,{class:["el-col",n],style:i},this.$slots.default)}};i.install=function(e){e.component(i.name,i)},t.default=i}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=67)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},3:function(e,t){e.exports=n(38)},4:function(e,t){e.exports=n(59)},48:function(e,t){e.exports=n(284)},67:function(e,t,n){"use strict";n.r(t);var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"el-form-item",class:[{"el-form-item--feedback":e.elForm&&e.elForm.statusIcon,"is-error":"error"===e.validateState,"is-validating":"validating"===e.validateState,"is-success":"success"===e.validateState,"is-required":e.isRequired||e.required,"is-no-asterisk":e.elForm&&e.elForm.hideRequiredAsterisk},e.sizeClass?"el-form-item--"+e.sizeClass:""]},[n("label-wrap",{attrs:{"is-auto-width":e.labelStyle&&"auto"===e.labelStyle.width,"update-all":"auto"===e.form.labelWidth}},[e.label||e.$slots.label?n("label",{staticClass:"el-form-item__label",style:e.labelStyle,attrs:{for:e.labelFor}},[e._t("label",[e._v(e._s(e.label+e.form.labelSuffix))])],2):e._e()]),n("div",{staticClass:"el-form-item__content",style:e.contentStyle},[e._t("default"),n("transition",{attrs:{name:"el-zoom-in-top"}},["error"===e.validateState&&e.showMessage&&e.form.showMessage?e._t("error",[n("div",{staticClass:"el-form-item__error",class:{"el-form-item__error--inline":"boolean"==typeof e.inlineMessage?e.inlineMessage:e.elForm&&e.elForm.inlineMessage||!1}},[e._v("\n "+e._s(e.validateMessage)+"\n ")])],{error:e.validateMessage}):e._e()],2)],2)],1)},i=[];o._withStripped=!0;var r=n(48),a=n.n(r),l=n(4),s=n.n(l),c=n(9),u=n.n(c),d=n(3),f={props:{isAutoWidth:Boolean,updateAll:Boolean},inject:["elForm","elFormItem"],render:function(){var e=arguments[0],t=this.$slots.default;if(!t)return null;if(this.isAutoWidth){var n=this.elForm.autoLabelWidth,o={};if(n&&"auto"!==n){var i=parseInt(n,10)-this.computedWidth;i&&(o.marginLeft=i+"px")}return e("div",{class:"el-form-item__label-wrap",style:o},[t])}return t[0]},methods:{getLabelWidth:function(){if(this.$el&&this.$el.firstElementChild){var e=window.getComputedStyle(this.$el.firstElementChild).width;return Math.ceil(parseFloat(e))}return 0},updateLabelWidth:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"update";this.$slots.default&&this.isAutoWidth&&this.$el.firstElementChild&&("update"===e?this.computedWidth=this.getLabelWidth():"remove"===e&&this.elForm.deregisterLabelWidth(this.computedWidth))}},watch:{computedWidth:function(e,t){this.updateAll&&(this.elForm.registerLabelWidth(e,t),this.elFormItem.updateComputedLabelWidth(e))}},data:function(){return{computedWidth:0}},mounted:function(){this.updateLabelWidth("update")},updated:function(){this.updateLabelWidth("update")},beforeDestroy:function(){this.updateLabelWidth("remove")}},p=f,h=n(0),g=Object(h.a)(p,void 0,void 0,!1,null,null,null);g.options.__file="packages/form/src/label-wrap.vue";var m=g.exports,v={name:"ElFormItem",componentName:"ElFormItem",mixins:[s.a],provide:function(){return{elFormItem:this}},inject:["elForm"],props:{label:String,labelWidth:String,prop:String,required:{type:Boolean,default:void 0},rules:[Object,Array],error:String,validateStatus:String,for:String,inlineMessage:{type:[String,Boolean],default:""},showMessage:{type:Boolean,default:!0},size:String},components:{LabelWrap:m},watch:{error:{immediate:!0,handler:function(e){this.validateMessage=e,this.validateState=e?"error":""}},validateStatus:function(e){this.validateState=e}},computed:{labelFor:function(){return this.for||this.prop},labelStyle:function(){var e={};if("top"===this.form.labelPosition)return e;var t=this.labelWidth||this.form.labelWidth;return t&&(e.width=t),e},contentStyle:function(){var e={},t=this.label;if("top"===this.form.labelPosition||this.form.inline)return e;if(!t&&!this.labelWidth&&this.isNested)return e;var n=this.labelWidth||this.form.labelWidth;return"auto"===n?"auto"===this.labelWidth?e.marginLeft=this.computedLabelWidth:"auto"===this.form.labelWidth&&(e.marginLeft=this.elForm.autoLabelWidth):e.marginLeft=n,e},form:function(){for(var e=this.$parent,t=e.$options.componentName;"ElForm"!==t;)"ElFormItem"===t&&(this.isNested=!0),e=e.$parent,t=e.$options.componentName;return e},fieldValue:function(){var e=this.form.model;if(e&&this.prop){var t=this.prop;return-1!==t.indexOf(":")&&(t=t.replace(/:/,".")),Object(d.getPropByPath)(e,t,!0).v}},isRequired:function(){var e=this.getRules(),t=!1;return e&&e.length&&e.every(function(e){return!e.required||(t=!0,!1)}),t},_formSize:function(){return this.elForm.size},elFormItemSize:function(){return this.size||this._formSize},sizeClass:function(){return this.elFormItemSize||(this.$ELEMENT||{}).size}},data:function(){return{validateState:"",validateMessage:"",validateDisabled:!1,validator:{},isNested:!1,computedLabelWidth:""}},methods:{validate:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:d.noop;this.validateDisabled=!1;var o=this.getFilteredRule(e);if((!o||0===o.length)&&void 0===this.required)return n(),!0;this.validateState="validating";var i={};o&&o.length>0&&o.forEach(function(e){delete e.trigger}),i[this.prop]=o;var r=new a.a(i),l={};l[this.prop]=this.fieldValue,r.validate(l,{firstFields:!0},function(e,o){t.validateState=e?"error":"success",t.validateMessage=e?e[0].message:"",n(t.validateMessage,o),t.elForm&&t.elForm.$emit("validate",t.prop,!e,t.validateMessage||null)})},clearValidate:function(){this.validateState="",this.validateMessage="",this.validateDisabled=!1},resetField:function(){var e=this;this.validateState="",this.validateMessage="";var t=this.form.model,n=this.fieldValue,o=this.prop;-1!==o.indexOf(":")&&(o=o.replace(/:/,"."));var i=Object(d.getPropByPath)(t,o,!0);this.validateDisabled=!0,Array.isArray(n)?i.o[i.k]=[].concat(this.initialValue):i.o[i.k]=this.initialValue,this.$nextTick(function(){e.validateDisabled=!1}),this.broadcast("ElTimeSelect","fieldReset",this.initialValue)},getRules:function(){var e=this.form.rules,t=this.rules,n=void 0!==this.required?{required:!!this.required}:[],o=Object(d.getPropByPath)(e,this.prop||"");return e=e?o.o[this.prop||""]||o.v:[],[].concat(t||e||[]).concat(n)},getFilteredRule:function(e){return this.getRules().filter(function(t){return!t.trigger||""===e||(Array.isArray(t.trigger)?t.trigger.indexOf(e)>-1:t.trigger===e)}).map(function(e){return u()({},e)})},onFieldBlur:function(){this.validate("blur")},onFieldChange:function(){if(this.validateDisabled)return void(this.validateDisabled=!1);this.validate("change")},updateComputedLabelWidth:function(e){this.computedLabelWidth=e?e+"px":""},addValidateEvents:function(){(this.getRules().length||void 0!==this.required)&&(this.$on("el.form.blur",this.onFieldBlur),this.$on("el.form.change",this.onFieldChange))},removeValidateEvents:function(){this.$off()}},mounted:function(){if(this.prop){this.dispatch("ElForm","el.form.addField",[this]);var e=this.fieldValue;Array.isArray(e)&&(e=[].concat(e)),Object.defineProperty(this,"initialValue",{value:e}),this.addValidateEvents()}},beforeDestroy:function(){this.dispatch("ElForm","el.form.removeField",[this])}},b=v,x=Object(h.a)(b,o,i,!1,null,null,null);x.options.__file="packages/form/src/form-item.vue";var y=x.exports;y.install=function(e){e.component(y.name,y)},t.default=y},9:function(e,t){e.exports=n(93)}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=121)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},121:function(e,t,n){"use strict";n.r(t);var o=function(){var e=this,t=e.$createElement;return(e._self._c||t)("form",{staticClass:"el-form",class:[e.labelPosition?"el-form--label-"+e.labelPosition:"",{"el-form--inline":e.inline}]},[e._t("default")],2)},i=[];o._withStripped=!0;var r=n(9),a=n.n(r),l={name:"ElForm",componentName:"ElForm",provide:function(){return{elForm:this}},props:{model:Object,rules:Object,labelPosition:String,labelWidth:String,labelSuffix:{type:String,default:""},inline:Boolean,inlineMessage:Boolean,statusIcon:Boolean,showMessage:{type:Boolean,default:!0},size:String,disabled:Boolean,validateOnRuleChange:{type:Boolean,default:!0},hideRequiredAsterisk:{type:Boolean,default:!1}},watch:{rules:function(){this.fields.forEach(function(e){e.removeValidateEvents(),e.addValidateEvents()}),this.validateOnRuleChange&&this.validate(function(){})}},computed:{autoLabelWidth:function(){if(!this.potentialLabelWidthArr.length)return 0;var e=Math.max.apply(Math,this.potentialLabelWidthArr);return e?e+"px":""}},data:function(){return{fields:[],potentialLabelWidthArr:[]}},created:function(){var e=this;this.$on("el.form.addField",function(t){t&&e.fields.push(t)}),this.$on("el.form.removeField",function(t){t.prop&&e.fields.splice(e.fields.indexOf(t),1)})},methods:{resetFields:function(){if(!this.model)return void console.warn("[Element Warn][Form]model is required for resetFields to work.");this.fields.forEach(function(e){e.resetField()})},clearValidate:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];(e.length?"string"==typeof e?this.fields.filter(function(t){return e===t.prop}):this.fields.filter(function(t){return e.indexOf(t.prop)>-1}):this.fields).forEach(function(e){e.clearValidate()})},validate:function(e){var t=this;if(!this.model)return void console.warn("[Element Warn][Form]model is required for validate to work!");var n=void 0;"function"!=typeof e&&window.Promise&&(n=new window.Promise(function(t,n){e=function(e){e?t(e):n(e)}}));var o=!0,i=0;0===this.fields.length&&e&&e(!0);var r={};return this.fields.forEach(function(n){n.validate("",function(n,l){n&&(o=!1),r=a()({},r,l),"function"==typeof e&&++i===t.fields.length&&e(o,r)})}),n||void 0},validateField:function(e,t){e=[].concat(e);var n=this.fields.filter(function(t){return-1!==e.indexOf(t.prop)});if(!n.length)return void console.warn("[Element Warn]please pass correct props!");n.forEach(function(e){e.validate("",t)})},getLabelWidthIndex:function(e){var t=this.potentialLabelWidthArr.indexOf(e);if(-1===t)throw new Error("[ElementForm]unpected width ",e);return t},registerLabelWidth:function(e,t){if(e&&t){var n=this.getLabelWidthIndex(t);this.potentialLabelWidthArr.splice(n,1,e)}else e&&this.potentialLabelWidthArr.push(e)},deregisterLabelWidth:function(e){var t=this.getLabelWidthIndex(e);this.potentialLabelWidthArr.splice(t,1)}}},s=l,c=n(0),u=Object(c.a)(s,o,i,!1,null,null,null);u.options.__file="packages/form/src/form.vue";var d=u.exports;d.install=function(e){e.component(d.name,d)},t.default=d},9:function(e,t){e.exports=n(93)}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=101)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},101:function(e,t,n){"use strict";n.r(t);var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("li",{staticClass:"el-menu-item",class:{"is-active":e.active,"is-disabled":e.disabled},style:[e.paddingStyle,e.itemStyle,{backgroundColor:e.backgroundColor}],attrs:{role:"menuitem",tabindex:"-1"},on:{click:e.handleClick,mouseenter:e.onMouseEnter,focus:e.onMouseEnter,blur:e.onMouseLeave,mouseleave:e.onMouseLeave}},["ElMenu"===e.parentMenu.$options.componentName&&e.rootMenu.collapse&&e.$slots.title?n("el-tooltip",{attrs:{effect:"dark",placement:"right"}},[n("div",{attrs:{slot:"content"},slot:"content"},[e._t("title")],2),n("div",{staticStyle:{position:"absolute",left:"0",top:"0",height:"100%",width:"100%",display:"inline-block","box-sizing":"border-box",padding:"0 20px"}},[e._t("default")],2)]):[e._t("default"),e._t("title")]],2)},i=[];o._withStripped=!0;var r=n(36),a=n(29),l=n.n(a),s=n(4),c=n.n(s),u={name:"ElMenuItem",componentName:"ElMenuItem",mixins:[r.a,c.a],components:{ElTooltip:l.a},props:{index:{default:null,validator:function(e){return"string"==typeof e||null===e}},route:[String,Object],disabled:Boolean},computed:{active:function(){return this.index===this.rootMenu.activeIndex},hoverBackground:function(){return this.rootMenu.hoverBackground},backgroundColor:function(){return this.rootMenu.backgroundColor||""},activeTextColor:function(){return this.rootMenu.activeTextColor||""},textColor:function(){return this.rootMenu.textColor||""},mode:function(){return this.rootMenu.mode},itemStyle:function(){var e={color:this.active?this.activeTextColor:this.textColor};return"horizontal"!==this.mode||this.isNested||(e.borderBottomColor=this.active?this.rootMenu.activeTextColor?this.activeTextColor:"":"transparent"),e},isNested:function(){return this.parentMenu!==this.rootMenu}},methods:{onMouseEnter:function(){("horizontal"!==this.mode||this.rootMenu.backgroundColor)&&(this.$el.style.backgroundColor=this.hoverBackground)},onMouseLeave:function(){("horizontal"!==this.mode||this.rootMenu.backgroundColor)&&(this.$el.style.backgroundColor=this.backgroundColor)},handleClick:function(){this.disabled||(this.dispatch("ElMenu","item-click",this),this.$emit("click",this))}},mounted:function(){this.parentMenu.addItem(this),this.rootMenu.addItem(this)},beforeDestroy:function(){this.parentMenu.removeItem(this),this.rootMenu.removeItem(this)}},d=u,f=n(0),p=Object(f.a)(d,o,i,!1,null,null,null);p.options.__file="packages/menu/src/menu-item.vue";var h=p.exports;h.install=function(e){e.component(h.name,h)},t.default=h},29:function(e,t){e.exports=n(221)},36:function(e,t,n){"use strict";t.a={inject:["rootMenu"],computed:{indexPath:function(){for(var e=[this.index],t=this.$parent;"ElMenu"!==t.$options.componentName;)t.index&&e.unshift(t.index),t=t.$parent;return e},parentMenu:function(){for(var e=this.$parent;e&&-1===["ElMenu","ElSubmenu"].indexOf(e.$options.componentName);)e=e.$parent;return e},paddingStyle:function(){if("vertical"!==this.rootMenu.mode)return{};var e=20,t=this.$parent;if(this.rootMenu.collapse)e=20;else for(;t&&"ElMenu"!==t.$options.componentName;)"ElSubmenu"===t.$options.componentName&&(e+=20),t=t.$parent;return{paddingLeft:e+"px"}}}}},4:function(e,t){e.exports=n(59)}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=69)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},11:function(e,t){e.exports=n(220)},2:function(e,t){e.exports=n(29)},4:function(e,t){e.exports=n(59)},69:function(e,t,n){"use strict";n.r(t);var o=n(4),i=n.n(o),r=n(11),a=n.n(r),l=l||{};l.Utils=l.Utils||{},l.Utils.focusFirstDescendant=function(e){for(var t=0;t=0;t--){var n=e.childNodes[t];if(l.Utils.attemptFocus(n)||l.Utils.focusLastDescendant(n))return!0}return!1},l.Utils.attemptFocus=function(e){if(!l.Utils.isFocusable(e))return!1;l.Utils.IgnoreUtilFocusChanges=!0;try{e.focus()}catch(e){}return l.Utils.IgnoreUtilFocusChanges=!1,document.activeElement===e},l.Utils.isFocusable=function(e){if(e.tabIndex>0||0===e.tabIndex&&null!==e.getAttribute("tabIndex"))return!0;if(e.disabled)return!1;switch(e.nodeName){case"A":return!!e.href&&"ignore"!==e.rel;case"INPUT":return"hidden"!==e.type&&"file"!==e.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},l.Utils.triggerEvent=function(e,t){var n=void 0;n=/^mouse|click/.test(t)?"MouseEvents":/^key/.test(t)?"KeyboardEvent":"HTMLEvents";for(var o=document.createEvent(n),i=arguments.length,r=Array(i>2?i-2:0),a=2;a=0;t--)e.splice(t,0,e[t]);e=e.join("")}return/^[0-9a-fA-F]{6}$/.test(e)?{red:parseInt(e.slice(0,2),16),green:parseInt(e.slice(2,4),16),blue:parseInt(e.slice(4,6),16)}:{red:255,green:255,blue:255}},mixColor:function(e,t){var n=this.getColorChannels(e),o=n.red,i=n.green,r=n.blue;return t>0?(o*=1-t,i*=1-t,r*=1-t):(o+=(255-o)*t,i+=(255-i)*t,r+=(255-r)*t),"rgb("+Math.round(o)+", "+Math.round(i)+", "+Math.round(r)+")"},addItem:function(e){this.$set(this.items,e.index,e)},removeItem:function(e){delete this.items[e.index]},addSubmenu:function(e){this.$set(this.submenus,e.index,e)},removeSubmenu:function(e){delete this.submenus[e.index]},openMenu:function(e,t){var n=this.openedMenus;-1===n.indexOf(e)&&(this.uniqueOpened&&(this.openedMenus=n.filter(function(e){return-1!==t.indexOf(e)})),this.openedMenus.push(e))},closeMenu:function(e){var t=this.openedMenus.indexOf(e);-1!==t&&this.openedMenus.splice(t,1)},handleSubmenuClick:function(e){var t=e.index,n=e.indexPath;-1!==this.openedMenus.indexOf(t)?(this.closeMenu(t),this.$emit("close",t,n)):(this.openMenu(t,n),this.$emit("open",t,n))},handleItemClick:function(e){var t=this,n=e.index,o=e.indexPath,i=this.activeIndex,r=null!==e.index;r&&(this.activeIndex=e.index),this.$emit("select",n,o,e),("horizontal"===this.mode||this.collapse)&&(this.openedMenus=[]),this.router&&r&&this.routeToItem(e,function(e){if(t.activeIndex=i,e){if("NavigationDuplicated"===e.name)return;console.error(e)}})},initOpenedMenu:function(){var e=this,t=this.activeIndex,n=this.items[t];n&&"horizontal"!==this.mode&&!this.collapse&&n.indexPath.forEach(function(t){var n=e.submenus[t];n&&e.openMenu(t,n.indexPath)})},routeToItem:function(e,t){var n=e.route||e.index;try{this.$router.push(n,function(){},t)}catch(e){console.error(e)}},open:function(e){var t=this,n=this.submenus[e.toString()].indexPath;n.forEach(function(e){return t.openMenu(e,n)})},close:function(e){this.closeMenu(e)}},mounted:function(){this.initOpenedMenu(),this.$on("item-click",this.handleItemClick),this.$on("submenu-click",this.handleSubmenuClick),"horizontal"===this.mode&&new h(this.$el),this.$watch("items",this.updateActiveIndex)}},v=m,b=n(0),x=Object(b.a)(v,void 0,void 0,!1,null,null,null);x.options.__file="packages/menu/src/menu.vue";var y=x.exports;y.install=function(e){e.component(y.name,y)},t.default=y}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=74)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},2:function(e,t){e.exports=n(29)},3:function(e,t){e.exports=n(38)},5:function(e,t){e.exports=n(94)},7:function(e,t){e.exports=n(21)},74:function(e,t,n){"use strict";n.r(t);var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("span",[n("transition",{attrs:{name:e.transition},on:{"after-enter":e.handleAfterEnter,"after-leave":e.handleAfterLeave}},[n("div",{directives:[{name:"show",rawName:"v-show",value:!e.disabled&&e.showPopper,expression:"!disabled && showPopper"}],ref:"popper",staticClass:"el-popover el-popper",class:[e.popperClass,e.content&&"el-popover--plain"],style:{width:e.width+"px"},attrs:{role:"tooltip",id:e.tooltipId,"aria-hidden":e.disabled||!e.showPopper?"true":"false"}},[e.title?n("div",{staticClass:"el-popover__title",domProps:{textContent:e._s(e.title)}}):e._e(),e._t("default",[e._v(e._s(e.content))])],2)]),n("span",{ref:"wrapper",staticClass:"el-popover__reference-wrapper"},[e._t("reference")],2)],1)},i=[];o._withStripped=!0;var r=n(5),a=n.n(r),l=n(2),s=n(3),c={name:"ElPopover",mixins:[a.a],props:{trigger:{type:String,default:"click",validator:function(e){return["click","focus","hover","manual"].indexOf(e)>-1}},openDelay:{type:Number,default:0},closeDelay:{type:Number,default:200},title:String,disabled:Boolean,content:String,reference:{},popperClass:String,width:{},visibleArrow:{default:!0},arrowOffset:{type:Number,default:0},transition:{type:String,default:"fade-in-linear"},tabindex:{type:Number,default:0}},computed:{tooltipId:function(){return"el-popover-"+Object(s.generateId)()}},watch:{showPopper:function(e){this.disabled||(e?this.$emit("show"):this.$emit("hide"))}},mounted:function(){var e=this,t=this.referenceElm=this.reference||this.$refs.reference,n=this.popper||this.$refs.popper;!t&&this.$refs.wrapper.children&&(t=this.referenceElm=this.$refs.wrapper.children[0]),t&&(Object(l.addClass)(t,"el-popover__reference"),t.setAttribute("aria-describedby",this.tooltipId),t.setAttribute("tabindex",this.tabindex),n.setAttribute("tabindex",0),"click"!==this.trigger&&(Object(l.on)(t,"focusin",function(){e.handleFocus();var n=t.__vue__;n&&"function"==typeof n.focus&&n.focus()}),Object(l.on)(n,"focusin",this.handleFocus),Object(l.on)(t,"focusout",this.handleBlur),Object(l.on)(n,"focusout",this.handleBlur)),Object(l.on)(t,"keydown",this.handleKeydown),Object(l.on)(t,"click",this.handleClick)),"click"===this.trigger?(Object(l.on)(t,"click",this.doToggle),Object(l.on)(document,"click",this.handleDocumentClick)):"hover"===this.trigger?(Object(l.on)(t,"mouseenter",this.handleMouseEnter),Object(l.on)(n,"mouseenter",this.handleMouseEnter),Object(l.on)(t,"mouseleave",this.handleMouseLeave),Object(l.on)(n,"mouseleave",this.handleMouseLeave)):"focus"===this.trigger&&(this.tabindex<0&&console.warn("[Element Warn][Popover]a negative taindex means that the element cannot be focused by tab key"),t.querySelector("input, textarea")?(Object(l.on)(t,"focusin",this.doShow),Object(l.on)(t,"focusout",this.doClose)):(Object(l.on)(t,"mousedown",this.doShow),Object(l.on)(t,"mouseup",this.doClose)))},beforeDestroy:function(){this.cleanup()},deactivated:function(){this.cleanup()},methods:{doToggle:function(){this.showPopper=!this.showPopper},doShow:function(){this.showPopper=!0},doClose:function(){this.showPopper=!1},handleFocus:function(){Object(l.addClass)(this.referenceElm,"focusing"),"click"!==this.trigger&&"focus"!==this.trigger||(this.showPopper=!0)},handleClick:function(){Object(l.removeClass)(this.referenceElm,"focusing")},handleBlur:function(){Object(l.removeClass)(this.referenceElm,"focusing"),"click"!==this.trigger&&"focus"!==this.trigger||(this.showPopper=!1)},handleMouseEnter:function(){var e=this;clearTimeout(this._timer),this.openDelay?this._timer=setTimeout(function(){e.showPopper=!0},this.openDelay):this.showPopper=!0},handleKeydown:function(e){27===e.keyCode&&"manual"!==this.trigger&&this.doClose()},handleMouseLeave:function(){var e=this;clearTimeout(this._timer),this.closeDelay?this._timer=setTimeout(function(){e.showPopper=!1},this.closeDelay):this.showPopper=!1},handleDocumentClick:function(e){var t=this.reference||this.$refs.reference,n=this.popper||this.$refs.popper;!t&&this.$refs.wrapper.children&&(t=this.referenceElm=this.$refs.wrapper.children[0]),this.$el&&t&&!this.$el.contains(e.target)&&!t.contains(e.target)&&n&&!n.contains(e.target)&&(this.showPopper=!1)},handleAfterEnter:function(){this.$emit("after-enter")},handleAfterLeave:function(){this.$emit("after-leave"),this.doDestroy()},cleanup:function(){(this.openDelay||this.closeDelay)&&clearTimeout(this._timer)}},destroyed:function(){var e=this.reference;Object(l.off)(e,"click",this.doToggle),Object(l.off)(e,"mouseup",this.doClose),Object(l.off)(e,"mousedown",this.doShow),Object(l.off)(e,"focusin",this.doShow),Object(l.off)(e,"focusout",this.doClose),Object(l.off)(e,"mousedown",this.doShow),Object(l.off)(e,"mouseup",this.doClose),Object(l.off)(e,"mouseleave",this.handleMouseLeave),Object(l.off)(e,"mouseenter",this.handleMouseEnter),Object(l.off)(document,"click",this.handleDocumentClick)}},u=c,d=n(0),f=Object(d.a)(u,o,i,!1,null,null,null);f.options.__file="packages/popover/src/main.vue";var p=f.exports,h=function(e,t,n){var o=t.expression?t.value:t.arg,i=n.context.$refs[o];i&&(Array.isArray(i)?i[0].$refs.reference=e:i.$refs.reference=e)},g={bind:function(e,t,n){h(e,t,n)},inserted:function(e,t,n){h(e,t,n)}},m=n(7);n.n(m).a.directive("popover",g),p.install=function(e){e.directive("popover",g),e.component(p.name,p)},p.directive=g,t.default=p}})},function(e,t){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=132)}({132:function(e,t,n){"use strict";n.r(t);var o={name:"ElRow",componentName:"ElRow",props:{tag:{type:String,default:"div"},gutter:Number,type:String,justify:{type:String,default:"start"},align:{type:String,default:"top"}},computed:{style:function(){var e={};return this.gutter&&(e.marginLeft="-"+this.gutter/2+"px",e.marginRight=e.marginLeft),e}},render:function(e){return e(this.tag,{class:["el-row","start"!==this.justify?"is-justify-"+this.justify:"","top"!==this.align?"is-align-"+this.align:"",{"el-row--flex":"flex"===this.type}],style:this.style},this.$slots.default)}};o.install=function(e){e.component(o.name,o)},t.default=o}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=129)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},129:function(e,t,n){"use strict";n.r(t);var o=n(28),i=n.n(o),r=n(36),a=n(4),l=n.n(a),s=n(5),c=n.n(s),u={props:{transformOrigin:{type:[Boolean,String],default:!1},offset:c.a.props.offset,boundariesPadding:c.a.props.boundariesPadding,popperOptions:c.a.props.popperOptions},data:c.a.data,methods:c.a.methods,beforeDestroy:c.a.beforeDestroy,deactivated:c.a.deactivated},d={name:"ElSubmenu",componentName:"ElSubmenu",mixins:[r.a,l.a,u],components:{ElCollapseTransition:i.a},props:{index:{type:String,required:!0},showTimeout:{type:Number,default:300},hideTimeout:{type:Number,default:300},popperClass:String,disabled:Boolean,popperAppendToBody:{type:Boolean,default:void 0}},data:function(){return{popperJS:null,timeout:null,items:{},submenus:{},mouseInChild:!1}},watch:{opened:function(e){var t=this;this.isMenuPopup&&this.$nextTick(function(e){t.updatePopper()})}},computed:{appendToBody:function(){return void 0===this.popperAppendToBody?this.isFirstLevel:this.popperAppendToBody},menuTransitionName:function(){return this.rootMenu.collapse?"el-zoom-in-left":"el-zoom-in-top"},opened:function(){return this.rootMenu.openedMenus.indexOf(this.index)>-1},active:function(){var e=!1,t=this.submenus,n=this.items;return Object.keys(n).forEach(function(t){n[t].active&&(e=!0)}),Object.keys(t).forEach(function(n){t[n].active&&(e=!0)}),e},hoverBackground:function(){return this.rootMenu.hoverBackground},backgroundColor:function(){return this.rootMenu.backgroundColor||""},activeTextColor:function(){return this.rootMenu.activeTextColor||""},textColor:function(){return this.rootMenu.textColor||""},mode:function(){return this.rootMenu.mode},isMenuPopup:function(){return this.rootMenu.isMenuPopup},titleStyle:function(){return"horizontal"!==this.mode?{color:this.textColor}:{borderBottomColor:this.active?this.rootMenu.activeTextColor?this.activeTextColor:"":"transparent",color:this.active?this.activeTextColor:this.textColor}},isFirstLevel:function(){for(var e=!0,t=this.$parent;t&&t!==this.rootMenu;){if(["ElSubmenu","ElMenuItemGroup"].indexOf(t.$options.componentName)>-1){e=!1;break}t=t.$parent}return e}},methods:{handleCollapseToggle:function(e){e?this.initPopper():this.doDestroy()},addItem:function(e){this.$set(this.items,e.index,e)},removeItem:function(e){delete this.items[e.index]},addSubmenu:function(e){this.$set(this.submenus,e.index,e)},removeSubmenu:function(e){delete this.submenus[e.index]},handleClick:function(){var e=this.rootMenu,t=this.disabled;"hover"===e.menuTrigger&&"horizontal"===e.mode||e.collapse&&"vertical"===e.mode||t||this.dispatch("ElMenu","submenu-click",this)},handleMouseenter:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.showTimeout;if("ActiveXObject"in window||"focus"!==e.type||e.relatedTarget){var o=this.rootMenu,i=this.disabled;"click"===o.menuTrigger&&"horizontal"===o.mode||!o.collapse&&"vertical"===o.mode||i||(this.dispatch("ElSubmenu","mouse-enter-child"),clearTimeout(this.timeout),this.timeout=setTimeout(function(){t.rootMenu.openMenu(t.index,t.indexPath)},n),this.appendToBody&&this.$parent.$el.dispatchEvent(new MouseEvent("mouseenter")))}},handleMouseleave:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],n=this.rootMenu;"click"===n.menuTrigger&&"horizontal"===n.mode||!n.collapse&&"vertical"===n.mode||(this.dispatch("ElSubmenu","mouse-leave-child"),clearTimeout(this.timeout),this.timeout=setTimeout(function(){!e.mouseInChild&&e.rootMenu.closeMenu(e.index)},this.hideTimeout),this.appendToBody&&t&&"ElSubmenu"===this.$parent.$options.name&&this.$parent.handleMouseleave(!0))},handleTitleMouseenter:function(){if("horizontal"!==this.mode||this.rootMenu.backgroundColor){var e=this.$refs["submenu-title"];e&&(e.style.backgroundColor=this.rootMenu.hoverBackground)}},handleTitleMouseleave:function(){if("horizontal"!==this.mode||this.rootMenu.backgroundColor){var e=this.$refs["submenu-title"];e&&(e.style.backgroundColor=this.rootMenu.backgroundColor||"")}},updatePlacement:function(){this.currentPlacement="horizontal"===this.mode&&this.isFirstLevel?"bottom-start":"right-start"},initPopper:function(){this.referenceElm=this.$el,this.popperElm=this.$refs.menu,this.updatePlacement()}},created:function(){var e=this;this.$on("toggle-collapse",this.handleCollapseToggle),this.$on("mouse-enter-child",function(){e.mouseInChild=!0,clearTimeout(e.timeout)}),this.$on("mouse-leave-child",function(){e.mouseInChild=!1,clearTimeout(e.timeout)})},mounted:function(){this.parentMenu.addSubmenu(this),this.rootMenu.addSubmenu(this),this.initPopper()},beforeDestroy:function(){this.parentMenu.removeSubmenu(this),this.rootMenu.removeSubmenu(this)},render:function(e){var t=this,n=this.active,o=this.opened,i=this.paddingStyle,r=this.titleStyle,a=this.backgroundColor,l=this.rootMenu,s=this.currentPlacement,c=this.menuTransitionName,u=this.mode,d=this.disabled,f=this.popperClass,p=this.$slots,h=this.isFirstLevel,g=e("transition",{attrs:{name:c}},[e("div",{ref:"menu",directives:[{name:"show",value:o}],class:["el-menu--"+u,f],on:{mouseenter:function(e){return t.handleMouseenter(e,100)},mouseleave:function(){return t.handleMouseleave(!0)},focus:function(e){return t.handleMouseenter(e,100)}}},[e("ul",{attrs:{role:"menu"},class:["el-menu el-menu--popup","el-menu--popup-"+s],style:{backgroundColor:l.backgroundColor||""}},[p.default])])]),m=e("el-collapse-transition",[e("ul",{attrs:{role:"menu"},class:"el-menu el-menu--inline",directives:[{name:"show",value:o}],style:{backgroundColor:l.backgroundColor||""}},[p.default])]),v="horizontal"===l.mode&&h||"vertical"===l.mode&&!l.collapse?"el-icon-arrow-down":"el-icon-arrow-right";return e("li",{class:{"el-submenu":!0,"is-active":n,"is-opened":o,"is-disabled":d},attrs:{role:"menuitem","aria-haspopup":"true","aria-expanded":o},on:{mouseenter:this.handleMouseenter,mouseleave:function(){return t.handleMouseleave(!1)},focus:this.handleMouseenter}},[e("div",{class:"el-submenu__title",ref:"submenu-title",on:{click:this.handleClick,mouseenter:this.handleTitleMouseenter,mouseleave:this.handleTitleMouseleave},style:[i,r,{backgroundColor:a}]},[p.title,e("i",{class:["el-submenu__icon-arrow",v]})]),this.isMenuPopup?g:m])}},f=d,p=n(0),h=Object(p.a)(f,void 0,void 0,!1,null,null,null);h.options.__file="packages/menu/src/submenu.vue";var g=h.exports;g.install=function(e){e.component(g.name,g)},t.default=g},28:function(e,t){e.exports=n(613)},36:function(e,t,n){"use strict";t.a={inject:["rootMenu"],computed:{indexPath:function(){for(var e=[this.index],t=this.$parent;"ElMenu"!==t.$options.componentName;)t.index&&e.unshift(t.index),t=t.$parent;return e},parentMenu:function(){for(var e=this.$parent;e&&-1===["ElMenu","ElSubmenu"].indexOf(e.$options.componentName);)e=e.$parent;return e},paddingStyle:function(){if("vertical"!==this.rootMenu.mode)return{};var e=20,t=this.$parent;if(this.rootMenu.collapse)e=20;else for(;t&&"ElMenu"!==t.$options.componentName;)"ElSubmenu"===t.$options.componentName&&(e+=20),t=t.$parent;return{paddingLeft:e+"px"}}}}},4:function(e,t){e.exports=n(59)},5:function(e,t){e.exports=n(94)}})},function(e,t,n){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=130)}({130:function(e,t,n){"use strict";function o(e,t){var n=t.row,o=t.column,i=t.$index,a=o.property,l=a&&Object(r.getPropByPath)(n,a).v;return o&&o.formatter?o.formatter(n,o,l,i):l}function i(e,t){var n=t.row,o=t.treeNode,i=t.store;if(!o)return null;var r=[],a=function(e){e.stopPropagation(),i.loadOrToggle(n)};if(o.indent&&r.push(e("span",{class:"el-table__indent",style:{"padding-left":o.indent+"px"}})),"boolean"!=typeof o.expanded||o.noLazyChildren)r.push(e("span",{class:"el-table__placeholder"}));else{var l=["el-table__expand-icon",o.expanded?"el-table__expand-icon--expanded":""],s=["el-icon-arrow-right"];o.loading&&(s=["el-icon-loading"]),r.push(e("div",{class:l,on:{click:a}},[e("i",{class:s})]))}return r}n.r(t);var r=n(3),a={default:{order:""},selection:{width:48,minWidth:48,realWidth:48,order:"",className:"el-table-column--selection"},expand:{width:48,minWidth:48,realWidth:48,order:""},index:{width:48,minWidth:48,realWidth:48,order:""}},l={selection:{renderHeader:function(e,t){var n=t.store;return e("el-checkbox",{attrs:{disabled:n.states.data&&0===n.states.data.length,indeterminate:n.states.selection.length>0&&!this.isAllSelected,value:this.isAllSelected},nativeOn:{click:this.toggleAllSelection}})},renderCell:function(e,t){var n=t.row,o=t.column,i=t.store,r=t.$index;return e("el-checkbox",{nativeOn:{click:function(e){return e.stopPropagation()}},attrs:{value:i.isSelected(n),disabled:!!o.selectable&&!o.selectable.call(null,n,r)},on:{input:function(){i.commit("rowSelectedChanged",n)}}})},sortable:!1,resizable:!1},index:{renderHeader:function(e,t){return t.column.label||"#"},renderCell:function(e,t){var n=t.$index,o=t.column,i=n+1,r=o.index;return"number"==typeof r?i=n+r:"function"==typeof r&&(i=r(n)),e("div",[i])},sortable:!1},expand:{renderHeader:function(e,t){return t.column.label||""},renderCell:function(e,t){var n=t.row,o=t.store,i=["el-table__expand-icon"];return o.states.expandRows.indexOf(n)>-1&&i.push("el-table__expand-icon--expanded"),e("div",{class:i,on:{click:function(e){e.stopPropagation(),o.toggleRowExpansion(n)}}},[e("i",{class:"el-icon el-icon-arrow-right"})])},sortable:!1,resizable:!1,className:"el-table__expand-column"}},s=n(8),c=n(18),u=n.n(c),d=Object.assign||function(e){for(var t=1;t-1})}}},data:function(){return{isSubColumn:!1,columns:[]}},computed:{owner:function(){for(var e=this.$parent;e&&!e.tableId;)e=e.$parent;return e},columnOrTableParent:function(){for(var e=this.$parent;e&&!e.tableId&&!e.columnId;)e=e.$parent;return e},realWidth:function(){return Object(s.l)(this.width)},realMinWidth:function(){return Object(s.k)(this.minWidth)},realAlign:function(){return this.align?"is-"+this.align:null},realHeaderAlign:function(){return this.headerAlign?"is-"+this.headerAlign:this.realAlign}},methods:{getPropsData:function(){for(var e=this,t=arguments.length,n=Array(t),o=0;o2&&void 0!==arguments[2]?arguments[2]:"children",i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",r=function(e){return!(Array.isArray(e)&&e.length)};e.forEach(function(e){if(e[i])return void t(e,null,0);var a=e[o];r(a)||n(e,a,0)})}n.d(t,"b",function(){return p}),n.d(t,"i",function(){return g}),n.d(t,"d",function(){return m}),n.d(t,"e",function(){return v}),n.d(t,"c",function(){return b}),n.d(t,"g",function(){return x}),n.d(t,"f",function(){return y}),n.d(t,"h",function(){return i}),n.d(t,"l",function(){return r}),n.d(t,"k",function(){return a}),n.d(t,"j",function(){return l}),n.d(t,"a",function(){return s}),n.d(t,"m",function(){return c}),n.d(t,"n",function(){return u});var d=n(3),f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},p=function(e){for(var t=e.target;t&&"HTML"!==t.tagName.toUpperCase();){if("TD"===t.tagName.toUpperCase())return t;t=t.parentNode}return null},h=function(e){return null!==e&&"object"===(void 0===e?"undefined":f(e))},g=function(e,t,n,o,i){if(!t&&!o&&(!i||Array.isArray(i)&&!i.length))return e;n="string"==typeof n?"descending"===n?-1:1:n&&n<0?-1:1;var r=o?null:function(n,o){return i?(Array.isArray(i)||(i=[i]),i.map(function(t){return"string"==typeof t?Object(d.getValueByPath)(n,t):t(n,o,e)})):("$key"!==t&&h(n)&&"$value"in n&&(n=n.$value),[h(n)?Object(d.getValueByPath)(n,t):n])},a=function(e,t){if(o)return o(e.value,t.value);for(var n=0,i=e.key.length;nt.key[n])return 1}return 0};return e.map(function(e,t){return{value:e,index:t,key:r?r(e,t):null}}).sort(function(e,t){var o=a(e,t);return o||(o=e.index-t.index),o*n}).map(function(e){return e.value})},m=function(e,t){var n=null;return e.columns.forEach(function(e){e.id===t&&(n=e)}),n},v=function(e,t){for(var n=null,o=0;o2&&void 0!==arguments[2]?arguments[2]:"children",i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",r=function(e){return!(Array.isArray(e)&&e.length)};e.forEach(function(e){if(e[i])return void t(e,null,0);var a=e[o];r(a)||n(e,a,0)})}n.d(t,"b",function(){return p}),n.d(t,"i",function(){return g}),n.d(t,"d",function(){return m}),n.d(t,"e",function(){return v}),n.d(t,"c",function(){return b}),n.d(t,"g",function(){return x}),n.d(t,"f",function(){return y}),n.d(t,"h",function(){return i}),n.d(t,"l",function(){return r}),n.d(t,"k",function(){return a}),n.d(t,"j",function(){return l}),n.d(t,"a",function(){return s}),n.d(t,"m",function(){return c}),n.d(t,"n",function(){return u});var d=n(3),f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},p=function(e){for(var t=e.target;t&&"HTML"!==t.tagName.toUpperCase();){if("TD"===t.tagName.toUpperCase())return t;t=t.parentNode}return null},h=function(e){return null!==e&&"object"===(void 0===e?"undefined":f(e))},g=function(e,t,n,o,i){if(!t&&!o&&(!i||Array.isArray(i)&&!i.length))return e;n="string"==typeof n?"descending"===n?-1:1:n&&n<0?-1:1;var r=o?null:function(n,o){return i?(Array.isArray(i)||(i=[i]),i.map(function(t){return"string"==typeof t?Object(d.getValueByPath)(n,t):t(n,o,e)})):("$key"!==t&&h(n)&&"$value"in n&&(n=n.$value),[h(n)?Object(d.getValueByPath)(n,t):n])},a=function(e,t){if(o)return o(e.value,t.value);for(var n=0,i=e.key.length;nt.key[n])return 1}return 0};return e.map(function(e,t){return{value:e,index:t,key:r?r(e,t):null}}).sort(function(e,t){var o=a(e,t);return o||(o=e.index-t.index),o*n}).map(function(e){return e.value})},m=function(e,t){var n=null;return e.columns.forEach(function(e){e.id===t&&(n=e)}),n},v=function(e,t){for(var n=null,o=0;o1&&void 0!==arguments[1]?arguments[1]:{};if(!e)throw new Error("Table is required.");var n=new D;return n.table=e,n.toggleAllSelection=R()(10,n._toggleAllSelection),Object.keys(t).forEach(function(e){n.states[e]=t[e]}),n}function i(e){var t={};return Object.keys(e).forEach(function(n){var o=e[n],i=void 0;"string"==typeof o?i=function(){return this.store.states[o]}:"function"==typeof o?i=function(){return o.call(this,this.store.states)}:console.error("invalid value type"),i&&(t[n]=i)}),t}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"el-table",class:[{"el-table--fit":e.fit,"el-table--striped":e.stripe,"el-table--border":e.border||e.isGroup,"el-table--hidden":e.isHidden,"el-table--group":e.isGroup,"el-table--fluid-height":e.maxHeight,"el-table--scrollable-x":e.layout.scrollX,"el-table--scrollable-y":e.layout.scrollY,"el-table--enable-row-hover":!e.store.states.isComplex,"el-table--enable-row-transition":0!==(e.store.states.data||[]).length&&(e.store.states.data||[]).length<100},e.tableSize?"el-table--"+e.tableSize:""],on:{mouseleave:function(t){e.handleMouseLeave(t)}}},[n("div",{ref:"hiddenColumns",staticClass:"hidden-columns"},[e._t("default")],2),e.showHeader?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"headerWrapper",staticClass:"el-table__header-wrapper"},[n("table-header",{ref:"tableHeader",style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"default-sort":e.defaultSort}})],1):e._e(),n("div",{ref:"bodyWrapper",staticClass:"el-table__body-wrapper",class:[e.layout.scrollX?"is-scrolling-"+e.scrollPosition:"is-scrolling-none"],style:[e.bodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{context:e.context,store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.data&&0!==e.data.length?e._e():n("div",{ref:"emptyBlock",staticClass:"el-table__empty-block",style:e.emptyBlockStyle},[n("span",{staticClass:"el-table__empty-text"},[e._t("empty",[e._v(e._s(e.emptyText||e.t("el.table.emptyText")))])],2)]),e.$slots.append?n("div",{ref:"appendWrapper",staticClass:"el-table__append-wrapper"},[e._t("append")],2):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"},{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"footerWrapper",staticClass:"el-table__footer-wrapper"},[n("table-footer",{style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,"default-sort":e.defaultSort}})],1):e._e(),e.fixedColumns.length>0?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"fixedWrapper",staticClass:"el-table__fixed",style:[{width:e.layout.fixedWidth?e.layout.fixedWidth+"px":""},e.fixedHeight]},[e.showHeader?n("div",{ref:"fixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[n("table-header",{ref:"fixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,store:e.store}})],1):e._e(),n("div",{ref:"fixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"left",store:e.store,stripe:e.stripe,highlight:e.highlightCurrentRow,"row-class-name":e.rowClassName,"row-style":e.rowStyle}}),e.$slots.append?n("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"fixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[n("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"rightFixedWrapper",staticClass:"el-table__fixed-right",style:[{width:e.layout.rightFixedWidth?e.layout.rightFixedWidth+"px":"",right:e.layout.scrollY?(e.border?e.layout.gutterWidth:e.layout.gutterWidth||0)+"px":""},e.fixedHeight]},[e.showHeader?n("div",{ref:"rightFixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[n("table-header",{ref:"rightFixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,store:e.store}})],1):e._e(),n("div",{ref:"rightFixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"right",store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.$slots.append?n("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"rightFixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[n("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?n("div",{ref:"rightFixedPatch",staticClass:"el-table__fixed-right-patch",style:{width:e.layout.scrollY?e.layout.gutterWidth+"px":"0",height:e.layout.headerHeight+"px"}}):e._e(),n("div",{directives:[{name:"show",rawName:"v-show",value:e.resizeProxyVisible,expression:"resizeProxyVisible"}],ref:"resizeProxy",staticClass:"el-table__column-resize-proxy"})])},l=[];a._withStripped=!0;var s=n(18),c=n.n(s),u=n(43),d=n(16),f=n(46),p=n.n(f),h="undefined"!=typeof navigator&&navigator.userAgent.toLowerCase().indexOf("firefox")>-1,g=function(e,t){e&&e.addEventListener&&e.addEventListener(h?"DOMMouseScroll":"mousewheel",function(e){var n=p()(e);t&&t.apply(this,[e,n])})},m={bind:function(e,t){g(e,t.value)}},v=n(6),b=n.n(v),x=n(11),y=n.n(x),_=n(7),w=n.n(_),k=n(9),S=n.n(k),M=n(8),C={data:function(){return{states:{defaultExpandAll:!1,expandRows:[]}}},methods:{updateExpandRows:function(){var e=this.states,t=e.data,n=void 0===t?[]:t,o=e.rowKey,i=e.defaultExpandAll,r=e.expandRows;if(i)this.states.expandRows=n.slice();else if(o){var a=Object(M.f)(r,o);this.states.expandRows=n.reduce(function(e,t){var n=Object(M.g)(t,o);return a[n]&&e.push(t),e},[])}else this.states.expandRows=[]},toggleRowExpansion:function(e,t){Object(M.m)(this.states.expandRows,e,t)&&(this.table.$emit("expand-change",e,this.states.expandRows.slice()),this.scheduleLayout())},setExpandRowKeys:function(e){this.assertRowKey();var t=this.states,n=t.data,o=t.rowKey,i=Object(M.f)(n,o);this.states.expandRows=e.reduce(function(e,t){var n=i[t];return n&&e.push(n.row),e},[])},isRowExpanded:function(e){var t=this.states,n=t.expandRows,o=void 0===n?[]:n,i=t.rowKey;return i?!!Object(M.f)(o,i)[Object(M.g)(e,i)]:-1!==o.indexOf(e)}}},A=n(3),T={data:function(){return{states:{_currentRowKey:null,currentRow:null}}},methods:{setCurrentRowKey:function(e){this.assertRowKey(),this.states._currentRowKey=e,this.setCurrentRowByKey(e)},restoreCurrentRowKey:function(){this.states._currentRowKey=null},setCurrentRowByKey:function(e){var t=this.states,n=t.data,o=void 0===n?[]:n,i=t.rowKey,r=null;i&&(r=Object(A.arrayFind)(o,function(t){return Object(M.g)(t,i)===e})),t.currentRow=r},updateCurrentRow:function(e){var t=this.states,n=this.table,o=t.currentRow;if(e&&e!==o)return t.currentRow=e,void n.$emit("current-change",e,o);!e&&o&&(t.currentRow=null,n.$emit("current-change",null,o))},updateCurrentRowData:function(){var e=this.states,t=this.table,n=e.rowKey,o=e._currentRowKey,i=e.data||[],r=e.currentRow;if(-1===i.indexOf(r)&&r){if(n){var a=Object(M.g)(r,n);this.setCurrentRowByKey(a)}else e.currentRow=null;null===e.currentRow&&t.$emit("current-change",null,r)}else o&&(this.setCurrentRowByKey(o),this.restoreCurrentRowKey())}}},E=Object.assign||function(e){for(var t=1;t0&&t[0]&&"selection"===t[0].type&&!t[0].fixed&&(t[0].fixed=!0,e.fixedColumns.unshift(t[0]));var n=t.filter(function(e){return!e.fixed});e.originColumns=[].concat(e.fixedColumns).concat(n).concat(e.rightFixedColumns);var o=L(n),i=L(e.fixedColumns),r=L(e.rightFixedColumns);e.leafColumnsLength=o.length,e.fixedLeafColumnsLength=i.length,e.rightFixedLeafColumnsLength=r.length,e.columns=[].concat(i).concat(o).concat(r),e.isComplex=e.fixedColumns.length>0||e.rightFixedColumns.length>0},scheduleLayout:function(e){e&&this.updateColumns(),this.table.debouncedUpdateLayout()},isSelected:function(e){var t=this.states.selection;return(void 0===t?[]:t).indexOf(e)>-1},clearSelection:function(){var e=this.states;e.isAllSelected=!1,e.selection.length&&(e.selection=[],this.table.$emit("selection-change",[]))},cleanSelection:function(){var e=this.states,t=e.data,n=e.rowKey,o=e.selection,i=void 0;if(n){i=[];var r=Object(M.f)(o,n),a=Object(M.f)(t,n);for(var l in r)r.hasOwnProperty(l)&&!a[l]&&i.push(r[l].row)}else i=o.filter(function(e){return-1===t.indexOf(e)});if(i.length){var s=o.filter(function(e){return-1===i.indexOf(e)});e.selection=s,this.table.$emit("selection-change",s.slice())}},toggleRowSelection:function(e,t){var n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];if(Object(M.m)(this.states.selection,e,t)){var o=(this.states.selection||[]).slice();n&&this.table.$emit("select",o,e),this.table.$emit("selection-change",o)}},_toggleAllSelection:function(){var e=this.states,t=e.data,n=void 0===t?[]:t,o=e.selection,i=e.selectOnIndeterminate?!e.isAllSelected:!(e.isAllSelected||o.length);e.isAllSelected=i;var r=!1;n.forEach(function(t,n){e.selectable?e.selectable.call(null,t,n)&&Object(M.m)(o,t,i)&&(r=!0):Object(M.m)(o,t,i)&&(r=!0)}),r&&this.table.$emit("selection-change",o?o.slice():[]),this.table.$emit("select-all",o)},updateSelectionByRowKey:function(){var e=this.states,t=e.selection,n=e.rowKey,o=e.data,i=Object(M.f)(t,n);o.forEach(function(e){var o=Object(M.g)(e,n),r=i[o];r&&(t[r.index]=e)})},updateAllSelected:function(){var e=this.states,t=e.selection,n=e.rowKey,o=e.selectable,i=e.data||[];if(0===i.length)return void(e.isAllSelected=!1);var r=void 0;n&&(r=Object(M.f)(t,n));for(var a=!0,l=0,s=0,c=i.length;s1?n-1:0),i=1;ithis.bodyHeight;return this.scrollY=o,n!==o}return!1},e.prototype.setHeight=function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"height";if(!w.a.prototype.$isServer){var o=this.table.$el;if(e=Object(M.j)(e),this.height=e,!o&&(e||0===e))return w.a.nextTick(function(){return t.setHeight(e,n)});"number"==typeof e?(o.style[n]=e+"px",this.updateElsHeight()):"string"==typeof e&&(o.style[n]=e,this.updateElsHeight())}},e.prototype.setMaxHeight=function(e){this.setHeight(e,"max-height")},e.prototype.getFlattenColumns=function(){var e=[];return this.table.columns.forEach(function(t){t.isColumnGroup?e.push.apply(e,t.columns):e.push(t)}),e},e.prototype.updateElsHeight=function(){var e=this;if(!this.table.$ready)return w.a.nextTick(function(){return e.updateElsHeight()});var t=this.table.$refs,n=t.headerWrapper,o=t.appendWrapper,i=t.footerWrapper;if(this.appendHeight=o?o.offsetHeight:0,!this.showHeader||n){var r=n?n.querySelector(".el-table__header tr"):null,a=this.headerDisplayNone(r),l=this.headerHeight=this.showHeader?n.offsetHeight:0;if(this.showHeader&&!a&&n.offsetWidth>0&&(this.table.columns||[]).length>0&&l<2)return w.a.nextTick(function(){return e.updateElsHeight()});var s=this.tableHeight=this.table.$el.clientHeight,c=this.footerHeight=i?i.offsetHeight:0;null!==this.height&&(this.bodyHeight=s-l-c+(i?1:0)),this.fixedBodyHeight=this.scrollX?this.bodyHeight-this.gutterWidth:this.bodyHeight;var u=!(this.store.states.data&&this.store.states.data.length);this.viewportHeight=this.scrollX?s-(u?0:this.gutterWidth):s,this.updateScrollY(),this.notifyObservers("scrollable")}},e.prototype.headerDisplayNone=function(e){if(!e)return!0;for(var t=e;"DIV"!==t.tagName;){if("none"===getComputedStyle(t).display)return!0;t=t.parentElement}return!1},e.prototype.updateColumnsWidth=function(){if(!w.a.prototype.$isServer){var e=this.fit,t=this.table.$el.clientWidth,n=0,o=this.getFlattenColumns(),i=o.filter(function(e){return"number"!=typeof e.width});if(o.forEach(function(e){"number"==typeof e.width&&e.realWidth&&(e.realWidth=null)}),i.length>0&&e){o.forEach(function(e){n+=e.width||e.minWidth||80});var r=this.scrollY?this.gutterWidth:0;if(n<=t-r){this.scrollX=!1;var a=t-r-n;if(1===i.length)i[0].realWidth=(i[0].minWidth||80)+a;else{var l=i.reduce(function(e,t){return e+(t.minWidth||80)},0),s=a/l,c=0;i.forEach(function(e,t){if(0!==t){var n=Math.floor((e.minWidth||80)*s);c+=n,e.realWidth=(e.minWidth||80)+n}}),i[0].realWidth=(i[0].minWidth||80)+a-c}}else this.scrollX=!0,i.forEach(function(e){e.realWidth=e.minWidth});this.bodyWidth=Math.max(n,t),this.table.resizeState.width=this.bodyWidth}else o.forEach(function(e){e.width||e.minWidth?e.realWidth=e.width||e.minWidth:e.realWidth=80,n+=e.realWidth}),this.scrollX=n>t,this.bodyWidth=n;var u=this.store.states.fixedColumns;if(u.length>0){var d=0;u.forEach(function(e){d+=e.realWidth||e.width}),this.fixedWidth=d}var f=this.store.states.rightFixedColumns;if(f.length>0){var p=0;f.forEach(function(e){p+=e.realWidth||e.width}),this.rightFixedWidth=p}this.notifyObservers("columns")}},e.prototype.addObserver=function(e){this.observers.push(e)},e.prototype.removeObserver=function(e){var t=this.observers.indexOf(e);-1!==t&&this.observers.splice(t,1)},e.prototype.notifyObservers=function(e){var t=this;this.observers.forEach(function(n){switch(e){case"columns":n.onColumnsChange(t);break;case"scrollable":n.onScrollableChange(t);break;default:throw new Error("Table Layout don't have event "+e+".")}})},e}(),V=F,j=n(2),$=n(29),H=n.n($),W={created:function(){this.tableLayout.addObserver(this)},destroyed:function(){this.tableLayout.removeObserver(this)},computed:{tableLayout:function(){var e=this.layout;if(!e&&this.table&&(e=this.table.layout),!e)throw new Error("Can not find table layout.");return e}},mounted:function(){this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout)},updated:function(){this.__updated__||(this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout),this.__updated__=!0)},methods:{onColumnsChange:function(e){var t=this.$el.querySelectorAll("colgroup > col");if(t.length){var n=e.getFlattenColumns(),o={};n.forEach(function(e){o[e.id]=e});for(var i=0,r=t.length;i col[name=gutter]"),n=0,o=t.length;n=this.leftFixedLeafCount:"right"===this.fixed?e=this.columnsCount-this.rightFixedLeafCount},getSpan:function(e,t,n,o){var i=1,r=1,a=this.table.spanMethod;if("function"==typeof a){var l=a({row:e,column:t,rowIndex:n,columnIndex:o});Array.isArray(l)?(i=l[0],r=l[1]):"object"===(void 0===l?"undefined":G(l))&&(i=l.rowspan,r=l.colspan)}return{rowspan:i,colspan:r}},getRowStyle:function(e,t){var n=this.table.rowStyle;return"function"==typeof n?n.call(null,{row:e,rowIndex:t}):n||null},getRowClass:function(e,t){var n=["el-table__row"];this.table.highlightCurrentRow&&e===this.store.states.currentRow&&n.push("current-row"),this.stripe&&t%2==1&&n.push("el-table__row--striped");var o=this.table.rowClassName;return"string"==typeof o?n.push(o):"function"==typeof o&&n.push(o.call(null,{row:e,rowIndex:t})),this.store.states.expandRows.indexOf(e)>-1&&n.push("expanded"),n},getCellStyle:function(e,t,n,o){var i=this.table.cellStyle;return"function"==typeof i?i.call(null,{rowIndex:e,columnIndex:t,row:n,column:o}):i},getCellClass:function(e,t,n,o){var i=[o.id,o.align,o.className];this.isColumnHidden(t)&&i.push("is-hidden");var r=this.table.cellClassName;return"string"==typeof r?i.push(r):"function"==typeof r&&i.push(r.call(null,{rowIndex:e,columnIndex:t,row:n,column:o})),i.join(" ")},getColspanRealWidth:function(e,t,n){return t<1?e[n].realWidth:e.map(function(e){return e.realWidth}).slice(n,n+t).reduce(function(e,t){return e+t},-1)},handleCellMouseEnter:function(e,t){var n=this.table,o=Object(M.b)(e);if(o){var i=Object(M.c)(n,o),r=n.hoverState={cell:o,column:i,row:t};n.$emit("cell-mouse-enter",r.row,r.column,r.cell,e)}var a=e.target.querySelector(".cell");if(Object(j.hasClass)(a,"el-tooltip")&&a.childNodes.length){var l=document.createRange();if(l.setStart(a,0),l.setEnd(a,a.childNodes.length),(l.getBoundingClientRect().width+((parseInt(Object(j.getStyle)(a,"paddingLeft"),10)||0)+(parseInt(Object(j.getStyle)(a,"paddingRight"),10)||0))>a.offsetWidth||a.scrollWidth>a.offsetWidth)&&this.$refs.tooltip){var s=this.$refs.tooltip;this.tooltipContent=o.innerText||o.textContent,s.referenceElm=o,s.$refs.popper&&(s.$refs.popper.style.display="none"),s.doDestroy(),s.setExpectedState(!0),this.activateTooltip(s)}}},handleCellMouseLeave:function(e){var t=this.$refs.tooltip;if(t&&(t.setExpectedState(!1),t.handleClosePopper()),Object(M.b)(e)){var n=this.table.hoverState||{};this.table.$emit("cell-mouse-leave",n.row,n.column,n.cell,e)}},handleMouseEnter:R()(30,function(e){this.store.commit("setHoverRow",e)}),handleMouseLeave:R()(30,function(){this.store.commit("setHoverRow",null)}),handleContextMenu:function(e,t){this.handleEvent(e,t,"contextmenu")},handleDoubleClick:function(e,t){this.handleEvent(e,t,"dblclick")},handleClick:function(e,t){this.store.commit("setCurrentRow",t),this.handleEvent(e,t,"click")},handleEvent:function(e,t,n){var o=this.table,i=Object(M.b)(e),r=void 0;i&&(r=Object(M.c)(o,i))&&o.$emit("cell-"+n,t,r,i,e),o.$emit("row-"+n,t,r,e)},rowRender:function(e,t,n){var o=this,i=this.$createElement,r=this.treeIndent,a=this.columns,l=this.firstDefaultColumnIndex,s=a.map(function(e,t){return o.isColumnHidden(t)}),c=this.getRowClass(e,t),u=!0;return n&&(c.push("el-table__row--level-"+n.level),u=n.display),i("tr",{style:[u?null:{display:"none"},this.getRowStyle(e,t)],class:c,key:this.getKeyOfRow(e,t),on:{dblclick:function(t){return o.handleDoubleClick(t,e)},click:function(t){return o.handleClick(t,e)},contextmenu:function(t){return o.handleContextMenu(t,e)},mouseenter:function(e){return o.handleMouseEnter(t)},mouseleave:this.handleMouseLeave}},[a.map(function(c,u){var d=o.getSpan(e,c,t,u),f=d.rowspan,p=d.colspan;if(!f||!p)return null;var h=U({},c);h.realWidth=o.getColspanRealWidth(a,p,u);var g={store:o.store,_self:o.context||o.table.$vnode.context,column:h,row:e,$index:t};return u===l&&n&&(g.treeNode={indent:n.level*r,level:n.level},"boolean"==typeof n.expanded&&(g.treeNode.expanded=n.expanded,"loading"in n&&(g.treeNode.loading=n.loading),"noLazyChildren"in n&&(g.treeNode.noLazyChildren=n.noLazyChildren))),i("td",{style:o.getCellStyle(t,u,e,c),class:o.getCellClass(t,u,e,c),attrs:{rowspan:f,colspan:p},on:{mouseenter:function(t){return o.handleCellMouseEnter(t,e)},mouseleave:o.handleCellMouseLeave}},[c.renderCell.call(o._renderProxy,o.$createElement,g,s[u])])})])},wrappedRowRender:function(e,t){var n=this,o=this.$createElement,i=this.store,r=i.isRowExpanded,a=i.assertRowKey,l=i.states,s=l.treeData,c=l.lazyTreeNodeMap,u=l.childrenColumnName,d=l.rowKey;if(this.hasExpandColumn&&r(e)){var f=this.table.renderExpanded,p=this.rowRender(e,t);return f?[[p,o("tr",{key:"expanded-row__"+p.key},[o("td",{attrs:{colspan:this.columnsCount},class:"el-table__expanded-cell"},[f(this.$createElement,{row:e,$index:t,store:this.store})])])]]:(console.error("[Element Error]renderExpanded is required."),p)}if(Object.keys(s).length){a();var h=Object(M.g)(e,d),g=s[h],m=null;g&&(m={expanded:g.expanded,level:g.level,display:!0},"boolean"==typeof g.lazy&&("boolean"==typeof g.loaded&&g.loaded&&(m.noLazyChildren=!(g.children&&g.children.length)),m.loading=g.loading));var v=[this.rowRender(e,t,m)];if(g){var b=0;g.display=!0;var x=c[h]||e[u];!function e(o,i){o&&o.length&&i&&o.forEach(function(o){var r={display:i.display&&i.expanded,level:i.level+1},a=Object(M.g)(o,d);if(void 0===a||null===a)throw new Error("for nested data item, row-key is required.");if(g=U({},s[a]),g&&(r.expanded=g.expanded,g.level=g.level||r.level,g.display=!(!g.expanded||!r.display),"boolean"==typeof g.lazy&&("boolean"==typeof g.loaded&&g.loaded&&(r.noLazyChildren=!(g.children&&g.children.length)),r.loading=g.loading)),b++,v.push(n.rowRender(o,t+b,r)),g){var l=c[a]||o[u];e(l,g)}})}(x,g)}return v}return this.rowRender(e,t)}}},Y=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"el-zoom-in-top"}},[e.multiple?n("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[n("div",{staticClass:"el-table-filter__content"},[n("el-scrollbar",{attrs:{"wrap-class":"el-table-filter__wrap"}},[n("el-checkbox-group",{staticClass:"el-table-filter__checkbox-group",model:{value:e.filteredValue,callback:function(t){e.filteredValue=t},expression:"filteredValue"}},e._l(e.filters,function(t){return n("el-checkbox",{key:t.value,attrs:{label:t.value}},[e._v(e._s(t.text))])}),1)],1)],1),n("div",{staticClass:"el-table-filter__bottom"},[n("button",{class:{"is-disabled":0===e.filteredValue.length},attrs:{disabled:0===e.filteredValue.length},on:{click:e.handleConfirm}},[e._v(e._s(e.t("el.table.confirmFilter")))]),n("button",{on:{click:e.handleReset}},[e._v(e._s(e.t("el.table.resetFilter")))])])]):n("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[n("ul",{staticClass:"el-table-filter__list"},[n("li",{staticClass:"el-table-filter__list-item",class:{"is-active":void 0===e.filterValue||null===e.filterValue},on:{click:function(t){e.handleSelect(null)}}},[e._v(e._s(e.t("el.table.clearFilter")))]),e._l(e.filters,function(t){return n("li",{key:t.value,staticClass:"el-table-filter__list-item",class:{"is-active":e.isActive(t)},attrs:{label:t.value},on:{click:function(n){e.handleSelect(t.value)}}},[e._v(e._s(t.text))])})],2)])])},Z=[];Y._withStripped=!0;var X=n(5),K=n.n(X),J=n(15),Q=n(12),ee=n.n(Q),te=[];!w.a.prototype.$isServer&&document.addEventListener("click",function(e){te.forEach(function(t){var n=e.target;t&&t.$el&&(n===t.$el||t.$el.contains(n)||t.handleOutsideClick&&t.handleOutsideClick(e))})});var ne={open:function(e){e&&te.push(e)},close:function(e){-1!==te.indexOf(e)&&te.splice(e,1)}},oe=n(39),ie=n.n(oe),re=n(14),ae=n.n(re),le={name:"ElTableFilterPanel",mixins:[K.a,b.a],directives:{Clickoutside:ee.a},components:{ElCheckbox:c.a,ElCheckboxGroup:ie.a,ElScrollbar:ae.a},props:{placement:{type:String,default:"bottom-end"}},methods:{isActive:function(e){return e.value===this.filterValue},handleOutsideClick:function(){var e=this;setTimeout(function(){e.showPopper=!1},16)},handleConfirm:function(){this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleReset:function(){this.filteredValue=[],this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleSelect:function(e){this.filterValue=e,void 0!==e&&null!==e?this.confirmFilter(this.filteredValue):this.confirmFilter([]),this.handleOutsideClick()},confirmFilter:function(e){this.table.store.commit("filterChange",{column:this.column,values:e}),this.table.store.updateAllSelected()}},data:function(){return{table:null,cell:null,column:null}},computed:{filters:function(){return this.column&&this.column.filters},filterValue:{get:function(){return(this.column.filteredValue||[])[0]},set:function(e){this.filteredValue&&(void 0!==e&&null!==e?this.filteredValue.splice(0,1,e):this.filteredValue.splice(0,1))}},filteredValue:{get:function(){return this.column?this.column.filteredValue||[]:[]},set:function(e){this.column&&(this.column.filteredValue=e)}},multiple:function(){return!this.column||this.column.filterMultiple}},mounted:function(){var e=this;this.popperElm=this.$el,this.referenceElm=this.cell,this.table.bodyWrapper.addEventListener("scroll",function(){e.updatePopper()}),this.$watch("showPopper",function(t){e.column&&(e.column.filterOpened=t),t?ne.open(e):ne.close(e)})},watch:{showPopper:function(e){!0===e&&parseInt(this.popperJS._popper.style.zIndex,10)1;return i&&(this.$parent.isGroup=!0),e("table",{class:"el-table__header",attrs:{cellspacing:"0",cellpadding:"0",border:"0"}},[e("colgroup",[this.columns.map(function(t){return e("col",{attrs:{name:t.id},key:t.id})}),this.hasGutter?e("col",{attrs:{name:"gutter"}}):""]),e("thead",{class:[{"is-group":i,"has-gutter":this.hasGutter}]},[this._l(o,function(n,o){return e("tr",{style:t.getHeaderRowStyle(o),class:t.getHeaderRowClass(o)},[n.map(function(i,r){return e("th",{attrs:{colspan:i.colSpan,rowspan:i.rowSpan},on:{mousemove:function(e){return t.handleMouseMove(e,i)},mouseout:t.handleMouseOut,mousedown:function(e){return t.handleMouseDown(e,i)},click:function(e){return t.handleHeaderClick(e,i)},contextmenu:function(e){return t.handleHeaderContextMenu(e,i)}},style:t.getHeaderCellStyle(o,r,n,i),class:t.getHeaderCellClass(o,r,n,i),key:i.id},[e("div",{class:["cell",i.filteredValue&&i.filteredValue.length>0?"highlight":"",i.labelClassName]},[i.renderHeader?i.renderHeader.call(t._renderProxy,e,{column:i,$index:r,store:t.store,_self:t.$parent.$vnode.context}):i.label,i.sortable?e("span",{class:"caret-wrapper",on:{click:function(e){return t.handleSortClick(e,i)}}},[e("i",{class:"sort-caret ascending",on:{click:function(e){return t.handleSortClick(e,i,"ascending")}}}),e("i",{class:"sort-caret descending",on:{click:function(e){return t.handleSortClick(e,i,"descending")}}})]):"",i.filterable?e("span",{class:"el-table__column-filter-trigger",on:{click:function(e){return t.handleFilterClick(e,i)}}},[e("i",{class:["el-icon-arrow-down",i.filterOpened?"el-icon-arrow-up":""]})]):""])])}),t.hasGutter?e("th",{class:"gutter"}):""])})])])},props:{fixed:String,store:{required:!0},border:Boolean,defaultSort:{type:Object,default:function(){return{prop:"",order:""}}}},components:{ElCheckbox:c.a},computed:fe({table:function(){return this.$parent},hasGutter:function(){return!this.fixed&&this.tableLayout.gutterWidth}},i({columns:"columns",isAllSelected:"isAllSelected",leftFixedLeafCount:"fixedLeafColumnsLength",rightFixedLeafCount:"rightFixedLeafColumnsLength",columnsCount:function(e){return e.columns.length},leftFixedCount:function(e){return e.fixedColumns.length},rightFixedCount:function(e){return e.rightFixedColumns.length}})),created:function(){this.filterPanels={}},mounted:function(){var e=this;this.$nextTick(function(){var t=e.defaultSort,n=t.prop,o=t.order;e.store.commit("sort",{prop:n,order:o,init:!0})})},beforeDestroy:function(){var e=this.filterPanels;for(var t in e)e.hasOwnProperty(t)&&e[t]&&e[t].$destroy(!0)},methods:{isCellHidden:function(e,t){for(var n=0,o=0;o=this.leftFixedLeafCount:"right"===this.fixed?n=this.columnsCount-this.rightFixedLeafCount},getHeaderRowStyle:function(e){var t=this.table.headerRowStyle;return"function"==typeof t?t.call(null,{rowIndex:e}):t},getHeaderRowClass:function(e){var t=[],n=this.table.headerRowClassName;return"string"==typeof n?t.push(n):"function"==typeof n&&t.push(n.call(null,{rowIndex:e})),t.join(" ")},getHeaderCellStyle:function(e,t,n,o){var i=this.table.headerCellStyle;return"function"==typeof i?i.call(null,{rowIndex:e,columnIndex:t,row:n,column:o}):i},getHeaderCellClass:function(e,t,n,o){var i=[o.id,o.order,o.headerAlign,o.className,o.labelClassName];0===e&&this.isCellHidden(t,n)&&i.push("is-hidden"),o.children||i.push("is-leaf"),o.sortable&&i.push("is-sortable");var r=this.table.headerCellClassName;return"string"==typeof r?i.push(r):"function"==typeof r&&i.push(r.call(null,{rowIndex:e,columnIndex:t,row:n,column:o})),i.join(" ")},toggleAllSelection:function(e){e.stopPropagation(),this.store.commit("toggleAllSelection")},handleFilterClick:function(e,t){e.stopPropagation();var n=e.target,o="TH"===n.tagName?n:n.parentNode;if(!Object(j.hasClass)(o,"noclick")){o=o.querySelector(".el-table__column-filter-trigger")||o;var i=this.$parent,r=this.filterPanels[t.id];if(r&&t.filterOpened)return void(r.showPopper=!1);r||(r=new w.a(de),this.filterPanels[t.id]=r,t.filterPlacement&&(r.placement=t.filterPlacement),r.table=i,r.cell=o,r.column=t,!this.$isServer&&r.$mount(document.createElement("div"))),setTimeout(function(){r.showPopper=!0},16)}},handleHeaderClick:function(e,t){!t.filters&&t.sortable?this.handleSortClick(e,t):t.filterable&&!t.sortable&&this.handleFilterClick(e,t),this.$parent.$emit("header-click",t,e)},handleHeaderContextMenu:function(e,t){this.$parent.$emit("header-contextmenu",t,e)},handleMouseDown:function(e,t){var n=this;if(!this.$isServer&&!(t.children&&t.children.length>0)&&this.draggingColumn&&this.border){this.dragging=!0,this.$parent.resizeProxyVisible=!0;var o=this.$parent,i=o.$el,r=i.getBoundingClientRect().left,a=this.$el.querySelector("th."+t.id),l=a.getBoundingClientRect(),s=l.left-r+30;Object(j.addClass)(a,"noclick"),this.dragState={startMouseLeft:e.clientX,startLeft:l.right-r,startColumnLeft:l.left-r,tableLeft:r};var c=o.$refs.resizeProxy;c.style.left=this.dragState.startLeft+"px",document.onselectstart=function(){return!1},document.ondragstart=function(){return!1};var u=function(e){var t=e.clientX-n.dragState.startMouseLeft,o=n.dragState.startLeft+t;c.style.left=Math.max(s,o)+"px"},d=function i(){if(n.dragging){var r=n.dragState,l=r.startColumnLeft,s=r.startLeft,d=parseInt(c.style.left,10),f=d-l;t.width=t.realWidth=f,o.$emit("header-dragend",t.width,s-l,t,e),n.store.scheduleLayout(),document.body.style.cursor="",n.dragging=!1,n.draggingColumn=null,n.dragState={},o.resizeProxyVisible=!1}document.removeEventListener("mousemove",u),document.removeEventListener("mouseup",i),document.onselectstart=null,document.ondragstart=null,setTimeout(function(){Object(j.removeClass)(a,"noclick")},0)};document.addEventListener("mousemove",u),document.addEventListener("mouseup",d)}},handleMouseMove:function(e,t){if(!(t.children&&t.children.length>0)){for(var n=e.target;n&&"TH"!==n.tagName;)n=n.parentNode;if(t&&t.resizable&&!this.dragging&&this.border){var o=n.getBoundingClientRect(),i=document.body.style;o.width>12&&o.right-e.pageX<8?(i.cursor="col-resize",Object(j.hasClass)(n,"is-sortable")&&(n.style.cursor="col-resize"),this.draggingColumn=t):this.dragging||(i.cursor="",Object(j.hasClass)(n,"is-sortable")&&(n.style.cursor="pointer"),this.draggingColumn=null)}}},handleMouseOut:function(){this.$isServer||(document.body.style.cursor="")},toggleOrder:function(e){var t=e.order,n=e.sortOrders;if(""===t)return n[0];var o=n.indexOf(t||null);return n[o>n.length-2?0:o+1]},handleSortClick:function(e,t,n){e.stopPropagation();for(var o=t.order===n?null:n||this.toggleOrder(t),i=e.target;i&&"TH"!==i.tagName;)i=i.parentNode;if(i&&"TH"===i.tagName&&Object(j.hasClass)(i,"noclick"))return void Object(j.removeClass)(i,"noclick");if(t.sortable){var r=this.store.states,a=r.sortProp,l=void 0,s=r.sortingColumn;(s!==t||s===t&&null===s.order)&&(s&&(s.order=null),r.sortingColumn=t,a=t.property),l=t.order=o||null,r.sortProp=a,r.sortOrder=l,this.store.commit("changeSortCondition")}}},data:function(){return{draggingColumn:null,dragging:!1,dragState:{}}}},me=Object.assign||function(e){for(var t=1;t=this.leftFixedLeafCount;if("right"===this.fixed){for(var o=0,i=0;i=this.columnsCount-this.rightFixedCount},getRowClasses:function(e,t){var n=[e.id,e.align,e.labelClassName];return e.className&&n.push(e.className),this.isCellHidden(t,this.columns,e)&&n.push("is-hidden"),e.children||n.push("is-leaf"),n}}},be=Object.assign||function(e){for(var t=1;t0){var o=n.scrollTop;t.pixelY<0&&0!==o&&e.preventDefault(),t.pixelY>0&&n.scrollHeight-n.clientHeight>o&&e.preventDefault(),n.scrollTop+=Math.ceil(t.pixelY/5)}else n.scrollLeft+=Math.ceil(t.pixelX/5)},handleHeaderFooterMousewheel:function(e,t){var n=t.pixelX,o=t.pixelY;Math.abs(n)>=Math.abs(o)&&(this.bodyWrapper.scrollLeft+=t.pixelX/5)},syncPostion:Object(u.throttle)(20,function(){var e=this.bodyWrapper,t=e.scrollLeft,n=e.scrollTop,o=e.offsetWidth,i=e.scrollWidth,r=this.$refs,a=r.headerWrapper,l=r.footerWrapper,s=r.fixedBodyWrapper,c=r.rightFixedBodyWrapper;a&&(a.scrollLeft=t),l&&(l.scrollLeft=t),s&&(s.scrollTop=n),c&&(c.scrollTop=n);var u=i-o-1;this.scrollPosition=t>=u?"right":0===t?"left":"middle"}),bindEvents:function(){this.bodyWrapper.addEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(d.addResizeListener)(this.$el,this.resizeListener)},unbindEvents:function(){this.bodyWrapper.removeEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(d.removeResizeListener)(this.$el,this.resizeListener)},resizeListener:function(){if(this.$ready){var e=!1,t=this.$el,n=this.resizeState,o=n.width,i=n.height,r=t.offsetWidth;o!==r&&(e=!0);var a=t.offsetHeight;(this.height||this.shouldUpdateHeight)&&i!==a&&(e=!0),e&&(this.resizeState.width=r,this.resizeState.height=a,this.doLayout())}},doLayout:function(){this.shouldUpdateHeight&&this.layout.updateElsHeight(),this.layout.updateColumnsWidth()},sort:function(e,t){this.store.commit("sort",{prop:e,order:t})},toggleAllSelection:function(){this.store.commit("toggleAllSelection")}},computed:be({tableSize:function(){return this.size||(this.$ELEMENT||{}).size},bodyWrapper:function(){return this.$refs.bodyWrapper},shouldUpdateHeight:function(){return this.height||this.maxHeight||this.fixedColumns.length>0||this.rightFixedColumns.length>0},bodyWidth:function(){var e=this.layout,t=e.bodyWidth,n=e.scrollY,o=e.gutterWidth;return t?t-(n?o:0)+"px":""},bodyHeight:function(){var e=this.layout,t=e.headerHeight,n=void 0===t?0:t,o=e.bodyHeight,i=e.footerHeight,r=void 0===i?0:i;if(this.height)return{height:o?o+"px":""};if(this.maxHeight){var a=Object(M.j)(this.maxHeight);if("number"==typeof a)return{"max-height":a-r-(this.showHeader?n:0)+"px"}}return{}},fixedBodyHeight:function(){if(this.height)return{height:this.layout.fixedBodyHeight?this.layout.fixedBodyHeight+"px":""};if(this.maxHeight){var e=Object(M.j)(this.maxHeight);if("number"==typeof e)return e=this.layout.scrollX?e-this.layout.gutterWidth:e,this.showHeader&&(e-=this.layout.headerHeight),e-=this.layout.footerHeight,{"max-height":e+"px"}}return{}},fixedHeight:function(){return this.maxHeight?this.showSummary?{bottom:0}:{bottom:this.layout.scrollX&&this.data.length?this.layout.gutterWidth+"px":""}:this.showSummary?{height:this.layout.tableHeight?this.layout.tableHeight+"px":""}:{height:this.layout.viewportHeight?this.layout.viewportHeight+"px":""}},emptyBlockStyle:function(){if(this.data&&this.data.length)return null;var e="100%";return this.layout.appendHeight&&(e="calc(100% - "+this.layout.appendHeight+"px)"),{width:this.bodyWidth,height:e}}},i({selection:"selection",columns:"columns",tableData:"data",fixedColumns:"fixedColumns",rightFixedColumns:"rightFixedColumns"})),watch:{height:{immediate:!0,handler:function(e){this.layout.setHeight(e)}},maxHeight:{immediate:!0,handler:function(e){this.layout.setMaxHeight(e)}},currentRowKey:{immediate:!0,handler:function(e){this.rowKey&&this.store.setCurrentRowKey(e)}},data:{immediate:!0,handler:function(e){this.store.commit("setData",e)}},expandRowKeys:{immediate:!0,handler:function(e){e&&this.store.setExpandRowKeysAdapter(e)}}},created:function(){var e=this;this.tableId="el-table_"+xe++,this.debouncedUpdateLayout=Object(u.debounce)(50,function(){return e.doLayout()})},mounted:function(){var e=this;this.bindEvents(),this.store.updateColumns(),this.doLayout(),this.resizeState={width:this.$el.offsetWidth,height:this.$el.offsetHeight},this.store.states.columns.forEach(function(t){t.filteredValue&&t.filteredValue.length&&e.store.commit("filterChange",{column:t,values:t.filteredValue,silent:!0})}),this.$ready=!0},destroyed:function(){this.unbindEvents()},data:function(){var e=this.treeProps,t=e.hasChildren,n=void 0===t?"hasChildren":t,i=e.children,r=void 0===i?"children":i;return this.store=o(this,{rowKey:this.rowKey,defaultExpandAll:this.defaultExpandAll,selectOnIndeterminate:this.selectOnIndeterminate,indent:this.indent,lazy:this.lazy,lazyColumnIdentifier:n,childrenColumnName:r}),{layout:new V({store:this.store,table:this,fit:this.fit,showHeader:this.showHeader}),isHidden:!1,renderExpanded:null,resizeProxyVisible:!1,resizeState:{width:null,height:null},isGroup:!1,scrollPosition:"left"}}},_e=ye,we=Object(ce.a)(_e,a,l,!1,null,null,null);we.options.__file="packages/table/src/table.vue";var ke=we.exports;ke.install=function(e){e.component(ke.name,ke)},t.default=ke}])},function(e,t){e.exports=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var i in e)t.d(o,i,function(t){return e[t]}.bind(null,i));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=124)}({0:function(e,t,n){"use strict";function o(e,t,n,o,i,r,a,l){var s="function"==typeof e?e.options:e;t&&(s.render=t,s.staticRenderFns=n,s._compiled=!0),o&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var c;if(a?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},s._ssrRegister=c):i&&(c=l?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(s.functional){s._injectStyles=c;var u=s.render;s.render=function(e,t){return c.call(t),u(e,t)}}else{var d=s.beforeCreate;s.beforeCreate=d?[].concat(d,c):[c]}return{exports:e,options:s}}n.d(t,"a",function(){return o})},124:function(e,t,n){"use strict";n.r(t);var o={name:"ElTag",props:{text:String,closable:Boolean,type:String,hit:Boolean,disableTransitions:Boolean,color:String,size:String,effect:{type:String,default:"light",validator:function(e){return-1!==["dark","light","plain"].indexOf(e)}}},methods:{handleClose:function(e){e.stopPropagation(),this.$emit("close",e)},handleClick:function(e){this.$emit("click",e)}},computed:{tagSize:function(){return this.size||(this.$ELEMENT||{}).size}},render:function(e){var t=this.type,n=this.tagSize,o=this.hit,i=this.effect,r=["el-tag",t?"el-tag--"+t:"",n?"el-tag--"+n:"",i?"el-tag--"+i:"",o&&"is-hit"],a=e("span",{class:r,style:{backgroundColor:this.color},on:{click:this.handleClick}},[this.$slots.default,this.closable&&e("i",{class:"el-tag__close el-icon-close",on:{click:this.handleClose}})]);return this.disableTransitions?a:e("transition",{attrs:{name:"el-zoom-in-center"}},[a])}},i=o,r=n(0),a=Object(r.a)(i,void 0,void 0,!1,null,null,null);a.options.__file="packages/tag/src/tag.vue";var l=a.exports;l.install=function(e){e.component(l.name,l)},t.default=l}})},function(e,t,n){var o=n(342);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(343);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(344);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(345);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(346);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(347);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(348);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(349);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(350);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(351);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(352);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(353);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(354);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){var o=n(355);"string"==typeof o&&(o=[[e.i,o,""]]),n(19)(o,{}),o.locals&&(e.exports=o.locals)},function(e,t,n){"use strict";var o=n(641),i=n(227),r=(n(639),n(30)),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t){!function(e){"use strict";function t(e){if("string"!=typeof e&&(e=String(e)),/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(e))throw new TypeError("Invalid character in header field name");return e.toLowerCase()}function n(e){return"string"!=typeof e&&(e=String(e)),e}function o(e){var t={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return v.iterable&&(t[Symbol.iterator]=function(){return t}),t}function i(e){this.map={},e instanceof i?e.forEach(function(e,t){this.append(t,e)},this):Array.isArray(e)?e.forEach(function(e){this.append(e[0],e[1])},this):e&&Object.getOwnPropertyNames(e).forEach(function(t){this.append(t,e[t])},this)}function r(e){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}function a(e){return new Promise(function(t,n){e.onload=function(){t(e.result)},e.onerror=function(){n(e.error)}})}function l(e){var t=new FileReader,n=a(t);return t.readAsArrayBuffer(e),n}function s(e){var t=new FileReader,n=a(t);return t.readAsText(e),n}function c(e){for(var t=new Uint8Array(e),n=new Array(t.length),o=0;o-1?t:e}function p(e,t){t=t||{};var n=t.body;if(e instanceof p){if(e.bodyUsed)throw new TypeError("Already read");this.url=e.url,this.credentials=e.credentials,t.headers||(this.headers=new i(e.headers)),this.method=e.method,this.mode=e.mode,n||null==e._bodyInit||(n=e._bodyInit,e.bodyUsed=!0)}else this.url=String(e);if(this.credentials=t.credentials||this.credentials||"omit",!t.headers&&this.headers||(this.headers=new i(t.headers)),this.method=f(t.method||this.method||"GET"),this.mode=t.mode||this.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(n)}function h(e){var t=new FormData;return e.trim().split("&").forEach(function(e){if(e){var n=e.split("="),o=n.shift().replace(/\+/g," "),i=n.join("=").replace(/\+/g," ");t.append(decodeURIComponent(o),decodeURIComponent(i))}}),t}function g(e){var t=new i;return e.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(e){var n=e.split(":"),o=n.shift().trim();if(o){var i=n.join(":").trim();t.append(o,i)}}),t}function m(e,t){t||(t={}),this.type="default",this.status=void 0===t.status?200:t.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in t?t.statusText:"OK",this.headers=new i(t.headers),this.url=t.url||"",this._initBody(e)}if(!e.fetch){var v={searchParams:"URLSearchParams"in e,iterable:"Symbol"in e&&"iterator"in Symbol,blob:"FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(e){return!1}}(),formData:"FormData"in e,arrayBuffer:"ArrayBuffer"in e};if(v.arrayBuffer)var b=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],x=function(e){return e&&DataView.prototype.isPrototypeOf(e)},y=ArrayBuffer.isView||function(e){return e&&b.indexOf(Object.prototype.toString.call(e))>-1};i.prototype.append=function(e,o){e=t(e),o=n(o);var i=this.map[e];this.map[e]=i?i+","+o:o},i.prototype.delete=function(e){delete this.map[t(e)]},i.prototype.get=function(e){return e=t(e),this.has(e)?this.map[e]:null},i.prototype.has=function(e){return this.map.hasOwnProperty(t(e))},i.prototype.set=function(e,o){this.map[t(e)]=n(o)},i.prototype.forEach=function(e,t){for(var n in this.map)this.map.hasOwnProperty(n)&&e.call(t,this.map[n],n,this)},i.prototype.keys=function(){var e=[];return this.forEach(function(t,n){e.push(n)}),o(e)},i.prototype.values=function(){var e=[];return this.forEach(function(t){e.push(t)}),o(e)},i.prototype.entries=function(){var e=[];return this.forEach(function(t,n){e.push([n,t])}),o(e)},v.iterable&&(i.prototype[Symbol.iterator]=i.prototype.entries);var _=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];p.prototype.clone=function(){return new p(this,{body:this._bodyInit})},d.call(p.prototype),d.call(m.prototype),m.prototype.clone=function(){return new m(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new i(this.headers),url:this.url})},m.error=function(){var e=new m(null,{status:0,statusText:""});return e.type="error",e};var w=[301,302,303,307,308];m.redirect=function(e,t){if(-1===w.indexOf(t))throw new RangeError("Invalid status code");return new m(null,{status:t,headers:{location:e}})},e.Headers=i,e.Request=p,e.Response=m,e.fetch=function(e,t){return new Promise(function(n,o){var i=new p(e,t),r=new XMLHttpRequest;r.onload=function(){var e={status:r.status,statusText:r.statusText,headers:g(r.getAllResponseHeaders()||"")};e.url="responseURL"in r?r.responseURL:e.headers.get("X-Request-URL");var t="response"in r?r.response:r.responseText;n(new m(t,e))},r.onerror=function(){o(new TypeError("Network request failed"))},r.ontimeout=function(){o(new TypeError("Network request failed"))},r.open(i.method,i.url,!0),"include"===i.credentials?r.withCredentials=!0:"omit"===i.credentials&&(r.withCredentials=!1),"responseType"in r&&v.blob&&(r.responseType="blob"),i.headers.forEach(function(e,t){r.setRequestHeader(t,e)}),r.send(void 0===i._bodyInit?null:i._bodyInit)})},e.fetch.polyfill=!0}}("undefined"!=typeof self?self:this)},function(e,t,n){"use strict";function o(e){this.rules=null,this._messages=u.a,this.define(e)}Object.defineProperty(t,"__esModule",{value:!0});var i=n(152),r=n.n(i),a=n(75),l=n.n(a),s=n(11),c=n(296),u=n(285);o.prototype={messages:function(e){return e&&(this._messages=n.i(s.a)(n.i(u.b)(),e)),this._messages},define:function(e){if(!e)throw new Error("Cannot configure a schema with no rules");if("object"!==(void 0===e?"undefined":l()(e))||Array.isArray(e))throw new Error("Rules must be an object");this.rules={};var t=void 0,n=void 0;for(t in e)e.hasOwnProperty(t)&&(n=e[t],this.rules[t]=Array.isArray(n)?n:[n])},validate:function(e){function t(e){var t=void 0,n=void 0,o=[],i={};for(t=0;t1&&void 0!==arguments[1]?arguments[1]:{},c=arguments[2],d=e,f=a,p=c;if("function"==typeof f&&(p=f,f={}),!this.rules||0===Object.keys(this.rules).length)return void(p&&p());if(f.messages){var h=this.messages();h===u.a&&(h=n.i(u.b)()),n.i(s.a)(h,f.messages),f.messages=h}else f.messages=this.messages();var g=void 0,m=void 0,v={};(f.keys||Object.keys(this.rules)).forEach(function(t){g=i.rules[t],m=d[t],g.forEach(function(n){var o=n;"function"==typeof o.transform&&(d===e&&(d=r()({},d)),m=d[t]=o.transform(m)),o="function"==typeof o?{validator:o}:r()({},o),o.validator=i.getValidationMethod(o),o.field=t,o.fullField=o.fullField||t,o.type=i.getType(o),o.validator&&(v[t]=v[t]||[],v[t].push({rule:o,value:m,source:d,field:t}))})});var b={};n.i(s.b)(v,f,function(e,t){function i(e,t){return r()({},t,{fullField:c.fullField+"."+e})}function a(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],l=a;if(Array.isArray(l)||(l=[l]),l.length&&n.i(s.c)("async-validator:",l),l.length&&c.message&&(l=[].concat(c.message)),l=l.map(n.i(s.d)(c)),f.first&&l.length)return b[c.field]=1,t(l);if(u){if(c.required&&!e.value)return l=c.message?[].concat(c.message).map(n.i(s.d)(c)):f.error?[f.error(c,n.i(s.e)(f.messages.required,c.field))]:[],t(l);var d={};if(c.defaultField)for(var p in e.value)e.value.hasOwnProperty(p)&&(d[p]=c.defaultField);d=r()({},d,e.rule.fields);for(var h in d)if(d.hasOwnProperty(h)){var g=Array.isArray(d[h])?d[h]:[d[h]];d[h]=g.map(i.bind(null,h))}var m=new o(d);m.messages(f.messages),e.rule.options&&(e.rule.options.messages=f.messages,e.rule.options.error=f.error),m.validate(e.value,e.rule.options||f,function(e){t(e&&e.length?l.concat(e):e)})}else t(l)}var c=e.rule,u=!("object"!==c.type&&"array"!==c.type||"object"!==l()(c.fields)&&"object"!==l()(c.defaultField));u=u&&(c.required||!c.required&&e.value),c.field=e.field;var d=c.validator(c,e.value,a,e.source,f);d&&d.then&&d.then(function(){return a()},function(e){return a(e)})},function(e){t(e)})},getType:function(e){if(void 0===e.type&&e.pattern instanceof RegExp&&(e.type="pattern"),"function"!=typeof e.validator&&e.type&&!c.a.hasOwnProperty(e.type))throw new Error(n.i(s.e)("Unknown rule type %s",e.type));return e.type||"string"},getValidationMethod:function(e){if("function"==typeof e.validator)return e.validator;var t=Object.keys(e),n=t.indexOf("message");return-1!==n&&t.splice(n,1),1===t.length&&"required"===t[0]?c.a.required:c.a[this.getType(e)]||!1}},o.register=function(e,t){if("function"!=typeof t)throw new Error("Cannot register a validator by type, validator is not a function");c.a[e]=t},o.messages=u.a,t.default=o},function(e,t,n){"use strict";function o(){return{default:"Validation error on field %s",required:"%s is required",enum:"%s must be one of %s",whitespace:"%s cannot be empty",date:{format:"%s date %s is invalid for format %s",parse:"%s date could not be parsed, %s is invalid ",invalid:"%s date %s is invalid"},types:{string:"%s is not a %s",method:"%s is not a %s (function)",array:"%s is not an %s",object:"%s is not an %s",number:"%s is not a %s",date:"%s is not a %s",boolean:"%s is not a %s",integer:"%s is not an %s",float:"%s is not a %s",regexp:"%s is not a valid %s",email:"%s is not a valid %s",url:"%s is not a valid %s",hex:"%s is not a valid %s"},string:{len:"%s must be exactly %s characters",min:"%s must be at least %s characters",max:"%s cannot be longer than %s characters",range:"%s must be between %s and %s characters"},number:{len:"%s must equal %s",min:"%s cannot be less than %s",max:"%s cannot be greater than %s",range:"%s must be between %s and %s"},array:{len:"%s must be exactly %s in length",min:"%s cannot be less than %s in length",max:"%s cannot be greater than %s in length",range:"%s must be between %s and %s in length"},pattern:{mismatch:"%s value %s does not match pattern %s"},clone:function(){var e=JSON.parse(JSON.stringify(this));return e.clone=this.clone,e}}}t.b=o,n.d(t,"a",function(){return i});var i=o()},function(e,t,n){"use strict";function o(e,t,n,o,a){e[r]=Array.isArray(e[r])?e[r]:[],-1===e[r].indexOf(t)&&o.push(i.e(a.messages[r],e.fullField,e[r].join(", ")))}var i=n(11),r="enum";t.a=o},function(e,t,n){"use strict";function o(e,t,n,o,r){if(e.pattern)if(e.pattern instanceof RegExp)e.pattern.lastIndex=0,e.pattern.test(t)||o.push(i.e(r.messages.pattern.mismatch,e.fullField,t,e.pattern));else if("string"==typeof e.pattern){var a=new RegExp(e.pattern);a.test(t)||o.push(i.e(r.messages.pattern.mismatch,e.fullField,t,e.pattern))}}var i=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,n,o,r){var a="number"==typeof e.len,l="number"==typeof e.min,s="number"==typeof e.max,c=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,u=t,d=null,f="number"==typeof t,p="string"==typeof t,h=Array.isArray(t);if(f?d="number":p?d="string":h&&(d="array"),!d)return!1;h&&(u=t.length),p&&(u=t.replace(c,"_").length),a?u!==e.len&&o.push(i.e(r.messages[d].len,e.fullField,e.len)):l&&!s&&ue.max?o.push(i.e(r.messages[d].max,e.fullField,e.max)):l&&s&&(ue.max)&&o.push(i.e(r.messages[d].range,e.fullField,e.min,e.max))}var i=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,i,s){if(e.required&&void 0===t)return void n.i(l.a)(e,t,o,i,s);var u=["integer","float","array","regexp","object","method","email","number","date","url","hex"],d=e.type;u.indexOf(d)>-1?c[d](t)||i.push(a.e(s.messages.types[d],e.fullField,e.type)):d&&(void 0===t?"undefined":r()(t))!==e.type&&i.push(a.e(s.messages.types[d],e.fullField,e.type))}var i=n(75),r=n.n(i),a=n(11),l=n(141),s={email:/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,url:new RegExp("^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$","i"),hex:/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i},c={integer:function(e){return c.number(e)&&parseInt(e,10)===e},float:function(e){return c.number(e)&&!c.integer(e)},array:function(e){return Array.isArray(e)},regexp:function(e){if(e instanceof RegExp)return!0;try{return!!new RegExp(e)}catch(e){return!1}},date:function(e){return"function"==typeof e.getTime&&"function"==typeof e.getMonth&&"function"==typeof e.getYear},number:function(e){return!isNaN(e)&&"number"==typeof e},object:function(e){return"object"===(void 0===e?"undefined":r()(e))&&!c.array(e)},method:function(e){return"function"==typeof e},email:function(e){return"string"==typeof e&&!!e.match(s.email)&&e.length<255},url:function(e){return"string"==typeof e&&!!e.match(s.url)},hex:function(e){return"string"==typeof e&&!!e.match(s.hex)}};t.a=o},function(e,t,n){"use strict";function o(e,t,n,o,r){(/^\s+$/.test(t)||""===t)&&o.push(i.e(r.messages.whitespace,e.fullField))}var i=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t,"array")&&!e.required)return o();i.a.required(e,t,a,s,l,"array"),n.i(r.f)(t,"array")||(i.a.type(e,t,a,s,l),i.a.range(e,t,a,s,l))}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(i.f)(t)&&!e.required)return o();r.a.required(e,t,a,s,l),void 0!==t&&r.a.type(e,t,a,s,l)}o(s)}var i=n(11),r=n(20);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();if(i.a.required(e,t,a,s,l),!n.i(r.f)(t)){var c=void 0;c="number"==typeof t?new Date(t):t,i.a.type(e,c,a,s,l),c&&i.a.range(e,c.getTime(),a,s,l)}}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,l,s){var c=[];if(e.required||!e.required&&l.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,l,c,s),t&&i.a[a](e,t,l,c,s)}o(c)}var i=n(20),r=n(11),a="enum";t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,a,s,l),void 0!==t&&(i.a.type(e,t,a,s,l),i.a.range(e,t,a,s,l))}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";var o=n(304),i=n(298),r=n(299),a=n(292),l=n(302),s=n(297),c=n(295),u=n(291),d=n(300),f=n(294),p=n(301),h=n(293),g=n(303),m=n(305);t.a={string:o.a,method:i.a,number:r.a,boolean:a.a,regexp:l.a,integer:s.a,float:c.a,array:u.a,object:d.a,enum:f.a,pattern:p.a,date:h.a,url:m.a,hex:m.a,email:m.a,required:g.a}},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,a,s,l),void 0!==t&&(i.a.type(e,t,a,s,l),i.a.range(e,t,a,s,l))}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,a,s,l),void 0!==t&&i.a.type(e,t,a,s,l)}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,a,s,l),void 0!==t&&(i.a.type(e,t,a,s,l),i.a.range(e,t,a,s,l))}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,a,s,l),void 0!==t&&i.a.type(e,t,a,s,l)}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t,"string")&&!e.required)return o();i.a.required(e,t,a,s,l),n.i(r.f)(t,"string")||i.a.pattern(e,t,a,s,l)}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t)&&!e.required)return o();i.a.required(e,t,a,s,l),n.i(r.f)(t)||i.a.type(e,t,a,s,l)}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,n,o,i){var l=[],s=Array.isArray(t)?"array":void 0===t?"undefined":r()(t);a.a.required(e,t,o,l,i,s),n(l)}var i=n(75),r=n.n(i),a=n(20);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t,"string")&&!e.required)return o();i.a.required(e,t,a,s,l,"string"),n.i(r.f)(t,"string")||(i.a.type(e,t,a,s,l),i.a.range(e,t,a,s,l),i.a.pattern(e,t,a,s,l),!0===e.whitespace&&i.a.whitespace(e,t,a,s,l))}o(s)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";function o(e,t,o,a,l){var s=e.type,c=[];if(e.required||!e.required&&a.hasOwnProperty(e.field)){if(n.i(r.f)(t,s)&&!e.required)return o();i.a.required(e,t,a,c,l,s),n.i(r.f)(t,s)||i.a.type(e,t,a,c,l)}o(c)}var i=n(20),r=n(11);t.a=o},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(280),i=(n.n(o),n(9)),r=(n.n(i),n(267)),a=n.n(r),l=n(273),s=(n.n(l),n(260)),c=n.n(s),u=n(277),d=(n.n(u),n(264)),f=n.n(d),p=n(274),h=(n.n(p),n(261)),g=n.n(h),m=n(275),v=(n.n(m),n(262)),b=n.n(v),x=n(278),y=(n.n(x),n(265)),_=n.n(y),w=n(279),k=(n.n(w),n(266)),S=n.n(k),M=n(269),C=(n.n(M),n(257)),A=n.n(C),T=n(276),E=(n.n(T),n(263)),I=n.n(E),O=n(270),L=(n.n(O),n(258)),P=n.n(L),D=n(271),z=(n.n(D),n(259)),R=n.n(z),N=n(268),B=(n.n(N),n(256)),F=n.n(B),V=n(21),j=n(140),$=n.n(j),H=n(139),W=n.n(H),G=n(272),U=(n.n(G),n(281)),q=(n.n(U),n(282)),Y=n(255),Z=n(283);n.n(Z),W.a.use($.a),V.default.use(F.a),V.default.use(R.a),V.default.use(P.a),V.default.use(I.a),V.default.use(A.a),V.default.use(S.a),V.default.use(_.a),V.default.use(b.a),V.default.use(g.a),V.default.use(f.a),V.default.use(c.a),V.default.use(a.a),V.default.config.productionTip=!1,new V.default({el:"#app",router:Y.a,template:"",components:{App:q.a}})},function(e,t,n){e.exports={default:n(312),__esModule:!0}},function(e,t,n){e.exports={default:n(313),__esModule:!0}},function(e,t,n){e.exports={default:n(314),__esModule:!0}},function(e,t,n){"use strict";function o(e){var t=e.length;if(t%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function i(e){var t=o(e),n=t[0],i=t[1];return 3*(n+i)/4-i}function r(e,t,n){return 3*(t+n)/4-n}function a(e){var t,n,i=o(e),a=i[0],l=i[1],s=new f(r(e,a,l)),c=0,u=l>0?a-4:a;for(n=0;n>16&255,s[c++]=t>>8&255,s[c++]=255&t;return 2===l&&(t=d[e.charCodeAt(n)]<<2|d[e.charCodeAt(n+1)]>>4,s[c++]=255&t),1===l&&(t=d[e.charCodeAt(n)]<<10|d[e.charCodeAt(n+1)]<<4|d[e.charCodeAt(n+2)]>>2,s[c++]=t>>8&255,s[c++]=255&t),s}function l(e){return u[e>>18&63]+u[e>>12&63]+u[e>>6&63]+u[63&e]}function s(e,t,n){for(var o,i=[],r=t;ra?a:r+16383));return 1===o?(t=e[n-1],i.push(u[t>>2]+u[t<<4&63]+"==")):2===o&&(t=(e[n-2]<<8)+e[n-1],i.push(u[t>>10]+u[t>>4&63]+u[t<<2&63]+"=")),i.join("")}t.byteLength=i,t.toByteArray=a,t.fromByteArray=c;for(var u=[],d=[],f="undefined"!=typeof Uint8Array?Uint8Array:Array,p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",h=0,g=p.length;h=o())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+o().toString(16)+" bytes");return 0|e}function g(e){return+e!=e&&(e=0),r.alloc(+e)}function m(e,t){if(r.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var o=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return W(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return q(e).length;default:if(o)return W(e).length;t=(""+t).toLowerCase(),o=!0}}function v(e,t,n){var o=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if(n>>>=0,t>>>=0,n<=t)return"";for(e||(e="utf8");;)switch(e){case"hex":return L(this,t,n);case"utf8":case"utf-8":return T(this,t,n);case"ascii":return I(this,t,n);case"latin1":case"binary":return O(this,t,n);case"base64":return A(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(o)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),o=!0}}function b(e,t,n){var o=e[t];e[t]=e[n],e[n]=o}function x(e,t,n,o,i){if(0===e.length)return-1;if("string"==typeof n?(o=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=i?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(i)return-1;n=e.length-1}else if(n<0){if(!i)return-1;n=0}if("string"==typeof t&&(t=r.from(t,o)),r.isBuffer(t))return 0===t.length?-1:y(e,t,n,o,i);if("number"==typeof t)return t&=255,r.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):y(e,[t],n,o,i);throw new TypeError("val must be string, number or Buffer")}function y(e,t,n,o,i){function r(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}var a=1,l=e.length,s=t.length;if(void 0!==o&&("ucs2"===(o=String(o).toLowerCase())||"ucs-2"===o||"utf16le"===o||"utf-16le"===o)){if(e.length<2||t.length<2)return-1;a=2,l/=2,s/=2,n/=2}var c;if(i){var u=-1;for(c=n;cl&&(n=l-s),c=n;c>=0;c--){for(var d=!0,f=0;fi&&(o=i):o=i;var r=t.length;if(r%2!=0)throw new TypeError("Invalid hex string");o>r/2&&(o=r/2);for(var a=0;a239?4:r>223?3:r>191?2:1;if(i+l<=n){var s,c,u,d;switch(l){case 1:r<128&&(a=r);break;case 2:128==(192&(s=e[i+1]))&&(d=(31&r)<<6|63&s)>127&&(a=d);break;case 3:s=e[i+1],c=e[i+2],128==(192&s)&&128==(192&c)&&(d=(15&r)<<12|(63&s)<<6|63&c)>2047&&(d<55296||d>57343)&&(a=d);break;case 4:s=e[i+1],c=e[i+2],u=e[i+3],128==(192&s)&&128==(192&c)&&128==(192&u)&&(d=(15&r)<<18|(63&s)<<12|(63&c)<<6|63&u)>65535&&d<1114112&&(a=d)}}null===a?(a=65533,l=1):a>65535&&(a-=65536,o.push(a>>>10&1023|55296),a=56320|1023&a),o.push(a),i+=l}return E(o)}function E(e){var t=e.length;if(t<=Q)return String.fromCharCode.apply(String,e);for(var n="",o=0;oo)&&(n=o);for(var i="",r=t;rn)throw new RangeError("Trying to access beyond buffer length")}function z(e,t,n,o,i,a){if(!r.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||te.length)throw new RangeError("Index out of range")}function R(e,t,n,o){t<0&&(t=65535+t+1);for(var i=0,r=Math.min(e.length-n,2);i>>8*(o?i:1-i)}function N(e,t,n,o){t<0&&(t=4294967295+t+1);for(var i=0,r=Math.min(e.length-n,4);i>>8*(o?i:3-i)&255}function B(e,t,n,o,i,r){if(n+o>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function F(e,t,n,o,i){return i||B(e,t,n,4,3.4028234663852886e38,-3.4028234663852886e38),K.write(e,t,n,o,23,4),n+4}function V(e,t,n,o,i){return i||B(e,t,n,8,1.7976931348623157e308,-1.7976931348623157e308),K.write(e,t,n,o,52,8),n+8}function j(e){if(e=$(e).replace(ee,""),e.length<2)return"";for(;e.length%4!=0;)e+="=";return e}function $(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}function H(e){return e<16?"0"+e.toString(16):e.toString(16)}function W(e,t){t=t||1/0;for(var n,o=e.length,i=null,r=[],a=0;a55295&&n<57344){if(!i){if(n>56319){(t-=3)>-1&&r.push(239,191,189);continue}if(a+1===o){(t-=3)>-1&&r.push(239,191,189);continue}i=n;continue}if(n<56320){(t-=3)>-1&&r.push(239,191,189),i=n;continue}n=65536+(i-55296<<10|n-56320)}else i&&(t-=3)>-1&&r.push(239,191,189);if(i=null,n<128){if((t-=1)<0)break;r.push(n)}else if(n<2048){if((t-=2)<0)break;r.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;r.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;r.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return r}function G(e){for(var t=[],n=0;n>8,i=n%256,r.push(i),r.push(o);return r}function q(e){return X.toByteArray(j(e))}function Y(e,t,n,o){for(var i=0;i=t.length||i>=e.length);++i)t[i+n]=e[i];return i}function Z(e){return e!==e}var X=n(310),K=n(618),J=n(619);t.Buffer=r,t.SlowBuffer=g,t.INSPECT_MAX_BYTES=50,r.TYPED_ARRAY_SUPPORT=void 0!==e.TYPED_ARRAY_SUPPORT?e.TYPED_ARRAY_SUPPORT:function(){try{var e=new Uint8Array(1);return e.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(e){return!1}}(),t.kMaxLength=o(),r.poolSize=8192,r._augment=function(e){return e.__proto__=r.prototype,e},r.from=function(e,t,n){return a(null,e,t,n)},r.TYPED_ARRAY_SUPPORT&&(r.prototype.__proto__=Uint8Array.prototype,r.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&r[Symbol.species]===r&&Object.defineProperty(r,Symbol.species,{value:null,configurable:!0})),r.alloc=function(e,t,n){return s(null,e,t,n)},r.allocUnsafe=function(e){return c(null,e)},r.allocUnsafeSlow=function(e){return c(null,e)},r.isBuffer=function(e){return!(null==e||!e._isBuffer)},r.compare=function(e,t){if(!r.isBuffer(e)||!r.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,o=t.length,i=0,a=Math.min(n,o);i0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},r.prototype.compare=function(e,t,n,o,i){if(!r.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===o&&(o=0),void 0===i&&(i=this.length),t<0||n>e.length||o<0||i>this.length)throw new RangeError("out of range index");if(o>=i&&t>=n)return 0;if(o>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,o>>>=0,i>>>=0,this===e)return 0;for(var a=i-o,l=n-t,s=Math.min(a,l),c=this.slice(o,i),u=e.slice(t,n),d=0;di)&&(n=i),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");o||(o="utf8");for(var r=!1;;)switch(o){case"hex":return _(this,e,t,n);case"utf8":case"utf-8":return w(this,e,t,n);case"ascii":return k(this,e,t,n);case"latin1":case"binary":return S(this,e,t,n);case"base64":return M(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,e,t,n);default:if(r)throw new TypeError("Unknown encoding: "+o);o=(""+o).toLowerCase(),r=!0}},r.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var Q=4096;r.prototype.slice=function(e,t){var n=this.length;e=~~e,t=void 0===t?n:~~t,e<0?(e+=n)<0&&(e=0):e>n&&(e=n),t<0?(t+=n)<0&&(t=0):t>n&&(t=n),t0&&(i*=256);)o+=this[e+--t]*i;return o},r.prototype.readUInt8=function(e,t){return t||D(e,1,this.length),this[e]},r.prototype.readUInt16LE=function(e,t){return t||D(e,2,this.length),this[e]|this[e+1]<<8},r.prototype.readUInt16BE=function(e,t){return t||D(e,2,this.length),this[e]<<8|this[e+1]},r.prototype.readUInt32LE=function(e,t){return t||D(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},r.prototype.readUInt32BE=function(e,t){return t||D(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},r.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var o=this[e],i=1,r=0;++r=i&&(o-=Math.pow(2,8*t)),o},r.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var o=t,i=1,r=this[e+--o];o>0&&(i*=256);)r+=this[e+--o]*i;return i*=128,r>=i&&(r-=Math.pow(2,8*t)),r},r.prototype.readInt8=function(e,t){return t||D(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},r.prototype.readInt16LE=function(e,t){t||D(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},r.prototype.readInt16BE=function(e,t){t||D(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},r.prototype.readInt32LE=function(e,t){return t||D(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},r.prototype.readInt32BE=function(e,t){return t||D(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},r.prototype.readFloatLE=function(e,t){return t||D(e,4,this.length),K.read(this,e,!0,23,4)},r.prototype.readFloatBE=function(e,t){return t||D(e,4,this.length),K.read(this,e,!1,23,4)},r.prototype.readDoubleLE=function(e,t){return t||D(e,8,this.length),K.read(this,e,!0,52,8)},r.prototype.readDoubleBE=function(e,t){return t||D(e,8,this.length),K.read(this,e,!1,52,8)},r.prototype.writeUIntLE=function(e,t,n,o){e=+e,t|=0,n|=0,o||z(this,e,t,n,Math.pow(2,8*n)-1,0);var i=1,r=0;for(this[t]=255&e;++r=0&&(r*=256);)this[t+i]=e/r&255;return t+n},r.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,1,255,0),r.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},r.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,2,65535,0),r.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):R(this,e,t,!0),t+2},r.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,2,65535,0),r.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):R(this,e,t,!1),t+2},r.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,4,4294967295,0),r.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):N(this,e,t,!0),t+4},r.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,4,4294967295,0),r.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):N(this,e,t,!1),t+4},r.prototype.writeIntLE=function(e,t,n,o){if(e=+e,t|=0,!o){var i=Math.pow(2,8*n-1);z(this,e,t,n,i-1,-i)}var r=0,a=1,l=0;for(this[t]=255&e;++r>0)-l&255;return t+n},r.prototype.writeIntBE=function(e,t,n,o){if(e=+e,t|=0,!o){var i=Math.pow(2,8*n-1);z(this,e,t,n,i-1,-i)}var r=n-1,a=1,l=0;for(this[t+r]=255&e;--r>=0&&(a*=256);)e<0&&0===l&&0!==this[t+r+1]&&(l=1),this[t+r]=(e/a>>0)-l&255;return t+n},r.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,1,127,-128),r.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},r.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,2,32767,-32768),r.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):R(this,e,t,!0),t+2},r.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,2,32767,-32768),r.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):R(this,e,t,!1),t+2},r.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,4,2147483647,-2147483648),r.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):N(this,e,t,!0),t+4},r.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||z(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),r.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):N(this,e,t,!1),t+4},r.prototype.writeFloatLE=function(e,t,n){return F(this,e,t,!0,n)},r.prototype.writeFloatBE=function(e,t,n){return F(this,e,t,!1,n)},r.prototype.writeDoubleLE=function(e,t,n){return V(this,e,t,!0,n)},r.prototype.writeDoubleBE=function(e,t,n){return V(this,e,t,!1,n)},r.prototype.copy=function(e,t,n,o){if(n||(n=0),o||0===o||(o=this.length),t>=e.length&&(t=e.length),t||(t=0),o>0&&o=this.length)throw new RangeError("sourceStart out of bounds");if(o<0)throw new RangeError("sourceEnd out of bounds");o>this.length&&(o=this.length),e.length-t=0;--i)e[i+t]=this[i+n];else if(a<1e3||!r.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,n=void 0===n?this.length:n>>>0,e||(e=0);var a;if("number"==typeof e)for(a=t;au;)if((l=s[u++])!=l)return!0}else for(;c>u;u++)if((e||u in s)&&s[u]===n)return e||u||0;return!e&&-1}}},function(e,t,n){var o=n(315);e.exports=function(e,t,n){if(o(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,o){return e.call(t,n,o)};case 3:return function(n,o,i){return e.call(t,n,o,i)}}return function(){return e.apply(t,arguments)}}},function(e,t,n){var o=n(78),i=n(105),r=n(79);e.exports=function(e){var t=o(e),n=i.f;if(n)for(var a,l=n(e),s=r.f,c=0;l.length>c;)s.call(e,a=l[c++])&&t.push(a);return t}},function(e,t,n){var o=n(33).document;e.exports=o&&o.documentElement},function(e,t,n){var o=n(153);e.exports=Array.isArray||function(e){return"Array"==o(e)}},function(e,t,n){"use strict";var o=n(158),i=n(80),r=n(106),a={};n(51)(a,n(54)("iterator"),function(){return this}),e.exports=function(e,t,n){e.prototype=o(a,{next:i(1,n)}),r(e,t+" Iterator")}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,n){var o=n(81)("meta"),i=n(64),r=n(41),a=n(52).f,l=0,s=Object.isExtensible||function(){return!0},c=!n(63)(function(){return s(Object.preventExtensions({}))}),u=function(e){a(e,o,{value:{i:"O"+ ++l,w:{}}})},d=function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!r(e,o)){if(!s(e))return"F";if(!t)return"E";u(e)}return e[o].i},f=function(e,t){if(!r(e,o)){if(!s(e))return!0;if(!t)return!1;u(e)}return e[o].w},p=function(e){return c&&h.NEED&&s(e)&&!r(e,o)&&u(e),e},h=e.exports={KEY:o,NEED:!1,fastKey:d,getWeak:f,onFreeze:p}},function(e,t,n){"use strict";var o=n(40),i=n(78),r=n(105),a=n(79),l=n(110),s=n(156),c=Object.assign;e.exports=!c||n(63)(function(){var e={},t={},n=Symbol(),o="abcdefghijklmnopqrst";return e[n]=7,o.split("").forEach(function(e){t[e]=e}),7!=c({},e)[n]||Object.keys(c({},t)).join("")!=o})?function(e,t){for(var n=l(e),c=arguments.length,u=1,d=r.f,f=a.f;c>u;)for(var p,h=s(arguments[u++]),g=d?i(h).concat(d(h)):i(h),m=g.length,v=0;m>v;)p=g[v++],o&&!f.call(h,p)||(n[p]=h[p]);return n}:c},function(e,t,n){var o=n(52),i=n(76),r=n(78);e.exports=n(40)?Object.defineProperties:function(e,t){i(e);for(var n,a=r(t),l=a.length,s=0;l>s;)o.f(e,n=a[s++],t[n]);return e}},function(e,t,n){var o=n(79),i=n(80),r=n(53),a=n(111),l=n(41),s=n(155),c=Object.getOwnPropertyDescriptor;t.f=n(40)?c:function(e,t){if(e=r(e),t=a(t,!0),s)try{return c(e,t)}catch(e){}if(l(e,t))return i(!o.f.call(e,t),e[t])}},function(e,t,n){var o=n(53),i=n(159).f,r={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],l=function(e){try{return i(e)}catch(e){return a.slice()}};e.exports.f=function(e){return a&&"[object Window]"==r.call(e)?l(e):i(o(e))}},function(e,t,n){var o=n(41),i=n(110),r=n(107)("IE_PROTO"),a=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=i(e),o(e,r)?e[r]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?a:null}},function(e,t,n){var o=n(109),i=n(101);e.exports=function(e){return function(t,n){var r,a,l=String(i(t)),s=o(n),c=l.length;return s<0||s>=c?e?"":void 0:(r=l.charCodeAt(s),r<55296||r>56319||s+1===c||(a=l.charCodeAt(s+1))<56320||a>57343?e?l.charAt(s):r:e?l.slice(s,s+2):a-56320+(r-55296<<10)+65536)}}},function(e,t,n){var o=n(109),i=Math.max,r=Math.min;e.exports=function(e,t){return e=o(e),e<0?i(e+t,0):r(e,t)}},function(e,t,n){var o=n(109),i=Math.min;e.exports=function(e){return e>0?i(o(e),9007199254740991):0}},function(e,t,n){"use strict";var o=n(316),i=n(323),r=n(104),a=n(53);e.exports=n(157)(Array,"Array",function(e,t){this._t=a(e),this._i=0,this._k=t},function(){var e=this._t,t=this._k,n=this._i++;return!e||n>=e.length?(this._t=void 0,i(1)):"keys"==t?i(0,n):"values"==t?i(0,e[n]):i(0,[n,e[n]])},"values"),r.Arguments=r.Array,o("keys"),o("values"),o("entries")},function(e,t,n){var o=n(103);o(o.S+o.F,"Object",{assign:n(325)})},function(e,t){},function(e,t,n){"use strict";var o=n(330)(!0);n(157)(String,"String",function(e){this._t=String(e),this._i=0},function(){var e,t=this._t,n=this._i;return n>=t.length?{value:void 0,done:!0}:(e=o(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){"use strict";var o=n(33),i=n(41),r=n(40),a=n(103),l=n(161),s=n(324).KEY,c=n(63),u=n(108),d=n(106),f=n(81),p=n(54),h=n(113),g=n(112),m=n(319),v=n(321),b=n(76),x=n(64),y=n(110),_=n(53),w=n(111),k=n(80),S=n(158),M=n(328),C=n(327),A=n(105),T=n(52),E=n(78),I=C.f,O=T.f,L=M.f,P=o.Symbol,D=o.JSON,z=D&&D.stringify,R=p("_hidden"),N=p("toPrimitive"),B={}.propertyIsEnumerable,F=u("symbol-registry"),V=u("symbols"),j=u("op-symbols"),$=Object.prototype,H="function"==typeof P&&!!A.f,W=o.QObject,G=!W||!W.prototype||!W.prototype.findChild,U=r&&c(function(){return 7!=S(O({},"a",{get:function(){return O(this,"a",{value:7}).a}})).a})?function(e,t,n){var o=I($,t);o&&delete $[t],O(e,t,n),o&&e!==$&&O($,t,o)}:O,q=function(e){var t=V[e]=S(P.prototype);return t._k=e,t},Y=H&&"symbol"==typeof P.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof P},Z=function(e,t,n){return e===$&&Z(j,t,n),b(e),t=w(t,!0),b(n),i(V,t)?(n.enumerable?(i(e,R)&&e[R][t]&&(e[R][t]=!1),n=S(n,{enumerable:k(0,!1)})):(i(e,R)||O(e,R,k(1,{})),e[R][t]=!0),U(e,t,n)):O(e,t,n)},X=function(e,t){b(e);for(var n,o=m(t=_(t)),i=0,r=o.length;r>i;)Z(e,n=o[i++],t[n]);return e},K=function(e,t){return void 0===t?S(e):X(S(e),t)},J=function(e){var t=B.call(this,e=w(e,!0));return!(this===$&&i(V,e)&&!i(j,e))&&(!(t||!i(this,e)||!i(V,e)||i(this,R)&&this[R][e])||t)},Q=function(e,t){if(e=_(e),t=w(t,!0),e!==$||!i(V,t)||i(j,t)){var n=I(e,t);return!n||!i(V,t)||i(e,R)&&e[R][t]||(n.enumerable=!0),n}},ee=function(e){for(var t,n=L(_(e)),o=[],r=0;n.length>r;)i(V,t=n[r++])||t==R||t==s||o.push(t);return o},te=function(e){for(var t,n=e===$,o=L(n?j:_(e)),r=[],a=0;o.length>a;)!i(V,t=o[a++])||n&&!i($,t)||r.push(V[t]);return r};H||(P=function(){if(this instanceof P)throw TypeError("Symbol is not a constructor!");var e=f(arguments.length>0?arguments[0]:void 0),t=function(n){this===$&&t.call(j,n),i(this,R)&&i(this[R],e)&&(this[R][e]=!1),U(this,e,k(1,n))};return r&&G&&U($,e,{configurable:!0,set:t}),q(e)},l(P.prototype,"toString",function(){return this._k}),C.f=Q,T.f=Z,n(159).f=M.f=ee,n(79).f=J,A.f=te,r&&!n(77)&&l($,"propertyIsEnumerable",J,!0),h.f=function(e){return q(p(e))}),a(a.G+a.W+a.F*!H,{Symbol:P});for(var ne="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),oe=0;ne.length>oe;)p(ne[oe++]);for(var ie=E(p.store),re=0;ie.length>re;)g(ie[re++]);a(a.S+a.F*!H,"Symbol",{for:function(e){return i(F,e+="")?F[e]:F[e]=P(e)},keyFor:function(e){if(!Y(e))throw TypeError(e+" is not a symbol!");for(var t in F)if(F[t]===e)return t},useSetter:function(){G=!0},useSimple:function(){G=!1}}),a(a.S+a.F*!H,"Object",{create:K,defineProperty:Z,defineProperties:X,getOwnPropertyDescriptor:Q,getOwnPropertyNames:ee,getOwnPropertySymbols:te});var ae=c(function(){A.f(1)});a(a.S+a.F*ae,"Object",{getOwnPropertySymbols:function(e){return A.f(y(e))}}),D&&a(a.S+a.F*(!H||c(function(){var e=P();return"[null]"!=z([e])||"{}"!=z({a:e})||"{}"!=z(Object(e))})),"JSON",{stringify:function(e){for(var t,n,o=[e],i=1;arguments.length>i;)o.push(arguments[i++]);if(n=t=o[1],(x(t)||void 0!==e)&&!Y(e))return v(t)||(t=function(e,t){if("function"==typeof n&&(t=n.call(this,e,t)),!Y(t))return t}),o[1]=t,z.apply(D,o)}}),P.prototype[N]||n(51)(P.prototype,N,P.prototype.valueOf),d(P,"Symbol"),d(Math,"Math",!0),d(o.JSON,"JSON",!0)},function(e,t,n){n(112)("asyncIterator")},function(e,t,n){n(112)("observable")},function(e,t,n){n(333);for(var o=n(33),i=n(51),r=n(104),a=n(54)("toStringTag"),l="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s.el-button.is-active,.el-button-group>.el-button.is-disabled,.el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover{z-index:1}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #dcdfe6;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button{margin-left:10px}.el-button:focus,.el-button:hover{color:#409eff;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#fff;border-color:#409eff;color:#409eff}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#fff;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#ebeef5;color:#c0c4cc}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:"";position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:hsla(0,0%,100%,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#fff;background-color:#409eff;border-color:#409eff}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#fff}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#fff}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#fff;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409eff;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409eff;border-color:#409eff;color:#fff}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#fff;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#fff;background-color:#67c23a;border-color:#67c23a}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#fff}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#fff}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#fff;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67c23a;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67c23a;border-color:#67c23a;color:#fff}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#fff;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#fff;background-color:#e6a23c;border-color:#e6a23c}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#fff}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#fff}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#fff;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#e6a23c;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#e6a23c;border-color:#e6a23c;color:#fff}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#fff;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#fff;background-color:#f56c6c;border-color:#f56c6c}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#fff}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#fff}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#fff;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#f56c6c;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#f56c6c;border-color:#f56c6c;color:#fff}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#fff;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#fff;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#fff}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#fff}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#fff;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#fff}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#fff;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--text,.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover,.el-button--text:active{border-color:transparent}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--mini,.el-button--small{font-size:12px;border-radius:3px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small,.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini.is-circle{padding:7px}.el-button--text{color:#409eff;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;background-color:transparent}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group:after,.el-button-group:before{display:table;content:""}.el-button-group:after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:not(:last-child){margin-right:-1px}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,".el-col-pull-0,.el-col-pull-1,.el-col-pull-2,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-pull-10,.el-col-pull-11,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-push-0,.el-col-push-1,.el-col-push-2,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24{position:relative}[class*=el-col-]{float:left;box-sizing:border-box}.el-col-0{display:none;width:0}.el-col-offset-0{margin-left:0}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{position:relative;right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0}.el-col-xs-offset-0{margin-left:0}.el-col-xs-pull-0{position:relative;right:0}.el-col-xs-push-0{position:relative;left:0}.el-col-xs-1{width:4.16667%}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{width:8.33333%}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{width:12.5%}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{display:none;width:0}.el-col-sm-offset-0{margin-left:0}.el-col-sm-pull-0{position:relative;right:0}.el-col-sm-push-0{position:relative;left:0}.el-col-sm-1{width:4.16667%}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{width:8.33333%}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{width:12.5%}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{display:none;width:0}.el-col-md-offset-0{margin-left:0}.el-col-md-pull-0{position:relative;right:0}.el-col-md-push-0{position:relative;left:0}.el-col-md-1{width:4.16667%}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{width:8.33333%}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{width:12.5%}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{display:none;width:0}.el-col-lg-offset-0{margin-left:0}.el-col-lg-pull-0{position:relative;right:0}.el-col-lg-push-0{position:relative;left:0}.el-col-lg-1{width:4.16667%}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{width:8.33333%}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{width:12.5%}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{display:none;width:0}.el-col-xl-offset-0{margin-left:0}.el-col-xl-pull-0{position:relative;right:0}.el-col-xl-push-0{position:relative;left:0}.el-col-xl-1{width:4.16667%}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{width:8.33333%}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{width:12.5%}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,"",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,'.el-form--inline .el-form-item,.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form-item:after,.el-form-item__content:after{clear:both}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{margin-right:10px}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item:after,.el-form-item:before{display:table;content:""}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content:after,.el-form-item__content:before{display:table;content:""}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#f56c6c;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:"*";color:#f56c6c;margin-right:4px}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus{border-color:#f56c6c}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#f56c6c}.el-form-item--feedback .el-input__validateIcon{display:inline-block}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,".el-pagination--small .arrow.disabled,.el-table--hidden,.el-table .hidden-columns,.el-table td.is-hidden>*,.el-table th.is-hidden>*{visibility:hidden}.el-input__suffix,.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-dropdown .el-dropdown-selfdefine:focus:active,.el-dropdown .el-dropdown-selfdefine:focus:not(.focusing),.el-message__closeBtn:focus,.el-message__content:focus,.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing),.el-rate:active,.el-rate:focus,.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing),.el-upload-list__item.is-success:active,.el-upload-list__item.is-success:not(.focusing):focus{outline-width:0}@font-face{font-family:element-icons;src:url("+n(226)+') format("woff"),url('+n(225)+') format("truetype");font-weight:400;font-display:"auto";font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\\E6A0"}.el-icon-ice-cream-square:before{content:"\\E6A3"}.el-icon-lollipop:before{content:"\\E6A4"}.el-icon-potato-strips:before{content:"\\E6A5"}.el-icon-milk-tea:before{content:"\\E6A6"}.el-icon-ice-drink:before{content:"\\E6A7"}.el-icon-ice-tea:before{content:"\\E6A9"}.el-icon-coffee:before{content:"\\E6AA"}.el-icon-orange:before{content:"\\E6AB"}.el-icon-pear:before{content:"\\E6AC"}.el-icon-apple:before{content:"\\E6AD"}.el-icon-cherry:before{content:"\\E6AE"}.el-icon-watermelon:before{content:"\\E6AF"}.el-icon-grape:before{content:"\\E6B0"}.el-icon-refrigerator:before{content:"\\E6B1"}.el-icon-goblet-square-full:before{content:"\\E6B2"}.el-icon-goblet-square:before{content:"\\E6B3"}.el-icon-goblet-full:before{content:"\\E6B4"}.el-icon-goblet:before{content:"\\E6B5"}.el-icon-cold-drink:before{content:"\\E6B6"}.el-icon-coffee-cup:before{content:"\\E6B8"}.el-icon-water-cup:before{content:"\\E6B9"}.el-icon-hot-water:before{content:"\\E6BA"}.el-icon-ice-cream:before{content:"\\E6BB"}.el-icon-dessert:before{content:"\\E6BC"}.el-icon-sugar:before{content:"\\E6BD"}.el-icon-tableware:before{content:"\\E6BE"}.el-icon-burger:before{content:"\\E6BF"}.el-icon-knife-fork:before{content:"\\E6C1"}.el-icon-fork-spoon:before{content:"\\E6C2"}.el-icon-chicken:before{content:"\\E6C3"}.el-icon-food:before{content:"\\E6C4"}.el-icon-dish-1:before{content:"\\E6C5"}.el-icon-dish:before{content:"\\E6C6"}.el-icon-moon-night:before{content:"\\E6EE"}.el-icon-moon:before{content:"\\E6F0"}.el-icon-cloudy-and-sunny:before{content:"\\E6F1"}.el-icon-partly-cloudy:before{content:"\\E6F2"}.el-icon-cloudy:before{content:"\\E6F3"}.el-icon-sunny:before{content:"\\E6F6"}.el-icon-sunset:before{content:"\\E6F7"}.el-icon-sunrise-1:before{content:"\\E6F8"}.el-icon-sunrise:before{content:"\\E6F9"}.el-icon-heavy-rain:before{content:"\\E6FA"}.el-icon-lightning:before{content:"\\E6FB"}.el-icon-light-rain:before{content:"\\E6FC"}.el-icon-wind-power:before{content:"\\E6FD"}.el-icon-baseball:before{content:"\\E712"}.el-icon-soccer:before{content:"\\E713"}.el-icon-football:before{content:"\\E715"}.el-icon-basketball:before{content:"\\E716"}.el-icon-ship:before{content:"\\E73F"}.el-icon-truck:before{content:"\\E740"}.el-icon-bicycle:before{content:"\\E741"}.el-icon-mobile-phone:before{content:"\\E6D3"}.el-icon-service:before{content:"\\E6D4"}.el-icon-key:before{content:"\\E6E2"}.el-icon-unlock:before{content:"\\E6E4"}.el-icon-lock:before{content:"\\E6E5"}.el-icon-watch:before{content:"\\E6FE"}.el-icon-watch-1:before{content:"\\E6FF"}.el-icon-timer:before{content:"\\E702"}.el-icon-alarm-clock:before{content:"\\E703"}.el-icon-map-location:before{content:"\\E704"}.el-icon-delete-location:before{content:"\\E705"}.el-icon-add-location:before{content:"\\E706"}.el-icon-location-information:before{content:"\\E707"}.el-icon-location-outline:before{content:"\\E708"}.el-icon-location:before{content:"\\E79E"}.el-icon-place:before{content:"\\E709"}.el-icon-discover:before{content:"\\E70A"}.el-icon-first-aid-kit:before{content:"\\E70B"}.el-icon-trophy-1:before{content:"\\E70C"}.el-icon-trophy:before{content:"\\E70D"}.el-icon-medal:before{content:"\\E70E"}.el-icon-medal-1:before{content:"\\E70F"}.el-icon-stopwatch:before{content:"\\E710"}.el-icon-mic:before{content:"\\E711"}.el-icon-copy-document:before{content:"\\E718"}.el-icon-full-screen:before{content:"\\E719"}.el-icon-switch-button:before{content:"\\E71B"}.el-icon-aim:before{content:"\\E71C"}.el-icon-crop:before{content:"\\E71D"}.el-icon-odometer:before{content:"\\E71E"}.el-icon-time:before{content:"\\E71F"}.el-icon-bangzhu:before{content:"\\E724"}.el-icon-close-notification:before{content:"\\E726"}.el-icon-microphone:before{content:"\\E727"}.el-icon-turn-off-microphone:before{content:"\\E728"}.el-icon-position:before{content:"\\E729"}.el-icon-postcard:before{content:"\\E72A"}.el-icon-message:before{content:"\\E72B"}.el-icon-chat-line-square:before{content:"\\E72D"}.el-icon-chat-dot-square:before{content:"\\E72E"}.el-icon-chat-dot-round:before{content:"\\E72F"}.el-icon-chat-square:before{content:"\\E730"}.el-icon-chat-line-round:before{content:"\\E731"}.el-icon-chat-round:before{content:"\\E732"}.el-icon-set-up:before{content:"\\E733"}.el-icon-turn-off:before{content:"\\E734"}.el-icon-open:before{content:"\\E735"}.el-icon-connection:before{content:"\\E736"}.el-icon-link:before{content:"\\E737"}.el-icon-cpu:before{content:"\\E738"}.el-icon-thumb:before{content:"\\E739"}.el-icon-female:before{content:"\\E73A"}.el-icon-male:before{content:"\\E73B"}.el-icon-guide:before{content:"\\E73C"}.el-icon-news:before{content:"\\E73E"}.el-icon-price-tag:before{content:"\\E744"}.el-icon-discount:before{content:"\\E745"}.el-icon-wallet:before{content:"\\E747"}.el-icon-coin:before{content:"\\E748"}.el-icon-money:before{content:"\\E749"}.el-icon-bank-card:before{content:"\\E74A"}.el-icon-box:before{content:"\\E74B"}.el-icon-present:before{content:"\\E74C"}.el-icon-sell:before{content:"\\E6D5"}.el-icon-sold-out:before{content:"\\E6D6"}.el-icon-shopping-bag-2:before{content:"\\E74D"}.el-icon-shopping-bag-1:before{content:"\\E74E"}.el-icon-shopping-cart-2:before{content:"\\E74F"}.el-icon-shopping-cart-1:before{content:"\\E750"}.el-icon-shopping-cart-full:before{content:"\\E751"}.el-icon-smoking:before{content:"\\E752"}.el-icon-no-smoking:before{content:"\\E753"}.el-icon-house:before{content:"\\E754"}.el-icon-table-lamp:before{content:"\\E755"}.el-icon-school:before{content:"\\E756"}.el-icon-office-building:before{content:"\\E757"}.el-icon-toilet-paper:before{content:"\\E758"}.el-icon-notebook-2:before{content:"\\E759"}.el-icon-notebook-1:before{content:"\\E75A"}.el-icon-files:before{content:"\\E75B"}.el-icon-collection:before{content:"\\E75C"}.el-icon-receiving:before{content:"\\E75D"}.el-icon-suitcase-1:before{content:"\\E760"}.el-icon-suitcase:before{content:"\\E761"}.el-icon-film:before{content:"\\E763"}.el-icon-collection-tag:before{content:"\\E765"}.el-icon-data-analysis:before{content:"\\E766"}.el-icon-pie-chart:before{content:"\\E767"}.el-icon-data-board:before{content:"\\E768"}.el-icon-data-line:before{content:"\\E76D"}.el-icon-reading:before{content:"\\E769"}.el-icon-magic-stick:before{content:"\\E76A"}.el-icon-coordinate:before{content:"\\E76B"}.el-icon-mouse:before{content:"\\E76C"}.el-icon-brush:before{content:"\\E76E"}.el-icon-headset:before{content:"\\E76F"}.el-icon-umbrella:before{content:"\\E770"}.el-icon-scissors:before{content:"\\E771"}.el-icon-mobile:before{content:"\\E773"}.el-icon-attract:before{content:"\\E774"}.el-icon-monitor:before{content:"\\E775"}.el-icon-search:before{content:"\\E778"}.el-icon-takeaway-box:before{content:"\\E77A"}.el-icon-paperclip:before{content:"\\E77D"}.el-icon-printer:before{content:"\\E77E"}.el-icon-document-add:before{content:"\\E782"}.el-icon-document:before{content:"\\E785"}.el-icon-document-checked:before{content:"\\E786"}.el-icon-document-copy:before{content:"\\E787"}.el-icon-document-delete:before{content:"\\E788"}.el-icon-document-remove:before{content:"\\E789"}.el-icon-tickets:before{content:"\\E78B"}.el-icon-folder-checked:before{content:"\\E77F"}.el-icon-folder-delete:before{content:"\\E780"}.el-icon-folder-remove:before{content:"\\E781"}.el-icon-folder-add:before{content:"\\E783"}.el-icon-folder-opened:before{content:"\\E784"}.el-icon-folder:before{content:"\\E78A"}.el-icon-edit-outline:before{content:"\\E764"}.el-icon-edit:before{content:"\\E78C"}.el-icon-date:before{content:"\\E78E"}.el-icon-c-scale-to-original:before{content:"\\E7C6"}.el-icon-view:before{content:"\\E6CE"}.el-icon-loading:before{content:"\\E6CF"}.el-icon-rank:before{content:"\\E6D1"}.el-icon-sort-down:before{content:"\\E7C4"}.el-icon-sort-up:before{content:"\\E7C5"}.el-icon-sort:before{content:"\\E6D2"}.el-icon-finished:before{content:"\\E6CD"}.el-icon-refresh-left:before{content:"\\E6C7"}.el-icon-refresh-right:before{content:"\\E6C8"}.el-icon-refresh:before{content:"\\E6D0"}.el-icon-video-play:before{content:"\\E7C0"}.el-icon-video-pause:before{content:"\\E7C1"}.el-icon-d-arrow-right:before{content:"\\E6DC"}.el-icon-d-arrow-left:before{content:"\\E6DD"}.el-icon-arrow-up:before{content:"\\E6E1"}.el-icon-arrow-down:before{content:"\\E6DF"}.el-icon-arrow-right:before{content:"\\E6E0"}.el-icon-arrow-left:before{content:"\\E6DE"}.el-icon-top-right:before{content:"\\E6E7"}.el-icon-top-left:before{content:"\\E6E8"}.el-icon-top:before{content:"\\E6E6"}.el-icon-bottom:before{content:"\\E6EB"}.el-icon-right:before{content:"\\E6E9"}.el-icon-back:before{content:"\\E6EA"}.el-icon-bottom-right:before{content:"\\E6EC"}.el-icon-bottom-left:before{content:"\\E6ED"}.el-icon-caret-top:before{content:"\\E78F"}.el-icon-caret-bottom:before{content:"\\E790"}.el-icon-caret-right:before{content:"\\E791"}.el-icon-caret-left:before{content:"\\E792"}.el-icon-d-caret:before{content:"\\E79A"}.el-icon-share:before{content:"\\E793"}.el-icon-menu:before{content:"\\E798"}.el-icon-s-grid:before{content:"\\E7A6"}.el-icon-s-check:before{content:"\\E7A7"}.el-icon-s-data:before{content:"\\E7A8"}.el-icon-s-opportunity:before{content:"\\E7AA"}.el-icon-s-custom:before{content:"\\E7AB"}.el-icon-s-claim:before{content:"\\E7AD"}.el-icon-s-finance:before{content:"\\E7AE"}.el-icon-s-comment:before{content:"\\E7AF"}.el-icon-s-flag:before{content:"\\E7B0"}.el-icon-s-marketing:before{content:"\\E7B1"}.el-icon-s-shop:before{content:"\\E7B4"}.el-icon-s-open:before{content:"\\E7B5"}.el-icon-s-management:before{content:"\\E7B6"}.el-icon-s-ticket:before{content:"\\E7B7"}.el-icon-s-release:before{content:"\\E7B8"}.el-icon-s-home:before{content:"\\E7B9"}.el-icon-s-promotion:before{content:"\\E7BA"}.el-icon-s-operation:before{content:"\\E7BB"}.el-icon-s-unfold:before{content:"\\E7BC"}.el-icon-s-fold:before{content:"\\E7A9"}.el-icon-s-platform:before{content:"\\E7BD"}.el-icon-s-order:before{content:"\\E7BE"}.el-icon-s-cooperation:before{content:"\\E7BF"}.el-icon-bell:before{content:"\\E725"}.el-icon-message-solid:before{content:"\\E799"}.el-icon-video-camera:before{content:"\\E772"}.el-icon-video-camera-solid:before{content:"\\E796"}.el-icon-camera:before{content:"\\E779"}.el-icon-camera-solid:before{content:"\\E79B"}.el-icon-download:before{content:"\\E77C"}.el-icon-upload2:before{content:"\\E77B"}.el-icon-upload:before{content:"\\E7C3"}.el-icon-picture-outline-round:before{content:"\\E75F"}.el-icon-picture-outline:before{content:"\\E75E"}.el-icon-picture:before{content:"\\E79F"}.el-icon-close:before{content:"\\E6DB"}.el-icon-check:before{content:"\\E6DA"}.el-icon-plus:before{content:"\\E6D9"}.el-icon-minus:before{content:"\\E6D8"}.el-icon-help:before{content:"\\E73D"}.el-icon-s-help:before{content:"\\E7B3"}.el-icon-circle-close:before{content:"\\E78D"}.el-icon-circle-check:before{content:"\\E720"}.el-icon-circle-plus-outline:before{content:"\\E723"}.el-icon-remove-outline:before{content:"\\E722"}.el-icon-zoom-out:before{content:"\\E776"}.el-icon-zoom-in:before{content:"\\E777"}.el-icon-error:before{content:"\\E79D"}.el-icon-success:before{content:"\\E79C"}.el-icon-circle-plus:before{content:"\\E7A0"}.el-icon-remove:before{content:"\\E7A2"}.el-icon-info:before{content:"\\E7A1"}.el-icon-question:before{content:"\\E7A4"}.el-icon-warning-outline:before{content:"\\E6C9"}.el-icon-warning:before{content:"\\E7A3"}.el-icon-goods:before{content:"\\E7C2"}.el-icon-s-goods:before{content:"\\E7B2"}.el-icon-star-off:before{content:"\\E717"}.el-icon-star-on:before{content:"\\E797"}.el-icon-more-outline:before{content:"\\E6CC"}.el-icon-more:before{content:"\\E794"}.el-icon-phone-outline:before{content:"\\E6CB"}.el-icon-phone:before{content:"\\E795"}.el-icon-user:before{content:"\\E6E3"}.el-icon-user-solid:before{content:"\\E7A5"}.el-icon-setting:before{content:"\\E6CA"}.el-icon-s-tools:before{content:"\\E7AC"}.el-icon-delete:before{content:"\\E6D7"}.el-icon-delete-solid:before{content:"\\E7C9"}.el-icon-eleme:before{content:"\\E7C7"}.el-icon-platform-eleme:before{content:"\\E7CA"}.el-icon-loading{animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@keyframes rotating{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.el-pagination{white-space:nowrap;padding:2px 5px;color:#303133;font-weight:700}.el-pagination:after,.el-pagination:before{display:table;content:""}.el-pagination:after{clear:both}.el-pagination button,.el-pagination span:not([class*=suffix]){display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;vertical-align:top;box-sizing:border-box}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield;line-height:normal}.el-pagination .el-input__suffix{right:0;transform:scale(.8)}.el-pagination .el-select .el-input{width:100px;margin:0 5px}.el-pagination .el-select .el-input .el-input__inner{padding-right:25px;border-radius:3px}.el-pagination button{border:none;padding:0 6px;background:0 0}.el-pagination button:focus{outline:0}.el-pagination button:hover{color:#409eff}.el-pagination button:disabled{color:#c0c4cc;background-color:#fff;cursor:not-allowed}.el-pagination .btn-next,.el-pagination .btn-prev{background:50% no-repeat #fff;background-size:16px;cursor:pointer;margin:0;color:#303133}.el-pagination .btn-next .el-icon,.el-pagination .btn-prev .el-icon{display:block;font-size:12px;font-weight:700}.el-pagination .btn-prev{padding-right:12px}.el-pagination .btn-next{padding-left:12px}.el-pagination .el-pager li.disabled{color:#c0c4cc;cursor:not-allowed}.el-pager li,.el-pager li.btn-quicknext:hover,.el-pager li.btn-quickprev:hover{cursor:pointer}.el-pagination--small .btn-next,.el-pagination--small .btn-prev,.el-pagination--small .el-pager li,.el-pagination--small .el-pager li.btn-quicknext,.el-pagination--small .el-pager li.btn-quickprev,.el-pagination--small .el-pager li:last-child{border-color:transparent;font-size:12px;line-height:22px;height:22px;min-width:22px}.el-pagination--small .more:before,.el-pagination--small li.more:before{line-height:24px}.el-pagination--small button,.el-pagination--small span:not([class*=suffix]){height:22px;line-height:22px}.el-pagination--small .el-pagination__editor,.el-pagination--small .el-pagination__editor.el-input .el-input__inner{height:22px}.el-pagination__sizes{margin:0 10px 0 0;font-weight:400;color:#606266}.el-pagination__sizes .el-input .el-input__inner{font-size:13px;padding-left:8px}.el-pagination__sizes .el-input .el-input__inner:hover{border-color:#409eff}.el-pagination__total{margin-right:10px;font-weight:400;color:#606266}.el-pagination__jump{margin-left:24px;font-weight:400;color:#606266}.el-pagination__jump .el-input__inner{padding:0 3px}.el-pagination__rightwrapper{float:right}.el-pagination__editor{line-height:18px;padding:0 2px;height:28px;text-align:center;margin:0 2px;box-sizing:border-box;border-radius:3px}.el-pager,.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev{padding:0}.el-pagination__editor.el-input{width:50px}.el-pagination__editor.el-input .el-input__inner{height:28px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button,.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev,.el-pagination.is-background .el-pager li{margin:0 5px;background-color:#f4f4f5;color:#606266;min-width:30px;border-radius:2px}.el-pagination.is-background .btn-next.disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-prev.disabled,.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .el-pager li.disabled{color:#c0c4cc}.el-pagination.is-background .el-pager li:not(.disabled):hover{color:#409eff}.el-pagination.is-background .el-pager li:not(.disabled).active{background-color:#409eff;color:#fff}.el-dialog,.el-pager li{background:#fff;-webkit-box-sizing:border-box}.el-pagination.is-background.el-pagination--small .btn-next,.el-pagination.is-background.el-pagination--small .btn-prev,.el-pagination.is-background.el-pagination--small .el-pager li{margin:0 3px;min-width:22px}.el-pager,.el-pager li{vertical-align:top;margin:0;display:inline-block}.el-pager{-ms-user-select:none;user-select:none;list-style:none;font-size:0}.el-date-table,.el-pager,.el-table th{-webkit-user-select:none;-moz-user-select:none}.el-pager .more:before{line-height:30px}.el-pager li{padding:0 4px;font-size:13px;min-width:35.5px;height:28px;line-height:28px;box-sizing:border-box;text-align:center}.el-menu--collapse .el-menu .el-submenu,.el-menu--popup{min-width:200px}.el-pager li.btn-quicknext,.el-pager li.btn-quickprev{line-height:28px;color:#303133}.el-pager li.btn-quicknext.disabled,.el-pager li.btn-quickprev.disabled{color:#c0c4cc}.el-pager li.active+li{border-left:0}.el-pager li:hover{color:#409eff}.el-pager li.active{color:#409eff;cursor:default}.el-dialog{position:relative;margin:0 auto 50px;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.3);box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#409eff}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px;word-break:break-all}.el-dialog__footer{padding:10px 20px 20px;text-align:right;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{animation:dialog-fade-in .3s}.dialog-fade-leave-active{animation:dialog-fade-out .3s}@keyframes dialog-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}to{transform:translateZ(0);opacity:1}}@keyframes dialog-fade-out{0%{transform:translateZ(0);opacity:1}to{transform:translate3d(0,-20px,0);opacity:0}}.el-autocomplete{position:relative;display:inline-block}.el-autocomplete-suggestion{margin:5px 0;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;border:1px solid #e4e7ed;box-sizing:border-box;background-color:#fff}.el-dropdown-menu,.el-menu--collapse .el-submenu .el-menu{z-index:10;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-autocomplete-suggestion__wrap{max-height:280px;padding:10px 0;box-sizing:border-box}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{padding:0 20px;margin:0;line-height:34px;cursor:pointer;color:#606266;font-size:14px;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-autocomplete-suggestion li.highlighted,.el-autocomplete-suggestion li:hover{background-color:#f5f7fa}.el-autocomplete-suggestion li.divider{margin-top:6px;border-top:1px solid #000}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{text-align:center;height:100px;line-height:100px;font-size:20px;color:#999}.el-autocomplete-suggestion.is-loading li:after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-autocomplete-suggestion.is-loading li:hover{background-color:#fff}.el-autocomplete-suggestion.is-loading .el-icon-loading{vertical-align:middle}.el-dropdown{display:inline-block;position:relative;color:#606266;font-size:14px}.el-dropdown .el-button-group{display:block}.el-dropdown .el-button-group .el-button{float:none}.el-dropdown .el-dropdown__caret-button{padding-left:5px;padding-right:5px;position:relative;border-left:none}.el-dropdown .el-dropdown__caret-button:before{content:"";position:absolute;display:block;width:1px;top:5px;bottom:5px;left:0;background:hsla(0,0%,100%,.5)}.el-dropdown .el-dropdown__caret-button.el-button--default:before{background:rgba(220,223,230,.5)}.el-dropdown .el-dropdown__caret-button:hover:before{top:0;bottom:0}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{padding-left:0}.el-dropdown__icon{font-size:12px;margin:0 3px}.el-dropdown-menu{position:absolute;top:0;left:0;padding:10px 0;margin:5px 0;background-color:#fff;border:1px solid #ebeef5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-dropdown-menu__item{list-style:none;line-height:36px;padding:0 20px;margin:0;font-size:14px;color:#606266;cursor:pointer;outline:0}.el-dropdown-menu__item:focus,.el-dropdown-menu__item:not(.is-disabled):hover{background-color:#ecf5ff;color:#66b1ff}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{position:relative;margin-top:6px;border-top:1px solid #ebeef5}.el-dropdown-menu__item--divided:before{content:"";height:6px;display:block;margin:0 -20px;background-color:#fff}.el-dropdown-menu__item.is-disabled{cursor:default;color:#bbb;pointer-events:none}.el-dropdown-menu--medium{padding:6px 0}.el-dropdown-menu--medium .el-dropdown-menu__item{line-height:30px;padding:0 17px;font-size:14px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:6px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:6px;margin:0 -17px}.el-dropdown-menu--small{padding:6px 0}.el-dropdown-menu--small .el-dropdown-menu__item{line-height:27px;padding:0 15px;font-size:13px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:4px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:4px;margin:0 -15px}.el-dropdown-menu--mini{padding:3px 0}.el-dropdown-menu--mini .el-dropdown-menu__item{line-height:24px;padding:0 10px;font-size:12px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:3px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:3px;margin:0 -10px}.el-menu{border-right:1px solid #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0}.el-menu,.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu:after,.el-menu:before{display:table;content:""}.el-menu:after{clear:both}.el-menu.el-menu--horizontal{border-bottom:1px solid #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409eff;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#fff;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409eff;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-submenu{position:relative}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;border:1px solid #e4e7ed;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;position:relative;-webkit-box-sizing:border-box;white-space:nowrap;list-style:none}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:none}.el-menu--popup{z-index:100;border:none;padding:5px 0;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409eff}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;transition:transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409eff}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:rotate(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.el-radio-button__inner,.el-radio-group{display:inline-block;line-height:1;vertical-align:middle}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{transition:.2s;opacity:0}.el-radio-group{font-size:0}.el-radio-button{position:relative;display:inline-block;outline:0}.el-radio-button__inner{white-space:nowrap;background:#fff;border:1px solid #dcdfe6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;position:relative;cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#409eff}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#fff;background-color:#409eff;border-color:#409eff;box-shadow:-1px 0 0 0 #409eff}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#f2f6fc}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-popover,.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active):not(.is-disabled){box-shadow:0 0 2px 2px #409eff}.el-switch{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.el-switch__core,.el-switch__label{display:inline-block;cursor:pointer}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{transition:.2s;height:20px;font-size:14px;font-weight:500;vertical-align:middle;color:#303133}.el-switch__label.is-active{color:#409eff}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{line-height:1;font-size:14px;display:inline-block}.el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.el-switch__core{margin:0;position:relative;width:40px;height:20px;border:1px solid #dcdfe6;outline:0;border-radius:10px;box-sizing:border-box;background:#dcdfe6;transition:border-color .3s,background-color .3s;vertical-align:middle}.el-switch__core:after{content:"";position:absolute;top:1px;left:1px;border-radius:100%;transition:all .3s;width:16px;height:16px;background-color:#fff}.el-switch.is-checked .el-switch__core{border-color:#409eff;background-color:#409eff}.el-switch.is-checked .el-switch__core:after{left:100%;margin-left:-17px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter,.el-switch .label-fade-leave-active{opacity:0}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #e4e7ed;border-radius:4px;background-color:#fff;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#409eff;background-color:#fff}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#f5f7fa}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected:after{position:absolute;right:20px;font-family:element-icons;content:"\\E6DA";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;box-sizing:border-box}.el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#fff}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#f5f7fa}.el-select-dropdown__item.selected{color:#409eff;font-weight:700}.el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type):after{content:"";position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#e4e7ed}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{display:inline-block;position:relative}.el-select .el-select__tags>span{display:contents}.el-select:hover .el-input__inner{border-color:#c0c4cc}.el-select .el-input__inner{cursor:pointer;padding-right:35px}.el-select .el-input__inner:focus{border-color:#409eff}.el-select .el-input .el-select__caret{color:#c0c4cc;font-size:14px;transition:transform .3s;transform:rotate(180deg);cursor:pointer}.el-select .el-input .el-select__caret.is-reverse{transform:rotate(0)}.el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;transform:rotate(180deg);border-radius:100%;color:#c0c4cc;transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-select .el-input .el-select__caret.is-show-close:hover{color:#909399}.el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.el-select .el-input.is-disabled .el-input__inner:hover{border-color:#e4e7ed}.el-select .el-input.is-focus .el-input__inner{border-color:#409eff}.el-select>.el-input{display:block}.el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.el-select__input.is-mini{height:14px}.el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#c0c4cc;line-height:18px;font-size:14px}.el-select__close:hover{color:#909399}.el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;transform:translateY(-50%);display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-select .el-tag__close{margin-top:-2px}.el-select .el-tag{box-sizing:border-box;border-color:transparent;margin:2px 0 2px 6px;background-color:#f0f2f5}.el-select .el-tag__close.el-icon-close{background-color:#c0c4cc;right:-7px;top:0;color:#fff}.el-select .el-tag__close.el-icon-close:hover{background-color:#909399}.el-table,.el-table__expanded-cell{background-color:#fff}.el-select .el-tag__close.el-icon-close:before{display:block;transform:translateY(.5px)}.el-table{position:relative;overflow:hidden;box-sizing:border-box;-ms-flex:1;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table--mini,.el-table--small,.el-table__expand-icon{font-size:12px}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;transition:transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit td.gutter,.el-table--fit th.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th{background:#f5f7fa}.el-table th,.el-table tr{background-color:#fff}.el-table td,.el-table th{padding:12px 0;min-width:0;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table td.is-center,.el-table th.is-center{text-align:center}.el-table td.is-right,.el-table th.is-right{text-align:right}.el-table td.gutter,.el-table th.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table--medium td,.el-table--medium th{padding:10px 0}.el-table--small td,.el-table--small th{padding:8px 0}.el-table--mini td,.el-table--mini th{padding:6px 0}.el-table--border td:first-child .cell,.el-table--border th:first-child .cell,.el-table .cell{padding-left:10px}.el-table tr input[type=checkbox]{margin:0}.el-table td,.el-table th.is-leaf{border-bottom:1px solid #ebeef5}.el-table th.is-sortable{cursor:pointer}.el-table th{overflow:hidden;-ms-user-select:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-table th>.cell{display:inline-block;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th>.cell.highlight{color:#409eff}.el-table th.required>div:before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td div{box-sizing:border-box}.el-table td.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #ebeef5}.el-table--border:after,.el-table--group:after,.el-table:before{content:"";position:absolute;background-color:#ebeef5;z-index:1}.el-table--border:after,.el-table--group:after{top:0;right:0;width:1px;height:100%}.el-table:before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border td,.el-table--border th,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #ebeef5}.el-table--border th.gutter:last-of-type{border-bottom:1px solid #ebeef5;border-bottom-width:1px}.el-table--border th,.el-table__fixed-right-patch{border-bottom:1px solid #ebeef5}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right:before,.el-table__fixed:before{content:"";position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#ebeef5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#fff}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td{border-top:1px solid #ebeef5;background-color:#f5f7fa;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td{border-top:1px solid #ebeef5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td,.el-table__header-wrapper tbody td{background-color:#f5f7fa;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{box-shadow:none}.el-picker-panel,.el-table-filter{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #ebeef5}.el-table .caret-wrapper{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#c0c4cc;top:5px}.el-table .sort-caret.descending{border-top-color:#c0c4cc;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409eff}.el-table .descending .sort-caret.descending{border-top-color:#409eff}.el-table .hidden-columns{position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td{background:#fafafa}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td,.el-table__body tr.hover-row.el-table__row--striped.current-row>td,.el-table__body tr.hover-row.el-table__row--striped>td,.el-table__body tr.hover-row>td{background-color:#f5f7fa}.el-table__body tr.current-row>td{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #ebeef5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;transform:scale(.75)}.el-table--enable-row-transition .el-table__body td{transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#f5f7fa}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #ebeef5;border-radius:2px;background-color:#fff;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-date-table td,.el-date-table td div{height:30px;-webkit-box-sizing:border-box}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409eff;color:#fff}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #ebeef5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-date-table.is-week-mode .el-date-table__row.current div,.el-date-table.is-week-mode .el-date-table__row:hover div,.el-date-table td.in-range div,.el-date-table td.in-range div:hover{background-color:#f2f6fc}.el-table-filter__bottom button:hover{color:#409eff}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-date-table{font-size:12px;-ms-user-select:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:#606266}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td{width:32px;padding:4px 0;box-sizing:border-box;text-align:center;cursor:pointer;position:relative}.el-date-table td div{padding:3px 0;box-sizing:border-box}.el-date-table td span{width:24px;height:24px;display:block;margin:0 auto;line-height:24px;position:absolute;left:50%;transform:translateX(-50%);border-radius:50%}.el-date-table td.next-month,.el-date-table td.prev-month{color:#c0c4cc}.el-date-table td.today{position:relative}.el-date-table td.today span{color:#409eff;font-weight:700}.el-date-table td.today.end-date span,.el-date-table td.today.start-date span{color:#fff}.el-date-table td.available:hover{color:#409eff}.el-date-table td.current:not(.disabled) span{color:#fff;background-color:#409eff}.el-date-table td.end-date div,.el-date-table td.start-date div{color:#fff}.el-date-table td.end-date span,.el-date-table td.start-date span{background-color:#409eff}.el-date-table td.start-date div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table td.end-date div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td.disabled div{background-color:#f5f7fa;opacity:1;cursor:not-allowed;color:#c0c4cc}.el-date-table td.selected div{margin-left:5px;margin-right:5px;background-color:#f2f6fc;border-radius:15px}.el-date-table td.selected div:hover{background-color:#f2f6fc}.el-date-table td.selected span{background-color:#409eff;color:#fff;border-radius:15px}.el-date-table td.week{font-size:80%;color:#606266}.el-month-table,.el-year-table{font-size:12px;border-collapse:collapse}.el-date-table th{padding:5px;color:#606266;font-weight:400;border-bottom:1px solid #ebeef5}.el-month-table{margin:-1px}.el-month-table td{text-align:center;padding:8px 0;cursor:pointer}.el-month-table td div{height:48px;padding:6px 0;box-sizing:border-box}.el-month-table td.today .cell{color:#409eff;font-weight:700}.el-month-table td.today.end-date .cell,.el-month-table td.today.start-date .cell{color:#fff}.el-month-table td.disabled .cell{background-color:#f5f7fa;cursor:not-allowed;color:#c0c4cc}.el-month-table td.disabled .cell:hover{color:#c0c4cc}.el-month-table td .cell{width:60px;height:36px;display:block;line-height:36px;color:#606266;margin:0 auto;border-radius:18px}.el-month-table td .cell:hover{color:#409eff}.el-month-table td.in-range div,.el-month-table td.in-range div:hover{background-color:#f2f6fc}.el-month-table td.end-date div,.el-month-table td.start-date div{color:#fff}.el-month-table td.end-date .cell,.el-month-table td.start-date .cell{color:#fff;background-color:#409eff}.el-month-table td.start-date div{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-month-table td.end-date div{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-month-table td.current:not(.disabled) .cell{color:#409eff}.el-year-table{margin:-1px}.el-year-table .el-icon{color:#303133}.el-year-table td{text-align:center;padding:20px 3px;cursor:pointer}.el-year-table td.today .cell{color:#409eff;font-weight:700}.el-year-table td.disabled .cell{background-color:#f5f7fa;cursor:not-allowed;color:#c0c4cc}.el-year-table td.disabled .cell:hover{color:#c0c4cc}.el-year-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#606266;margin:0 auto}.el-year-table td .cell:hover,.el-year-table td.current:not(.disabled) .cell{color:#409eff}.el-date-range-picker{width:646px}.el-date-range-picker.has-sidebar{width:756px}.el-date-range-picker table{table-layout:fixed;width:100%}.el-date-range-picker .el-picker-panel__body{min-width:513px}.el-date-range-picker .el-picker-panel__content{margin:0}.el-date-range-picker__header{position:relative;text-align:center;height:28px}.el-date-range-picker__header [class*=arrow-left]{float:left}.el-date-range-picker__header [class*=arrow-right]{float:right}.el-date-range-picker__header div{font-size:16px;font-weight:500;margin-right:50px}.el-date-range-picker__content{float:left;width:50%;box-sizing:border-box;margin:0;padding:16px}.el-date-range-picker__content.is-left{border-right:1px solid #e4e4e4}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;box-sizing:border-box}.el-date-range-picker__time-header>.el-icon-arrow-right{font-size:20px;vertical-align:middle;display:table-cell;color:#303133}.el-date-range-picker__time-picker-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-range-picker__time-picker-wrap .el-picker-panel{position:absolute;top:13px;right:0;z-index:1;background:#fff}.el-date-picker{width:322px}.el-date-picker.has-sidebar.has-time{width:434px}.el-date-picker.has-sidebar{width:438px}.el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-picker .el-picker-panel__content{width:292px}.el-date-picker table{table-layout:fixed;width:100%}.el-date-picker__editor-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;box-sizing:border-box}.el-date-picker__header{margin:12px;text-align:center}.el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:1px solid #ebeef5}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#606266}.el-date-picker__header-label.active,.el-date-picker__header-label:hover{color:#409eff}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{padding:10px;text-align:center}.el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.time-select{margin:5px 0;min-width:0}.time-select .el-picker-panel__content{max-height:200px;margin:0}.time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.time-select-item.selected:not(.disabled){color:#409eff;font-weight:700}.time-select-item.disabled{color:#e4e7ed;cursor:not-allowed}.time-select-item:hover{background-color:#f5f7fa;font-weight:700;cursor:pointer}.el-date-editor{position:relative;display:inline-block;text-align:left}.el-date-editor.el-input,.el-date-editor.el-input__inner{width:220px}.el-date-editor--monthrange.el-input,.el-date-editor--monthrange.el-input__inner{width:300px}.el-date-editor--daterange.el-input,.el-date-editor--daterange.el-input__inner,.el-date-editor--timerange.el-input,.el-date-editor--timerange.el-input__inner{width:350px}.el-date-editor--datetimerange.el-input,.el-date-editor--datetimerange.el-input__inner{width:400px}.el-date-editor--dates .el-input__inner{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .el-icon-circle-close{cursor:pointer}.el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#c0c4cc;float:left;line-height:32px}.el-date-editor .el-range-input,.el-date-editor .el-range-separator{height:100%;margin:0;text-align:center;display:inline-block;font-size:14px}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;padding:0;width:39%;color:#606266}.el-date-editor .el-range-input::-webkit-input-placeholder{color:#c0c4cc}.el-date-editor .el-range-input:-ms-input-placeholder,.el-date-editor .el-range-input::-ms-input-placeholder{color:#c0c4cc}.el-date-editor .el-range-input::placeholder{color:#c0c4cc}.el-date-editor .el-range-separator{padding:0 5px;line-height:32px;width:5%;color:#303133}.el-date-editor .el-range__close-icon{font-size:14px;color:#c0c4cc;width:25px;display:inline-block;float:right;line-height:32px}.el-range-editor.el-input__inner{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding:3px 10px}.el-range-editor .el-range-input{line-height:1}.el-range-editor.is-active,.el-range-editor.is-active:hover{border-color:#409eff}.el-range-editor--medium.el-input__inner{height:36px}.el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.el-range-editor--medium .el-range-input{font-size:14px}.el-range-editor--medium .el-range__close-icon,.el-range-editor--medium .el-range__icon{line-height:28px}.el-range-editor--small.el-input__inner{height:32px}.el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.el-range-editor--small .el-range-input{font-size:13px}.el-range-editor--small .el-range__close-icon,.el-range-editor--small .el-range__icon{line-height:24px}.el-range-editor--mini.el-input__inner{height:28px}.el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.el-range-editor--mini .el-range-input{font-size:12px}.el-range-editor--mini .el-range__close-icon,.el-range-editor--mini .el-range__icon{line-height:20px}.el-range-editor.is-disabled{background-color:#f5f7fa;border-color:#e4e7ed;color:#c0c4cc;cursor:not-allowed}.el-range-editor.is-disabled:focus,.el-range-editor.is-disabled:hover{border-color:#e4e7ed}.el-range-editor.is-disabled input{background-color:#f5f7fa;color:#c0c4cc;cursor:not-allowed}.el-range-editor.is-disabled input::-webkit-input-placeholder{color:#c0c4cc}.el-range-editor.is-disabled input:-ms-input-placeholder,.el-range-editor.is-disabled input::-ms-input-placeholder{color:#c0c4cc}.el-range-editor.is-disabled input::placeholder{color:#c0c4cc}.el-range-editor.is-disabled .el-range-separator{color:#c0c4cc}.el-picker-panel{color:#606266;border:1px solid #e4e7ed;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#fff;border-radius:4px;line-height:30px;margin:5px 0}.el-popover,.el-time-panel{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-picker-panel__body-wrapper:after,.el-picker-panel__body:after{content:"";display:table;clear:both}.el-picker-panel__content{position:relative;margin:15px}.el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#fff;position:relative;font-size:0}.el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#606266;padding-left:12px;text-align:left;outline:0;cursor:pointer}.el-picker-panel__shortcut:hover{color:#409eff}.el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#409eff}.el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.el-picker-panel__icon-btn{font-size:12px;color:#303133;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.el-picker-panel__icon-btn:hover{color:#409eff}.el-picker-panel__icon-btn.is-disabled{color:#bbb}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;box-sizing:border-box;padding-top:6px;background-color:#fff;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__input.el-input .el-input__inner,.el-time-spinner__list{padding:0;text-align:center}.el-time-spinner__wrapper.is-arrow{box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#fff;cursor:default}.el-time-spinner__arrow{font-size:12px;color:#909399;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.el-time-spinner__arrow:hover{color:#409eff}.el-time-spinner__arrow.el-icon-arrow-up{top:10px}.el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__list{margin:0;list-style:none}.el-time-spinner__list:after,.el-time-spinner__list:before{content:"";display:block;width:100%;height:80px}.el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#606266}.el-time-spinner__item:hover:not(.disabled):not(.active){background:#f5f7fa;cursor:pointer}.el-time-spinner__item.active:not(.disabled){color:#303133;font-weight:700}.el-time-spinner__item.disabled{color:#c0c4cc;cursor:not-allowed}.el-time-panel{margin:5px 0;border:1px solid #e4e7ed;background-color:#fff;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;user-select:none;box-sizing:content-box}.el-slider__button,.el-slider__button-wrapper,.el-time-panel{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content:after,.el-time-panel__content:before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #e4e7ed;border-bottom:1px solid #e4e7ed}.el-time-panel__content:after{left:50%;margin-left:12%;margin-right:12%}.el-time-panel__content:before{padding-left:50%;margin-right:12%;margin-left:12%}.el-time-panel__content.has-seconds:after{left:66.66667%}.el-time-panel__content.has-seconds:before{padding-left:33.33333%}.el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;box-sizing:border-box}.el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#303133}.el-time-panel__btn.confirm{font-weight:800;color:#409eff}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{position:relative;text-align:center;padding:10px}.el-time-range-picker__cell{box-sizing:border-box;margin:0;padding:4px 7px 7px;width:50%;display:inline-block}.el-time-range-picker__header{margin-bottom:5px;text-align:center;font-size:14px}.el-time-range-picker__body{border-radius:2px;border:1px solid #e4e7ed}.el-popover{position:absolute;background:#fff;min-width:150px;border:1px solid #ebeef5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.v-modal-enter{animation:v-modal-in .2s ease}.v-modal-leave{animation:v-modal-out .2s ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#fff;border-radius:4px;border:1px solid #ebeef5;font-size:18px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;overflow:hidden;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper:after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus,.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#f56c6c}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409eff}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__status{position:absolute;top:50%;transform:translateY(-50%);font-size:24px!important}.el-message-box__status:before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67c23a}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#e6a23c}.el-message-box__status.el-icon-error{color:#f56c6c}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#f56c6c;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{-ms-flex-direction:row-reverse;flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{animation:msgbox-fade-out .3s}@keyframes msgbox-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}to{transform:translateZ(0);opacity:1}}@keyframes msgbox-fade-out{0%{transform:translateZ(0);opacity:1}to{transform:translate3d(0,-20px,0);opacity:0}}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb:after,.el-breadcrumb:before{display:table;content:""}.el-breadcrumb:after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner.is-link,.el-breadcrumb__inner a{font-weight:700;text-decoration:none;transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner.is-link:hover,.el-breadcrumb__inner a:hover{color:#409eff;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{display:inline-block;margin-right:10px;vertical-align:top}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item:after,.el-form-item:before{display:table;content:""}.el-form-item:after{clear:both}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content:after,.el-form-item__content:before{display:table;content:""}.el-form-item__content:after{clear:both}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#f56c6c;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:"*";color:#f56c6c;margin-right:4px}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#f56c6c}.el-form-item--feedback .el-input__validateIcon{display:inline-block}.el-tabs__header{padding:0;position:relative;margin:0 0 15px}.el-tabs__active-bar{position:absolute;bottom:0;left:0;height:2px;background-color:#409eff;z-index:1;transition:transform .3s cubic-bezier(.645,.045,.355,1);list-style:none}.el-tabs__new-tab{float:right;border:1px solid #d3dce6;height:18px;width:18px;line-height:18px;margin:12px 0 9px 10px;border-radius:3px;text-align:center;font-size:12px;color:#d3dce6;cursor:pointer;transition:all .15s}.el-collapse-item__arrow,.el-tabs__nav{-webkit-transition:-webkit-transform .3s}.el-tabs__new-tab .el-icon-plus{transform:scale(.8)}.el-tabs__new-tab:hover{color:#409eff}.el-tabs__nav-wrap{overflow:hidden;margin-bottom:-1px;position:relative}.el-tabs__nav-wrap:after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background-color:#e4e7ed;z-index:1}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap:after,.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs__nav-wrap.is-scrollable{padding:0 20px;box-sizing:border-box}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{position:absolute;cursor:pointer;line-height:44px;font-size:12px;color:#909399}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;position:relative;transition:transform .3s;float:left;z-index:2}.el-tabs__nav.is-stretch{min-width:100%;display:-ms-flexbox;display:flex}.el-tabs__nav.is-stretch>*{-ms-flex:1;flex:1;text-align:center}.el-tabs__item{padding:0 20px;height:40px;box-sizing:border-box;line-height:40px;display:inline-block;list-style:none;font-size:14px;font-weight:500;color:#303133;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:0}.el-tabs__item:focus.is-active.is-focus:not(:active){box-shadow:inset 0 0 2px 2px #409eff;border-radius:3px}.el-tabs__item .el-icon-close{border-radius:50%;text-align:center;transition:all .3s cubic-bezier(.645,.045,.355,1);margin-left:5px}.el-tabs__item .el-icon-close:before{transform:scale(.9);display:inline-block}.el-tabs__item .el-icon-close:hover{background-color:#c0c4cc;color:#fff}.el-tabs__item.is-active{color:#409eff}.el-tabs__item:hover{color:#409eff;cursor:pointer}.el-tabs__item.is-disabled{color:#c0c4cc;cursor:default}.el-tabs__content{overflow:hidden;position:relative}.el-tabs--card>.el-tabs__header{border-bottom:1px solid #e4e7ed}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid #e4e7ed;border-bottom:none;border-radius:4px 4px 0 0;box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__active-bar{display:none}.el-tabs--card>.el-tabs__header .el-tabs__item .el-icon-close{position:relative;font-size:12px;width:0;height:14px;vertical-align:middle;line-height:15px;overflow:hidden;top:-1px;right:-2px;transform-origin:100% 50%}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .el-icon-close,.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .el-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid #e4e7ed;transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:#fff}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--border-card{background:#fff;border:1px solid #dcdfe6;box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:#f5f7fa;border-bottom:1px solid #e4e7ed;margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__item{transition:all .3s cubic-bezier(.645,.045,.355,1);border:1px solid transparent;margin-top:-1px;color:#909399}.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item,.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child{margin-left:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:#409eff;background-color:#fff;border-right-color:#dcdfe6;border-left-color:#dcdfe6}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:#409eff}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:#c0c4cc}.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2),.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),.el-tabs--top .el-tabs__item.is-top:nth-child(2){padding-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:last-child,.el-tabs--bottom .el-tabs__item.is-top:last-child,.el-tabs--top .el-tabs__item.is-bottom:last-child,.el-tabs--top .el-tabs__item.is-top:last-child{padding-right:0}.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--bottom .el-tabs__header.is-bottom{margin-bottom:0;margin-top:10px}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid #dcdfe6}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-top:-1px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid transparent}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-scroll,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{top:0;bottom:auto;width:2px;height:auto}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{height:30px;line-height:30px;width:100%;text-align:center;cursor:pointer}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i{transform:rotate(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{left:auto;top:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{right:auto;bottom:0}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__nav-wrap.is-left:after{right:0;left:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left:after,.el-tabs--left .el-tabs__nav-wrap.is-right:after,.el-tabs--right .el-tabs__nav-wrap.is-left:after,.el-tabs--right .el-tabs__nav-wrap.is-right:after{height:100%;width:2px;bottom:auto;top:0}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{float:none}.el-tabs--left .el-tabs__item.is-left,.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-right{display:block}.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left,.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs--left .el-tabs__header.is-left{float:left;margin-bottom:0;margin-right:10px}.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid #e4e7ed;border-bottom:none;border-top:1px solid #e4e7ed;text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid #e4e7ed;border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid #e4e7ed;border-right-color:#fff;border-left:none;border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-radius:4px 0 0 4px;border-bottom:1px solid #e4e7ed;border-right:none}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid #dfe4ed}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid transparent;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 transparent}.el-tabs--right .el-tabs__header.is-right{float:right;margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right:after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid #e4e7ed}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid #e4e7ed;border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid #e4e7ed;border-left-color:#fff;border-right:none;border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-radius:0 4px 4px 0;border-bottom:1px solid #e4e7ed;border-left:none}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid #dfe4ed}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid transparent;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 transparent}.slideInLeft-transition,.slideInRight-transition{display:inline-block}.slideInRight-enter{animation:slideInRight-enter .3s}.slideInRight-leave{position:absolute;left:0;right:0;animation:slideInRight-leave .3s}.slideInLeft-enter{animation:slideInLeft-enter .3s}.slideInLeft-leave{position:absolute;left:0;right:0;animation:slideInLeft-leave .3s}@keyframes slideInRight-enter{0%{opacity:0;transform-origin:0 0;transform:translateX(100%)}to{opacity:1;transform-origin:0 0;transform:translateX(0)}}@keyframes slideInRight-leave{0%{transform-origin:0 0;transform:translateX(0);opacity:1}to{transform-origin:0 0;transform:translateX(100%);opacity:0}}@keyframes slideInLeft-enter{0%{opacity:0;transform-origin:0 0;transform:translateX(-100%)}to{opacity:1;transform-origin:0 0;transform:translateX(0)}}@keyframes slideInLeft-leave{0%{transform-origin:0 0;transform:translateX(0);opacity:1}to{transform-origin:0 0;transform:translateX(-100%);opacity:0}}.el-tree{position:relative;cursor:default;background:#fff;color:#606266}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);color:#909399;font-size:14px}.el-tree__drop-indicator{position:absolute;left:0;right:0;height:1px;background-color:#409eff}.el-tree-node{white-space:nowrap;outline:0}.el-tree-node:focus>.el-tree-node__content{background-color:#f5f7fa}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:#409eff;color:#fff}.el-tree-node__content{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;height:26px;cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree-node__content:hover{background-color:#f5f7fa}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:#c0c4cc;font-size:12px;transform:rotate(0);transition:transform .3s ease-in-out}.el-tree-node__expand-icon.expanded{transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__label{font-size:14px}.el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#c0c4cc}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#f0f7ff}.el-alert{width:100%;padding:8px 16px;margin:0;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;transition:opacity .2s}.el-alert.is-light .el-alert__closebtn{color:#c0c4cc}.el-alert.is-dark .el-alert__closebtn,.el-alert.is-dark .el-alert__description{color:#fff}.el-alert.is-center{-ms-flex-pack:center;justify-content:center}.el-alert--success.is-light{background-color:#f0f9eb;color:#67c23a}.el-alert--success.is-light .el-alert__description{color:#67c23a}.el-alert--success.is-dark{background-color:#67c23a;color:#fff}.el-alert--info.is-light{background-color:#f4f4f5;color:#909399}.el-alert--info.is-dark{background-color:#909399;color:#fff}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning.is-light{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning.is-light .el-alert__description{color:#e6a23c}.el-alert--warning.is-dark{background-color:#e6a23c;color:#fff}.el-alert--error.is-light{background-color:#fef0f0;color:#f56c6c}.el-alert--error.is-light .el-alert__description{color:#f56c6c}.el-alert--error.is-dark{background-color:#f56c6c;color:#fff}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert-fade-enter,.el-alert-fade-leave-active,.el-loading-fade-enter,.el-loading-fade-leave-active,.el-notification-fade-leave-active{opacity:0}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-notification{display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;box-sizing:border-box;border:1px solid #ebeef5;position:fixed;background-color:#fff;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{margin-left:13px;margin-right:8px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67c23a}.el-notification .el-icon-error{color:#f56c6c}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#e6a23c}.el-notification-fade-enter.right{right:0;transform:translateX(100%)}.el-notification-fade-enter.left{left:0;transform:translateX(-100%)}.el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.el-input-number .el-input{display:block}.el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.el-input-number__decrease,.el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#f5f7fa;color:#606266;cursor:pointer;font-size:13px}.el-input-number__decrease:hover,.el-input-number__increase:hover{color:#409eff}.el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#409eff}.el-input-number__decrease.is-disabled,.el-input-number__increase.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #dcdfe6}.el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #dcdfe6}.el-input-number.is-disabled .el-input-number__decrease,.el-input-number.is-disabled .el-input-number__increase{border-color:#e4e7ed;color:#e4e7ed}.el-input-number.is-disabled .el-input-number__decrease:hover,.el-input-number.is-disabled .el-input-number__increase:hover{color:#e4e7ed;cursor:not-allowed}.el-input-number--medium{width:200px;line-height:34px}.el-input-number--medium .el-input-number__decrease,.el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.el-input-number--small{width:130px;line-height:30px}.el-input-number--small .el-input-number__decrease,.el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.el-input-number--small .el-input-number__decrease [class*=el-icon],.el-input-number--small .el-input-number__increase [class*=el-icon]{transform:scale(.9)}.el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.el-input-number--mini{width:130px;line-height:26px}.el-input-number--mini .el-input-number__decrease,.el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.el-input-number--mini .el-input-number__decrease [class*=el-icon],.el-input-number--mini .el-input-number__increase [class*=el-icon]{transform:scale(.8)}.el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.el-input-number.is-controls-right .el-input-number__decrease,.el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #dcdfe6}.el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #dcdfe6;border-radius:0 0 4px}.el-input-number.is-controls-right[class*=medium] [class*=decrease],.el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.el-input-number.is-controls-right[class*=small] [class*=decrease],.el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.el-input-number.is-controls-right[class*=mini] [class*=decrease],.el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow:after{content:" ";border-width:5px}.el-progress-bar__inner:after,.el-row:after,.el-row:before,.el-slider:after,.el-slider:before,.el-slider__button-wrapper:after,.el-upload-cover:after{content:""}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow:after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow:after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow:after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow:after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#fff}.el-tooltip__popper.is-light{background:#fff;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow:after{border-top-color:#fff}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow:after{border-bottom-color:#fff}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow:after{border-left-color:#fff}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow:after{border-right-color:#fff}.el-slider:after,.el-slider:before{display:table}.el-slider__button-wrapper .el-tooltip,.el-slider__button-wrapper:after{vertical-align:middle;display:inline-block}.el-slider:after{clear:both}.el-slider__runway{width:100%;height:6px;margin:16px 0;background-color:#e4e7ed;border-radius:3px;position:relative;cursor:pointer;vertical-align:middle}.el-slider__runway.show-input{margin-right:160px;width:auto}.el-slider__runway.disabled{cursor:default}.el-slider__runway.disabled .el-slider__bar{background-color:#c0c4cc}.el-slider__runway.disabled .el-slider__button{border-color:#c0c4cc}.el-slider__runway.disabled .el-slider__button-wrapper.dragging,.el-slider__runway.disabled .el-slider__button-wrapper.hover,.el-slider__runway.disabled .el-slider__button-wrapper:hover{cursor:not-allowed}.el-slider__runway.disabled .el-slider__button.dragging,.el-slider__runway.disabled .el-slider__button.hover,.el-slider__runway.disabled .el-slider__button:hover{transform:scale(1);cursor:not-allowed}.el-slider__button-wrapper,.el-slider__stop{-webkit-transform:translateX(-50%);position:absolute}.el-slider__input{float:right;margin-top:3px;width:130px}.el-slider__input.el-input-number--mini{margin-top:5px}.el-slider__input.el-input-number--medium{margin-top:0}.el-slider__input.el-input-number--large{margin-top:-2px}.el-slider__bar{height:6px;background-color:#409eff;border-top-left-radius:3px;border-bottom-left-radius:3px;position:absolute}.el-slider__button-wrapper{height:36px;width:36px;z-index:1001;top:-15px;transform:translateX(-50%);background-color:transparent;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:normal}.el-slider__button-wrapper:after{height:100%}.el-slider__button-wrapper.hover,.el-slider__button-wrapper:hover{cursor:grab}.el-slider__button-wrapper.dragging{cursor:grabbing}.el-slider__button{width:16px;height:16px;border:2px solid #409eff;background-color:#fff;border-radius:50%;transition:.2s;user-select:none}.el-image-viewer__btn,.el-slider__button,.el-step__icon-inner{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-slider__button.dragging,.el-slider__button.hover,.el-slider__button:hover{transform:scale(1.2)}.el-slider__button.hover,.el-slider__button:hover{cursor:grab}.el-slider__button.dragging{cursor:grabbing}.el-slider__stop{height:6px;width:6px;border-radius:100%;background-color:#fff;transform:translateX(-50%)}.el-slider__marks{top:0;left:12px;width:18px;height:100%}.el-slider__marks-text{position:absolute;transform:translateX(-50%);font-size:14px;color:#909399;margin-top:15px}.el-slider.is-vertical{position:relative}.el-slider.is-vertical .el-slider__runway{width:6px;height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:6px;height:auto;border-radius:0 0 3px 3px}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:-15px;transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{transform:translateY(50%)}.el-slider.is-vertical.el-slider--with-input{padding-bottom:58px}.el-slider.is-vertical.el-slider--with-input .el-slider__input{overflow:visible;float:none;position:absolute;bottom:22px;width:36px;margin-top:15px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input__inner{text-align:center;padding-left:5px;padding-right:5px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{top:32px;margin-top:-1px;border:1px solid #dcdfe6;line-height:20px;box-sizing:border-box;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease{width:18px;right:18px;border-bottom-left-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{width:19px;border-bottom-right-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase~.el-input .el-input__inner{border-bottom-left-radius:0;border-bottom-right-radius:0}.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__increase{border-color:#c0c4cc}.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__increase{border-color:#409eff}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;transform:translateY(50%)}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:hsla(0,0%,100%,.9);margin:0;top:0;right:0;bottom:0;left:0;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-col-pull-0,.el-col-pull-1,.el-col-pull-2,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-pull-10,.el-col-pull-11,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-push-0,.el-col-push-1,.el-col-push-2,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24,.el-row{position:relative}.el-loading-spinner .el-loading-text{color:#409eff;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#409eff;stroke-linecap:round}.el-loading-spinner i{color:#409eff}@keyframes loading-rotate{to{transform:rotate(1turn)}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.el-row{box-sizing:border-box}.el-row:after,.el-row:before{display:table}.el-row:after{clear:both}.el-row--flex{display:-ms-flexbox;display:flex}.el-col-0,.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-middle{-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-ms-flex-align:end;align-items:flex-end}[class*=el-col-]{float:left;box-sizing:border-box}.el-upload--picture-card,.el-upload-dragger{-webkit-box-sizing:border-box;cursor:pointer}.el-col-0{width:0}.el-col-offset-0{margin-left:0}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{position:relative;right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0}.el-col-xs-offset-0{margin-left:0}.el-col-xs-pull-0{position:relative;right:0}.el-col-xs-push-0{position:relative;left:0}.el-col-xs-1{width:4.16667%}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{width:8.33333%}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{width:12.5%}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{display:none;width:0}.el-col-sm-offset-0{margin-left:0}.el-col-sm-pull-0{position:relative;right:0}.el-col-sm-push-0{position:relative;left:0}.el-col-sm-1{width:4.16667%}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{width:8.33333%}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{width:12.5%}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{display:none;width:0}.el-col-md-offset-0{margin-left:0}.el-col-md-pull-0{position:relative;right:0}.el-col-md-push-0{position:relative;left:0}.el-col-md-1{width:4.16667%}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{width:8.33333%}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{width:12.5%}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{display:none;width:0}.el-col-lg-offset-0{margin-left:0}.el-col-lg-pull-0{position:relative;right:0}.el-col-lg-push-0{position:relative;left:0}.el-col-lg-1{width:4.16667%}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{width:8.33333%}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{width:12.5%}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{display:none;width:0}.el-col-xl-offset-0{margin-left:0}.el-col-xl-pull-0{position:relative;right:0}.el-col-xl-push-0{position:relative;left:0}.el-col-xl-1{width:4.16667%}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{width:8.33333%}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{width:12.5%}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}.el-upload{display:inline-block;text-align:center;cursor:pointer;outline:0}.el-upload__input{display:none}.el-upload__tip{font-size:12px;color:#606266;margin-top:7px}.el-upload iframe{position:absolute;z-index:-1;top:0;left:0;opacity:0;filter:alpha(opacity=0)}.el-upload--picture-card{background-color:#fbfdff;border:1px dashed #c0ccda;border-radius:6px;box-sizing:border-box;width:148px;height:148px;line-height:146px;vertical-align:top}.el-upload--picture-card i{font-size:28px;color:#8c939d}.el-upload--picture-card:hover,.el-upload:focus{border-color:#409eff;color:#409eff}.el-upload:focus .el-upload-dragger{border-color:#409eff}.el-upload-dragger{background-color:#fff;border:1px dashed #d9d9d9;border-radius:6px;box-sizing:border-box;width:360px;height:180px;text-align:center;position:relative;overflow:hidden}.el-upload-dragger .el-icon-upload{font-size:67px;color:#c0c4cc;margin:40px 0 16px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:1px solid #dcdfe6;margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:#606266;font-size:14px;text-align:center}.el-upload-dragger .el-upload__text em{color:#409eff;font-style:normal}.el-upload-dragger:hover{border-color:#409eff}.el-upload-dragger.is-dragover{background-color:rgba(32,159,255,.06);border:2px dashed #409eff}.el-upload-list{margin:0;padding:0;list-style:none}.el-upload-list__item{transition:all .5s cubic-bezier(.55,0,.1,1);font-size:14px;color:#606266;line-height:1.8;margin-top:5px;position:relative;box-sizing:border-box;border-radius:4px;width:100%}.el-upload-list__item .el-progress{position:absolute;top:20px;width:100%}.el-upload-list__item .el-progress__text{position:absolute;right:0;top:-13px}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item:first-child{margin-top:10px}.el-upload-list__item .el-icon-upload-success{color:#67c23a}.el-upload-list__item .el-icon-close{display:none;position:absolute;top:5px;right:5px;cursor:pointer;opacity:.75;color:#606266}.el-upload-list__item .el-icon-close:hover{opacity:1}.el-upload-list__item .el-icon-close-tip{display:none;position:absolute;top:5px;right:5px;font-size:12px;cursor:pointer;opacity:1;color:#409eff}.el-upload-list__item:hover{background-color:#f5f7fa}.el-upload-list__item:hover .el-icon-close{display:inline-block}.el-upload-list__item:hover .el-progress__text{display:none}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:block}.el-upload-list__item.is-success .el-upload-list__item-name:focus,.el-upload-list__item.is-success .el-upload-list__item-name:hover{color:#409eff;cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon-close-tip{display:inline-block}.el-upload-list__item.is-success:active .el-icon-close-tip,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:not(.focusing):focus .el-icon-close-tip{display:none}.el-upload-list.is-disabled .el-upload-list__item:hover .el-upload-list__item-status-label{display:block}.el-upload-list__item-name{color:#606266;display:block;margin-right:40px;overflow:hidden;padding-left:4px;text-overflow:ellipsis;transition:color .3s;white-space:nowrap}.el-upload-list__item-name [class^=el-icon]{height:100%;margin-right:7px;color:#909399;line-height:inherit}.el-upload-list__item-status-label{position:absolute;right:5px;top:0;line-height:inherit;display:none}.el-upload-list__item-delete{position:absolute;right:10px;top:0;font-size:12px;color:#606266;display:none}.el-upload-list__item-delete:hover{color:#409eff}.el-upload-list--picture-card{margin:0;display:inline;vertical-align:top}.el-upload-list--picture-card .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;box-sizing:border-box;width:148px;height:148px;margin:0 8px 8px 0;display:inline-block}.el-upload-list--picture-card .el-upload-list__item .el-icon-check,.el-upload-list--picture-card .el-upload-list__item .el-icon-circle-check{color:#fff}.el-upload-list--picture-card .el-upload-list__item .el-icon-close,.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;transform:rotate(45deg);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-list--picture-card .el-upload-list__item-status-label i{font-size:12px;margin-top:11px;transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{position:absolute;width:100%;height:100%;left:0;top:0;cursor:default;text-align:center;color:#fff;opacity:0;font-size:20px;background-color:rgba(0,0,0,.5);transition:opacity .3s}.el-upload-list--picture-card .el-upload-list__item-actions:after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-upload-list--picture-card .el-upload-list__item-actions span{display:none;cursor:pointer}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:15px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{position:static;font-size:inherit;color:inherit}.el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-block}.el-upload-list--picture-card .el-progress{top:50%;left:50%;transform:translate(-50%,-50%);bottom:auto;width:126px}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{overflow:hidden;z-index:0;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;box-sizing:border-box;margin-top:10px;padding:10px 10px 10px 90px;height:92px}.el-upload-list--picture .el-upload-list__item .el-icon-check,.el-upload-list--picture .el-upload-list__item .el-icon-circle-check{color:#fff}.el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{background:0 0;box-shadow:none;top:-2px;right:-12px}.el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name{line-height:70px;margin-top:0}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.el-upload-list--picture .el-upload-list__item-thumbnail{vertical-align:middle;display:inline-block;width:70px;height:70px;float:left;position:relative;z-index:1;margin-left:-80px;background-color:#fff}.el-upload-list--picture .el-upload-list__item-name{display:block;margin-top:20px}.el-upload-list--picture .el-upload-list__item-name i{font-size:70px;line-height:1;position:absolute;left:9px;top:10px}.el-upload-list--picture .el-upload-list__item-status-label{position:absolute;right:-17px;top:-7px;width:46px;height:26px;background:#13ce66;text-align:center;transform:rotate(45deg);box-shadow:0 1px 1px #ccc}.el-upload-list--picture .el-upload-list__item-status-label i{font-size:12px;margin-top:12px;transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;z-index:10;cursor:default}.el-upload-cover:after{display:inline-block;height:100%;vertical-align:middle}.el-upload-cover img{display:block;width:100%;height:100%}.el-upload-cover__label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;transform:rotate(45deg);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-cover__label i{font-size:12px;margin-top:11px;transform:rotate(-45deg);color:#fff}.el-upload-cover__progress{display:inline-block;vertical-align:middle;position:static;width:243px}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{position:absolute;top:0;left:0;width:100%;height:100%}.el-upload-cover__interact{position:absolute;bottom:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.72);text-align:center}.el-upload-cover__interact .btn{display:inline-block;color:#fff;font-size:14px;cursor:pointer;vertical-align:middle;transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);margin-top:60px}.el-upload-cover__interact .btn span{opacity:0;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#fff;display:block;font-size:24px;line-height:inherit;margin:0 auto 5px}.el-upload-cover__title{position:absolute;bottom:0;left:0;background-color:#fff;height:36px;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;text-align:left;padding:0 10px;margin:0;line-height:36px;font-size:14px;color:#303133}.el-upload-cover+.el-upload__inner{opacity:0;position:relative;z-index:1}.el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle,.el-progress--dashboard{display:inline-block}.el-progress--circle .el-progress__text,.el-progress--dashboard .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;transform:translateY(-50%)}.el-progress--circle .el-progress__text i,.el-progress--dashboard .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.el-progress-bar,.el-progress-bar__inner:after,.el-progress-bar__innerText,.el-spinner{display:inline-block;vertical-align:middle}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67c23a}.el-progress.is-success .el-progress__text{color:#67c23a}.el-progress.is-warning .el-progress-bar__inner{background-color:#e6a23c}.el-progress.is-warning .el-progress__text{color:#e6a23c}.el-progress.is-exception .el-progress-bar__inner{background-color:#f56c6c}.el-progress.is-exception .el-progress__text{color:#f56c6c}.el-progress-bar{padding-right:50px;width:100%;margin-right:-55px;box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#ebeef5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#409eff;text-align:right;border-radius:100px;line-height:1;white-space:nowrap;transition:width .6s ease}.el-card,.el-message{border-radius:4px;overflow:hidden}.el-progress-bar__inner:after{height:100%}.el-progress-bar__innerText{color:#fff;font-size:12px;margin:0 5px}@keyframes progress{0%{background-position:0 0}to{background-position:32px 0}}.el-time-spinner{width:100%;white-space:nowrap}.el-spinner-inner{animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;animation:dash 1.5s ease-in-out infinite}@keyframes rotate{to{transform:rotate(1turn)}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}to{stroke-dasharray:90,150;stroke-dashoffset:-124}}.el-message{min-width:380px;box-sizing:border-box;border:1px solid #ebeef5;position:fixed;left:50%;top:20px;transform:translateX(-50%);background-color:#edf2fc;transition:opacity .3s,transform .4s,top .4s;padding:15px 15px 15px 20px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.el-message.is-center{-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67c23a}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#e6a23c}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#f56c6c}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;transform:translateY(-50%);cursor:pointer;color:#c0c4cc;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67c23a}.el-message .el-icon-error{color:#f56c6c}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#e6a23c}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;transform:translate(-50%,-100%)}.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#f56c6c;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;transform:translateY(-50%) translateX(100%)}.el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#409eff}.el-badge__content--success{background-color:#67c23a}.el-badge__content--warning{background-color:#e6a23c}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#f56c6c}.el-card{border:1px solid #ebeef5;background-color:#fff;color:#303133;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;box-sizing:border-box}.el-card__body{padding:20px}.el-rate{height:20px;line-height:1}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#c0c4cc;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-step.is-vertical,.el-steps{display:-ms-flexbox}.el-rate__text{font-size:14px;vertical-align:middle}.el-steps{display:-ms-flexbox;display:flex}.el-steps--simple{padding:13px 8%;border-radius:4px;background:#f5f7fa}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{height:100%;-ms-flex-flow:column;flex-flow:column}.el-step{position:relative;-ms-flex-negative:1;flex-shrink:1}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{-ms-flex-preferred-size:auto!important;flex-basis:auto!important;-ms-flex-negative:0;flex-shrink:0;-ms-flex-positive:0;flex-grow:0}.el-step:last-of-type .el-step__description,.el-step:last-of-type .el-step__main{padding-right:0}.el-step__head{position:relative;width:100%}.el-step__head.is-process{color:#303133;border-color:#303133}.el-step__head.is-wait{color:#c0c4cc;border-color:#c0c4cc}.el-step__head.is-success{color:#67c23a;border-color:#67c23a}.el-step__head.is-error{color:#f56c6c;border-color:#f56c6c}.el-step__head.is-finish{color:#409eff;border-color:#409eff}.el-step__icon{position:relative;z-index:1;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;width:24px;height:24px;font-size:14px;box-sizing:border-box;background:#fff;transition:.15s ease-out}.el-step__icon.is-text{border-radius:50%;border:2px solid;border-color:inherit}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{display:inline-block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-align:center;font-weight:700;line-height:1;color:inherit}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{transform:translateY(1px)}.el-step__line{position:absolute;border-color:inherit;background-color:#c0c4cc}.el-step__line-inner{display:block;border-width:1px;border-style:solid;border-color:inherit;transition:.15s ease-out;box-sizing:border-box;width:0;height:0}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{font-weight:700;color:#303133}.el-step__title.is-wait{color:#c0c4cc}.el-step__title.is-success{color:#67c23a}.el-step__title.is-error{color:#f56c6c}.el-step__title.is-finish{color:#409eff}.el-step__description{padding-right:10%;margin-top:-5px;font-size:12px;line-height:20px;font-weight:400}.el-step__description.is-process{color:#303133}.el-step__description.is-wait{color:#c0c4cc}.el-step__description.is-success{color:#67c23a}.el-step__description.is-error{color:#f56c6c}.el-step__description.is-finish{color:#409eff}.el-step.is-horizontal{display:inline-block}.el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.el-step.is-vertical{display:-ms-flexbox;display:flex}.el-step.is-vertical .el-step__head{-ms-flex-positive:0;flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{padding-left:10px;-ms-flex-positive:1;flex-grow:1}.el-step.is-vertical .el-step__title{line-height:24px;padding-bottom:8px}.el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.el-step.is-vertical .el-step__icon.is-icon{width:24px}.el-step.is-center .el-step__head,.el-step.is-center .el-step__main{text-align:center}.el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.el-step.is-center .el-step__line{left:50%;right:-50%}.el-step.is-simple{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.el-step.is-simple .el-step__head{width:auto;font-size:0;padding-right:10px}.el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.el-step.is-simple .el-step__icon-inner.is-status{transform:scale(.8) translateY(1px)}.el-step.is-simple .el-step__main{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:stretch;align-items:stretch;-ms-flex-positive:1;flex-grow:1}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{max-width:50%;word-break:break-all}.el-step.is-simple .el-step__arrow{-ms-flex-positive:1;flex-grow:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.el-step.is-simple .el-step__arrow:after,.el-step.is-simple .el-step__arrow:before{content:"";display:inline-block;position:absolute;height:15px;width:1px;background:#c0c4cc}.el-step.is-simple .el-step__arrow:before{transform:rotate(-45deg) translateY(-4px);transform-origin:0 0}.el-step.is-simple .el-step__arrow:after{transform:rotate(45deg) translateY(4px);transform-origin:100% 100%}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-carousel{position:relative}.el-carousel--horizontal{overflow-x:hidden}.el-carousel--vertical{overflow-y:hidden}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;margin:0;padding:0;z-index:2}.el-carousel__indicators--horizontal{bottom:0;left:50%;transform:translateX(-50%)}.el-carousel__indicators--vertical{right:0;top:50%;transform:translateY(-50%)}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#c0c4cc;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{background-color:transparent;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{display:inline-block;padding:12px 4px}.el-carousel__indicator--vertical{padding:4px 12px}.el-carousel__indicator--vertical .el-carousel__button{width:2px;height:15px}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;transition:.3s}.el-carousel__item,.el-carousel__mask{height:100%;top:0;left:0;position:absolute}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{transform:translateY(-50%) translateX(10px);opacity:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item--card,.el-carousel__item.is-animating{transition:transform .4s ease-in-out}.el-carousel__item--card{width:50%}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#fff;opacity:.24;transition:.2s}.el-fade-in-enter,.el-fade-in-leave-active,.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active,.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:opacity .2s linear}.el-fade-in-enter-active,.el-fade-in-leave-active,.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transform:scale(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45)}.collapse-transition{transition:height .3s ease-in-out,padding-top .3s ease-in-out,padding-bottom .3s ease-in-out}.horizontal-collapse-transition{transition:width .3s ease-in-out,padding-left .3s ease-in-out,padding-right .3s ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;transform:translateY(-30px)}.el-opacity-transition{transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-collapse{border-top:1px solid #ebeef5;border-bottom:1px solid #ebeef5}.el-collapse-item.is-disabled .el-collapse-item__header{color:#bbb;cursor:not-allowed}.el-collapse-item__header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;height:48px;line-height:48px;background-color:#fff;color:#303133;cursor:pointer;border-bottom:1px solid #ebeef5;font-size:13px;font-weight:500;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin:0 8px 0 auto;transition:transform .3s;font-weight:300}.el-collapse-item__arrow.is-active{transform:rotate(90deg)}.el-collapse-item__header.focusing:focus:not(:hover){color:#409eff}.el-collapse-item__header.is-active{border-bottom-color:transparent}.el-collapse-item__wrap{will-change:height;background-color:#fff;overflow:hidden;box-sizing:border-box;border-bottom:1px solid #ebeef5}.el-cascader__tags,.el-tag{-webkit-box-sizing:border-box}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item:last-child{margin-bottom:-1px}.el-popper .popper__arrow,.el-popper .popper__arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;filter:drop-shadow(0 2px 12px rgba(0,0,0,.03))}.el-popper .popper__arrow:after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow:after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow:after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow:after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow:after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-tag{background-color:#ecf5ff;border:1px solid #d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409eff;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409eff}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67c23a}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close:before{display:block}.el-tag--dark{background-color:#409eff;color:#fff}.el-tag--dark,.el-tag--dark.is-hit{border-color:#409eff}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#fff;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#fff;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67c23a}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#fff;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409eff}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}.el-cascader{display:inline-block;position:relative;font-size:14px;line-height:40px}.el-cascader:not(.is-disabled):hover .el-input__inner{cursor:pointer;border-color:#c0c4cc}.el-cascader .el-input .el-input__inner:focus,.el-cascader .el-input.is-focus .el-input__inner{border-color:#409eff}.el-cascader .el-input{cursor:pointer}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-icon-arrow-down{transition:transform .3s;font-size:14px}.el-cascader .el-input .el-icon-arrow-down.is-reverse{transform:rotate(180deg)}.el-cascader .el-input .el-icon-circle-close:hover{color:#909399}.el-cascader--medium{font-size:14px;line-height:36px}.el-cascader--small{font-size:13px;line-height:32px}.el-cascader--mini{font-size:12px;line-height:28px}.el-cascader.is-disabled .el-cascader__label{z-index:2;color:#c0c4cc}.el-cascader__dropdown{margin:5px 0;font-size:14px;background:#fff;border:1px solid #e4e7ed;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-cascader__tags{position:absolute;left:0;right:30px;top:50%;transform:translateY(-50%);display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;line-height:normal;text-align:left;box-sizing:border-box}.el-cascader__tags .el-tag{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;max-width:100%;margin:2px 0 2px 6px;text-overflow:ellipsis;background:#f0f2f5}.el-cascader__tags .el-tag:not(.is-hit){border-color:transparent}.el-cascader__tags .el-tag>span{-ms-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis}.el-cascader__tags .el-tag .el-icon-close{-ms-flex:none;flex:none;background-color:#c0c4cc;color:#fff}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:#909399}.el-cascader__suggestion-panel{border-radius:4px}.el-cascader__suggestion-list{max-height:204px;margin:0;padding:6px 0;font-size:14px;color:#606266;text-align:center}.el-cascader__suggestion-item{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;height:34px;padding:0 15px;text-align:left;outline:0;cursor:pointer}.el-cascader__suggestion-item:focus,.el-cascader__suggestion-item:hover{background:#f5f7fa}.el-cascader__suggestion-item.is-checked{color:#409eff;font-weight:700}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__empty-text{margin:10px 0;color:#c0c4cc}.el-cascader__search-input{-ms-flex:1;flex:1;height:24px;min-width:60px;margin:2px 0 2px 15px;padding:0;color:#606266;border:none;outline:0;box-sizing:border-box}.el-cascader__search-input::-webkit-input-placeholder{color:#c0c4cc}.el-cascader__search-input:-ms-input-placeholder,.el-cascader__search-input::-ms-input-placeholder{color:#c0c4cc}.el-cascader__search-input::placeholder{color:#c0c4cc}.el-color-predefine{display:-ms-flexbox;display:flex;font-size:12px;margin-top:8px;width:280px}.el-color-predefine__colors{display:-ms-flexbox;display:flex;-ms-flex:1;flex:1;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-color-predefine__color-selector{margin:0 0 8px 8px;width:20px;height:20px;border-radius:4px;cursor:pointer}.el-color-predefine__color-selector:nth-child(10n+1){margin-left:0}.el-color-predefine__color-selector.selected{box-shadow:0 0 3px 2px #409eff}.el-color-predefine__color-selector>div{display:-ms-flexbox;display:flex;height:100%;border-radius:3px}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{position:relative;box-sizing:border-box;width:280px;height:12px;background-color:red;padding:0 2px}.el-color-hue-slider__bar{position:relative;background:linear-gradient(90deg,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red);height:100%}.el-color-hue-slider__thumb{position:absolute;cursor:pointer;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:linear-gradient(180deg,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-svpanel{position:relative;width:280px;height:180px}.el-color-svpanel__black,.el-color-svpanel__white{position:absolute;top:0;left:0;right:0;bottom:0}.el-color-svpanel__white{background:linear-gradient(90deg,#fff,hsla(0,0%,100%,0))}.el-color-svpanel__black{background:linear-gradient(0deg,#000,transparent)}.el-color-svpanel__cursor{position:absolute}.el-color-svpanel__cursor>div{cursor:head;width:4px;height:4px;box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;transform:translate(-2px,-2px)}.el-color-alpha-slider{position:relative;box-sizing:border-box;width:280px;height:12px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-alpha-slider__bar{position:relative;background:linear-gradient(90deg,hsla(0,0%,100%,0) 0,#fff);height:100%}.el-color-alpha-slider__thumb{position:absolute;cursor:pointer;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:linear-gradient(180deg,hsla(0,0%,100%,0) 0,#fff)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-dropdown{width:300px}.el-color-dropdown__main-wrapper{margin-bottom:6px}.el-color-dropdown__main-wrapper:after{content:"";display:table;clear:both}.el-color-dropdown__btns{margin-top:6px;text-align:right}.el-color-dropdown__value{float:left;line-height:26px;font-size:12px;color:#000;width:160px}.el-color-dropdown__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-color-dropdown__btn[disabled]{color:#ccc;cursor:not-allowed}.el-color-dropdown__btn:hover{color:#409eff;border-color:#409eff}.el-color-dropdown__link-btn{cursor:pointer;color:#409eff;text-decoration:none;padding:15px;font-size:12px}.el-color-dropdown__link-btn:hover{color:tint(#409eff,20%)}.el-color-picker{display:inline-block;position:relative;line-height:normal;height:40px}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed}.el-color-picker--medium{height:36px}.el-color-picker--medium .el-color-picker__trigger{height:36px;width:36px}.el-color-picker--medium .el-color-picker__mask{height:34px;width:34px}.el-color-picker--small{height:32px}.el-color-picker--small .el-color-picker__trigger{height:32px;width:32px}.el-color-picker--small .el-color-picker__mask{height:30px;width:30px}.el-color-picker--small .el-color-picker__empty,.el-color-picker--small .el-color-picker__icon{transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker--mini{height:28px}.el-color-picker--mini .el-color-picker__trigger{height:28px;width:28px}.el-color-picker--mini .el-color-picker__mask{height:26px;width:26px}.el-color-picker--mini .el-color-picker__empty,.el-color-picker--mini .el-color-picker__icon{transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker__mask{height:38px;width:38px;border-radius:4px;position:absolute;top:1px;left:1px;z-index:1;cursor:not-allowed;background-color:hsla(0,0%,100%,.7)}.el-color-picker__trigger{display:inline-block;box-sizing:border-box;height:40px;width:40px;padding:4px;border:1px solid #e6e6e6;border-radius:4px;font-size:0;position:relative;cursor:pointer}.el-color-picker__color{position:relative;display:block;box-sizing:border-box;border:1px solid #999;border-radius:2px;width:100%;height:100%;text-align:center}.el-color-picker__color.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-picker__color-inner{position:absolute;left:0;top:0;right:0;bottom:0}.el-color-picker__empty,.el-color-picker__icon{top:50%;left:50%;font-size:12px;position:absolute}.el-color-picker__empty{color:#999;transform:translate3d(-50%,-50%,0)}.el-color-picker__icon{display:inline-block;width:100%;transform:translate3d(-50%,-50%,0);color:#fff;text-align:center}.el-color-picker__panel{position:absolute;z-index:10;padding:6px;box-sizing:content-box;background-color:#fff;border:1px solid #ebeef5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-textarea{position:relative;display:inline-block;width:100%;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#fff;background-image:none;border:1px solid #dcdfe6;border-radius:4px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner::-webkit-input-placeholder{color:#c0c4cc}.el-textarea__inner:-ms-input-placeholder,.el-textarea__inner::-ms-input-placeholder{color:#c0c4cc}.el-textarea__inner::placeholder{color:#c0c4cc}.el-textarea__inner:hover{border-color:#c0c4cc}.el-textarea__inner:focus{outline:0;border-color:#409eff}.el-textarea .el-input__count{color:#909399;background:#fff;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#e4e7ed;color:#c0c4cc;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#c0c4cc}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder,.el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#c0c4cc}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#c0c4cc}.el-textarea.is-exceed .el-textarea__inner{border-color:#f56c6c}.el-textarea.is-exceed .el-input__count{color:#f56c6c}.el-input{position:relative;font-size:14px;display:inline-block;width:100%}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner,.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#c0c4cc;font-size:14px;cursor:pointer;transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;color:#909399;font-size:12px}.el-input .el-input__count .el-input__count-inner{background:#fff;line-height:normal;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #dcdfe6;box-sizing:border-box;color:#606266;display:inline-block;font-size:inherit;height:40px;line-height:40px;outline:0;padding:0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__prefix,.el-input__suffix{position:absolute;top:0;-webkit-transition:all .3s;height:100%;color:#c0c4cc;text-align:center}.el-input__inner::-webkit-input-placeholder{color:#c0c4cc}.el-input__inner:-ms-input-placeholder,.el-input__inner::-ms-input-placeholder{color:#c0c4cc}.el-input__inner::placeholder{color:#c0c4cc}.el-input__inner:hover{border-color:#c0c4cc}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409eff;outline:0}.el-input__suffix{right:5px;transition:all .3s}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;transition:all .3s;line-height:40px}.el-input__icon:after{content:"";height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#e4e7ed;color:#c0c4cc;cursor:not-allowed}.el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__inner:-ms-input-placeholder,.el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__inner::placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-link,.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-input.is-exceed .el-input__inner{border-color:#f56c6c}.el-input.is-exceed .el-input__suffix .el-input__count{color:#f56c6c}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#f5f7fa;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #dcdfe6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--prepend .el-input__inner,.el-input-group__append{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-transfer{font-size:14px}.el-transfer__buttons{display:inline-block;vertical-align:middle;padding:0 30px}.el-transfer__button{display:block;margin:0 auto;padding:10px;border-radius:50%;color:#fff;background-color:#409eff;font-size:0}.el-transfer-panel__item+.el-transfer-panel__item,.el-transfer__button [class*=el-icon-]+span{margin-left:0}.el-transfer__button.is-with-texts{border-radius:4px}.el-transfer__button.is-disabled,.el-transfer__button.is-disabled:hover{border:1px solid #dcdfe6;background-color:#f5f7fa;color:#c0c4cc}.el-transfer__button:first-child{margin-bottom:10px}.el-transfer__button:nth-child(2){margin:0}.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer-panel{border:1px solid #ebeef5;border-radius:4px;overflow:hidden;background:#fff;display:inline-block;vertical-align:middle;width:200px;max-height:100%;box-sizing:border-box;position:relative}.el-transfer-panel__body{height:246px}.el-transfer-panel__body.is-with-footer{padding-bottom:40px}.el-transfer-panel__list{margin:0;padding:6px 0;list-style:none;height:246px;overflow:auto;box-sizing:border-box}.el-transfer-panel__list.is-filterable{height:194px;padding-top:0}.el-transfer-panel__item{height:30px;line-height:30px;padding-left:15px;display:block!important}.el-transfer-panel__item.el-checkbox{color:#606266}.el-transfer-panel__item:hover{color:#409eff}.el-transfer-panel__item.el-checkbox .el-checkbox__label{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;box-sizing:border-box;padding-left:24px;line-height:30px}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;margin:15px;box-sizing:border-box;display:block;width:auto}.el-transfer-panel__filter .el-input__inner{height:32px;width:100%;font-size:12px;display:inline-block;box-sizing:border-box;border-radius:16px;padding-right:10px;padding-left:30px}.el-transfer-panel__filter .el-input__icon{margin-left:5px}.el-transfer-panel .el-transfer-panel__header{height:40px;line-height:40px;background:#f5f7fa;margin:0;padding-left:15px;border-bottom:1px solid #ebeef5;box-sizing:border-box;color:#000}.el-transfer-panel .el-transfer-panel__header .el-checkbox{display:block;line-height:40px}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{font-size:16px;color:#303133;font-weight:400}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label span{position:absolute;right:15px;color:#909399;font-size:12px;font-weight:400}.el-divider__text,.el-link{font-weight:500;font-size:14px}.el-transfer-panel .el-transfer-panel__footer{height:40px;background:#fff;margin:0;padding:0;border-top:1px solid #ebeef5;position:absolute;bottom:0;left:0;width:100%;z-index:1}.el-transfer-panel .el-transfer-panel__footer:after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-container,.el-timeline-item__node{display:-ms-flexbox}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{padding-left:20px;color:#606266}.el-transfer-panel .el-transfer-panel__empty{margin:0;height:30px;line-height:30px;padding:6px 15px 0;color:#909399;text-align:center}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-transfer-panel .el-checkbox__inner{height:14px;width:14px;border-radius:3px}.el-transfer-panel .el-checkbox__inner:after{height:6px;width:3px;left:4px}.el-container{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;box-sizing:border-box;min-width:0}.el-aside,.el-header{-webkit-box-sizing:border-box}.el-container.is-vertical{-ms-flex-direction:column;flex-direction:column}.el-header{padding:0 20px}.el-aside,.el-header{box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-aside{overflow:auto}.el-footer,.el-main{-webkit-box-sizing:border-box}.el-main{display:block;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;overflow:auto;padding:20px}.el-footer,.el-main{box-sizing:border-box}.el-footer{padding:0 20px;-ms-flex-negative:0;flex-shrink:0}.el-timeline{margin:0;font-size:14px;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline-item{position:relative;padding-bottom:20px}.el-timeline-item__wrapper{position:relative;padding-left:28px;top:-3px}.el-timeline-item__tail{position:absolute;left:4px;height:100%;border-left:2px solid #e4e7ed}.el-timeline-item__icon{color:#fff;font-size:13px}.el-timeline-item__node{position:absolute;background-color:#e4e7ed;border-radius:50%;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.el-image__error,.el-timeline-item__dot{display:-ms-flexbox}.el-timeline-item__node--normal{left:-1px;width:12px;height:12px}.el-timeline-item__node--large{left:-2px;width:14px;height:14px}.el-timeline-item__node--primary{background-color:#409eff}.el-timeline-item__node--success{background-color:#67c23a}.el-timeline-item__node--warning{background-color:#e6a23c}.el-timeline-item__node--danger{background-color:#f56c6c}.el-timeline-item__node--info{background-color:#909399}.el-timeline-item__dot{position:absolute;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.el-timeline-item__content{color:#303133}.el-timeline-item__timestamp{color:#909399;line-height:1;font-size:13px}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-link{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;vertical-align:middle;position:relative;text-decoration:none;outline:0;padding:0}.el-link.is-underline:hover:after{content:"";position:absolute;left:0;right:0;height:0;bottom:0;border-bottom:1px solid #409eff}.el-link.el-link--default:after,.el-link.el-link--primary.is-underline:hover:after,.el-link.el-link--primary:after{border-color:#409eff}.el-link.is-disabled{cursor:not-allowed}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link.el-link--default{color:#606266}.el-link.el-link--default:hover{color:#409eff}.el-link.el-link--default.is-disabled{color:#c0c4cc}.el-link.el-link--primary{color:#409eff}.el-link.el-link--primary:hover{color:#66b1ff}.el-link.el-link--primary.is-disabled{color:#a0cfff}.el-link.el-link--danger.is-underline:hover:after,.el-link.el-link--danger:after{border-color:#f56c6c}.el-link.el-link--danger{color:#f56c6c}.el-link.el-link--danger:hover{color:#f78989}.el-link.el-link--danger.is-disabled{color:#fab6b6}.el-link.el-link--success.is-underline:hover:after,.el-link.el-link--success:after{border-color:#67c23a}.el-link.el-link--success{color:#67c23a}.el-link.el-link--success:hover{color:#85ce61}.el-link.el-link--success.is-disabled{color:#b3e19d}.el-link.el-link--warning.is-underline:hover:after,.el-link.el-link--warning:after{border-color:#e6a23c}.el-link.el-link--warning{color:#e6a23c}.el-link.el-link--warning:hover{color:#ebb563}.el-link.el-link--warning.is-disabled{color:#f3d19e}.el-link.el-link--info.is-underline:hover:after,.el-link.el-link--info:after{border-color:#909399}.el-link.el-link--info{color:#909399}.el-link.el-link--info:hover{color:#a6a9ad}.el-link.el-link--info.is-disabled{color:#c8c9cc}.el-divider{background-color:#dcdfe6;position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative}.el-divider__text{position:absolute;background-color:#fff;padding:0 20px;color:#303133}.el-image__error,.el-image__placeholder{background:#f5f7fa}.el-divider__text.is-left{left:20px;transform:translateY(-50%)}.el-divider__text.is-center{left:50%;transform:translateX(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;transform:translateY(-50%)}.el-image__error,.el-image__inner,.el-image__placeholder{width:100%;height:100%}.el-image{position:relative;display:inline-block;overflow:hidden}.el-image__inner{vertical-align:top}.el-image__inner--center{position:relative;top:50%;left:50%;transform:translate(-50%,-50%);display:block}.el-image__error{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;font-size:14px;color:#c0c4cc;vertical-align:middle}.el-image__preview{cursor:pointer}.el-image-viewer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0}.el-image-viewer__btn{position:absolute;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;border-radius:50%;opacity:.8;cursor:pointer;box-sizing:border-box;user-select:none}.el-button,.el-checkbox,.el-image-viewer__btn{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-image-viewer__close{top:40px;right:40px;width:40px;height:40px;font-size:24px;color:#fff;background-color:#606266}.el-image-viewer__canvas{width:100%;height:100%;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.el-image-viewer__actions{left:50%;bottom:30px;transform:translateX(-50%);width:282px;height:44px;padding:0 23px;background-color:#606266;border-color:#fff;border-radius:22px}.el-image-viewer__actions__inner{width:100%;height:100%;text-align:justify;cursor:default;font-size:23px;color:#fff;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:distribute;justify-content:space-around}.el-image-viewer__next,.el-image-viewer__prev{top:50%;width:44px;height:44px;font-size:24px;color:#fff;background-color:#606266;border-color:#fff}.el-image-viewer__prev{transform:translateY(-50%);left:40px}.el-image-viewer__next{transform:translateY(-50%);right:40px;text-indent:2px}.el-image-viewer__mask{position:absolute;width:100%;height:100%;top:0;left:0;opacity:.5;background:#000}.viewer-fade-enter-active{animation:viewer-fade-in .3s}.viewer-fade-leave-active{animation:viewer-fade-out .3s}@keyframes viewer-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}to{transform:translateZ(0);opacity:1}}@keyframes viewer-fade-out{0%{transform:translateZ(0);opacity:1}to{transform:translate3d(0,-20px,0);opacity:0}}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #dcdfe6;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:.1s;font-weight:500;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button{margin-left:10px}.el-button:focus,.el-button:hover{color:#409eff;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#fff;border-color:#409eff;color:#409eff}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#fff;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#ebeef5;color:#c0c4cc}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:"";position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:hsla(0,0%,100%,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#fff;background-color:#409eff;border-color:#409eff}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#fff}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#fff}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#fff;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409eff;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409eff;border-color:#409eff;color:#fff}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#fff;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#fff;background-color:#67c23a;border-color:#67c23a}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#fff}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#fff}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#fff;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67c23a;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67c23a;border-color:#67c23a;color:#fff}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#fff;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#fff;background-color:#e6a23c;border-color:#e6a23c}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#fff}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#fff}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#fff;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#e6a23c;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#e6a23c;border-color:#e6a23c;color:#fff}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#fff;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#fff;background-color:#f56c6c;border-color:#f56c6c}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#fff}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#fff}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#fff;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#f56c6c;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#f56c6c;border-color:#f56c6c;color:#fff}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#fff;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#fff;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#fff}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#fff}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#fff;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#fff}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#fff;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--text,.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover,.el-button--text:active{border-color:transparent}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--mini,.el-button--small{font-size:12px;border-radius:3px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small,.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini.is-circle{padding:7px}.el-button--text{color:#409eff;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;background-color:transparent}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group:after,.el-button-group:before{display:table;content:""}.el-button-group:after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button.is-disabled{z-index:1}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:not(:last-child){margin-right:-1px}.el-button-group>.el-button.is-active,.el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover{z-index:1}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-calendar{background-color:#fff}.el-calendar__header{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;padding:12px 20px;border-bottom:1px solid #ebeef5}.el-backtop,.el-page-header{display:-ms-flexbox}.el-calendar__title{color:#000;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{padding:12px 0;color:#606266;font-weight:400}.el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{color:#c0c4cc}.el-backtop,.el-calendar-table td.is-today{color:#409eff}.el-calendar-table td{border-bottom:1px solid #ebeef5;border-right:1px solid #ebeef5;vertical-align:top;transition:background-color .2s ease}.el-calendar-table td.is-selected{background-color:#f2f8fe}.el-calendar-table tr:first-child td{border-top:1px solid #ebeef5}.el-calendar-table tr td:first-child{border-left:1px solid #ebeef5}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{box-sizing:border-box;padding:8px;height:85px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:#f2f8fe}.el-backtop{position:fixed;background-color:#fff;width:40px;height:40px;border-radius:50%;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;font-size:20px;box-shadow:0 0 6px rgba(0,0,0,.12);cursor:pointer;z-index:5}.el-backtop:hover{background-color:#f2f6fc}.el-page-header{display:-ms-flexbox;display:flex;line-height:24px}.el-page-header__left{display:-ms-flexbox;display:flex;cursor:pointer;margin-right:40px;position:relative}.el-page-header__left:after{content:"";position:absolute;width:1px;height:16px;right:-20px;top:50%;transform:translateY(-50%);background-color:#dcdfe6}.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-page-header__left .el-icon-back{font-size:18px;margin-right:6px;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{font-size:18px;color:#303133}.el-checkbox{color:#606266;font-size:14px;cursor:pointer;user-select:none;margin-right:30px}.el-checkbox,.el-checkbox-button__inner,.el-radio{font-weight:500;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #dcdfe6;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409eff}.el-checkbox.is-bordered.is-disabled{border-color:#ebeef5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#dcdfe6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:#c0c4cc}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:#c0c4cc}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:#c0c4cc;border-color:#c0c4cc}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409eff;border-color:#409eff}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#c0c4cc;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner:after{transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409eff}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409eff}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";position:absolute;display:block;background-color:#fff;height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #dcdfe6;border-radius:2px;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409eff}.el-checkbox__inner:after{box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{display:inline-block;position:relative}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #dcdfe6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409eff}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-radio,.el-radio__input{line-height:1;white-space:nowrap;outline:0}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#409eff;border-color:#409eff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409eff}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#ebeef5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409eff}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-radio,.el-radio--medium.is-bordered .el-radio__label{font-size:14px}.el-radio{color:#606266;cursor:pointer;margin-right:30px}.el-cascader-node>.el-radio,.el-radio:last-child{margin-right:0}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #dcdfe6;box-sizing:border-box;height:40px}.el-radio.is-bordered.is-checked{border-color:#409eff}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#ebeef5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#f5f7fa;border-color:#e4e7ed}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio__input{cursor:pointer;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner:after{cursor:not-allowed;background-color:#f5f7fa}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner:after{background-color:#c0c4cc}.el-radio__input.is-disabled+span.el-radio__label{color:#c0c4cc;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#409eff;background:#409eff}.el-radio__input.is-checked .el-radio__inner:after{transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#409eff}.el-radio__input.is-focus .el-radio__inner{border-color:#409eff}.el-radio__inner{border:1px solid #dcdfe6;border-radius:100%;width:14px;height:14px;background-color:#fff;cursor:pointer;box-sizing:border-box}.el-radio__inner:hover{border-color:#409eff}.el-radio__inner:after{width:4px;height:4px;border-radius:100%;background-color:#fff;content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(0);transition:transform .15s ease-in}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{box-shadow:0 0 2px 2px #409eff}.el-radio__label{font-size:14px;padding-left:10px}.el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;transition:opacity .34s ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:hsla(220,4%,58%,.3);transition:background-color .3s}.el-scrollbar__thumb:hover{background-color:hsla(220,4%,58%,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;transition:opacity .12s ease-out}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-cascader-panel{display:-ms-flexbox;display:flex;border-radius:4px;font-size:14px}.el-cascader-panel.is-bordered{border:1px solid #e4e7ed;border-radius:4px}.el-cascader-menu{min-width:180px;box-sizing:border-box;color:#606266;border-right:1px solid #e4e7ed}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu:last-child .el-cascader-node{padding-right:20px}.el-cascader-menu__wrap{height:204px}.el-cascader-menu__list{position:relative;min-height:100%;margin:0;padding:6px 0;list-style:none;box-sizing:border-box}.el-avatar,.el-drawer{-webkit-box-sizing:border-box;overflow:hidden}.el-cascader-menu__hover-zone{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.el-cascader-menu__empty-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:#c0c4cc}.el-cascader-node{position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:0 30px 0 20px;height:34px;line-height:34px;outline:0}.el-cascader-node.is-selectable.in-active-path{color:#606266}.el-cascader-node.in-active-path,.el-cascader-node.is-active,.el-cascader-node.is-selectable.in-checked-path{color:#409eff;font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):focus,.el-cascader-node:not(.is-disabled):hover{background:#f5f7fa}.el-cascader-node.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{-ms-flex:1;flex:1;padding:0 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-avatar{display:inline-block;box-sizing:border-box;text-align:center;color:#fff;background:#c0c4cc;width:40px;height:40px;line-height:40px;font-size:14px}.el-avatar>img{display:block;height:100%;vertical-align:middle}.el-drawer,.el-drawer__header{display:-ms-flexbox}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:4px}.el-avatar--icon{font-size:18px}.el-avatar--large{width:40px;height:40px;line-height:40px}.el-avatar--medium{width:36px;height:36px;line-height:36px}.el-avatar--small{width:28px;height:28px;line-height:28px}.el-drawer.btt,.el-drawer.ttb,.el-drawer__container{left:0;right:0;width:100%}.el-drawer.ltr,.el-drawer.rtl,.el-drawer__container{top:0;bottom:0;height:100%}@keyframes el-drawer-fade-in{0%{opacity:0}to{opacity:1}}@keyframes rtl-drawer-in{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes rtl-drawer-out{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes ltr-drawer-in{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes ltr-drawer-out{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes ttb-drawer-in{0%{transform:translateY(-100%)}to{transform:translate(0)}}@keyframes ttb-drawer-out{0%{transform:translate(0)}to{transform:translateY(-100%)}}@keyframes btt-drawer-in{0%{transform:translateY(100%)}to{transform:translate(0)}}@keyframes btt-drawer-out{0%{transform:translate(0)}to{transform:translateY(100%)}}.el-drawer{position:absolute;box-sizing:border-box;background-color:#fff;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);outline:0}.el-drawer.rtl{animation:rtl-drawer-out .3s;right:0}.el-drawer__open .el-drawer.rtl{animation:rtl-drawer-in .3s 1ms}.el-drawer.ltr{animation:ltr-drawer-out .3s;left:0}.el-drawer__open .el-drawer.ltr{animation:ltr-drawer-in .3s 1ms}.el-drawer.ttb{animation:ttb-drawer-out .3s;top:0}.el-drawer__open .el-drawer.ttb{animation:ttb-drawer-in .3s 1ms}.el-drawer.btt{animation:btt-drawer-out .3s;bottom:0}.el-drawer__open .el-drawer.btt{animation:btt-drawer-in .3s 1ms}.el-drawer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:hidden;margin:0}.el-drawer__header{-ms-flex-align:center;align-items:center;color:#72767b;display:-ms-flexbox;display:flex;margin-bottom:32px;padding:20px 20px 0}.el-drawer__header>:first-child{-ms-flex:1;flex:1}.el-drawer__title{margin:0;-ms-flex:1;flex:1;line-height:inherit;font-size:1rem}.el-drawer__close-btn{border:none;cursor:pointer;font-size:20px;color:inherit;background-color:transparent}.el-drawer__body{-ms-flex:1;flex:1}.el-drawer__body>*{box-sizing:border-box}.el-drawer__container{position:relative}.el-drawer-fade-enter-active{animation:el-drawer-fade-in .3s}.el-drawer-fade-leave-active{animation:el-drawer-fade-in .3s reverse}.el-popconfirm__main{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin:0}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,"",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,'.el-fade-in-enter,.el-fade-in-leave-active,.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-menu--collapse .el-menu .el-submenu,.el-menu--popup{min-width:200px}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active,.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:opacity .2s linear}.el-fade-in-enter-active,.el-fade-in-leave-active,.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transform:scaleY(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transform:scale(1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45)}.collapse-transition{transition:height .3s ease-in-out,padding-top .3s ease-in-out,padding-bottom .3s ease-in-out}.horizontal-collapse-transition{transition:width .3s ease-in-out,padding-left .3s ease-in-out,padding-right .3s ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;transform:translateY(-30px)}.el-opacity-transition{transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-menu{border-right:1px solid #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0}.el-menu,.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu:after,.el-menu:before{display:table;content:""}.el-menu:after{clear:both}.el-menu.el-menu--horizontal{border-bottom:1px solid #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409eff;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#fff;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409eff;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-submenu{position:relative}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;z-index:10;border:1px solid #e4e7ed;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;list-style:none;position:relative;white-space:nowrap}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:none}.el-menu--popup{z-index:100;border:none;padding:5px 0;border-radius:2px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409eff}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;transition:transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409eff}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{transform:rotate(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{transition:.2s;opacity:0}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,'.el-popper .popper__arrow,.el-popper .popper__arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;filter:drop-shadow(0 2px 12px rgba(0,0,0,.03))}.el-popper .popper__arrow:after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow:after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow:after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow:after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow:after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-popover{position:absolute;background:#fff;min-width:150px;border-radius:4px;border:1px solid #ebeef5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,'.el-row{position:relative;box-sizing:border-box}.el-row:after,.el-row:before{display:table;content:""}.el-row:after{clear:both}.el-row--flex{display:-ms-flexbox;display:flex}.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-middle{-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-ms-flex-align:end;align-items:flex-end}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,"",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,'.el-checkbox,.el-checkbox__input{white-space:nowrap;display:inline-block;position:relative}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin-right:30px}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #dcdfe6;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409eff}.el-checkbox.is-bordered.is-disabled{border-color:#ebeef5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#dcdfe6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:#c0c4cc}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:#c0c4cc}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:#c0c4cc;border-color:#c0c4cc}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409eff;border-color:#409eff}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#c0c4cc;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner:after{transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409eff}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409eff}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";position:absolute;display:block;background-color:#fff;height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #dcdfe6;border-radius:2px;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409eff}.el-checkbox__inner:after{box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox-button__inner,.el-tag{-webkit-box-sizing:border-box;white-space:nowrap}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{position:relative;display:inline-block}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #dcdfe6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409eff}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#409eff;border-color:#409eff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409eff}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#ebeef5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409eff}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-tag{background-color:#ecf5ff;border:1px solid #d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409eff;border-radius:4px;box-sizing:border-box}.el-tag.is-hit{border-color:#409eff}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67c23a}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close:before{display:block}.el-tag--dark{background-color:#409eff;color:#fff}.el-tag--dark,.el-tag--dark.is-hit{border-color:#409eff}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#fff;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#fff;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67c23a}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#fff;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409eff}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #ebeef5;border-radius:2px;background-color:#fff;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409eff;color:#fff}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #ebeef5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-table-filter__bottom button:hover{color:#409eff}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,'.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-table,.el-table__append-wrapper{overflow:hidden}.el-table--hidden,.el-table td.is-hidden>*,.el-table th.is-hidden>*{visibility:hidden}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;user-select:none;margin-right:30px}.el-checkbox,.el-checkbox-button__inner,.el-table th{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #dcdfe6;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409eff}.el-checkbox.is-bordered.is-disabled{border-color:#ebeef5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#dcdfe6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:#c0c4cc}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:#c0c4cc}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:#c0c4cc;border-color:#c0c4cc}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409eff;border-color:#409eff}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#c0c4cc;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner:after{transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409eff}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409eff}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";position:absolute;display:block;background-color:#fff;height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #dcdfe6;border-radius:2px;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409eff}.el-checkbox__inner:after{box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{position:relative;display:inline-block}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #dcdfe6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409eff}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#409eff;border-color:#409eff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409eff}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#ebeef5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409eff}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-tag{background-color:#ecf5ff;border:1px solid #d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409eff;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409eff}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67c23a}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close:before{display:block}.el-tag--dark{background-color:#409eff;color:#fff}.el-tag--dark,.el-tag--dark.is-hit{border-color:#409eff}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#fff;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#fff;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67c23a}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#fff;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409eff}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow:after{content:" ";border-width:5px}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow:after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow:after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow:after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow:after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#fff}.el-table,.el-table__expanded-cell{background-color:#fff}.el-tooltip__popper.is-light{background:#fff;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow:after{border-top-color:#fff}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow:after{border-bottom-color:#fff}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow:after{border-left-color:#fff}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow:after{border-right-color:#fff}.el-table{position:relative;box-sizing:border-box;-ms-flex:1;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table--mini,.el-table--small,.el-table__expand-icon{font-size:12px}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;transition:transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit td.gutter,.el-table--fit th.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th{background:#f5f7fa}.el-table th,.el-table tr{background-color:#fff}.el-table td,.el-table th{padding:12px 0;min-width:0;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table td.is-center,.el-table th.is-center{text-align:center}.el-table td.is-right,.el-table th.is-right{text-align:right}.el-table td.gutter,.el-table th.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table--medium td,.el-table--medium th{padding:10px 0}.el-table--small td,.el-table--small th{padding:8px 0}.el-table--mini td,.el-table--mini th{padding:6px 0}.el-table--border td:first-child .cell,.el-table--border th:first-child .cell,.el-table .cell{padding-left:10px}.el-table tr input[type=checkbox]{margin:0}.el-table td,.el-table th.is-leaf{border-bottom:1px solid #ebeef5}.el-table th.is-sortable{cursor:pointer}.el-table th{overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-table th>.cell{display:inline-block;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th>.cell.highlight{color:#409eff}.el-table th.required>div:before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td div{box-sizing:border-box}.el-table td.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #ebeef5}.el-table--border:after,.el-table--group:after,.el-table:before{content:"";position:absolute;background-color:#ebeef5;z-index:1}.el-table--border:after,.el-table--group:after{top:0;right:0;width:1px;height:100%}.el-table:before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border td,.el-table--border th,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #ebeef5}.el-table--border th.gutter:last-of-type{border-bottom:1px solid #ebeef5;border-bottom-width:1px}.el-table--border th,.el-table__fixed-right-patch{border-bottom:1px solid #ebeef5}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right:before,.el-table__fixed:before{content:"";position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#ebeef5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#fff}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td{border-top:1px solid #ebeef5;background-color:#f5f7fa;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td{border-top:1px solid #ebeef5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td,.el-table__header-wrapper tbody td{background-color:#f5f7fa;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{box-shadow:none}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #ebeef5}.el-table .caret-wrapper{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#c0c4cc;top:5px}.el-table .sort-caret.descending{border-top-color:#c0c4cc;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409eff}.el-table .descending .sort-caret.descending{border-top-color:#409eff}.el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td{background:#fafafa}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td,.el-table__body tr.hover-row.el-table__row--striped.current-row>td,.el-table__body tr.hover-row.el-table__row--striped>td,.el-table__body tr.hover-row>td{background-color:#f5f7fa}.el-table__body tr.current-row>td{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #ebeef5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;transform:scale(.75)}.el-table--enable-row-transition .el-table__body td{transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#f5f7fa}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}',""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,".el-tag{background-color:#ecf5ff;border:1px solid #d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409eff;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409eff}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67c23a}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close:before{display:block}.el-tag--dark{background-color:#409eff;color:#fff}.el-tag--dark,.el-tag--dark.is-hit{border-color:#409eff}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#fff;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#fff;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67c23a}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#fff;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409eff}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;transform:scale(.7)}",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,".el-form-item span{margin-left:15px}.demo-table-expand{font-size:0}.demo-table-expand label{width:90px;color:#99a9bf}.demo-table-expand .el-form-item{margin-right:0;margin-bottom:0;width:50%}",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,"body{background-color:#fafafa;margin:0;font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,sans-serif}header{width:100%;height:60px}.header-color{background:#58b7ff}#content{margin-top:20px;padding-right:40px}.brand{color:#fff;background-color:transparent;margin-left:20px;float:left;line-height:25px;font-size:25px;padding:15px;height:30px;text-decoration:none}",""])},function(e,t,n){t=e.exports=n(17)(void 0),t.push([e.i,".source{border:1px solid #eaeefb;border-radius:4px;transition:.2s;padding:24px}.server_info{margin-left:40px;font-size:0}.server_info label{width:150px;color:#99a9bf}.server_info .el-form-item{margin-right:0;margin-bottom:0;width:100%}",""])},function(e,t,n){"use strict";function o(e){return!!e&&"object"==typeof e}function i(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||r(e)}function r(e){return e.$$typeof===p}function a(e){return Array.isArray(e)?[]:{}}function l(e,t){return t&&!0===t.clone&&d(e)?u(a(e),e,t):e}function s(e,t,n){var o=e.slice();return t.forEach(function(t,i){void 0===o[i]?o[i]=l(t,n):d(t)?o[i]=u(e[i],t,n):-1===e.indexOf(t)&&o.push(l(t,n))}),o}function c(e,t,n){var o={};return d(e)&&Object.keys(e).forEach(function(t){o[t]=l(e[t],n)}),Object.keys(t).forEach(function(i){d(t[i])&&e[i]?o[i]=u(e[i],t[i],n):o[i]=l(t[i],n)}),o}function u(e,t,n){var o=Array.isArray(t),i=Array.isArray(e),r=n||{arrayMerge:s};return o===i?o?(r.arrayMerge||s)(e,t,n):c(e,t,n):l(t,n)}var d=function(e){return o(e)&&!i(e)},f="function"==typeof Symbol&&Symbol.for,p=f?Symbol.for("react.element"):60103;u.all=function(e,t){if(!Array.isArray(e)||e.length<2)throw new Error("first argument should be an array with at least two elements");return e.reduce(function(e,n){return u(e,n,t)})};var h=u;e.exports=h},function(e,t,n){var o=n(1);!function(){for(var e in o){if(null==o||!o.hasOwnProperty(e)||"default"===e||"__esModule"===e)return;t[e]=o[e]}}();var i=n(214);!function(){for(var e in i){if(null==i||!i.hasOwnProperty(e)||"default"===e||"__esModule"===e)return;t[e]=i[e]}}(),n(412),n(165),n(175),n(447),n(437),n(421),n(455),n(462),n(389),n(385),n(381),n(428),n(442),n(366),n(371),n(378),n(416),n(402),n(432),n(450),n(377),n(503),n(504),n(511),n(198),n(67),n(522),n(501),n(195),n(196),n(482),n(489),n(197),n(491),n(547),n(514),n(513),n(512),n(526),n(535),n(696),n(691)},function(e,t,n){function o(e){i.each(r,function(t){this[t]=i.bind(e[t],e)},this)}var i=n(0),r=["getDom","getZr","getWidth","getHeight","getDevicePixelRatio","dispatchAction","isDisposed","on","off","getDataURL","getConnectedDataURL","getModel","getOption","getViewOfComponentModel","getViewOfSeriesModel"],a=o;e.exports=a},function(e,t,n){var o=n(166),i=o.extend({type:"series.bar",dependencies:["grid","polar"],brushSelector:"rect"});e.exports=i},function(e,t,n){function o(e,t,n){n.style.text=null,u.updateProps(n,{shape:{width:0}},t,e,function(){n.parent&&n.parent.remove(n)})}function i(e,t,n){n.style.text=null,u.updateProps(n,{shape:{r:n.shape.r0}},t,e,function(){n.parent&&n.parent.remove(n)})}function r(e,t,n,o,i,r,a,l){var s=t.getItemVisual(n,"color"),d=t.getItemVisual(n,"opacity"),p=o.getModel("itemStyle.normal"),h=o.getModel("itemStyle.emphasis").getBarItemStyle();l||e.setShape("r",p.get("barBorderRadius")||0),e.useStyle(c.defaults({fill:s,opacity:d},p.getBarItemStyle()));var g=o.getShallow("cursor");g&&e.attr("cursor",g);var m=a?i.height>0?"bottom":"top":i.width>0?"left":"right";l||f(e.style,h,o,s,r,n,m),u.setHoverStyle(e,h)}function a(e,t){var n=e.get(g)||0;return Math.min(n,Math.abs(t.width),Math.abs(t.height))}var l=n(4),s=(l.__DEV__,n(1)),c=n(0),u=n(2),d=n(167),f=d.setLabel,p=n(12),h=n(365),g=["itemStyle","normal","barBorderWidth"];c.extend(p.prototype,h);var m=s.extendChartView({type:"bar",render:function(e,t,n){var o=e.get("coordinateSystem");return"cartesian2d"!==o&&"polar"!==o||this._render(e,t,n),this.group},dispose:c.noop,_render:function(e,t,n){var a,l=this.group,s=e.getData(),c=this._data,d=e.coordinateSystem,f=d.getBaseAxis();"cartesian2d"===d.type?a=f.isHorizontal():"polar"===d.type&&(a="angle"===f.dim);var p=e.isAnimationEnabled()?e:null;s.diff(c).add(function(t){if(s.hasValue(t)){var n=s.getItemModel(t),o=b[d.type](s,t,n),i=v[d.type](s,t,n,o,a,p);s.setItemGraphicEl(t,i),l.add(i),r(i,s,t,n,o,e,a,"polar"===d.type)}}).update(function(t,n){var o=c.getItemGraphicEl(n);if(!s.hasValue(t))return void l.remove(o);var i=s.getItemModel(t),f=b[d.type](s,t,i);o?u.updateProps(o,{shape:f},p,t):o=v[d.type](s,t,i,f,a,p,!0),s.setItemGraphicEl(t,o),l.add(o),r(o,s,t,i,f,e,a,"polar"===d.type)}).remove(function(e){var t=c.getItemGraphicEl(e);"cartesian2d"===d.type?t&&o(e,p,t):t&&i(e,p,t)}).execute(),this._data=s},remove:function(e,t){var n=this.group,r=this._data;e.get("animation")?r&&r.eachItemGraphicEl(function(t){"sector"===t.type?i(t.dataIndex,e,t):o(t.dataIndex,e,t)}):n.removeAll()}}),v={cartesian2d:function(e,t,n,o,i,r,a){var l=new u.Rect({shape:c.extend({},o)});if(r){var s=l.shape,d=i?"height":"width",f={};s[d]=0,f[d]=o[d],u[a?"updateProps":"initProps"](l,{shape:f},r,t)}return l},polar:function(e,t,n,o,i,r,a){var l=new u.Sector({shape:c.extend({},o)});if(r){var s=l.shape,d=i?"r":"endAngle",f={};s[d]=i?0:o.startAngle,f[d]=o[d],u[a?"updateProps":"initProps"](l,{shape:f},r,t)}return l}},b={cartesian2d:function(e,t,n){var o=e.getItemLayout(t),i=a(n,o),r=o.width>0?1:-1,l=o.height>0?1:-1;return{x:o.x+r*i/2,y:o.y+l*i/2,width:o.width-r*i,height:o.height-l*i}},polar:function(e,t,n){var o=e.getItemLayout(t);return{cx:o.cx,cy:o.cy,r0:o.r0,r:o.r,startAngle:o.startAngle,endAngle:o.endAngle}}};e.exports=m},function(e,t,n){var o=n(166),i=o.extend({type:"series.pictorialBar",dependencies:["grid"],defaultOption:{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",progressive:0,hoverAnimation:!1},getInitialData:function(e){return e.stack=null,i.superApply(this,"getInitialData",arguments)}}),r=i;e.exports=r},function(e,t,n){function o(e,t,n,o){var r=e.getItemLayout(t),c=n.get("symbolRepeat"),u=n.get("symbolClip"),d=n.get("symbolPosition")||"start",f=n.get("symbolRotate"),p=(f||0)*Math.PI/180||0,h=n.get("symbolPatternSize")||2,g=n.isAnimationEnabled(),m={dataIndex:t,layout:r,itemModel:n,symbolType:e.getItemVisual(t,"symbol")||"circle",color:e.getItemVisual(t,"color"),symbolClip:u,symbolRepeat:c,symbolRepeatDirection:n.get("symbolRepeatDirection"),symbolPatternSize:h,rotation:p,animationModel:g?n:null,hoverAnimation:g&&n.get("hoverAnimation"),z2:n.getShallow("z",!0)||0};i(n,c,r,o,m),a(e,t,r,c,u,m.boundingLength,m.pxSign,h,o,m),l(n,m.symbolScale,p,o,m);var v=m.symbolSize,b=n.get("symbolOffset");return A.isArray(b)&&(b=[L(b[0],v[0]),L(b[1],v[1])]),s(n,v,r,c,u,b,d,m.valueLineWidth,m.boundingLength,m.repeatCutLength,o,m),m}function i(e,t,n,o,i){var a,l=o.valueDim,s=e.get("symbolBoundingData"),c=o.coordSys.getOtherAxis(o.coordSys.getBaseAxis()),u=c.toGlobalCoord(c.dataToCoord(0)),d=1-+(n[l.wh]<=0);if(A.isArray(s)){var f=[r(c,s[0])-u,r(c,s[1])-u];f[1]0?1:a<0?-1:0}function r(e,t){return e.toGlobalCoord(e.dataToCoord(e.scale.parse(t)))}function a(e,t,n,o,i,r,a,l,s,c){var u=s.valueDim,d=s.categoryDim,f=Math.abs(n[d.wh]),p=e.getItemVisual(t,"symbolSize");A.isArray(p)?p=p.slice():(null==p&&(p="100%"),p=[p,p]),p[d.index]=L(p[d.index],f),p[u.index]=L(p[u.index],o?f:Math.abs(r)),c.symbolSize=p,(c.symbolScale=[p[0]/l,p[1]/l])[u.index]*=(s.isHorizontal?-1:1)*a}function l(e,t,n,o,i){var r=e.get(R)||0;r&&(B.attr({scale:t.slice(),rotation:n}),B.updateTransform(),r/=B.getLineScale(),r*=t[o.valueDim.index]),i.valueLineWidth=r}function s(e,t,n,o,i,r,a,l,s,c,u,d){var f=u.categoryDim,p=u.valueDim,h=d.pxSign,g=Math.max(t[p.index]+l,0),m=g;if(o){var v=Math.abs(s),b=A.retrieve(e.get("symbolMargin"),"15%")+"",x=!1;b.lastIndexOf("!")===b.length-1&&(x=!0,b=b.slice(0,b.length-1)),b=L(b,t[p.index]);var y=Math.max(g+2*b,0),_=x?0:2*b,w=P(o),k=w?o:M((v+_)/y);b=(v-k*g)/2/(x?k:k-1),y=g+2*b,_=x?0:2*b,w||"fixed"===o||(k=c?M((Math.abs(c)+_)/y):0),m=k*y-_,d.repeatTimes=k,d.symbolMargin=b}var S=h*(m/2),C=d.pathPosition=[];C[f.index]=n[f.wh]/2,C[p.index]="start"===a?S:"end"===a?s-S:s/2,r&&(C[0]+=r[0],C[1]+=r[1]);var T=d.bundlePosition=[];T[f.index]=n[f.xy],T[p.index]=n[p.xy];var E=d.barRectShape=A.extend({},n);E[p.wh]=h*Math.max(Math.abs(n[p.wh]),Math.abs(C[p.index]+S)),E[f.wh]=n[f.wh];var I=d.clipShape={};I[f.xy]=-n[f.xy],I[f.wh]=u.ecSize[f.wh],I[p.xy]=0,I[p.wh]=n[p.wh]}function c(e){var t=e.symbolPatternSize,n=I(e.symbolType,-t/2,-t/2,t,t,e.color);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function u(e,t,n,o){function i(e){var t=d.slice(),o=n.pxSign,i=e;return("start"===n.symbolRepeatDirection?o>0:o<0)&&(i=p-1-e),t[f.index]=g*(i-p/2+.5)+d[f.index],{position:t,scale:n.symbolScale.slice(),rotation:n.rotation}}function r(){w(e,function(e){e.trigger("emphasis")})}function a(){w(e,function(e){e.trigger("normal")})}var l=e.__pictorialBundle,s=n.symbolSize,u=n.valueLineWidth,d=n.pathPosition,f=t.valueDim,p=n.repeatTimes||0,h=0,g=s[t.valueDim.index]+u+2*n.symbolMargin;for(w(e,function(e){e.__pictorialAnimationIndex=h,e.__pictorialRepeatTimes=p,h0)],d=e.__pictorialBarRect;z(d.style,c,r,o,t.seriesModel,i,u),T.setHoverStyle(d,c)}function M(e){var t=Math.round(e);return Math.abs(e-t)<1e-4?t:Math.ceil(e)}var C=n(1),A=n(0),T=n(2),E=n(23),I=E.createSymbol,O=n(3),L=O.parsePercent,P=O.isNumeric,D=n(167),z=D.setLabel,R=["itemStyle","normal","borderWidth"],N=[{xy:"x",wh:"width",index:0,posDesc:["left","right"]},{xy:"y",wh:"height",index:1,posDesc:["top","bottom"]}],B=new T.Circle,F=C.extendChartView({type:"pictorialBar",render:function(e,t,n){var i=this.group,r=e.getData(),a=this._data,l=e.coordinateSystem,s=l.getBaseAxis(),c=!!s.isHorizontal(),u=l.grid.getRect(),d={ecSize:{width:n.getWidth(),height:n.getHeight()},seriesModel:e,coordSys:l,coordSysExtent:[[u.x,u.x+u.width],[u.y,u.y+u.height]],isHorizontal:c,valueDim:N[+c],categoryDim:N[1-c]};return r.diff(a).add(function(e){if(r.hasValue(e)){var t=h(r,e),n=o(r,e,t,d),a=b(r,d,n);r.setItemGraphicEl(e,a),i.add(a),S(a,d,n)}}).update(function(e,t){var n=a.getItemGraphicEl(t);if(!r.hasValue(e))return void i.remove(n);var l=h(r,e),s=o(r,e,l,d),c=_(r,s);n&&c!==n.__pictorialShapeStr&&(i.remove(n),r.setItemGraphicEl(e,null),n=null),n?x(n,d,s):n=b(r,d,s,!0),r.setItemGraphicEl(e,n),n.__pictorialSymbolMeta=s,i.add(n),S(n,d,s)}).remove(function(e){var t=a.getItemGraphicEl(e);t&&y(a,e,t.__pictorialSymbolMeta.animationModel,t)}).execute(),this._data=r,this.group},dispose:A.noop,remove:function(e,t){var n=this.group,o=this._data;e.get("animation")?o&&o.eachItemGraphicEl(function(t){y(o,t.dataIndex,e,t)}):n.removeAll()}}),V=F;e.exports=V},function(e,t,n){var o=n(58),i=o([["fill","color"],["stroke","borderColor"],["lineWidth","borderWidth"],["stroke","barBorderColor"],["lineWidth","barBorderWidth"],["opacity"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["shadowColor"]]),r={getBarItemStyle:function(e){var t=i(this,e);if(this.getBorderLineDash){var n=this.getBorderLineDash();n&&(t.lineDash=n)}return t}};e.exports=r},function(e,t,n){var o=n(1);n(367),n(368);var i=n(370),r=n(369);o.registerVisual(i),o.registerLayout(r)},function(e,t,n){var o=n(0),i=n(18),r=n(83),a=r.seriesModelMixin,l=i.extend({type:"series.boxplot",dependencies:["xAxis","yAxis","grid"],defaultValueDimensions:["min","Q1","median","Q3","max"],dimensions:null,defaultOption:{zlevel:0,z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,hoverAnimation:!0,layout:null,boxWidth:[7,50],itemStyle:{normal:{color:"#fff",borderWidth:1},emphasis:{borderWidth:2,shadowBlur:5,shadowOffsetX:2,shadowOffsetY:2,shadowColor:"rgba(0,0,0,0.4)"}},animationEasing:"elasticOut",animationDuration:800}});o.mixin(l,a,!0);var s=l;e.exports=s},function(e,t,n){function o(e,t,n){var o=t.getItemModel(n),i=o.getModel(u),r=t.getItemVisual(n,"color"),l=i.getItemStyle(["borderColor"]),s=e.childAt(e.whiskerIndex);s.style.set(l),s.style.stroke=r,s.dirty();var c=e.childAt(e.bodyIndex);c.style.set(l),c.style.stroke=r,c.dirty();var f=o.getModel(d).getItemStyle();a.setHoverStyle(e,f)}var i=n(0),r=n(37),a=n(2),l=n(83),s=l.viewMixin,c=r.extend({type:"boxplot",getStyleUpdater:function(){return o},dispose:i.noop});i.mixin(c,s,!0);var u=["itemStyle","normal"],d=["itemStyle","emphasis"],f=c;e.exports=f},function(e,t,n){function o(e){var t=i(e);u(t,function(e){var t=e.seriesModels;t.length&&(r(e),u(t,function(t,n){a(t,e.boxOffsetList[n],e.boxWidthList[n])}))})}function i(e){var t=[],n=[];return e.eachSeriesByType("boxplot",function(e){var o=e.getBaseAxis(),i=l.indexOf(n,o);i<0&&(i=n.length,n[i]=o,t[i]={axis:o,seriesModels:[]}),t[i].seriesModels.push(e)}),t}function r(e){var t,n,o=e.axis,i=e.seriesModels,r=i.length,a=e.boxWidthList=[],s=e.boxOffsetList=[],d=[];if("category"===o.type)n=o.getBandWidth();else{var f=0;u(i,function(e){f=Math.max(f,e.getData().count())}),t=o.getExtent(),Math.abs(t[1]-t[0])}u(i,function(e){var t=e.get("boxWidth");l.isArray(t)||(t=[t,t]),d.push([c(t[0],n)||0,c(t[1],n)||0])});var p=.8*n-2,h=p/r*.3,g=(p-h*(r-1))/r,m=g/2-p/2;u(i,function(e,t){s.push(m),m+=h+g,a.push(Math.min(Math.max(g,d[t][0]),d[t][1]))})}function a(e,t,n){var o,i=e.coordinateSystem,r=e.getData(),a=n/2,s=e.get("layout"),c="horizontal"===s?0:1,u=1-c,d=["x","y"],f=[];l.each(r.dimensions,function(e){var t=r.getDimensionInfo(e),n=t.coordDim;n===d[u]?f.push(e):n===d[c]&&(o=e)}),null==o||f.length<5||r.each([o].concat(f),function(){function e(e){var n=[];n[c]=d,n[u]=e;var o;return isNaN(d)||isNaN(e)?o=[NaN,NaN]:(o=i.dataToPoint(n),o[c]+=t),o}function n(e,t){var n=e.slice(),o=e.slice();n[c]+=a,o[c]-=a,t?b.push(n,o):b.push(o,n)}function o(e){var t=[e.slice(),e.slice()];t[0][c]-=a,t[1][c]+=a,v.push(t)}var l=arguments,d=l[0],p=l[f.length+1],h=e(l[3]),g=e(l[1]),m=e(l[5]),v=[[g,e(l[2])],[m,e(l[4])]];o(g),o(m),o(h);var b=[];n(v[0][1],0),n(v[1][1],1),r.setItemLayout(p,{chartLayout:s,initBaseline:h[u],median:h,bodyEnds:b,whiskerEnds:v})})}var l=n(0),s=n(3),c=s.parsePercent,u=l.each;e.exports=o},function(e,t){function n(e,t){var n=e.get("color");e.eachRawSeriesByType("boxplot",function(t){var i=n[t.seriesIndex%n.length],r=t.getData();r.setVisual({legendSymbol:"roundRect",color:t.get(o)||i}),e.isSeriesFiltered(t)||r.each(function(e){var t=r.getItemModel(e);r.setItemVisual(e,{color:t.get(o,!0)})})})}var o=["itemStyle","normal","borderColor"];e.exports=n},function(e,t,n){var o=n(1);n(372),n(373);var i=n(376),r=n(375),a=n(374);o.registerPreprocessor(i),o.registerVisual(r),o.registerLayout(a)},function(e,t,n){var o=n(0),i=n(18),r=n(83),a=r.seriesModelMixin,l=i.extend({type:"series.candlestick",dependencies:["xAxis","yAxis","grid"],defaultValueDimensions:["open","close","lowest","highest"],dimensions:null,defaultOption:{zlevel:0,z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,hoverAnimation:!0,layout:null,itemStyle:{normal:{color:"#c23531",color0:"#314656",borderWidth:1,borderColor:"#c23531",borderColor0:"#314656"},emphasis:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,animationUpdate:!1,animationEasing:"linear",animationDuration:300},getShadowDim:function(){return"open"},brushSelector:function(e,t,n){var o=t.getItemLayout(e);return n.rect(o.brushRect)}});o.mixin(l,a,!0);var s=l;e.exports=s},function(e,t,n){function o(e,t,n){var o=t.getItemModel(n),i=o.getModel(u),r=t.getItemVisual(n,"color"),l=t.getItemVisual(n,"borderColor")||r,s=i.getItemStyle(["color","color0","borderColor","borderColor0"]),c=e.childAt(e.whiskerIndex);c.useStyle(s),c.style.stroke=l;var f=e.childAt(e.bodyIndex);f.useStyle(s),f.style.fill=r,f.style.stroke=l;var p=o.getModel(d).getItemStyle();a.setHoverStyle(e,p)}var i=n(0),r=n(37),a=n(2),l=n(83),s=l.viewMixin,c=r.extend({type:"candlestick",getStyleUpdater:function(){return o},dispose:i.noop});i.mixin(c,s,!0);var u=["itemStyle","normal"],d=["itemStyle","emphasis"],f=c;e.exports=f},function(e,t,n){function o(e){e.eachSeriesByType("candlestick",function(e){var t,n=e.coordinateSystem,o=e.getData(),a=i(e,o),l=e.get("layout"),s="horizontal"===l?0:1,u=1-s,d=["x","y"],f=[];if(r.each(o.dimensions,function(e){var n=o.getDimensionInfo(e),i=n.coordDim;i===d[u]?f.push(e):i===d[s]&&(t=e)}),!(null==t||f.length<4)){var p=0;o.each([t].concat(f),function(){function e(e){var t=[];return t[s]=d,t[u]=e,isNaN(d)||isNaN(e)?[NaN,NaN]:n.dataToPoint(t)}function t(e,t){var n=e.slice(),o=e.slice();n[s]=c(n[s]+a/2,1,!1),o[s]=c(o[s]-a/2,1,!0),t?C.push(n,o):C.push(o,n)}function i(e){return e[s]=c(e[s],1),e}var r=arguments,d=r[0],h=r[f.length+1],g=r[1],m=r[2],v=r[3],b=r[4],x=Math.min(g,m),y=Math.max(g,m),_=e(x),w=e(y),k=e(v),S=e(b),M=[[i(S),i(w)],[i(k),i(_)]],C=[];t(w,0),t(_,1);var A;A=g>m?-1:g0?o.getItemModel(p-1).get()[2]<=m?1:-1:1,o.setItemLayout(h,{chartLayout:l,sign:A,initBaseline:g>m?w[u]:_[u],bodyEnds:C,whiskerEnds:M,brushRect:function(){var t=e(Math.min(g,m,v,b)),n=e(Math.max(g,m,v,b));return t[s]-=a/2,n[s]-=a/2,{x:t[0],y:t[1],width:u?a:n[0]-t[0],height:u?n[1]-t[1]:a}}()}),++p},!0)}})}function i(e,t){var n,o=e.getBaseAxis(),i="category"===o.type?o.getBandWidth():(n=o.getExtent(),Math.abs(n[1]-n[0])/t.count()),r=l(u(e.get("barMaxWidth"),i),i),a=l(u(e.get("barMinWidth"),1),i),s=e.get("barWidth");return null!=s?l(s,i):Math.max(Math.min(i/2,r),a)}var r=n(0),a=n(3),l=a.parsePercent,s=n(2),c=s.subPixelOptimize,u=r.retrieve2;e.exports=o},function(e,t){function n(e,t){e.eachRawSeriesByType("candlestick",function(t){var n=t.getData();n.setVisual({legendSymbol:"roundRect"}),e.isSeriesFiltered(t)||n.each(function(e){var t=n.getItemModel(e),l=n.getItemLayout(e).sign;n.setItemVisual(e,{color:t.get(l>0?r:a),borderColor:t.get(l>0?o:i)})})})}var o=["itemStyle","normal","borderColor"],i=["itemStyle","normal","borderColor0"],r=["itemStyle","normal","color"],a=["itemStyle","normal","color0"];e.exports=n},function(e,t,n){function o(e){e&&i.isArray(e.series)&&i.each(e.series,function(e){i.isObject(e)&&"k"===e.type&&(e.type="candlestick")})}var i=n(0);e.exports=o},function(e,t,n){function o(e){var t,n=e.type;if("path"===n){var o=e.shape;t=v.makePath(o.pathData,null,{x:o.x||0,y:o.y||0,width:o.width||0,height:o.height||0},"center"),t.__customPathData=e.pathData}else if("image"===n)t=new v.Image({}),t.__customImagePath=e.style.image;else if("text"===n)t=new v.Text({}),t.__customText=e.style.text;else{var i=v[n.charAt(0).toUpperCase()+n.slice(1)];t=new i}return t.__customGraphicType=n,t.name=e.name,t}function i(e,t,n,o,i,a){var l={},s=n.style||{};if(n.shape&&(l.shape=m.clone(n.shape)),n.position&&(l.position=n.position.slice()),n.scale&&(l.scale=n.scale.slice()),n.origin&&(l.origin=n.origin.slice()),n.rotation&&(l.rotation=n.rotation),"image"===e.type&&n.style){var c=l.style={};m.each(["x","y","width","height"],function(t){r(t,c,s,e.style,a)})}if("text"===e.type&&n.style){var c=l.style={};m.each(["x","y"],function(t){r(t,c,s,e.style,a)}),!s.hasOwnProperty("textFill")&&s.fill&&(s.textFill=s.fill),!s.hasOwnProperty("textStroke")&&s.stroke&&(s.textStroke=s.stroke)}if("group"!==e.type&&(e.useStyle(s),a)){e.style.opacity=0;var u=s.opacity;null==u&&(u=1),v.initProps(e,{style:{opacity:u}},o,t)}a?e.attr(l):v.updateProps(e,l,o,t),e.attr({z2:n.z2||0,silent:n.silent}),!1!==n.styleEmphasis&&v.setHoverStyle(e,n.styleEmphasis)}function r(e,t,n,o,i){null==n[e]||i||(t[e]=n[e],n[e]=o[e])}function a(e,t,n,o){function i(e){null==e&&(e=b),L&&(y=t.getItemModel(e),w=y.getModel(I),k=y.getModel(O),S=x(t),M=t.getItemVisual(e,"color"),L=!1)}function r(e,n){return null==n&&(n=b),t.get(t.getDimension(e||0),n)}function a(n,o){null==o&&(o=b),i(o);var r=y.getModel(T).getItemStyle();null!=M&&(r.fill=M);var a=t.getItemVisual(o,"opacity");return null!=a&&(r.opacity=a),null!=S&&(v.setTextStyle(r,w,null,{autoColor:M,isRectText:!0}),r.text=w.getShallow("show")?m.retrieve2(e.getFormattedLabel(o,"normal"),t.get(S,o)):null),n&&m.extend(r,n),r}function s(n,o){null==o&&(o=b),i(o);var r=y.getModel(E).getItemStyle();return null!=S&&(v.setTextStyle(r,k,null,{isRectText:!0},!0),r.text=k.getShallow("show")?m.retrieve3(e.getFormattedLabel(o,"emphasis"),e.getFormattedLabel(o,"normal"),t.get(S,o)):null),n&&m.extend(r,n),r}function c(e,n){return null==n&&(n=b),t.getItemVisual(n,e)}function u(e){if(h.getBaseAxis){var t=h.getBaseAxis();return _.getLayoutOnAxis(m.defaults({axis:t},e),o)}}function d(){return n.getCurrentSeriesIndices()}function f(e){return v.getFont(e,n)}var p=e.get("renderItem"),h=e.coordinateSystem,g={};h&&(g=h.prepareCustoms?h.prepareCustoms():P[h.type](h));var b,y,w,k,S,M,C=m.defaults({getWidth:o.getWidth,getHeight:o.getHeight,getZr:o.getZr,getDevicePixelRatio:o.getDevicePixelRatio,value:r,style:a,styleEmphasis:s,visual:c,barLayout:u,currentSeriesIndices:d,font:f},g.api||{}),A={context:{},seriesId:e.id,seriesName:e.name,seriesIndex:e.seriesIndex,coordSys:g.coordSys,dataInsideLength:t.count(),encode:l(e.getData())},L=!0;return function(e){return b=e,L=!0,p&&p(m.defaults({dataIndexInside:e,dataIndex:t.getRawIndex(e)},A),C)||{}}}function l(e){var t={};return m.each(e.dimensions,function(n,o){var i=e.getDimensionInfo(n);if(!i.isExtraCoord){var r=i.coordDim;(t[r]=t[r]||[])[i.coordDimIndex]=o}}),t}function s(e,t,n,o,i,r){(e=c(e,t,n,o,i,r))&&r.setItemGraphicEl(t,e)}function c(e,t,n,r,a,l){var s=n.type;if(!e||s===e.__customGraphicType||"path"===s&&n.pathData===e.__customPathData||"image"===s&&n.style.image===e.__customImagePath||"text"===s&&n.style.text===e.__customText||(a.remove(e),e=null),null!=s){var d=!e;if(!e&&(e=o(n)),i(e,t,n,r,l,d),"group"===s){var f=e.children()||[],p=n.children||[];if(n.diffChildrenByName)u({oldChildren:f,newChildren:p,dataIndex:t,animatableModel:r,group:e,data:l});else{for(var h=0;h=e&&(0===t?0:o[t-1][0]).4?"bottom":"middle",textAlign:O<-.4?"left":O>.4?"right":"center"},{autoColor:R}),silent:!0}))}if(x.get("show")&&I!==_){for(var N=0;N<=w;N++){var O=Math.cos(M),L=Math.sin(M),B=new a.Line({shape:{x1:O*g+p,y1:L*g+h,x2:O*(g-S)+p,y2:L*(g-S)+h},silent:!0,style:E});"auto"===E.stroke&&B.setStyle({stroke:o((I+N/w)/_)}),f.add(B),M+=A}M-=A}else M+=C}},_renderPointer:function(e,t,n,o,i,l,s,u){var f=this.group,p=this._data;if(!e.get("pointer.show"))return void(p&&p.eachItemGraphicEl(function(e){f.remove(e)}));var h=[+e.get("min"),+e.get("max")],g=[l,s],m=e.getData();m.diff(p).add(function(t){var n=new r({shape:{angle:l}});a.initProps(n,{shape:{angle:d(m.get("value",t),h,g,!0)}},e),f.add(n),m.setItemGraphicEl(t,n)}).update(function(t,n){var o=p.getItemGraphicEl(n);a.updateProps(o,{shape:{angle:d(m.get("value",t),h,g,!0)}},e),f.add(o),m.setItemGraphicEl(t,o)}).remove(function(e){var t=p.getItemGraphicEl(e);f.remove(t)}).execute(),m.eachItemGraphicEl(function(e,t){var n=m.getItemModel(t),r=n.getModel("pointer");e.setShape({x:i.cx,y:i.cy,width:c(r.get("width"),i.r),r:c(r.get("length"),i.r)}),e.useStyle(n.getModel("itemStyle.normal").getItemStyle()),"auto"===e.style.fill&&e.setStyle("fill",o(d(m.get("value",t),h,[0,1],!0))),a.setHoverStyle(e,n.getModel("itemStyle.emphasis").getItemStyle())}),this._data=m},_renderTitle:function(e,t,n,o,i){var r=e.getModel("title");if(r.get("show")){var l=r.get("offsetCenter"),s=i.cx+c(l[0],i.r),u=i.cy+c(l[1],i.r),f=+e.get("min"),p=+e.get("max"),h=e.getData().get("value",0),g=o(d(h,[f,p],[0,1],!0));this.group.add(new a.Text({silent:!0,style:a.setTextStyle({},r,{x:s,y:u,text:e.getData().getName(0),textAlign:"center",textVerticalAlign:"middle"},{autoColor:g,forceRich:!0})}))}},_renderDetail:function(e,t,n,o,r){var l=e.getModel("detail"),s=+e.get("min"),u=+e.get("max");if(l.get("show")){var f=l.get("offsetCenter"),p=r.cx+c(f[0],r.r),h=r.cy+c(f[1],r.r),g=c(l.get("width"),r.r),m=c(l.get("height"),r.r),v=e.getData().get("value",0),b=o(d(v,[s,u],[0,1],!0));this.group.add(new a.Text({silent:!0,style:a.setTextStyle({},l,{x:p,y:h,text:i(v,l.get("formatter")),textWidth:isNaN(g)?null:g,textHeight:isNaN(m)?null:m,textAlign:"center",textVerticalAlign:"middle"},{autoColor:b,forceRich:!0})}))}}}),h=p;e.exports=h},function(e,t,n){var o=n(16),i=o.extend({type:"echartsGaugePointer",shape:{angle:0,width:10,r:10,x:0,y:0},buildPath:function(e,t){var n=Math.cos,o=Math.sin,i=t.r,r=t.width,a=t.angle,l=t.x-n(a)*r*(r>=i/3?1:2),s=t.y-o(a)*r*(r>=i/3?1:2);a=t.angle-Math.PI/2,e.moveTo(l,s),e.lineTo(t.x+n(a)*r,t.y+o(a)*r),e.lineTo(t.x+n(t.angle)*i,t.y+o(t.angle)*i),e.lineTo(t.x-n(a)*r,t.y-o(a)*r),e.lineTo(l,s)}});e.exports=i},function(e,t,n){var o=n(1),i=n(0);n(390),n(391),n(400);var r=n(393),a=n(46),l=n(394),s=n(397),c=n(401),u=n(395),d=n(399),f=n(396);o.registerProcessor(r),o.registerVisual(i.curry(a,"graph","circle",null)),o.registerVisual(l),o.registerVisual(s),o.registerLayout(c),o.registerLayout(u),o.registerLayout(d),o.registerCoordinateSystem("graphView",{create:f})},function(e,t,n){var o=n(1),i=n(13),r=n(0),a=n(5),l=a.defaultEmphasis,s=n(12),c=n(8),u=c.encodeHTML,d=n(172),f=o.extendSeriesModel({type:"series.graph",init:function(e){f.superApply(this,"init",arguments),this.legendDataProvider=function(){return this._categoriesData},this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},mergeOption:function(e){f.superApply(this,"mergeOption",arguments),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},mergeDefaultAndTheme:function(e){f.superApply(this,"mergeDefaultAndTheme",arguments),l(e.edgeLabel,["show"])},getInitialData:function(e,t){function n(e,n){function o(e){return e=this.parsePath(e),e&&"label"===e[0]?a:this.parentModel}e.wrapMethod("getItemModel",function(e){var t=r._categoriesModels,n=e.getShallow("category"),o=t[n];return o&&(o.parentModel=e.parentModel,e.parentModel=o),e});var i=r.getModel("edgeLabel"),a=new s({label:i.option},i.parentModel,t);n.wrapMethod("getItemModel",function(e){return e.customizeGetParent(o),e})}var o=e.edges||e.links||[],i=e.data||e.nodes||[],r=this;if(i&&o)return d(i,o,this,!0,n).data},getGraph:function(){return this.getData().graph},getEdgeData:function(){return this.getGraph().edgeData},getCategoriesData:function(){return this._categoriesData},formatTooltip:function(e,t,n){if("edge"===n){var o=this.getData(),i=this.getDataParams(e,n),r=o.graph.getEdgeByIndex(e),a=o.getName(r.node1.dataIndex),l=o.getName(r.node2.dataIndex),s=[];return null!=a&&s.push(a),null!=l&&s.push(l),s=u(s.join(" > ")),i.value&&(s+=" : "+u(i.value)),s}return f.superApply(this,"formatTooltip",arguments)},_updateCategoriesData:function(){var e=r.map(this.option.categories||[],function(e){return null!=e.value?e:r.extend({value:0},e)}),t=new i(["value"],this);t.initData(e),this._categoriesData=t,this._categoriesModels=t.mapArray(function(e){return t.getItemModel(e,!0)})},setZoom:function(e){this.option.zoom=e},setCenter:function(e){this.option.center=e},isAnimationEnabled:function(){return f.superCall(this,"isAnimationEnabled")&&!("force"===this.get("layout")&&this.get("force.layoutAnimation"))},defaultOption:{zlevel:0,z:2,coordinateSystem:"view",legendHoverLink:!0,hoverAnimation:!0,layout:null,focusNodeAdjacency:!1,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{normal:{position:"middle"},emphasis:{}},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{normal:{show:!1,formatter:"{b}"},emphasis:{show:!0}},itemStyle:{normal:{},emphasis:{}},lineStyle:{normal:{color:"#aaa",width:1,curveness:0,opacity:.5},emphasis:{}}}}),p=f;e.exports=p},function(e,t,n){function o(e,t){return e.getVisual("opacity")||e.getModel().get(t)}function i(e,t,n){var i=e.getGraphicEl(),r=o(e,t);null!=n&&(null==r&&(r=1),r*=n),i.downplay&&i.downplay(),i.traverse(function(e){"group"!==e.type&&e.setStyle("opacity",r)})}function r(e,t){var n=o(e,t),i=e.getGraphicEl();i.highlight&&i.highlight(),i.traverse(function(e){"group"!==e.type&&e.setStyle("opacity",n)})}var a=n(1),l=n(0),s=n(65),c=n(115),u=n(86),d=n(192),f=n(119),p=f.onIrrelevantElement,h=n(2),g=n(392),m=["itemStyle","normal","opacity"],v=["lineStyle","normal","opacity"],b=a.extendChartView({type:"graph",init:function(e,t){var n=new s,o=new c,i=this.group;this._controller=new u(t.getZr()),this._controllerHost={target:i},i.add(n.group),i.add(o.group),this._symbolDraw=n,this._lineDraw=o,this._firstRender=!0},render:function(e,t,n){var o=e.coordinateSystem;this._model=e,this._nodeScaleRatio=e.get("nodeScaleRatio");var i=this._symbolDraw,r=this._lineDraw,a=this.group;if("view"===o.type){var l={position:o.position,scale:o.scale};this._firstRender?a.attr(l):h.updateProps(a,l,e)}g(e.getGraph(),this._getNodeGlobalScale(e));var s=e.getData();i.updateData(s);var c=e.getEdgeData();r.updateData(c),this._updateNodeAndLinkScale(),this._updateController(e,t,n),clearTimeout(this._layoutTimeout);var u=e.forceLayout,d=e.get("force.layoutAnimation");u&&this._startForceLayoutIteration(u,d),s.eachItemGraphicEl(function(t,o){var i=s.getItemModel(o);t.off("drag").off("dragend");var r=s.getItemModel(o).get("draggable");r&&t.on("drag",function(){u&&(u.warmUp(),!this._layouting&&this._startForceLayoutIteration(u,d),u.setFixed(o),s.setItemLayout(o,t.position))},this).on("dragend",function(){u&&u.setUnfixed(o)},this),t.setDraggable(r&&u),t.off("mouseover",t.__focusNodeAdjacency),t.off("mouseout",t.__unfocusNodeAdjacency),i.get("focusNodeAdjacency")&&(t.on("mouseover",t.__focusNodeAdjacency=function(){n.dispatchAction({type:"focusNodeAdjacency",seriesId:e.id,dataIndex:t.dataIndex})}),t.on("mouseout",t.__unfocusNodeAdjacency=function(){n.dispatchAction({type:"unfocusNodeAdjacency",seriesId:e.id})}))},this),s.graph.eachEdge(function(t){var o=t.getGraphicEl();o.off("mouseover",o.__focusNodeAdjacency),o.off("mouseout",o.__unfocusNodeAdjacency),t.getModel().get("focusNodeAdjacency")&&(o.on("mouseover",o.__focusNodeAdjacency=function(){n.dispatchAction({type:"focusNodeAdjacency",seriesId:e.id,edgeDataIndex:t.dataIndex})}),o.on("mouseout",o.__unfocusNodeAdjacency=function(){n.dispatchAction({type:"unfocusNodeAdjacency",seriesId:e.id})}))});var f="circular"===e.get("layout")&&e.get("circular.rotateLabel"),p=s.getLayout("cx"),m=s.getLayout("cy");s.eachItemGraphicEl(function(e,t){var n=e.getSymbolPath();if(f){var o=s.getItemLayout(t),i=Math.atan2(o[1]-m,o[0]-p);i<0&&(i=2*Math.PI+i);var r=o[0]=0?o+=g:o-=g:y>=0?o-=g:o+=g}return o}function i(e,t){function n(e){var t=e.getVisual("symbolSize");return t instanceof Array&&(t=(t[0]+t[1])/2),t}var i=[],l=r.quadraticSubdivide,s=[[],[],[]],c=[[],[]],u=[];t/=2,e.eachEdge(function(e,r){var d=e.getLayout(),f=e.getVisual("fromSymbol"),p=e.getVisual("toSymbol");d.__original||(d.__original=[a.clone(d[0]),a.clone(d[1])],d[2]&&d.__original.push(a.clone(d[2])));var h=d.__original;if(null!=d[2]){if(a.copy(s[0],h[0]),a.copy(s[1],h[2]),a.copy(s[2],h[1]),f&&"none"!=f){var g=n(e.node1),m=o(s,h[0],g*t);l(s[0][0],s[1][0],s[2][0],m,i),s[0][0]=i[3],s[1][0]=i[4],l(s[0][1],s[1][1],s[2][1],m,i),s[0][1]=i[3],s[1][1]=i[4]}if(p&&"none"!=p){var g=n(e.node2),m=o(s,h[1],g*t);l(s[0][0],s[1][0],s[2][0],m,i),s[1][0]=i[1],s[2][0]=i[2],l(s[0][1],s[1][1],s[2][1],m,i),s[1][1]=i[1],s[2][1]=i[2]}a.copy(d[0],s[0]),a.copy(d[1],s[2]),a.copy(d[2],s[1])}else{if(a.copy(c[0],h[0]),a.copy(c[1],h[1]),a.sub(u,c[1],c[0]),a.normalize(u,u),f&&"none"!=f){var g=n(e.node1);a.scaleAndAdd(c[0],c[0],u,g*t)}if(p&&"none"!=p){var g=n(e.node2);a.scaleAndAdd(c[1],c[1],u,-g*t)}a.copy(d[0],c[0]),a.copy(d[1],c[1])}})}var r=n(39),a=n(7),l=[],s=[],c=[],u=r.quadraticAt,d=a.distSquare,f=Math.abs;e.exports=i},function(e,t){function n(e){var t=e.findComponents({mainType:"legend"});t&&t.length&&e.eachSeriesByType("graph",function(e){var n=e.getCategoriesData(),o=e.getGraph(),i=o.data,r=n.mapArray(n.getName);i.filterSelf(function(e){var n=i.getItemModel(e),o=n.getShallow("category");if(null!=o){"number"==typeof o&&(o=r[o]);for(var a=0;a0){var A=r(b)?l:s;b>0&&(b=b*M+k),y[_++]=A[C],y[_++]=A[C+1],y[_++]=A[C+2],y[_++]=A[C+3]*b*256}else _+=4}return d.putImageData(x,0,0),u},_getBrush:function(){var e=this._brushCanvas||(this._brushCanvas=i.createCanvas()),t=this.pointSize+this.blurSize,n=2*t;e.width=n,e.height=n;var o=e.getContext("2d");return o.clearRect(0,0,n,n),o.shadowOffsetX=n,o.shadowBlur=this.blurSize,o.shadowColor="#000",o.beginPath(),o.arc(-t,t,this.pointSize,0,2*Math.PI,!0),o.closePath(),o.fill(),e},_getGradient:function(e,t,n){for(var o=this._gradientPixels,i=o[n]||(o[n]=new Uint8ClampedArray(1024)),r=[0,0,0,0],a=0,l=0;l<256;l++)t[n](l/255,!0,r),i[a++]=r[0],i[a++]=r[1],i[a++]=r[2],i[a++]=r[3];return i}};var r=o;e.exports=r},function(e,t,n){var o=n(18),i=n(34),r=o.extend({type:"series.heatmap",getInitialData:function(e,t){return i(e.data,this,t)},defaultOption:{coordinateSystem:"cartesian2d",zlevel:0,z:2,geoIndex:0,blurSize:30,pointSize:20,maxOpacity:1,minOpacity:0}});e.exports=r},function(e,t,n){function o(e,t,n){var o=e[1]-e[0];t=u.map(t,function(t){return{interval:[(t.interval[0]-e[0])/o,(t.interval[1]-e[0])/o]}});var i=t.length,r=0;return function(e){for(var o=r;o=0;o--){var a=t[o].interval;if(a[0]<=e&&e<=a[1]){r=o;break}}return o>=0&&o=t[0]&&e<=t[1]}}function r(e){var t=e.dimensions;return"lng"===t[0]&&"lat"===t[1]}var a=n(4),l=(a.__DEV__,n(1)),s=n(2),c=n(403),u=n(0),d=l.extendChartView({type:"heatmap",render:function(e,t,n){var o;t.eachComponent("visualMap",function(t){t.eachTargetSeries(function(n){n===e&&(o=t)})}),this.group.removeAll();var i=e.coordinateSystem;"cartesian2d"===i.type||"calendar"===i.type?this._renderOnCartesianAndCalendar(i,e,n):r(i)&&this._renderOnGeo(i,e,o,n)},dispose:function(){},_renderOnCartesianAndCalendar:function(e,t,n){if("cartesian2d"===e.type)var o=e.getAxis("x"),i=e.getAxis("y"),r=o.getBandWidth(),a=i.getBandWidth();var l=this.group,c=t.getData(),d=t.getModel("itemStyle.normal").getItemStyle(["color"]),f=t.getModel("itemStyle.emphasis").getItemStyle(),p=t.getModel("label.normal"),h=t.getModel("label.emphasis"),g=e.type,m="cartesian2d"===g?[t.coordDimToDataDim("x")[0],t.coordDimToDataDim("y")[0],t.coordDimToDataDim("value")[0]]:[t.coordDimToDataDim("time")[0],t.coordDimToDataDim("value")[0]];c.each(function(n){var o;if("cartesian2d"===g){if(isNaN(c.get(m[2],n)))return;var i=e.dataToPoint([c.get(m[0],n),c.get(m[1],n)]);o=new s.Rect({shape:{x:i[0]-r/2,y:i[1]-a/2,width:r,height:a},style:{fill:c.getItemVisual(n,"color"),opacity:c.getItemVisual(n,"opacity")}})}else{if(isNaN(c.get(m[1],n)))return;o=new s.Rect({z2:1,shape:e.dataToRect([c.get(m[0],n)]).contentShape,style:{fill:c.getItemVisual(n,"color"),opacity:c.getItemVisual(n,"opacity")}})}var v=c.getItemModel(n);c.hasItemOption&&(d=v.getModel("itemStyle.normal").getItemStyle(["color"]),f=v.getModel("itemStyle.emphasis").getItemStyle(),p=v.getModel("label.normal"),h=v.getModel("label.emphasis"));var b=t.getRawValue(n),x="-";b&&null!=b[2]&&(x=b[2]),s.setLabelStyle(d,f,p,h,{labelFetcher:t,labelDataIndex:n,defaultText:x,isRectText:!0}),o.setStyle(d),s.setHoverStyle(o,c.hasItemOption?f:u.extend({},f)),l.add(o),c.setItemGraphicEl(n,o)})},_renderOnGeo:function(e,t,n,r){var a=n.targetVisuals.inRange,l=n.targetVisuals.outOfRange,u=t.getData(),d=this._hmLayer||this._hmLayer||new c;d.blurSize=t.get("blurSize"),d.pointSize=t.get("pointSize"),d.minOpacity=t.get("minOpacity"),d.maxOpacity=t.get("maxOpacity");var f=e.getViewRect().clone(),p=e.getRoamTransform().transform;f.applyTransform(p);var h=Math.max(f.x,0),g=Math.max(f.y,0),m=Math.min(f.width+f.x,r.getWidth()),v=Math.min(f.height+f.y,r.getHeight()),b=m-h,x=v-g,y=u.mapArray(["lng","lat","value"],function(t,n,o){var i=e.dataToPoint([t,n]);return i[0]-=h,i[1]-=g,i.push(o),i}),_=n.getExtent(),w="visualMap.continuous"===n.type?i(_,n.option.range):o(_,n.getPieceList(),n.option.selected);d.update(y,b,x,a.color.getNormalizer(),{inRange:a.color.getColorMapper(),outOfRange:l.color.getColorMapper()},w);var k=new s.Image({style:{width:b,height:x,x:h,y:g,image:d.canvas},silent:!0});this.group.add(k)}});e.exports=d},function(e,t,n){function o(e,t,n){a.call(this,e,t,n),this._lastFrame=0,this._lastFramePercent=0}var i=n(171),r=n(0),a=n(170),l=n(7),s=o.prototype;s.createLine=function(e,t,n){return new i(e,t,n)},s.updateAnimationPoints=function(e,t){this._points=t;for(var n=[0],o=0,i=1;i=0&&!(o[r]<=t);r--);r=Math.min(r,i-2)}else{for(var r=a;rt);r++);r=Math.min(r-1,i-2)}l.lerp(e.position,n[r],n[r+1],(t-o[r])/(o[r+1]-o[r]));var c=n[r+1][0]-n[r][0],u=n[r+1][1]-n[r][1];e.rotation=-Math.atan2(u,c)-Math.PI/2,this._lastFrame=r,this._lastFramePercent=t,e.ignore=!1}},r.inherits(o,a);var c=o;e.exports=c},function(e,t,n){function o(e){return a.isArray(e)||(e=[+e,+e]),e}function i(e,t){e.eachChild(function(e){e.attr({z:t.z,zlevel:t.zlevel,style:{stroke:"stroke"===t.brushType?t.color:null,fill:"fill"===t.brushType?t.color:null}})})}function r(e,t){u.call(this);var n=new p(e,t),o=new u;this.add(n),this.add(o),o.beforeUpdate=function(){this.attr(n.getScale())},this.updateData(e,t)}var a=n(0),l=n(23),s=l.createSymbol,c=n(2),u=c.Group,d=n(3),f=d.parsePercent,p=n(82),h=r.prototype;h.stopEffectAnimation=function(){this.childAt(1).removeAll()},h.startEffectAnimation=function(e){for(var t=e.symbolType,n=e.color,o=this.childAt(1),r=0;r<3;r++){var a=s(t,-1,-1,2,2,n);a.attr({style:{strokeNoScale:!0},z2:99,silent:!0,scale:[.5,.5]});var l=-r/3*e.period+e.effectOffset;a.animate("",!0).when(e.period,{scale:[e.rippleScale/2,e.rippleScale/2]}).delay(l).start(),a.animateStyle(!0).when(e.period,{opacity:0}).delay(l).start(),o.add(a)}i(o,e)},h.updateEffectAnimation=function(e){for(var t=this._effectCfg,n=this.childAt(1),o=["symbolType","period","rippleScale"],r=0;r2?e.quadraticCurveTo(r[2][0],r[2][1],r[1][0],r[1][1]):e.lineTo(r[1][0],r[1][1])}},findDataIndex:function(e,t){for(var n=this.shape,o=n.segs,i=n.polyline,l=Math.max(this.style.lineWidth,1),s=0;s2){if(a.containStroke(c[0][0],c[0][1],c[2][0],c[2][1],c[1][0],c[1][1],l,e,t))return s}else if(r.containStroke(c[0][0],c[0][1],c[1][0],c[1][1],l,e,t))return s}return-1}}),s=o.prototype;s.updateData=function(e){this.group.removeAll();var t=this._lineEl,n=e.hostModel;t.setShape({segs:e.mapArray(e.getItemLayout),polyline:n.get("polyline")}),t.useStyle(n.getModel("lineStyle.normal").getLineStyle());var o=e.getVisual("color");o&&t.setStyle("stroke",o),t.setStyle("fill"),t.seriesIndex=n.seriesIndex,t.on("mousemove",function(e){t.dataIndex=null;var n=t.findDataIndex(e.offsetX,e.offsetY);n>0&&(t.dataIndex=n)}),this.group.add(t)},s.updateLayout=function(e){var t=e.getData();this._lineEl.setShape({segs:t.mapArray(t.getItemLayout)})},s.remove=function(){this.group.removeAll()};var c=o;e.exports=c},function(e,t,n){function o(){this.group=new i.Group,this._symbolEl=new l({})}var i=n(2),r=n(23),a=r.createSymbol,l=i.extendShape({shape:{points:null,sizes:null},symbolProxy:null,buildPath:function(e,t){for(var n=t.points,o=t.sizes,i=this.symbolProxy,r=i.shape,a=0;a=0;r--){var a=o[r],l=i[r],s=a[0]-l[0]/2,c=a[1]-l[1]/2;if(e>=s&&t>=c&&e<=s+l[0]&&t<=c+l[1])return r}return-1}}),s=o.prototype;s.updateData=function(e){this.group.removeAll();var t=this._symbolEl,n=e.hostModel;t.setShape({points:e.mapArray(e.getItemLayout),sizes:e.mapArray(function(t){var n=e.getItemVisual(t,"symbolSize");return n instanceof Array||(n=[n,n]),n})}),t.symbolProxy=a(e.getVisual("symbol"),0,0,0,0),t.setColor=t.symbolProxy.setColor,t.useStyle(n.getModel("itemStyle.normal").getItemStyle(["color"]));var o=e.getVisual("color");o&&t.setColor(o),t.seriesIndex=n.seriesIndex,t.on("mousemove",function(e){t.dataIndex=null;var n=t.findDataIndex(e.offsetX,e.offsetY);n>=0&&(t.dataIndex=n)}),this.group.add(t)},s.updateLayout=function(e){var t=e.getData();this._symbolEl.setShape({points:t.mapArray(t.getItemLayout)})},s.remove=function(){this.group.removeAll()};var c=o;e.exports=c},function(e,t,n){function o(e){return isNaN(+e.cpx1)||isNaN(+e.cpy1)}var i=n(2),r=n(7),a=i.Line.prototype,l=i.BezierCurve.prototype,s=i.extendShape({type:"ec-line",style:{stroke:"#000",fill:null},shape:{x1:0,y1:0,x2:0,y2:0,percent:1,cpx1:null,cpy1:null},buildPath:function(e,t){(o(t)?a:l).buildPath(e,t)},pointAt:function(e){return o(this.shape)?a.pointAt.call(this,e):l.pointAt.call(this,e)},tangentAt:function(e){var t=this.shape,n=o(t)?[t.x2-t.x1,t.y2-t.y1]:l.tangentAt.call(this,e);return r.normalize(n,n)}});e.exports=s},function(e,t,n){function o(e,t,n,o){s.Group.call(this),this.bodyIndex,this.whiskerIndex,this.styleUpdater=n,this._createContent(e,t,o),this.updateData(e,t,o),this._seriesModel}function i(e,t,n){return l.map(e,function(e){return e=e.slice(),e[t]=n.initBaseline,e})}function r(e){var t={};return l.each(e,function(e,n){t["ends"+n]=e}),t}function a(e){this.group=new s.Group,this.styleUpdater=e}var l=n(0),s=n(2),c=n(16),u=c.extend({type:"whiskerInBox",shape:{},buildPath:function(e,t){for(var n in t)if(t.hasOwnProperty(n)&&0===n.indexOf("ends")){var o=t[n];e.moveTo(o[0][0],o[0][1]),e.lineTo(o[1][0],o[1][1])}}}),d=o.prototype;d._createContent=function(e,t,n){var o=e.getItemLayout(t),a="horizontal"===o.chartLayout?1:0,c=0;this.add(new s.Polygon({shape:{points:n?i(o.bodyEnds,a,o):o.bodyEnds},style:{strokeNoScale:!0},z2:100})),this.bodyIndex=c++;var d=l.map(o.whiskerEnds,function(e){return n?i(e,a,o):e});this.add(new u({shape:r(d),style:{strokeNoScale:!0},z2:100})),this.whiskerIndex=c++},d.updateData=function(e,t,n){var o=this._seriesModel=e.hostModel,i=e.getItemLayout(t),a=s[n?"initProps":"updateProps"];a(this.childAt(this.bodyIndex),{shape:{points:i.bodyEnds}},o,t),a(this.childAt(this.whiskerIndex),{shape:r(i.whiskerEnds)},o,t),this.styleUpdater.call(null,this,e,t)},l.inherits(o,s.Group);var f=a.prototype;f.updateData=function(e){var t=this.group,n=this._data,i=this.styleUpdater;e.diff(n).add(function(n){if(e.hasValue(n)){var r=new o(e,n,i,!0);e.setItemGraphicEl(n,r),t.add(r)}}).update(function(r,a){var l=n.getItemGraphicEl(a);if(!e.hasValue(r))return void t.remove(l);l?l.updateData(e,r):l=new o(e,r,i),t.add(l),e.setItemGraphicEl(r,l)}).remove(function(e){var o=n.getItemGraphicEl(e);o&&t.remove(o)}).execute(),this._data=e},f.remove=function(){var e=this.group,t=this._data;this._data=null,t&&t.eachItemGraphicEl(function(t){t&&e.remove(t)})};var p=a;e.exports=p},function(e,t,n){var o=n(1),i=n(0);n(413),n(414);var r=n(46),a=n(127),l=n(602);n(70),o.registerVisual(i.curry(r,"line","circle","line")),o.registerLayout(i.curry(a,"line")),o.registerProcessor(o.PRIORITY.PROCESSOR.STATISTIC,i.curry(l,"line"))},function(e,t,n){var o=n(4),i=(o.__DEV__,n(34)),r=n(18),a=r.extend({type:"series.line",dependencies:["grid","polar"],getInitialData:function(e,t){return i(e.data,this,t)},defaultOption:{zlevel:0,z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,hoverAnimation:!0,clipOverflow:!0,label:{normal:{position:"top"}},lineStyle:{normal:{width:2,type:"solid"}},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:!1,connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0}});e.exports=a},function(e,t,n){function o(e,t){if(e.length===t.length){for(var n=0;nt[0]?1:-1;t[0]+=o*n,t[1]-=o*n}return t}function a(e){return e>=0?1:-1}function l(e,t){var n=e.getBaseAxis(),o=e.getOtherAxis(n),i=0;if(!n.onZero){var r=o.scale.getExtent();r[0]>0?i=r[0]:r[1]<0&&(i=r[1])}var l=o.dim,s="x"===l||"radius"===l?1:0;return t.mapArray([l],function(o,r){for(var c,u=t.stackedOn;u&&a(u.get(l,r))===a(o);){c=u;break}var d=[];return d[s]=t.get(n.dim,r),d[1-s]=c?c.get(l,r,!0):i,e.dataToPoint(d)},!0)}function s(e,t,n){var o=r(e.getAxis("x")),i=r(e.getAxis("y")),a=e.getBaseAxis().isHorizontal(),l=Math.min(o[0],o[1]),s=Math.min(i[0],i[1]),c=Math.max(o[0],o[1])-l,u=Math.max(i[0],i[1])-s,d=n.get("lineStyle.normal.width")||2,f=n.get("clipOverflow")?d/2:Math.max(c,u);a?(s-=f,u+=2*f):(l-=f,c+=2*f);var p=new b.Rect({shape:{x:l,y:s,width:c,height:u}});return t&&(p.shape[a?"width":"height"]=0,b.initProps(p,{shape:{width:c,height:u}},n)),p}function c(e,t,n){var o=e.getAngleAxis(),i=e.getRadiusAxis(),r=i.getExtent(),a=o.getExtent(),l=Math.PI/180,s=new b.Sector({shape:{cx:e.cx,cy:e.cy,r0:r[0],r:r[1],startAngle:-a[0]*l,endAngle:-a[1]*l,clockwise:o.inverse}});return t&&(s.shape.endAngle=-a[0]*l,b.initProps(s,{shape:{endAngle:-a[1]*l}},n)),s}function u(e,t,n){return"polar"===e.type?c(e,t,n):s(e,t,n)}function d(e,t,n){for(var o=t.getBaseAxis(),i="x"===o.dim||"radius"===o.dim?0:1,r=[],a=0;a=0;i--)if(n[i].dimension<2){o=n[i];break}if(o&&"cartesian2d"===t.type){var r=o.dimension,a=e.dimensions[r],l=t.getAxis(a),s=h.map(o.stops,function(e){return{coord:l.toGlobalCoord(l.dataToCoord(e.value)),color:e.color}}),c=s.length,u=o.outerColors.slice();c&&s[0].coord>s[c-1].coord&&(s.reverse(),u.reverse());var d=s[0].coord-10,f=s[c-1].coord+10,p=f-d;if(p<.001)return"transparent";h.each(s,function(e){e.offset=(e.coord-d)/p}),s.push({offset:c?s[c-1].offset:.5,color:u[1]||"transparent"}),s.unshift({offset:c?s[0].offset:.5,color:u[0]||"transparent"});var g=new b.LinearGradient(0,0,0,0,s,!0);return g[a]=d,g[a+"2"]=f,g}}}var p=n(4),h=(p.__DEV__,n(0)),g=n(65),m=n(82),v=n(415),b=n(2),x=n(5),y=n(174),_=y.Polyline,w=y.Polygon,k=n(37),S=k.extend({type:"line",init:function(){var e=new b.Group,t=new g;this.group.add(t.group),this._symbolDraw=t,this._lineGroup=e},render:function(e,t,n){var r=e.coordinateSystem,a=this.group,s=e.getData(),c=e.getModel("lineStyle.normal"),p=e.getModel("areaStyle.normal"),g=s.mapArray(s.getItemLayout,!0),m="polar"===r.type,v=this._coordSys,b=this._symbolDraw,x=this._polyline,y=this._polygon,_=this._lineGroup,w=e.get("animation"),k=!p.isEmpty(),S=l(r,s),M=e.get("showSymbol"),C=M&&!m&&!e.get("showAllSymbol")&&this._getSymbolIgnoreFunc(s,r),A=this._data;A&&A.eachItemGraphicEl(function(e,t){e.__temp&&(a.remove(e),A.setItemGraphicEl(t,null))}),M||b.remove(),a.add(_);var T=!m&&e.get("step");x&&v.type===r.type&&T===this._step?(k&&!y?y=this._newPolygon(g,S,r,w):y&&!k&&(_.remove(y),y=this._polygon=null),_.setClipPath(u(r,!1,e)),M&&b.updateData(s,C),s.eachItemGraphicEl(function(e){e.stopAnimation(!0)}),o(this._stackedOnPoints,S)&&o(this._points,g)||(w?this._updateAnimation(s,S,r,n,T):(T&&(g=d(g,r,T),S=d(S,r,T)),x.setShape({points:g}),y&&y.setShape({points:g,stackedOnPoints:S})))):(M&&b.updateData(s,C),T&&(g=d(g,r,T),S=d(S,r,T)),x=this._newPolyline(g,r,w),k&&(y=this._newPolygon(g,S,r,w)),_.setClipPath(u(r,!0,e)));var E=f(s,r)||s.getVisual("color");x.useStyle(h.defaults(c.getLineStyle(),{fill:"none",stroke:E,lineJoin:"bevel"}));var I=e.get("smooth");if(I=i(e.get("smooth")),x.setShape({smooth:I,smoothMonotone:e.get("smoothMonotone"),connectNulls:e.get("connectNulls")}),y){var O=s.stackedOn,L=0;y.useStyle(h.defaults(p.getAreaStyle(),{fill:E,opacity:.7,lineJoin:"bevel"})),O&&(L=i(O.hostModel.get("smooth"))),y.setShape({smooth:I,stackedOnSmooth:L,smoothMonotone:e.get("smoothMonotone"),connectNulls:e.get("connectNulls")})}this._data=s,this._coordSys=r,this._stackedOnPoints=S,this._points=g,this._step=T},dispose:function(){},highlight:function(e,t,n,o){var i=e.getData(),r=x.queryDataIndex(i,o);if(!(r instanceof Array)&&null!=r&&r>=0){var a=i.getItemGraphicEl(r);if(!a){var l=i.getItemLayout(r);if(!l)return;a=new m(i,r),a.position=l,a.setZ(e.get("zlevel"),e.get("z")),a.ignore=isNaN(l[0])||isNaN(l[1]),a.__temp=!0,i.setItemGraphicEl(r,a),a.stopSymbolAnimation(!0),this.group.add(a)}a.highlight()}else k.prototype.highlight.call(this,e,t,n,o)},downplay:function(e,t,n,o){var i=e.getData(),r=x.queryDataIndex(i,o);if(null!=r&&r>=0){var a=i.getItemGraphicEl(r);a&&(a.__temp?(i.setItemGraphicEl(r,null),this.group.remove(a)):a.downplay())}else k.prototype.downplay.call(this,e,t,n,o)},_newPolyline:function(e){var t=this._polyline;return t&&this._lineGroup.remove(t),t=new _({shape:{points:e},silent:!0,z2:10}),this._lineGroup.add(t),this._polyline=t,t},_newPolygon:function(e,t){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new w({shape:{points:e,stackedOnPoints:t},silent:!0}),this._lineGroup.add(n),this._polygon=n,n},_getSymbolIgnoreFunc:function(e,t){var n=t.getAxesByScale("ordinal")[0];if(n&&n.isLabelIgnored)return h.bind(n.isLabelIgnored,n)},_updateAnimation:function(e,t,n,o,i){var r=this._polyline,a=this._polygon,l=e.hostModel,s=v(this._data,e,this._stackedOnPoints,t,this._coordSys,n),c=s.current,u=s.stackedOnCurrent,f=s.next,p=s.stackedOnNext;i&&(c=d(s.current,n,i),u=d(s.stackedOnCurrent,n,i),f=d(s.next,n,i),p=d(s.stackedOnNext,n,i)),r.shape.__points=s.current,r.shape.points=c,b.updateProps(r,{shape:{points:f}},l),a&&(a.setShape({points:c,stackedOnPoints:u}),b.updateProps(a,{shape:{points:f,stackedOnPoints:p}},l));for(var h=[],g=s.status,m=0;m=0?1:-1}function o(e,t,o){for(var i,r=e.getBaseAxis(),a=e.getOtherAxis(r),l=r.onZero?0:a.scale.getExtent()[0],s=a.dim,c="x"===s||"radius"===s?1:0,u=t.stackedOn,d=t.get(s,o);u&&n(u.get(s,o))===n(d);){i=u;break}var f=[];return f[c]=t.get(r.dim,o),f[1-c]=i?i.get(s,o,!0):l,e.dataToPoint(f)}function i(e,t){var n=[];return t.diff(e).add(function(e){n.push({cmd:"+",idx:e})}).update(function(e,t){n.push({cmd:"=",idx:t,idx1:e})}).remove(function(e){n.push({cmd:"-",idx:e})}).execute(),n}function r(e,t,n,r,a,l){for(var s=i(e,t),c=[],u=[],d=[],f=[],p=[],h=[],g=[],m=l.dimensions,v=0;v "))},defaultOption:{coordinateSystem:"geo",zlevel:0,z:2,legendHoverLink:!0,hoverAnimation:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,label:{normal:{show:!1,position:"end"}},lineStyle:{normal:{opacity:.5}}}})),d=u;e.exports=d},function(e,t,n){var o=n(4),i=(o.__DEV__,n(1)),r=n(115),a=n(170),l=n(114),s=n(171),c=n(406),u=n(408),d=i.extendChartView({type:"lines",init:function(){},render:function(e,t,n){var o=e.getData(),i=this._lineDraw,d=e.get("effect.show"),f=e.get("polyline"),p=e.get("large")&&o.count()>=e.get("largeThreshold");d===this._hasEffet&&f===this._isPolyline&&p===this._isLarge||(i&&i.remove(),i=this._lineDraw=p?new u:new r(f?d?c:s:d?a:l),this._hasEffet=d,this._isPolyline=f,this._isLarge=p);var h=e.get("zlevel"),g=e.get("effect.trailLength"),m=n.getZr(),v="svg"===m.painter.getType();v||m.painter.getLayer(h).clear(!0),null==this._lastZlevel||v||m.configLayer(this._lastZlevel,{motionBlur:!1}),d&&g&&(v||m.configLayer(h,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(g/10+.9,1),0)})),this.group.add(i.group),i.updateData(o),this._lastZlevel=h},updateLayout:function(e,t,n){this._lineDraw.updateLayout(e);var o=n.getZr();"svg"===o.painter.getType()||o.painter.getLayer(this._lastZlevel).clear(!0)},remove:function(e,t){this._lineDraw&&this._lineDraw.remove(t,!0);var n=t.getZr();"svg"===n.painter.getType()||n.painter.getLayer(this._lastZlevel).clear(!0)},dispose:function(){}});e.exports=d},function(e,t,n){function o(e){e.eachSeriesByType("lines",function(e){var t=e.coordinateSystem,n=e.getData();n.each(function(o){var i=n.getItemModel(o),r=i.option instanceof Array?i.option:i.get("coords"),a=[];if(e.get("polyline"))for(var l=0;l"+s(o+" : "+n)},getTooltipPosition:function(e){if(null!=e){var t=this.getData().getName(e),n=this.coordinateSystem,o=n.getRegion(t);return o&&n.dataToPoint(o.center)}},setZoom:function(e){this.option.zoom=e},setCenter:function(e){this.option.center=e},defaultOption:{zlevel:0,z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:.75,showLegendSymbol:!0,dataRangeHoverLink:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,label:{normal:{show:!1,color:"#000"},emphasis:{show:!0,color:"rgb(100,0,0)"}},itemStyle:{normal:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{areaColor:"rgba(255,215,0,0.8)"}}}});o.mixin(f,u);var p=f;e.exports=p},function(e,t,n){var o=n(1),i=n(0),r=n(2),a=n(188),l=o.extendChartView({type:"map",render:function(e,t,n,o){if(!o||"mapToggleSelect"!==o.type||o.from!==this.uid){var i=this.group;if(i.removeAll(),!e.getHostGeoModel()){if(o&&"geoRoam"===o.type&&"series"===o.componentType&&o.seriesId===e.id){var r=this._mapDraw;r&&i.add(r.group)}else if(e.needsDrawMap){var r=this._mapDraw||new a(n,!0);i.add(r.group),r.draw(e,t,n,this,o),this._mapDraw=r}else this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null;e.get("showLegendSymbol")&&t.getComponent("legend")&&this._renderSymbols(e,t,n)}}},remove:function(){this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null,this.group.removeAll()},dispose:function(){this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null},_renderSymbols:function(e,t,n){var o=e.originalData,a=this.group;o.each("value",function(t,n){if(!isNaN(t)){var l=o.getItemLayout(n);if(l&&l.point){var s=l.point,c=l.offset,u=new r.Circle({style:{fill:e.getData().getVisual("color")},shape:{cx:s[0]+9*c,cy:s[1],r:3},silent:!0,z2:c?8:10});if(!c){var d=e.mainSeries.getData(),f=o.getName(n),p=d.indexOfName(f),h=o.getItemModel(n),g=h.getModel("label.normal"),m=h.getModel("label.emphasis"),v=d.getItemGraphicEl(p),b=i.retrieve2(e.getFormattedLabel(n,"normal"),f),x=i.retrieve2(e.getFormattedLabel(n,"emphasis"),b),y=function(){var e=r.setTextStyle({},m,{text:m.get("show")?x:null},{isRectText:!0,useInsideStyle:!1},!0);u.style.extendFrom(e),u.__mapOriginalZ2=u.z2,u.z2+=1},_=function(){r.setTextStyle(u.style,g,{text:g.get("show")?b:null,textPosition:g.getShallow("position")||"bottom"},{isRectText:!0,useInsideStyle:!1}),null!=u.__mapOriginalZ2&&(u.z2=u.__mapOriginalZ2,u.__mapOriginalZ2=null)};v.on("mouseover",y).on("mouseout",_).on("emphasis",y).on("normal",_),_()}a.add(u)}}})}});e.exports=l},function(e,t,n){function o(e){var t=[];i.each(e.series,function(e){e&&"map"===e.type&&(t.push(e),e.map=e.map||e.mapType,i.defaults(e,e.mapLocation))})}var i=n(0);e.exports=o},function(e,t,n){function o(e,t){var n={},o=["value"];return r.each(e,function(e){e.each(o,function(t,o){var i="ec-"+e.getName(o);n[i]=n[i]||[],isNaN(t)||n[i].push(t)})}),e[0].map(o,function(o,i){for(var r="ec-"+e[0].getName(i),a=0,l=1/0,s=-1/0,c=n[r].length,u=0;u=0?t:NaN}})}function i(e){return+e.replace("dim","")}function r(e,t){var n=0;l.each(e,function(e){var t=i(e);t>n&&(n=t)});var o=t[0];o&&o.length-1>n&&(n=o.length-1);for(var r=[],a=0;a<=n;a++)r.push("dim"+a);return r}var a=n(13),l=n(0),s=n(18),c=n(25),u=s.extend({type:"series.parallel",dependencies:["parallel"],visualColorAccessPath:"lineStyle.normal.color",getInitialData:function(e,t){var n=t.getComponent("parallel",this.get("parallelIndex")),i=n.parallelAxisIndex,s=e.data,u=n.dimensions,d=r(u,s),f=l.map(d,function(e,n){var r=l.indexOf(u,e),a=r>=0&&t.getComponent("parallelAxis",i[r]);return a&&"category"===a.get("type")?(o(a,e,s),{name:e,type:"ordinal"}):r<0&&c.guessOrdinal(s,n)?{name:e,type:"ordinal"}:e}),p=new a(f,this);return p.initData(s),this.option.progressive&&(this.option.animation=!1),p},getRawIndicesByActiveState:function(e){var t=this.coordinateSystem,n=this.getData(),o=[];return t.eachActiveState(n,function(t,i){e===t&&o.push(n.getRawIndex(i))}),o},defaultOption:{zlevel:0,z:2,coordinateSystem:"parallel",parallelIndex:0,label:{normal:{show:!1},emphasis:{show:!1}},inactiveOpacity:.05,activeOpacity:1,lineStyle:{normal:{width:1,opacity:.45,type:"solid"}},progressive:!1,smooth:!1,animationEasing:"linear"}});e.exports=u},function(e,t,n){function o(e,t,n){var o=e.model,i=e.getRect(),r=new s.Rect({shape:{x:i.x,y:i.y,width:i.width,height:i.height}}),a="horizontal"===o.get("layout")?"width":"height";return r.setShape(a,0),s.initProps(r,{shape:{width:i.width,height:i.height}},t,n),r}function i(e,t,n,o){for(var i=[],r=0;r0&&"scale"!==f){var g=l.getItemLayout(0),m=Math.max(n.getWidth(),n.getHeight())/2,v=a.bind(c.removeClipPath,c);c.setClipPath(this._createClipPath(g.cx,g.cy,m,g.startAngle,g.clockwise,v,e))}this._data=l}},dispose:function(){},_createClipPath:function(e,t,n,o,i,r,a){var s=new l.Sector({shape:{cx:e,cy:t,r0:0,r:n,startAngle:o,endAngle:o,clockwise:i}});return l.initProps(s,{shape:{endAngle:o+(i?1:-1)*Math.PI*2}},a,r),s},containPoint:function(e,t){var n=t.getData(),o=n.getItemLayout(0);if(o){var i=e[0]-o.cx,r=e[1]-o.cy,a=Math.sqrt(i*i+r*r);return a<=o.r&&a>=o.r0}}}),d=u;e.exports=d},function(e,t,n){function o(e,t,n,o,i,r,a){function l(t,n){for(var o=t;o>=0&&(e[o].y-=n,!(o>0&&e[o].y>e[o-1].y+e[o-1].height));o--);}function s(e,t,n,o,i,r){for(var a=t?Number.MAX_VALUE:0,l=0,s=e.length;l=a&&(f=a-10),!t&&f<=a&&(f=a+10),e[l].x=n+f*r,a=f}}e.sort(function(e,t){return e.y-t.y});for(var c,u=0,d=e.length,f=[],p=[],h=0;ht&&r+1e[r].y+e[r].height)return void l(r,o/2);l(n-1,o/2)}(h,d,-c),u=e[h].y+e[h].height;a-u<0&&l(d-1,u-a);for(var h=0;h=n?p.push(e[h]):f.push(e[h]);s(f,!1,t,n,o,i),s(p,!0,t,n,o,i)}function i(e,t,n,i,r,a){for(var l=[],s=[],c=0;c0?"left":"right"}var I=g.getFont(),O=g.get("rotate")?_<0?-y+Math.PI:-y:0,L=e.getFormattedLabel(n,"normal")||s.getName(n),P=a.getBoundingRect(L,I,f,"top");u=!!O,p.label={x:o,y:i,position:m,height:P.height,len:b,len2:x,linePoints:d,textAlign:f,verticalAlign:"middle",rotation:O,inside:k},k||c.push(p.label)}),!u&&e.get("avoidLabelOverlap")&&i(c,r,l,t,n,o)}var a=n(27);e.exports=r},function(e,t,n){function o(e,t,n,o){t.eachSeriesByType(e,function(e){var t=e.get("center"),o=e.get("radius");s.isArray(o)||(o=[0,o]),s.isArray(t)||(t=[t,t]);var i=n.getWidth(),d=n.getHeight(),f=Math.min(i,d),p=r(t[0],i),h=r(t[1],d),g=r(o[0],f/2),m=r(o[1],f/2),v=e.getData(),b=-e.get("startAngle")*u,x=e.get("minAngle")*u,y=0;v.each("value",function(e){!isNaN(e)&&y++});var _=v.getSum("value"),w=Math.PI/(_||y)*2,k=e.get("clockwise"),S=e.get("roseType"),M=e.get("stillShowZeroSum"),C=v.getDataExtent("value");C[0]=0;var A=c,T=0,E=b,I=k?1:-1;if(v.each("value",function(e,t){var n;if(isNaN(e))return void v.setItemLayout(t,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:k,cx:p,cy:h,r0:g,r:S?NaN:m});n="area"!==S?0===_&&M?w:e*w:c/y,n"+a.map(o,function(e,n){return s(e.name+" : "+t[n])}).join("
")},defaultOption:{zlevel:0,z:2,coordinateSystem:"radar",legendHoverLink:!0,radarIndex:0,lineStyle:{normal:{width:2,type:"solid"}},label:{normal:{position:"top"}},symbol:"emptyCircle",symbolSize:4}}),u=c;e.exports=u},function(e,t,n){function o(e){return a.isArray(e)||(e=[+e,+e]),e}var i=n(1),r=n(2),a=n(0),l=n(23),s=i.extendChartView({type:"radar",render:function(e,t,n){function i(e,t){var n=e.getItemVisual(t,"symbol")||"circle",i=e.getItemVisual(t,"color");if("none"!==n){var r=o(e.getItemVisual(t,"symbolSize")),a=l.createSymbol(n,-1,-1,2,2,i);return a.attr({style:{strokeNoScale:!0},z2:100,scale:[r[0]/2,r[1]/2]}),a}}function s(t,n,o,a,l,s){o.removeAll();for(var c=0;c0;i--)a*=.99,p(r,a),f(r,o,n),g(r,a),f(r,o,n)}function d(e,t,n,o,i){var r=[];A.each(t,function(e){var t=e.length,n=0;A.each(e,function(e){n+=e.getLayout().value});var a=(o-(t-1)*i)/n;r.push(a)}),r.sort(function(e,t){return e-t});var a=r[0];A.each(t,function(e){A.each(e,function(e,t){e.setLayout({y:t},!0);var n=e.getLayout().value*a;e.setLayout({dy:n},!0)})}),A.each(n,function(e){var t=+e.getValue()*a;e.setLayout({dy:t},!0)})}function f(e,t,n){A.each(e,function(e){var o,i,r,a=0,l=e.length;for(e.sort(w),r=0;r0){var s=o.getLayout().y+i;o.setLayout({y:s},!0)}a=o.getLayout().y+o.getLayout().dy+t}if((i=a-t-n)>0){var s=o.getLayout().y-i;for(o.setLayout({y:s},!0),a=o.getLayout().y,r=l-2;r>=0;--r)o=e[r],i=o.getLayout().y+o.getLayout().dy+t-a,i>0&&(s=o.getLayout().y-i,o.setLayout({y:s},!0)),a=o.getLayout().y}})}function p(e,t){A.each(e.slice().reverse(),function(e){A.each(e,function(e){if(e.outEdges.length){var n=y(e.outEdges,h)/y(e.outEdges,S),o=e.getLayout().y+(n-_(e))*t;e.setLayout({y:o},!0)}})})}function h(e){return _(e.node2)*e.getValue()}function g(e,t){A.each(e,function(e){A.each(e,function(e){if(e.inEdges.length){var n=y(e.inEdges,m)/y(e.inEdges,S),o=e.getLayout().y+(n-_(e))*t;e.setLayout({y:o},!0)}})})}function m(e){return _(e.node1)*e.getValue()}function v(e){A.each(e,function(e){e.outEdges.sort(b),e.inEdges.sort(x)}),A.each(e,function(e){var t=0,n=0;A.each(e.outEdges,function(e){e.setLayout({sy:t},!0),t+=e.getLayout().dy}),A.each(e.inEdges,function(e){e.setLayout({ty:n},!0),n+=e.getLayout().dy})})}function b(e,t){return e.node2.getLayout().y-t.node2.getLayout().y}function x(e,t){return e.node1.getLayout().y-t.node1.getLayout().y}function y(e,t){for(var n=0,o=e.length,i=-1;++it?1:e===t?0:NaN}function S(e){return e.getValue()}var M=n(6),C=n(217),A=n(0);e.exports=o},function(e,t,n){function o(e,t){e.eachSeriesByType("sankey",function(e){var t=e.getGraph(),n=t.nodes;n.sort(function(e,t){return e.getLayout().value-t.getLayout().value});var o=n[0].getLayout().value,a=n[n.length-1].getLayout().value;r.each(n,function(t){var n=new i({type:"color",mappingMethod:"linear",dataExtent:[o,a],visual:e.get("color")}),r=n.mapValueToVisual(t.getLayout().value);t.setVisual("color",r);var l=t.getModel(),s=l.get("itemStyle.normal.color");null!=s&&t.setVisual("color",s)})})}var i=n(45),r=n(0);e.exports=o},function(e,t,n){var o=n(1),i=n(0);n(448),n(449);var r=n(46),a=n(127);n(70),o.registerVisual(i.curry(r,"scatter","circle",null)),o.registerLayout(i.curry(a,"scatter"))},function(e,t,n){var o=n(34),i=n(18),r=i.extend({type:"series.scatter",dependencies:["grid","polar","geo","singleAxis","calendar"],getInitialData:function(e,t){return o(e.data,this,t)},brushSelector:"point",defaultOption:{coordinateSystem:"cartesian2d",zlevel:0,z:2,legendHoverLink:!0,hoverAnimation:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{normal:{opacity:.8}}}});e.exports=r},function(e,t,n){var o=n(1),i=n(65),r=n(409);o.extendChartView({type:"scatter",init:function(){this._normalSymbolDraw=new i,this._largeSymbolDraw=new r},render:function(e,t,n){var o=e.getData(),i=this._largeSymbolDraw,r=this._normalSymbolDraw,a=this.group,l=e.get("large")&&o.count()>e.get("largeThreshold")?i:r;this._symbolDraw=l,l.updateData(o),a.add(l.group),a.remove(l===i?r.group:i.group)},updateLayout:function(e){this._symbolDraw.updateLayout(e)},remove:function(e,t){this._symbolDraw&&this._symbolDraw.remove(t,!0)},dispose:function(){}})},function(e,t,n){var o=n(1),i=n(0);n(196),n(451),n(452);var r=n(453),a=n(454),l=n(89);o.registerLayout(r),o.registerVisual(a),o.registerProcessor(i.curry(l,"themeRiver"))},function(e,t,n){var o=n(25),i=n(18),r=n(13),a=n(0),l=n(8),s=l.encodeHTML,c=n(217),u=i.extend({type:"series.themeRiver",dependencies:["singleAxis"],nameMap:null,init:function(e){u.superApply(this,"init",arguments),this.legendDataProvider=function(){return this.getRawData()}},fixData:function(e){for(var t=e.length,n=c().key(function(e){return e[2]}).entries(e),o=a.map(n,function(e){return{name:e.key,dataList:e.values}}),i=o.length,r=-1,l=-1,s=0;sr&&(r=u,l=s)}for(var d=0;da&&(a=t),i.push(t)}for(var u=0;ua&&(a=f)}return l.y0=r,l.max=a,l}var a=n(0),l=n(3);e.exports=o},function(e,t,n){function o(e){e.eachSeriesByType("themeRiver",function(e){var t=e.getData(),n=e.getRawData(),o=e.get("color"),i=r();t.each(function(e){i.set(t.getRawIndex(e),e)}),n.each(function(r){var a=n.getName(r),l=o[(e.nameMap.get(a)-1)%o.length];n.setItemVisual(r,"color",l);var s=i.get(r);null!=s&&t.setItemVisual(s,"color",l)})})}var i=n(0),r=i.createHashMap;e.exports=o},function(e,t,n){var o=n(1),i=n(0);n(456),n(457),n(461);var r=n(46),a=n(458),l=n(459);o.registerVisual(i.curry(r,"tree","circle",null)),o.registerLayout(a),o.registerLayout(l)},function(e,t,n){var o=n(18),i=n(212),r=n(8),a=r.encodeHTML,l=o.extend({type:"series.tree",layoutInfo:null,layoutMode:"box",getInitialData:function(e){var t={name:e.name,children:e.data},n=e.leaves||{},o={};o.leaves=n;var r=i.createTree(t,this,o),a=0;r.eachNode("preorder",function(e){e.depth>a&&(a=e.depth)});var l=e.expandAndCollapse,s=l&&e.initialTreeDepth>=0?e.initialTreeDepth:a;return r.root.eachNode("preorder",function(e){var t=e.hostTree.data.getRawDataItem(e.dataIndex);e.isExpand=t&&null!=t.collapsed?!t.collapsed:e.depth<=s}),r.data},formatTooltip:function(e){for(var t=this.getData().tree,n=t.root.children[0],o=t.getNodeByDataIndex(e),i=o.getValue(),r=o.name;o&&o!==n;)r=o.parentNode.name+"."+r,o=o.parentNode;return a(r+(isNaN(i)||null==i?"":" : "+i))},defaultOption:{zlevel:0,z:2,left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",orient:"horizontal",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{normal:{color:"#ccc",width:1.5,curveness:.5}},itemStyle:{normal:{color:"lightsteelblue",borderColor:"#c23531",borderWidth:1.5}},label:{normal:{show:!0,color:"#555"}},leaves:{label:{normal:{show:!0}}},animationEasing:"linear",animationDuration:700,animationDurationUpdate:1e3}});e.exports=l},function(e,t,n){function o(e,t){var n=e.getItemLayout(t);return n&&!isNaN(n.x)&&!isNaN(n.y)&&"none"!==e.getItemVisual(t,"symbol")}function i(e,t,n){return n.itemModel=t,n.itemStyle=t.getModel("itemStyle.normal").getItemStyle(),n.hoverItemStyle=t.getModel("itemStyle.emphasis").getItemStyle(),n.lineStyle=t.getModel("lineStyle.normal").getLineStyle(),n.labelModel=t.getModel("label.normal"),n.hoverLabelModel=t.getModel("label.emphasis"),!1===e.isExpand&&0!==e.children.length?n.symbolInnerColor=n.itemStyle.fill:n.symbolInnerColor="#fff",n}function r(e,t,n,o,r,a){var d=!n,f=e.tree.getNodeByDataIndex(t),p=f.getModel(),a=i(f,p,a),h=e.tree.root,g=f.parentNode===h?f:f.parentNode||f,m=e.getItemGraphicEl(g.dataIndex),v=g.getLayout(),b=m?{x:m.position[0],y:m.position[1],rawX:m.__radialOldRawX,rawY:m.__radialOldRawY}:v,x=f.getLayout();d?(n=new u(e,t,a),n.attr("position",[b.x,b.y])):n.updateData(e,t,a),n.__radialOldRawX=n.__radialRawX,n.__radialOldRawY=n.__radialRawY,n.__radialRawX=x.rawX,n.__radialRawY=x.rawY,o.add(n),e.setItemGraphicEl(t,n),c.updateProps(n,{position:[x.x,x.y]},r);var y=n.getSymbolPath();if("radial"===a.layout){var _,w,k=h.children[0],S=k.getLayout(),M=k.children.length;if(x.x===S.x&&!0===f.isExpand){var C={};C.x=(k.children[0].getLayout().x+k.children[M-1].getLayout().x)/2,C.y=(k.children[0].getLayout().y+k.children[M-1].getLayout().y)/2,_=Math.atan2(C.y-S.y,C.x-S.x),_<0&&(_=2*Math.PI+_),(w=C.xS.x)||(_-=Math.PI);var A=w?"left":"right";y.setStyle({textPosition:A,textRotation:-_,textOrigin:"center",verticalAlign:"middle"})}if(f.parentNode&&f.parentNode!==h){var T=n.__edge;T||(T=n.__edge=new c.BezierCurve({shape:l(a,b,b),style:s.defaults({opacity:0},a.lineStyle)})),c.updateProps(T,{shape:l(a,v,x),style:{opacity:1}},r),o.add(T)}}function a(e,t,n,o,r,a){for(var s,u=e.tree.getNodeByDataIndex(t),d=e.tree.root,f=u.getModel(),a=i(u,f,a),p=u.parentNode===d?u:u.parentNode||u;null==(s=p.getLayout());)p=p.parentNode===d?p:p.parentNode||p;c.updateProps(n,{position:[s.x+1,s.y+1]},r,function(){o.remove(n),e.setItemGraphicEl(t,null)}),n.fadeOut(null,{keepLabel:!0});var h=n.__edge;h&&c.updateProps(h,{shape:l(a,s,s),style:{opacity:0}},r,function(){o.remove(h)})}function l(e,t,n){var o,i,r,a,l=e.orient;if("radial"===e.layout){var s=t.rawX,c=t.rawY,u=n.rawX,d=n.rawY,p=f(s,c),h=f(s,c+(d-c)*e.curvature),g=f(u,d+(c-d)*e.curvature),m=f(u,d);return{x1:p.x,y1:p.y,x2:m.x,y2:m.y,cpx1:h.x,cpy1:h.y,cpx2:g.x,cpy2:g.y}}var s=t.x,c=t.y,u=n.x,d=n.y;return"horizontal"===l&&(o=s+(u-s)*e.curvature,i=c,r=u+(s-u)*e.curvature,a=d),"vertical"===l&&(o=s,i=c+(d-c)*e.curvature,r=u,a=d+(c-d)*e.curvature),{x1:s,y1:c,x2:u,y2:d,cpx1:o,cpy1:i,cpx2:r,cpy2:a}}var s=n(0),c=n(2),u=n(82),d=n(177),f=d.radialCoordinate,p=n(1),h=p.extendChartView({type:"tree",init:function(e,t){this._oldTree,this._mainGroup=new c.Group,this.group.add(this._mainGroup)},render:function(e,t,n,i){var l=e.getData(),s=e.layoutInfo,c=this._mainGroup,u=e.get("layout");"radial"===u?c.attr("position",[s.x+s.width/2,s.y+s.height/2]):c.attr("position",[s.x,s.y]);var d=this._data,f={expandAndCollapse:e.get("expandAndCollapse"),layout:u,orient:e.get("orient"),curvature:e.get("lineStyle.normal.curveness"),symbolRotate:e.get("symbolRotate"),symbolOffset:e.get("symbolOffset"),hoverAnimation:e.get("hoverAnimation"),useNameLabel:!0,fadeIn:!0};l.diff(d).add(function(t){o(l,t)&&r(l,t,null,c,e,f)}).update(function(t,n){var i=d.getItemGraphicEl(n);if(!o(l,t))return void(i&&a(l,t,i,c,e,f));r(l,t,i,c,e,f)}).remove(function(t){var n=d.getItemGraphicEl(t);a(l,t,n,c,e,f)}).execute(),!0===f.expandAndCollapse&&l.eachItemGraphicEl(function(t,o){t.off("click").on("click",function(){n.dispatchAction({type:"treeExpandAndCollapse",seriesId:e.id,dataIndex:o})})}),this._data=l},dispose:function(){},remove:function(){this._mainGroup.removeAll(),this._data=null}});e.exports=h},function(e,t,n){function o(e,t){e.eachSeriesByType("tree",function(e){i(e,t)})}var i=n(176);e.exports=o},function(e,t,n){function o(e,t){e.eachSeriesByType("tree",function(e){i(e,t)})}var i=n(176);e.exports=o},function(e,t){function n(e,t,n){for(var o,i=[e],r=[];o=i.pop();)if(r.push(o),o.isExpand){var a=o.children;if(a.length)for(var l=0;l=0;r--)o.push(i[r])}}t.eachAfter=n,t.eachBefore=o},function(e,t,n){n(1).registerAction({type:"treeExpandAndCollapse",event:"treeExpandAndCollapse",update:"update"},function(e,t){t.eachComponent({mainType:"series",subType:"tree",query:e},function(t){var n=e.dataIndex,o=t.getData().tree,i=o.getNodeByDataIndex(n);i.isExpand=!i.isExpand})})},function(e,t,n){var o=n(1);n(464),n(465),n(466);var i=n(468),r=n(467);o.registerVisual(i),o.registerLayout(r)},function(e,t,n){function o(e){this.group=new a.Group,e.add(this.group)}function i(e,t,n,o,i,r){var a=[[i?e:e-d,t],[e+n,t],[e+n,t+o],[i?e:e-d,t+o]];return!r&&a.splice(2,0,[e+n+d,t+o/2]),!i&&a.push([e,t+o/2]),a}function r(e,t,n){e.eventData={componentType:"series",componentSubType:"treemap",seriesIndex:t.componentIndex,seriesName:t.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&u(n,t)}}var a=n(2),l=n(6),s=n(0),c=n(66),u=c.wrapTreePathInfo,d=5;o.prototype={constructor:o,render:function(e,t,n,o){var i=e.getModel("breadcrumb"),r=this.group;if(r.removeAll(),i.get("show")&&n){var a=i.getModel("itemStyle.normal"),s=a.getModel("textStyle"),c={pos:{left:i.get("left"),right:i.get("right"),top:i.get("top"),bottom:i.get("bottom")},box:{width:t.getWidth(),height:t.getHeight()},emptyItemWidth:i.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,c,s),this._renderContent(e,c,a,s,o),l.positionElement(r,c.pos,c.box)}},_prepare:function(e,t,n){for(var o=e;o;o=o.parentNode){var i=o.getModel().get("name"),r=n.getTextRect(i),a=Math.max(r.width+16,t.emptyItemWidth);t.totalWidth+=a+8,t.renderList.push({node:o,text:i,width:a})}},_renderContent:function(e,t,n,o,c){for(var u=0,d=t.emptyItemWidth,f=e.get("breadcrumb.height"),p=l.getAvailableSize(t.pos,t.box),h=t.totalWidth,g=t.renderList,m=g.length-1;m>=0;m--){var v=g[m],b=v.node,x=v.width,y=v.text;h>p.width&&(h-=x-d,x=d,y=null);var _=new a.Polygon({shape:{points:i(u,0,x,f,m===g.length-1,0===m)},style:s.defaults(n.getItemStyle(),{lineJoin:"bevel",text:y,textFill:o.getTextColor(),textFont:o.getFont()}),z:10,onclick:s.curry(c,b)});this.group.add(_),r(_,e,b),u+=x+8}},remove:function(){this.group.removeAll()}};var f=o;e.exports=f},function(e,t,n){function o(e){var t=0;r.each(e.children,function(e){o(e);var n=e.value;r.isArray(n)&&(n=n[0]),t+=n});var n=e.value;r.isArray(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=t),n<0&&(n=0),r.isArray(e.value)?e.value[0]=n:e.value=n}function i(e,t){var n=t.get("color");if(n){e=e||[];var o;return r.each(e,function(e){var t=new s(e),n=t.get("color");(t.get("itemStyle.normal.color")||n&&"none"!==n)&&(o=!0)}),o||((e[0]||(e[0]={})).color=n.slice()),e}}var r=n(0),a=n(18),l=n(212),s=n(12),c=n(8),u=c.encodeHTML,d=c.addCommas,f=n(66),p=f.wrapTreePathInfo,h=a.extend({type:"series.treemap",layoutMode:"box",dependencies:["grid","polar"],_viewRoot:null,defaultOption:{progressive:0,hoverLayerThreshold:1/0,left:"center",top:"middle",right:null,bottom:null,width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{normal:{color:"rgba(0,0,0,0.7)",borderColor:"rgba(255,255,255,0.7)",borderWidth:1,shadowColor:"rgba(150,150,150,1)",shadowBlur:3,shadowOffsetX:0,shadowOffsetY:0,textStyle:{color:"#fff"}},emphasis:{textStyle:{}}}},label:{normal:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",ellipsis:!0}},upperLabel:{normal:{show:!1,position:[0,"50%"],height:20,color:"#fff",ellipsis:!0,verticalAlign:"middle"},emphasis:{show:!0,position:[0,"50%"],color:"#fff",ellipsis:!0,verticalAlign:"middle"}},itemStyle:{normal:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},getInitialData:function(e,t){var n={name:e.name,children:e.data};o(n);var r=e.levels||[];r=e.levels=i(r,t);var a={};return a.levels=r,l.createTree(n,this,a).data},optionUpdated:function(){this.resetViewRoot()},formatTooltip:function(e){var t=this.getData(),n=this.getRawValue(e),o=d(r.isArray(n)?n[0]:n),i=t.getName(e);return u(i+": "+o)},getDataParams:function(e){var t=a.prototype.getDataParams.apply(this,arguments),n=this.getData().tree.getNodeByDataIndex(e);return t.treePathInfo=p(n,this),t},setLayoutInfo:function(e){this.layoutInfo=this.layoutInfo||{},r.extend(this.layoutInfo,e)},mapIdToIndex:function(e){var t=this._idIndexMap;t||(t=this._idIndexMap=r.createHashMap(),this._idIndexMapCount=0);var n=t.get(e);return null==n&&t.set(e,n=this._idIndexMapCount++),n},getViewRoot:function(){return this._viewRoot},resetViewRoot:function(e){e?this._viewRoot=e:e=this._viewRoot;var t=this.getData().tree.root;e&&(e===t||t.contains(e))||(this._viewRoot=t)}});e.exports=h},function(e,t,n){function o(){return{nodeGroup:[],background:[],content:[]}}function i(e,t,n,o,i,a,c,u,d,f){function p(e,t){L?!e.invisible&&a.push(e):(t(),e.__tmWillVisible||(e.invisible=!1))}function h(t,n,o,i,r,a){var u=c.getModel(),d=l.retrieve(e.getFormattedLabel(c.dataIndex,"normal",null,null,a?"upperLabel":"label"),u.get("name"));if(!a&&y.isLeafRoot){var f=e.get("drillDownIcon",!0);d=f?f+" "+d:d}var p=u.getModel(a?k:_),h=u.getModel(a?S:w),g=p.getShallow("show");s.setLabelStyle(t,n,p,h,{defaultText:g?d:null,autoColor:o,isRectText:!0}),a&&(t.textRect=l.clone(a)),t.truncate=g&&p.get("ellipsis")?{outerWidth:i,outerHeight:r,minChar:2}:null}function g(e,o,a,l){var s=null!=D&&n[e][D],c=i[e];return s?(n[e][D]=null,m(c,s,e)):L||(s=new o({z:r(a,l)}),s.__tmDepth=a,s.__tmStorageName=e,v(c,s,e)),t[e][P]=s}function m(e,t,n){(e[P]={}).old="nodeGroup"===n?t.position.slice():l.extend({},t.shape)}function v(e,t,n){var r=e[P]={},a=c.parentNode;if(a&&(!o||"drillDown"===o.direction)){var l=0,s=0,u=i.background[a.getRawIndex()];!o&&u&&u.old&&(l=u.old.width,s=u.old.height),r.old="nodeGroup"===n?[0,s]:{x:l,y:s,width:0,height:0}}r.fadein="nodeGroup"!==n}if(c){var y=c.getLayout();if(y&&y.isInView){var M=y.width,I=y.height,O=y.borderWidth,L=y.invisible,P=c.getRawIndex(),D=u&&u.getRawIndex(),z=c.viewChildren,R=y.upperHeight,N=z&&z.length,B=c.getModel("itemStyle.normal"),F=c.getModel("itemStyle.emphasis"),V=g("nodeGroup",b);if(V){if(d.add(V),V.attr("position",[y.x||0,y.y||0]),V.__tmNodeWidth=M,V.__tmNodeHeight=I,y.isAboveViewRoot)return V;var j=g("background",x,f,C);if(j&&function(t,n,o){n.dataIndex=c.dataIndex,n.seriesIndex=e.seriesIndex,n.setShape({x:0,y:0,width:M,height:I});var i=c.getVisual("borderColor",!0),r=F.get("borderColor");p(n,function(){var e=E(B);e.fill=i;var t=T(F);if(t.fill=r,o){var a=M-2*O;h(e,t,i,a,R,{x:O,y:0,width:a,height:R})}else e.text=t.text=null;n.setStyle(e),s.setHoverStyle(n,t)}),t.add(n)}(V,j,N&&y.upperHeight),!N){var $=g("content",x,f,A);$&&function(t,n){n.dataIndex=c.dataIndex,n.seriesIndex=e.seriesIndex;var o=Math.max(M-2*O,0),i=Math.max(I-2*O,0);n.culling=!0,n.setShape({x:O,y:O,width:o,height:i});var r=c.getVisual("color",!0);p(n,function(){var e=E(B);e.fill=r;var t=T(F);h(e,t,r,o,i),n.setStyle(e),s.setHoverStyle(n,t)}),t.add(n)}(V,$)}return V}}}}function r(e,t){var n=e*M+t;return(n-1)/n}var a=n(1),l=n(0),s=n(2),c=n(56),u=n(66),d=n(463),f=n(86),p=n(10),h=n(24),g=n(606),m=n(58),v=l.bind,b=s.Group,x=s.Rect,y=l.each,_=["label","normal"],w=["label","emphasis"],k=["upperLabel","normal"],S=["upperLabel","emphasis"],M=10,C=1,A=2,T=m([["fill","color"],["stroke","strokeColor"],["lineWidth","strokeWidth"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["shadowColor"]]),E=function(e){var t=T(e);return t.stroke=t.fill=t.lineWidth=null,t},I=a.extendChartView({type:"treemap",init:function(e,t){this._containerGroup,this._storage=o(),this._oldTree,this._breadcrumb,this._controller,this._state="ready"},render:function(e,t,n,o){var i=t.findComponents({mainType:"series",subType:"treemap",query:o});if(!(l.indexOf(i,e)<0)){this.seriesModel=e,this.api=n,this.ecModel=t;var r=u.retrieveTargetInfo(o,e),a=o&&o.type,s=e.layoutInfo,c=!this._oldTree,d=this._storage,f="treemapRootToNode"===a&&r&&d?{rootNodeGroup:d.nodeGroup[r.node.getRawIndex()],direction:o.direction}:null,p=this._giveContainerGroup(s),h=this._doRender(p,e,f);c||a&&"treemapZoomToNode"!==a&&"treemapRootToNode"!==a?h.renderFinally():this._doAnimation(p,h,e,f),this._resetController(n),this._renderBreadcrumb(e,n,r)}},_giveContainerGroup:function(e){var t=this._containerGroup;return t||(t=this._containerGroup=new b,this._initEvents(t),this.group.add(t)),t.attr("position",[e.x,e.y]),t},_doRender:function(e,t,n){function r(e,t,n,o,i){function a(e){return e.getId()}function s(a,l){var s=null!=a?e[a]:null,c=null!=l?t[l]:null,u=g(s,c,n,i);u&&r(s&&s.viewChildren||[],c&&c.viewChildren||[],u,o,i+1)}o?(t=e,y(e,function(e,t){!e.isRemoved()&&s(t,t)})):new c(t,e,a,a).add(s).update(s).remove(l.curry(s,null)).execute()}function a(){y(m,function(e){y(e,function(e){e.parent&&e.parent.remove(e)})}),y(h,function(e){e.invisible=!0,e.dirty()})}var s=t.getData().tree,u=this._oldTree,d=o(),f=o(),p=this._storage,h=[],g=l.curry(i,t,f,p,n,d,h);r(s.root?[s.root]:[],u&&u.root?[u.root]:[],e,s===u||!u,0);var m=function(e){var t=o();return e&&y(e,function(e,n){var o=t[n];y(e,function(e){e&&(o.push(e),e.__tmWillDelete=1)})}),t}(p);return this._oldTree=s,this._storage=f,{lastsForAnimation:d,willDeleteEls:m,renderFinally:a}},_doAnimation:function(e,t,n,o){if(n.get("animation")){var i=n.get("animationDurationUpdate"),r=n.get("animationEasing"),a=g.createWrap();y(t.willDeleteEls,function(e,t){y(e,function(e,n){if(!e.invisible){var l,s=e.parent;if(o&&"drillDown"===o.direction)l=s===o.rootNodeGroup?{shape:{x:0,y:0,width:s.__tmNodeWidth,height:s.__tmNodeHeight},style:{opacity:0}}:{style:{opacity:0}};else{var c=0,u=0;s.__tmWillDelete||(c=s.__tmNodeWidth/2,u=s.__tmNodeHeight/2),l="nodeGroup"===t?{position:[c,u],style:{opacity:0}}:{shape:{x:c,y:u,width:0,height:0},style:{opacity:0}}}l&&a.add(e,l,i,r)}})}),y(this._storage,function(e,n){y(e,function(e,o){var s=t.lastsForAnimation[n][o],c={};s&&("nodeGroup"===n?s.old&&(c.position=e.position.slice(),e.attr("position",s.old)):(s.old&&(c.shape=l.extend({},e.shape),e.setShape(s.old)),s.fadein?(e.setStyle("opacity",0),c.style={opacity:1}):1!==e.style.opacity&&(c.style={opacity:1})),a.add(e,c,i,r))})},this),this._state="animating",a.done(v(function(){this._state="ready",t.renderFinally()},this)).start()}},_resetController:function(e){var t=this._controller;t||(t=this._controller=new f(e.getZr()),t.enable(this.seriesModel.get("roam")),t.on("pan",v(this._onPan,this)),t.on("zoom",v(this._onZoom,this)));var n=new p(0,0,e.getWidth(),e.getHeight());t.setPointerChecker(function(e,t,o){return n.contain(t,o)})},_clearController:function(){var e=this._controller;e&&(e.dispose(),e=null)},_onPan:function(e,t){if("animating"!==this._state&&(Math.abs(e)>3||Math.abs(t)>3)){var n=this.seriesModel.getData().tree.root;if(!n)return;var o=n.getLayout();if(!o)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:o.x+e,y:o.y+t,width:o.width,height:o.height}})}},_onZoom:function(e,t,n){if("animating"!==this._state){var o=this.seriesModel.getData().tree.root;if(!o)return;var i=o.getLayout();if(!i)return;var r=new p(i.x,i.y,i.width,i.height),a=this.seriesModel.layoutInfo;t-=a.x,n-=a.y;var l=h.create();h.translate(l,l,[-t,-n]),h.scale(l,l,[e,e]),h.translate(l,l,[t,n]),r.applyTransform(l),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:r.x,y:r.y,width:r.width,height:r.height}})}},_initEvents:function(e){e.on("click",function(e){if("ready"===this._state){var t=this.seriesModel.get("nodeClick",!0);if(t){var n=this.findTarget(e.offsetX,e.offsetY);if(n){var o=n.node;if(o.getLayout().isLeafRoot)this._rootToNode(n);else if("zoomToNode"===t)this._zoomToNode(n);else if("link"===t){var i=o.hostTree.data.getItemModel(o.dataIndex),r=i.get("link",!0),a=i.get("target",!0)||"blank";r&&window.open(r,a)}}}}},this)},_renderBreadcrumb:function(e,t,n){function o(t){"animating"!==this._state&&(u.aboveViewRoot(e.getViewRoot(),t)?this._rootToNode({node:t}):this._zoomToNode({node:t}))}n||(n=null!=e.get("leafDepth",!0)?{node:e.getViewRoot()}:this.findTarget(t.getWidth()/2,t.getHeight()/2))||(n={node:e.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new d(this.group))).render(e,t,n.node,v(o,this))},remove:function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage=o(),this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},dispose:function(){this._clearController()},_zoomToNode:function(e){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:e.node})},_rootToNode:function(e){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:e.node})},findTarget:function(e,t){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},function(o){var i=this._storage.background[o.getRawIndex()];if(i){var r=i.transformCoordToLocal(e,t),a=i.shape;if(!(a.x<=r[0]&&r[0]<=a.x+a.width&&a.y<=r[1]&&r[1]<=a.y+a.height))return!1;n={node:o,offsetX:r[0],offsetY:r[1]}}},this),n}});e.exports=I},function(e,t,n){for(var o=n(1),i=n(66),r=function(){},a=["treemapZoomToNode","treemapRender","treemapMove"],l=0;l=0;s--){var c=i["asc"===o?a-s-1:s].getValue();c/n*ta[1]&&(a[1]=t)})}else a=[NaN,NaN];return{sum:o,dataExtent:a}}function c(e,t,n){for(var o,i=0,r=1/0,a=0,l=e.length;ai&&(i=o));var s=e.area*e.area,c=t*t*n;return s?w(c*i/s,s/(c*r)):1/0}function u(e,t,n,o,i){var r=t===n.width?0:1,a=1-r,l=["x","y"],s=["width","height"],c=n[l[r]],u=t?e.area/t:0;(i||u>n[s[a]])&&(u=n[s[a]]);for(var d=0,f=e.length;dx&&(c=x),r=l}c=s.length||e===s[e.depth])&&i(e,d(f,x,e,t,k,u),n,o,s,u)})}else g=a(x),e.setVisual("color",g)}}function r(e,t,n,o){var i=h.extend({},t);return h.each(["color","colorAlpha","colorSaturation"],function(r){var a=e.get(r,!0);null==a&&n&&(a=n[r]),null==a&&(a=t[r]),null==a&&(a=o.get(r)),null!=a&&(i[r]=a)}),i}function a(e){var t=s(e,"color");if(t){var n=s(e,"colorAlpha"),o=s(e,"colorSaturation");return o&&(t=p.modifyHSL(t,null,null,o)),n&&(t=p.modifyAlpha(t,n)),t}}function l(e,t){return null!=t?p.modifyHSL(t,null,null,e):null}function s(e,t){var n=e[t];if(null!=n&&"none"!==n)return n}function c(e,t,n,o,i,r){if(r&&r.length){var a=u(t,"color")||null!=i.color&&"none"!==i.color&&(u(t,"colorAlpha")||u(t,"colorSaturation"));if(a){var l=t.get("visualMin"),s=t.get("visualMax"),c=n.dataExtent.slice();null!=l&&lc[1]&&(c[1]=s);var d=t.get("colorMappingBy"),p={type:a.name,dataExtent:c,visual:a.range};"color"!==p.type||"index"!==d&&"id"!==d?p.mappingMethod="linear":(p.mappingMethod="category",p.loop=!0);var h=new f(p);return h.__drColorMappingBy=d,h}}}function u(e,t){var n=e.get(t);return g(n)&&n.length?{name:t,range:n}:null}function d(e,t,n,o,i,r){var a=h.extend({},t);if(i){var l=i.type,s="color"===l&&i.__drColorMappingBy,c="index"===s?o:"id"===s?r.mapIdToIndex(n.getId()):n.getValue(e.get("visualDimension"));a[l]=i.mapValueToVisual(c)}return a}var f=n(45),p=n(32),h=n(0),g=h.isArray,m="itemStyle.normal";e.exports=o},function(e,t,n){n(125),n(471)},function(e,t,n){n(208),n(472)},function(e,t,n){function o(e,t,n){t[1]>t[0]&&(t=t.slice().reverse());var o=e.coordToPoint([t[0],n]),i=e.coordToPoint([t[1],n]);return{x1:o[0],y1:o[1],x2:i[0],y2:i[1]}}function i(e){return e.getRadiusAxis().inverse?0:1}var r=n(0),a=n(2),l=n(12),s=n(43),c=["axisLine","axisLabel","axisTick","splitLine","splitArea"],u=s.extend({type:"angleAxis",axisPointerClass:"PolarAxisPointer",render:function(e,t){if(this.group.removeAll(),e.get("show")){var n=e.axis,o=n.polar,i=o.getRadiusAxis().getExtent(),a=n.getTicksCoords();"category"!==n.type&&a.pop(),r.each(c,function(t){!e.get(t+".show")||n.scale.isBlank()&&"axisLine"!==t||this["_"+t](e,o,a,i)},this)}},_axisLine:function(e,t,n,o){var r=e.getModel("axisLine.lineStyle"),l=new a.Circle({shape:{cx:t.cx,cy:t.cy,r:o[i(t)]},style:r.getLineStyle(),z2:1,silent:!0});l.style.fill=null,this.group.add(l)},_axisTick:function(e,t,n,l){var s=e.getModel("axisTick"),c=(s.get("inside")?-1:1)*s.get("length"),u=l[i(t)],d=r.map(n,function(e){return new a.Line({shape:o(t,[u,u+c],e)})});this.group.add(a.mergePath(d,{style:r.defaults(s.getModel("lineStyle").getLineStyle(),{stroke:e.get("axisLine.lineStyle.color")})}))},_axisLabel:function(e,t,n,o){for(var r=e.axis,s=e.get("data"),c=e.getModel("axisLabel"),u=e.getFormattedLabels(),d=c.get("margin"),f=r.getLabelsCoords(),p=0;pm?"left":"right",x=Math.abs(g[1]-v)/h<.3?"middle":g[1]>v?"top":"bottom";s&&s[p]&&s[p].textStyle&&(c=new l(s[p].textStyle,c,c.ecModel));var y=new a.Text({silent:!0});this.group.add(y),a.setTextStyle(y.style,c,{x:g[0],y:g[1],textFill:c.getTextColor()||e.get("axisLine.lineStyle.color"),text:u[p],textAlign:b,textVerticalAlign:x})}},_splitLine:function(e,t,n,i){var l=e.getModel("splitLine"),s=l.getModel("lineStyle"),c=s.get("color"),u=0;c=c instanceof Array?c:[c];for(var d=[],f=0;f=0)&&n({type:"updateAxisPointer",currTrigger:e,x:t&&t.offsetX,y:t&&t.offsetY})})},remove:function(e,t){i.unregister(t.getZr(),"axisPointer"),r.superApply(this._model,"remove",arguments)},dispose:function(e,t){i.unregister("axisPointer",t),r.superApply(this._model,"dispose",arguments)}}),a=r;e.exports=a},function(e,t,n){function o(e,t,n,o,i){var r=t.axis,l=r.dataToCoord(e),u=o.getAngleAxis().getExtent()[0];u=u/180*Math.PI;var d,f,p,h=o.getRadiusAxis().getExtent();if("radius"===r.dim){var g=s.create();s.rotate(g,g,u),s.translate(g,g,[o.cx,o.cy]),d=a.applyTransform([l,-i],g);var m=t.getModel("axisLabel").get("rotate")||0,v=c.innerTextLayout(u,m*Math.PI/180,-1);f=v.textAlign,p=v.textVerticalAlign}else{var b=h[1];d=o.coordToPoint([b+i,l]);var x=o.cx,y=o.cy;f=Math.abs(d[0]-x)/b<.3?"center":d[0]>x?"left":"right",p=Math.abs(d[1]-y)/b<.3?"middle":d[1]>y?"top":"bottom"}return{position:d,align:f,verticalAlign:p}}var i=n(8),r=n(116),a=n(2),l=n(85),s=n(24),c=n(42),u=n(43),d=r.extend({makeElOption:function(e,t,n,r,a){var s=n.axis;"angle"===s.dim&&(this.animationThreshold=Math.PI/18);var c,u=s.polar,d=u.getOtherAxis(s),p=d.getExtent();c=s["dataTo"+i.capitalFirst(s.dim)](t);var h=r.get("type");if(h&&"none"!==h){var g=l.buildElStyle(r),m=f[h](s,u,c,p,g);m.style=g,e.graphicKey=m.type,e.pointer=m}var v=r.get("label.margin"),b=o(t,n,r,u,v);l.buildLabelElOption(e,n,r,a,b)}}),f={line:function(e,t,n,o,i){return"angle"===e.dim?{type:"Line",shape:l.makeLineShape(t.coordToPoint([o[0],n]),t.coordToPoint([o[1],n]))}:{type:"Circle",shape:{cx:t.cx,cy:t.cy,r:n}}},shadow:function(e,t,n,o,i){var r=e.getBandWidth(),a=Math.PI/180;return"angle"===e.dim?{type:"Sector",shape:l.makeSectorShape(t.cx,t.cy,o[0],o[1],(-n-r/2)*a,(r/2-n)*a)}:{type:"Sector",shape:l.makeSectorShape(t.cx,t.cy,n-r/2,n+r/2,0,2*Math.PI)}}};u.registerAxisPointerClass("PolarAxisPointer",d);var p=d;e.exports=p},function(e,t,n){function o(e){return e.isHorizontal()?0:1}function i(e,t){var n=e.getRect();return[n[u[t]],n[u[t]]+n[d[t]]]}var r=n(2),a=n(116),l=n(85),s=n(179),c=n(43),u=["x","y"],d=["width","height"],f=a.extend({makeElOption:function(e,t,n,r,a){var c=n.axis,u=c.coordinateSystem,d=i(u,1-o(c)),f=u.dataToPoint(t)[0],h=r.get("type");if(h&&"none"!==h){var g=l.buildElStyle(r),m=p[h](c,f,d,g);m.style=g,e.graphicKey=m.type,e.pointer=m}var v=s.layout(n);l.buildCartesianSingleLabelElOption(t,e,v,n,r,a)},getHandleTransform:function(e,t,n){var o=s.layout(t,{labelInside:!1});return o.labelMargin=n.get("handle.margin"),{position:l.getTransformedPosition(t.axis,e,o),rotation:o.rotation+(o.labelDirection<0?Math.PI:0)}},updateHandleTransform:function(e,t,n,r){var a=n.axis,l=a.coordinateSystem,s=o(a),c=i(l,s),u=e.position;u[s]+=t[s],u[s]=Math.min(c[1],u[s]),u[s]=Math.max(c[0],u[s]);var d=i(l,1-s),f=(d[1]+d[0])/2,p=[f,f];return p[s]=u[s],{position:u,rotation:e.rotation,cursorPoint:p,tooltipOption:{verticalAlign:"middle"}}}}),p={line:function(e,t,n,i){var a=l.makeLineShape([t,n[0]],[t,n[1]],o(e));return r.subPixelOptimizeLine({shape:a,style:i}),{type:"Line",shape:a}},shadow:function(e,t,n,i){var r=e.getBandWidth(),a=n[1]-n[0];return{type:"Rect",shape:l.makeRectShape([t-r/2,n[0]],[r,a],o(e))}}};c.registerAxisPointerClass("SingleAxisPointer",f);var h=f;e.exports=h},function(e,t,n){function o(e,t,n){var o=e.currTrigger,r=[e.x,e.y],g=e,m=e.dispatchAction||h.bind(n.dispatchAction,n),y=t.getComponent("axisPointer").coordSysAxesInfo;if(y){p(r)&&(r=v({seriesIndex:g.seriesIndex,dataIndex:g.dataIndex},t).point);var _=p(r),w=g.axesInfo,k=y.axesInfo,S="leave"===o||p(r),M={},C={},A={list:[],map:{}},T={showPointer:x(a,C),showTooltip:x(l,A)};b(y.coordSysMap,function(e,t){var n=_||e.containPoint(r);b(y.coordSysAxesInfo[t],function(e,t){var o=e.axis,a=d(w,e);if(!S&&n&&(!w||a)){var l=a&&a.value;null!=l||_||(l=o.pointToData(r)),null!=l&&i(e,l,T,!1,M)}})});var E={};return b(k,function(e,t){var n=e.linkGroup;n&&!C[t]&&b(n.axesInfo,function(t,o){var i=C[o];if(t!==e&&i){var r=i.value;n.mapper&&(r=e.axis.scale.parse(n.mapper(r,f(t),f(e)))),E[e.key]=r}})}),b(E,function(e,t){i(k[t],e,T,!0,M)}),s(C,k,M),c(A,r,e,m),u(k,m,n),M}}function i(e,t,n,o,i){var a=e.axis;if(!a.scale.isBlank()&&a.containData(t)){if(!e.involveSeries)return void n.showPointer(e,t);var l=r(t,e),s=l.payloadBatch,c=l.snapToValue;s[0]&&null==i.seriesIndex&&h.extend(i,s[0]),!o&&e.snap&&a.containData(c)&&null!=c&&(t=c),n.showPointer(e,t,s,i),n.showTooltip(e,l,c)}}function r(e,t){var n=t.axis,o=n.dim,i=e,r=[],a=Number.MAX_VALUE,l=-1;return b(t.seriesModels,function(t,s){var c,u,d=t.coordDimToDataDim(o);if(t.getAxisTooltipData){var f=t.getAxisTooltipData(d,e,n);u=f.dataIndices,c=f.nestestValue}else{if(u=t.getData().indicesOfNearest(d[0],e,!1,"category"===n.type?.5:null),!u.length)return;c=t.getData().get(d[0],u[0])}if(null!=c&&isFinite(c)){var p=e-c,h=Math.abs(p);h<=a&&((h=0&&l<0)&&(a=h,l=p,i=c,r.length=0),b(u,function(e){r.push({seriesIndex:t.seriesIndex,dataIndexInside:e,dataIndex:t.getData().getRawIndex(e)})}))}}),{payloadBatch:r,snapToValue:i}}function a(e,t,n,o){e[t.key]={value:n,payloadBatch:o}}function l(e,t,n,o){var i=n.payloadBatch,r=t.axis,a=r.model,l=t.axisPointerModel;if(t.triggerTooltip&&i.length){var s=t.coordSys.model,c=m.makeKey(s),u=e.map[c];u||(u=e.map[c]={coordSysId:s.id,coordSysIndex:s.componentIndex,coordSysType:s.type,coordSysMainType:s.mainType,dataByAxis:[]},e.list.push(u)),u.dataByAxis.push({axisDim:r.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:o,valueLabelOpt:{precision:l.get("label.precision"),formatter:l.get("label.formatter")},seriesDataIndices:i.slice()})}}function s(e,t,n){var o=n.axesInfo=[];b(t,function(t,n){var i=t.axisPointerModel.option,r=e[n];r?(!t.useHandle&&(i.status="show"),i.value=r.value,i.seriesDataIndices=(r.payloadBatch||[]).slice()):!t.useHandle&&(i.status="hide"),"show"===i.status&&o.push({axisDim:t.axis.dim,axisIndex:t.axis.model.componentIndex,value:i.value})})}function c(e,t,n,o){if(p(t)||!e.list.length)return void o({type:"hideTip"});var i=((e.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};o({type:"showTip",escapeConnect:!0,x:t[0],y:t[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:i.dataIndexInside,dataIndex:i.dataIndex,seriesIndex:i.seriesIndex,dataByCoordSys:e.list})}function u(e,t,n){var o=n.getZr(),i=y(o).axisPointerLastHighlights||{},r=y(o).axisPointerLastHighlights={};b(e,function(e,t){var n=e.axisPointerModel.option;"show"===n.status&&b(n.seriesDataIndices,function(e){var t=e.seriesIndex+" | "+e.dataIndex;r[t]=e})});var a=[],l=[];h.each(i,function(e,t){!r[t]&&l.push(e)}),h.each(r,function(e,t){!i[t]&&a.push(e)}),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,batch:l}),a.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,batch:a})}function d(e,t){for(var n=0;n<(e||[]).length;n++){var o=e[n];if(t.axis.dim===o.axisDim&&t.axis.model.componentIndex===o.axisIndex)return o}}function f(e){var t=e.axis.model,n={},o=n.axisDim=e.axis.dim;return n.axisIndex=n[o+"AxisIndex"]=t.componentIndex,n.axisName=n[o+"AxisName"]=t.name,n.axisId=n[o+"AxisId"]=t.id,n}function p(e){return!e||null==e[0]||isNaN(e[0])||null==e[1]||isNaN(e[1])}var h=n(0),g=n(5),m=n(84),v=n(181),b=h.each,x=h.curry,y=g.makeGetter();e.exports=o},function(e,t,n){var o=n(1),i=n(486);n(488),n(483),n(484),n(485),n(538),o.registerPreprocessor(i)},function(e,t,n){function o(e,t){return a.merge({brushType:e.brushType,brushMode:e.brushMode,transformable:e.transformable,brushStyle:new s(e.brushStyle).getItemStyle(),removeOnClick:e.removeOnClick,z:e.z},t,!0)}var i=n(4),r=(i.__DEV__,n(1)),a=n(0),l=n(92),s=n(12),c=["#ddd"],u=r.extendComponentModel({type:"brush",dependencies:["geo","grid","xAxis","yAxis","parallel","series"],defaultOption:{toolbox:null,brushLink:null,seriesIndex:"all",geoIndex:null,xAxisIndex:null,yAxisIndex:null,brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:"rgba(120,140,180,0.3)",borderColor:"rgba(120,140,180,0.8)"},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4},areas:[],brushType:null,brushOption:{},coordInfoList:[],optionUpdated:function(e,t){var n=this.option;!t&&l.replaceVisualOption(n,e,["inBrush","outOfBrush"]),n.inBrush=n.inBrush||{},n.outOfBrush=n.outOfBrush||{color:c}},setAreas:function(e){e&&(this.areas=a.map(e,function(e){return o(this.option,e)},this))},setBrushOption:function(e){this.brushOption=o(this.option,e),this.brushType=this.brushOption.brushType}}),d=u;e.exports=d},function(e,t,n){function o(e,t,n,o){(!o||o.$from!==e.id)&&this._brushController.setPanels(e.brushTargetManager.makePanelOpts(n)).enableBrush(e.brushOption).updateCovers(e.areas.slice())}var i=n(1),r=n(0),a=n(118),l=i.extendComponentView({type:"brush",init:function(e,t){this.ecModel=e,this.api=t,this.model,(this._brushController=new a(t.getZr())).on("brush",r.bind(this._onBrush,this)).mount()},render:function(e){return this.model=e,o.apply(this,arguments)},updateView:o,updateLayout:o,updateVisual:o,dispose:function(){this._brushController.dispose()},_onBrush:function(e,t){var n=this.model.id;this.model.brushTargetManager.setOutputRanges(e,this.ecModel),(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:n,areas:r.clone(e),$from:n})}});e.exports=l},function(e,t,n){var o=n(1);o.registerAction({type:"brush",event:"brush",update:"updateView"},function(e,t){t.eachComponent({mainType:"brush",query:e},function(t){t.setAreas(e.areas)})}),o.registerAction({type:"brushSelect",event:"brushSelected",update:"none"},function(){})},function(e,t,n){function o(e,t){var n=e&&e.brush;if(r.isArray(n)||(n=n?[n]:[]),n.length){var o=[];r.each(n,function(e){var t=e.hasOwnProperty("toolbox")?e.toolbox:[];t instanceof Array&&(o=o.concat(t))});var l=e&&e.toolbox;r.isArray(l)&&(l=l[0]),l||(l={feature:{}},e.toolbox=[l]);var s=l.feature||(l.feature={}),c=s.brush||(s.brush={}),u=c.type||(c.type=[]);u.push.apply(u,o),i(u),t&&!u.length&&u.push.apply(u,a)}}function i(e){var t={};r.each(e,function(e){t[e]=1}),e.length=0,r.each(t,function(t,n){e.push(n)})}var r=n(0),a=["rect","polygon","keep","clear"];e.exports=o},function(e,t,n){function o(e){var t=["x","y"],n=["width","height"];return{point:function(t,n,o){if(t){var r=o.range;return i(t[e],r)}},rect:function(o,r,a){if(o){var l=a.range,s=[o[t[e]],o[t[e]]+o[n[e]]];return s[1]1)return!1;var f=s(n-e,i-e,o-t,r-t)/u;return!(f<0||f>1)}function l(e){return e<=1e-6&&e>=-1e-6}function s(e,t,n,o){return e*o-t*n}var c=n(240),u=n(10),d={lineX:o(0),lineY:o(1),rect:{point:function(e,t,n){return e&&n.boundingRect.contain(e[0],e[1])},rect:function(e,t,n){return e&&n.boundingRect.intersect(e)}},polygon:{point:function(e,t,n){return e&&n.boundingRect.contain(e[0],e[1])&&c.contain(n.range,e[0],e[1])},rect:function(e,t,n){var o=n.range;if(!e||o.length<=1)return!1;var i=e.x,a=e.y,l=e.width,s=e.height,d=o[0];return!!(c.contain(o,i,a)||c.contain(o,i+l,a)||c.contain(o,i,a+s)||c.contain(o,i+l,a+s)||u.create(e).contain(d[0],d[1])||r(i,a,i+l,a,o)||r(i,a,i,a+s,o)||r(i+l,a,i+l,a+s,o)||r(i,a+s,i+l,a+s,o))||void 0}}},f=d;e.exports=f},function(e,t,n){function o(e,t,n,o,r){if(r){var a=e.getZr();a[x]||(a[b]||(a[b]=i),g.createOrUpdate(a,b,n,t)(e,o))}}function i(e,t){if(!e.isDisposed()){var n=e.getZr();n[x]=!0,e.dispatchAction({type:"brushSelect",batch:t}),n[x]=!1}}function r(e,t,n,o){for(var i=0,r=t.length;it[0][1]&&(t[0][1]=r[0]),r[1]t[1][1]&&(t[1][1]=r[1])}return t&&c(t)}}},function(e,t,n){n(554),n(555),n(490)},function(e,t,n){var o=n(1),i=n(0),r=n(2),a=n(8),l=n(3),s={EN:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],CN:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]},c={EN:["S","M","T","W","T","F","S"],CN:["日","一","二","三","四","五","六"]},u=o.extendComponentView({type:"calendar",_tlpoints:null,_blpoints:null,_firstDayOfMonth:null,_firstDayPoints:null,render:function(e,t,n){var o=this.group;o.removeAll();var i=e.coordinateSystem,r=i.getRangeInfo(),a=i.getOrient();this._renderDayRect(e,r,o),this._renderLines(e,r,a,o),this._renderYearText(e,r,a,o),this._renderMonthText(e,a,o),this._renderWeekText(e,r,a,o)},_renderDayRect:function(e,t,n){for(var o=e.coordinateSystem,i=e.getModel("itemStyle.normal").getItemStyle(),a=o.getCellWidth(),l=o.getCellHeight(),s=t.start.time;s<=t.end.time;s=o.getNextNDay(s,1).time){var c=o.dataToRect([s],!1).tl,u=new r.Rect({shape:{x:c[0],y:c[1],width:a,height:l},cursor:"default",style:i});n.add(u)}},_renderLines:function(e,t,n,o){function i(t){r._firstDayOfMonth.push(a.getDateInfo(t)),r._firstDayPoints.push(a.dataToRect([t],!1).tl);var i=r._getLinePointsOfOneWeek(e,t,n);r._tlpoints.push(i[0]),r._blpoints.push(i[i.length-1]),s&&r._drawSplitline(i,l,o)}var r=this,a=e.coordinateSystem,l=e.getModel("splitLine.lineStyle").getLineStyle(),s=e.get("splitLine.show"),c=l.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var u=t.start,d=0;u.time<=t.end.time;d++){i(u.formatedDate),0===d&&(u=a.getDateInfo(t.start.y+"-"+t.start.m));var f=u.date;f.setMonth(f.getMonth()+1),u=a.getDateInfo(f)}i(a.getNextNDay(t.end.time,1).formatedDate),s&&this._drawSplitline(r._getEdgesPoints(r._tlpoints,c,n),l,o),s&&this._drawSplitline(r._getEdgesPoints(r._blpoints,c,n),l,o)},_getEdgesPoints:function(e,t,n){var o=[e[0].slice(),e[e.length-1].slice()],i="horizontal"===n?0:1;return o[0][i]=o[0][i]-t/2,o[1][i]=o[1][i]+t/2,o},_drawSplitline:function(e,t,n){var o=new r.Polyline({z2:20,shape:{points:e},style:t});n.add(o)},_getLinePointsOfOneWeek:function(e,t,n){var o=e.coordinateSystem;t=o.getDateInfo(t);for(var i=[],r=0;r<7;r++){var a=o.getNextNDay(t.time,r),l=o.dataToRect([a.time],!1);i[2*a.day]=l.tl,i[2*a.day+1]=l["horizontal"===n?"bl":"tr"]}return i},_formatterLabel:function(e,t){return"string"==typeof e&&e?a.formatTplSimple(e,t):"function"==typeof e?e(t):t.nameMap},_yearTextPositionControl:function(e,t,n,o,i){t=t.slice();var r=["center","bottom"];"bottom"===o?(t[1]+=i,r=["center","top"]):"left"===o?t[0]-=i:"right"===o?(t[0]+=i,r=["center","top"]):t[1]-=i;var a=0;return"left"!==o&&"right"!==o||(a=Math.PI/2),{rotation:a,position:t,style:{textAlign:r[0],textVerticalAlign:r[1]}}},_renderYearText:function(e,t,n,o){var i=e.getModel("yearLabel");if(i.get("show")){var a=i.get("margin"),l=i.get("position");l||(l="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],c=(s[0][0]+s[1][0])/2,u=(s[0][1]+s[1][1])/2,d="horizontal"===n?0:1,f={top:[c,s[d][1]],bottom:[c,s[1-d][1]],left:[s[1-d][0],u],right:[s[d][0],u]},p=t.start.y;+t.end.y>+t.start.y&&(p=p+"-"+t.end.y);var h=i.get("formatter"),g={start:t.start.y,end:t.end.y,nameMap:p},m=this._formatterLabel(h,g),v=new r.Text({z2:30});r.setTextStyle(v.style,i,{text:m}),v.attr(this._yearTextPositionControl(v,f[l],n,l,a)),o.add(v)}},_monthTextPositionControl:function(e,t,n,o,i){var r="left",a="top",l=e[0],s=e[1];return"horizontal"===n?(s+=i,t&&(r="center"),"start"===o&&(a="bottom")):(l+=i,t&&(a="middle"),"start"===o&&(r="right")),{x:l,y:s,textAlign:r,textVerticalAlign:a}},_renderMonthText:function(e,t,n){var o=e.getModel("monthLabel");if(o.get("show")){var a=o.get("nameMap"),l=o.get("margin"),c=o.get("position"),u=o.get("align"),d=[this._tlpoints,this._blpoints];i.isString(a)&&(a=s[a.toUpperCase()]||[]);var f="start"===c?0:1,p="horizontal"===t?0:1;l="start"===c?-l:l;for(var h="center"===u,g=0;go[1]&&(o[1]=t[1])})}),o[1]0?0:NaN);var a=n.getMax(!0);return null!=a&&"dataMax"!==a&&"function"!=typeof a?t[1]=a:i&&(t[1]=r>0?r-1:NaN),n.get("scale",!0)||(t[0]>0&&(t[0]=0),t[1]<0&&(t[1]=0)),t}function r(e,t){var n=e.getAxisModel(),o=e._percentWindow,i=e._valueWindow;if(o){var r=s.getPixelPrecision(i,[0,500]);r=Math.min(r,20);var a=t||0===o[0]&&100===o[1];n.setRange(a?null:+i[0].toFixed(r),a?null:+i[1].toFixed(r))}}function a(e){var t=e._minMaxSpan={},n=e._dataZoomModel;u(["min","max"],function(o){t[o+"Span"]=n.get(o+"Span");var i=n.get(o+"ValueSpan");if(null!=i&&(t[o+"ValueSpan"]=i,null!=(i=e.getAxisModel().axis.scale.parse(i)))){var r=e._dataExtent;t[o+"Span"]=s.linearMap(r[0]+i,r,[0,100],!0)}})}var l=n(0),s=n(3),c=n(117),u=l.each,d=s.asc,f=function(e,t,n,o){this._dimName=e,this._axisIndex=t,this._valueWindow,this._percentWindow,this._dataExtent,this._minMaxSpan,this.ecModel=o,this._dataZoomModel=n};f.prototype={constructor:f,hostedBy:function(e){return this._dataZoomModel===e},getDataValueWindow:function(){return this._valueWindow.slice()},getDataPercentWindow:function(){return this._percentWindow.slice()},getTargetSeriesModels:function(){var e=[],t=this.ecModel;return t.eachSeries(function(n){if(c.isCoordSupported(n.get("coordinateSystem"))){var o=this._dimName,i=t.queryComponents({mainType:o+"Axis",index:n.get(o+"AxisIndex"),id:n.get(o+"AxisId")})[0];this._axisIndex===(i&&i.componentIndex)&&e.push(n)}},this),e},getAxisModel:function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},getOtherAxisModel:function(){var e,t,n=this._dimName,o=this.ecModel,i=this.getAxisModel();"x"===n||"y"===n?(t="gridIndex",e="x"===n?"y":"x"):(t="polarIndex",e="angle"===n?"radius":"angle");var r;return o.eachComponent(e+"Axis",function(e){(e.get(t)||0)===(i.get(t)||0)&&(r=e)}),r},getMinMaxSpan:function(){return l.clone(this._minMaxSpan)},calculateDataWindow:function(e){var t=this._dataExtent,n=this.getAxisModel(),o=n.axis.scale,i=this._dataZoomModel.getRangePropMode(),r=[0,100],a=[e.start,e.end],l=[];return u(["startValue","endValue"],function(t){l.push(null!=e[t]?o.parse(e[t]):null)}),u([0,1],function(e){var n=l[e],c=a[e];"percent"===i[e]?(null==c&&(c=r[e]),n=o.parse(s.linearMap(c,r,t,!0))):c=s.linearMap(n,t,r,!0),l[e]=n,a[e]=c}),{valueWindow:d(l),percentWindow:d(a)}},reset:function(e){if(e===this._dataZoomModel){this._dataExtent=o(this,this._dimName,this.getTargetSeriesModels());var t=this.calculateDataWindow(e.option);this._valueWindow=t.valueWindow,this._percentWindow=t.percentWindow,a(this),r(this)}},restore:function(e){e===this._dataZoomModel&&(this._valueWindow=this._percentWindow=null,r(this,!0))},filterData:function(e){function t(e){return e>=r[0]&&e<=r[1]}if(e===this._dataZoomModel){var n=this._dimName,o=this.getTargetSeriesModels(),i=e.get("filterMode"),r=this._valueWindow;if("none"!==i){var a=this.getOtherAxisModel();e.get("$fromToolbox")&&a&&"category"===a.get("type")&&(i="empty"),u(o,function(e){var o=e.getData(),a=e.coordDimToDataDim(n);"weakFilter"===i?o&&o.filterSelf(function(e){for(var t,n,i,l=0;lr[1];if(c&&!u&&!d)return!0;c&&(i=!0),u&&(t=!0),d&&(n=!0)}return i&&t&&n}):o&&u(a,function(n){"empty"===i?e.setData(o.map(n,function(e){return t(e)?e:NaN})):o.filterSelf(n,t)})})}}}};var p=f;e.exports=p},function(e,t,n){var o=n(68),i=o.extend({type:"dataZoom.inside",defaultOption:{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,preventDefaultMouseMove:!0}});e.exports=i},function(e,t,n){var o=n(0),i=n(69),r=n(71),a=n(499),l=o.bind,s=i.extend({type:"dataZoom.inside",init:function(e,t){this._range},render:function(e,t,n,i){s.superApply(this,"render",arguments),a.shouldRecordRange(i,e.id)&&(this._range=e.getPercentRange()),o.each(this.getTargetCoordInfo(),function(t,i){var r=o.map(t,function(e){return a.generateCoordId(e.model)});o.each(t,function(t){var o=t.model,s=e.option;a.register(n,{coordId:a.generateCoordId(o),allCoordIds:r,containsPoint:function(e,t,n){return o.coordinateSystem.containPoint([t,n])},dataZoomId:e.id,throttleRate:e.get("throttle",!0),panGetRange:l(this._onPan,this,t,i),zoomGetRange:l(this._onZoom,this,t,i),zoomLock:s.zoomLock,disabled:s.disabled,roamControllerOpt:{zoomOnMouseWheel:s.zoomOnMouseWheel,moveOnMouseMove:s.moveOnMouseMove,preventDefaultMouseMove:s.preventDefaultMouseMove}})},this)},this)},dispose:function(){a.unregister(this.api,this.dataZoomModel.id),s.superApply(this,"dispose",arguments),this._range=null},_onPan:function(e,t,n,o,i,a,l,s,u){var d=this._range.slice(),f=e.axisModels[0];if(f){var p=c[t]([a,l],[s,u],f,n,e),h=p.signal*(d[1]-d[0])*p.pixel/p.pixelLength;return r(h,d,[0,100],"all"),this._range=d}},_onZoom:function(e,t,n,o,i,a){var l=this._range.slice(),s=e.axisModels[0];if(s){var u=c[t](null,[i,a],s,n,e),d=(u.signal>0?u.pixelStart+u.pixelLength-u.pixel:u.pixel-u.pixelStart)/u.pixelLength*(l[1]-l[0])+l[0];o=Math.max(1/o,0),l[0]=(l[0]-d)*o+d,l[1]=(l[1]-d)*o+d;var f=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return r(0,l,[0,100],0,f.minSpan,f.maxSpan),this._range=l}}}),c={grid:function(e,t,n,o,i){var r=n.axis,a={},l=i.model.coordinateSystem.getRect();return e=e||[0,0],"x"===r.dim?(a.pixel=t[0]-e[0],a.pixelLength=l.width,a.pixelStart=l.x,a.signal=r.inverse?1:-1):(a.pixel=t[1]-e[1],a.pixelLength=l.height,a.pixelStart=l.y,a.signal=r.inverse?-1:1),a},polar:function(e,t,n,o,i){var r=n.axis,a={},l=i.model.coordinateSystem,s=l.getRadiusAxis().getExtent(),c=l.getAngleAxis().getExtent();return e=e?l.pointToCoord(e):[0,0],t=l.pointToCoord(t),"radiusAxis"===n.mainType?(a.pixel=t[0]-e[0],a.pixelLength=s[1]-s[0],a.pixelStart=s[0],a.signal=r.inverse?1:-1):(a.pixel=t[1]-e[1],a.pixelLength=c[1]-c[0],a.pixelStart=c[0],a.signal=r.inverse?-1:1),a},singleAxis:function(e,t,n,o,i){var r=n.axis,a=i.model.coordinateSystem.getRect(),l={};return e=e||[0,0],"horizontal"===r.orient?(l.pixel=t[0]-e[0],l.pixelLength=a.width,l.pixelStart=a.x,l.signal=r.inverse?1:-1):(l.pixel=t[1]-e[1],l.pixelLength=a.height,l.pixelStart=a.y,l.signal=r.inverse?-1:1),l}},u=s;e.exports=u},function(e,t,n){var o=n(68),i=o.extend({type:"dataZoom.select"});e.exports=i},function(e,t,n){var o=n(69),i=o.extend({type:"dataZoom.select"});e.exports=i},function(e,t,n){var o=n(68),i=o.extend({type:"dataZoom.slider",layoutMode:"box",defaultOption:{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#2f4554",width:.5,opacity:.3},areaStyle:{color:"rgba(47,69,84,0.3)",opacity:.3}},borderColor:"#ddd",fillerColor:"rgba(167,183,204,0.4)",handleIcon:"M8.2,13.6V3.9H6.3v9.7H3.1v14.9h3.3v9.7h1.8v-9.7h3.3V13.6H8.2z M9.7,24.4H4.8v-1.4h4.9V24.4z M9.7,19.1H4.8v-1.4h4.9V19.1z",handleSize:"100%",handleStyle:{color:"#a7b7cc"},labelPrecision:null,labelFormatter:null,showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#333"}}}),r=i;e.exports=r},function(e,t,n){function o(e){return{x:"y",y:"x",radius:"angle",angle:"radius"}[e]}function i(e){return"vertical"===e?"ns-resize":"ew-resize"}var r=n(0),a=n(31),l=n(2),s=n(44),c=n(69),u=n(3),d=n(6),f=n(71),p=l.Rect,h=u.linearMap,g=u.asc,m=r.bind,v=r.each,b="horizontal",x=5,y=["line","bar","candlestick","scatter"],_=c.extend({type:"dataZoom.slider",init:function(e,t){this._displayables={},this._orient,this._range,this._handleEnds,this._size,this._handleWidth,this._handleHeight,this._location,this._dragging,this._dataShadowInfo,this.api=t},render:function(e,t,n,o){if(_.superApply(this,"render",arguments),s.createOrUpdate(this,"_dispatchZoomAction",this.dataZoomModel.get("throttle"),"fixRate"),this._orient=e.get("orient"),!1===this.dataZoomModel.get("show"))return void this.group.removeAll();o&&"dataZoom"===o.type&&o.from===this.uid||this._buildView(),this._updateView()},remove:function(){_.superApply(this,"remove",arguments),s.clear(this,"_dispatchZoomAction")},dispose:function(){_.superApply(this,"dispose",arguments),s.clear(this,"_dispatchZoomAction")},_buildView:function(){var e=this.group;e.removeAll(),this._resetLocation(),this._resetInterval();var t=this._displayables.barGroup=new l.Group;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),e.add(t),this._positionGroup()},_resetLocation:function(){var e=this.dataZoomModel,t=this.api,n=this._findCoordRect(),o={width:t.getWidth(),height:t.getHeight()},i=this._orient===b?{right:o.width-n.x-n.width,top:o.height-30-7,width:n.width,height:30}:{right:7,top:n.y,width:30,height:n.height},a=d.getLayoutParams(e.option);r.each(["right","top","width","height"],function(e){"ph"===a[e]&&(a[e]=i[e])});var l=d.getLayoutRect(a,o,e.padding);this._location={x:l.x,y:l.y},this._size=[l.width,l.height],"vertical"===this._orient&&this._size.reverse()},_positionGroup:function(){var e=this.group,t=this._location,n=this._orient,o=this.dataZoomModel.getFirstTargetAxisModel(),i=o&&o.get("inverse"),r=this._displayables.barGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;r.attr(n!==b||i?n===b&&i?{scale:a?[-1,1]:[-1,-1]}:"vertical"!==n||i?{scale:a?[-1,-1]:[-1,1],rotation:Math.PI/2}:{scale:a?[1,-1]:[1,1],rotation:Math.PI/2}:{scale:a?[1,1]:[1,-1]});var l=e.getBoundingRect([r]);e.attr("position",[t.x-l.x,t.y-l.y])},_getViewExtent:function(){return[0,this._size[0]]},_renderBackground:function(){var e=this.dataZoomModel,t=this._size,n=this._displayables.barGroup;n.add(new p({silent:!0,shape:{x:0,y:0,width:t[0],height:t[1]},style:{fill:e.get("backgroundColor")},z2:-40})),n.add(new p({shape:{x:0,y:0,width:t[0],height:t[1]},style:{fill:"transparent"},z2:0,onclick:r.bind(this._onClickPanelClick,this)}))},_renderDataShadow:function(){var e=this._dataShadowInfo=this._prepareDataShadowInfo();if(e){var t=this._size,n=e.series,o=n.getRawData(),i=n.getShadowDim?n.getShadowDim():e.otherDim;if(null!=i){var a=o.getDataExtent(i),s=.3*(a[1]-a[0]);a=[a[0]-s,a[1]+s];var c,u=[0,t[1]],d=[0,t[0]],f=[[t[0],0],[0,0]],p=[],g=d[1]/(o.count()-1),m=0,v=Math.round(o.count()/t[0]);o.each([i],function(e,t){if(v>0&&t%v)return void(m+=g);var n=null==e||isNaN(e)||""===e,o=n?0:h(e,a,u,!0);n&&!c&&t?(f.push([f[f.length-1][0],0]),p.push([p[p.length-1][0],0])):!n&&c&&(f.push([m,0]),p.push([m,0])),f.push([m,o]),p.push([m,o]),m+=g,c=n});var b=this.dataZoomModel;this._displayables.barGroup.add(new l.Polygon({shape:{points:f},style:r.defaults({fill:b.get("dataBackgroundColor")},b.getModel("dataBackground.areaStyle").getAreaStyle()),silent:!0,z2:-20})),this._displayables.barGroup.add(new l.Polyline({shape:{points:p},style:b.getModel("dataBackground.lineStyle").getLineStyle(),silent:!0,z2:-19}))}}},_prepareDataShadowInfo:function(){var e=this.dataZoomModel,t=e.get("showDataShadow");if(!1!==t){var n,i=this.ecModel;return e.eachTargetAxis(function(a,l){var s=e.getAxisProxy(a.name,l).getTargetSeriesModels();r.each(s,function(e){if(!(n||!0!==t&&r.indexOf(y,e.get("type"))<0)){var s,c=i.getComponent(a.axis,l).axis,u=o(a.name),d=e.coordinateSystem;null!=u&&d.getOtherAxis&&(s=d.getOtherAxis(c).inverse),n={thisAxis:c,series:e,thisDim:a.name,otherDim:u,otherAxisInverse:s}}},this)},this),n}},_renderHandle:function(){var e=this._displayables,t=e.handles=[],n=e.handleLabels=[],o=this._displayables.barGroup,r=this._size,s=this.dataZoomModel;o.add(e.filler=new p({draggable:!0,cursor:i(this._orient),drift:m(this._onDragMove,this,"all"),onmousemove:function(e){a.stop(e.event)},ondragstart:m(this._showDataInfo,this,!0),ondragend:m(this._onDragEnd,this),onmouseover:m(this._showDataInfo,this,!0),onmouseout:m(this._showDataInfo,this,!1),style:{fill:s.get("fillerColor"),textPosition:"inside"}})),o.add(new p(l.subPixelOptimizeRect({silent:!0,shape:{x:0,y:0,width:r[0],height:r[1]},style:{stroke:s.get("dataBackgroundColor")||s.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}}))),v([0,1],function(e){var r=l.createIcon(s.get("handleIcon"),{cursor:i(this._orient),draggable:!0,drift:m(this._onDragMove,this,e),onmousemove:function(e){a.stop(e.event)},ondragend:m(this._onDragEnd,this),onmouseover:m(this._showDataInfo,this,!0),onmouseout:m(this._showDataInfo,this,!1)},{x:-1,y:0,width:2,height:2}),c=r.getBoundingRect();this._handleHeight=u.parsePercent(s.get("handleSize"),this._size[1]),this._handleWidth=c.width/c.height*this._handleHeight,r.setStyle(s.getModel("handleStyle").getItemStyle());var d=s.get("handleColor");null!=d&&(r.style.fill=d),o.add(t[e]=r);var f=s.textStyleModel;this.group.add(n[e]=new l.Text({silent:!0,invisible:!0,style:{x:0,y:0,text:"",textVerticalAlign:"middle",textAlign:"center",textFill:f.getTextColor(),textFont:f.getFont()},z2:10}))},this)},_resetInterval:function(){var e=this._range=this.dataZoomModel.getPercentRange(),t=this._getViewExtent();this._handleEnds=[h(e[0],[0,100],t,!0),h(e[1],[0,100],t,!0)]},_updateInterval:function(e,t){var n=this.dataZoomModel,o=this._handleEnds,i=this._getViewExtent(),r=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];f(t,o,i,n.get("zoomLock")?"all":e,null!=r.minSpan?h(r.minSpan,a,i,!0):null,null!=r.maxSpan?h(r.maxSpan,a,i,!0):null),this._range=g([h(o[0],i,a,!0),h(o[1],i,a,!0)])},_updateView:function(e){var t=this._displayables,n=this._handleEnds,o=g(n.slice()),i=this._size;v([0,1],function(e){var o=t.handles[e],r=this._handleHeight;o.attr({scale:[r/2,r/2],position:[n[e],i[1]/2-r/2]})},this),t.filler.setShape({x:o[0],y:0,width:o[1]-o[0],height:i[1]}),this._updateDataInfo(e)},_updateDataInfo:function(e){function t(e){var t=l.getTransform(o.handles[e].parent,this.group),n=l.transformDirection(0===e?"right":"left",t),s=this._handleWidth/2+x,c=l.applyTransform([f[e]+(0===e?-s:s),this._size[1]/2],t);i[e].setStyle({x:c[0],y:c[1],textVerticalAlign:r===b?"middle":n,textAlign:r===b?n:"center",text:a[e]})}var n=this.dataZoomModel,o=this._displayables,i=o.handleLabels,r=this._orient,a=["",""];if(n.get("showDetail")){var s=n.findRepresentativeAxisProxy();if(s){var c=s.getAxisModel().axis,u=this._range,d=e?s.calculateDataWindow({start:u[0],end:u[1]}).valueWindow:s.getDataValueWindow();a=[this._formatLabel(d[0],c),this._formatLabel(d[1],c)]}}var f=g(this._handleEnds.slice());t.call(this,0),t.call(this,1)},_formatLabel:function(e,t){var n=this.dataZoomModel,o=n.get("labelFormatter"),i=n.get("labelPrecision");null!=i&&"auto"!==i||(i=t.getPixelPrecision());var a=null==e||isNaN(e)?"":"category"===t.type||"time"===t.type?t.scale.getLabel(Math.round(e)):e.toFixed(Math.min(i,20));return r.isFunction(o)?o(e,a):r.isString(o)?o.replace("{value}",a):a},_showDataInfo:function(e){e=this._dragging||e;var t=this._displayables.handleLabels;t[0].attr("invisible",!e),t[1].attr("invisible",!e)},_onDragMove:function(e,t,n){this._dragging=!0;var o=this._displayables.barGroup.getLocalTransform(),i=l.applyTransform([t,n],o,!0);this._updateInterval(e,i[0]);var r=this.dataZoomModel.get("realtime");this._updateView(!r),r&&r&&this._dispatchZoomAction()},_onDragEnd:function(){this._dragging=!1,this._showDataInfo(!1),this._dispatchZoomAction()},_onClickPanelClick:function(e){var t=this._size,n=this._displayables.barGroup.transformCoordToLocal(e.offsetX,e.offsetY);if(!(n[0]<0||n[0]>t[0]||n[1]<0||n[1]>t[1])){var o=this._handleEnds,i=(o[0]+o[1])/2;this._updateInterval("all",n[0]-i),this._updateView(),this._dispatchZoomAction()}},_dispatchZoomAction:function(){var e=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,start:e[0],end:e[1]})},_findCoordRect:function(){var e;if(v(this.getTargetCoordInfo(),function(t){if(!e&&t.length){var n=t[0].model.coordinateSystem;e=n.getRect&&n.getRect()}}),!e){var t=this.api.getWidth(),n=this.api.getHeight();e={x:.2*t,y:.2*n,width:.6*t,height:.6*n}}return e}}),w=_;e.exports=w},function(e,t,n){function o(e,t){var n=l(e),o=t.dataZoomId,i=t.coordId;g.each(n,function(e,n){var r=e.dataZoomInfos;r[o]&&g.indexOf(t.allCoordIds,i)<0&&(delete r[o],e.count--)}),c(n);var r=n[i];r||(r=n[i]={coordId:i,dataZoomInfos:{},count:0},r.controller=s(e,r),r.dispatchAction=g.curry(p,e)),!r.dataZoomInfos[o]&&r.count++,r.dataZoomInfos[o]=t;var a=h(r.dataZoomInfos);r.controller.enable(a.controlType,a.opt),r.controller.setPointerChecker(t.containsPoint),v.createOrUpdate(r,"dispatchAction",t.throttleRate,"fixRate")}function i(e,t){var n=l(e);g.each(n,function(e){e.controller.dispose();var n=e.dataZoomInfos;n[t]&&(delete n[t],e.count--)}),c(n)}function r(e,t){if(e&&"dataZoom"===e.type&&e.batch)for(var n=0,o=e.batch.length;no[t]&&(t=i),g.extend(n,e.roamControllerOpt)}),{controlType:t,opt:n}}var g=n(0),m=n(86),v=n(44),b=g.curry,x="\0_ec_dataZoom_roams";t.register=o,t.unregister=i,t.shouldRecordRange=r,t.generateCoordId=a},function(e,t,n){n(186),n(68),n(69),n(495),n(496),n(184),n(183)},function(e,t,n){function o(e,t){t.update="updateView",i.registerAction(t,function(t,n){var o={};return n.eachComponent({mainType:"geo",query:t},function(n){n[e](t.name);var i=n.coordinateSystem;r.each(i.regions,function(e){o[e.name]=n.isSelected(e.name)||!1})}),{selected:o,name:t.name}})}var i=n(1),r=n(0);n(563),n(88),n(502),n(163),o("toggleSelected",{type:"geoToggleSelect",event:"geoselectchanged"}),o("select",{type:"geoSelect",event:"geoselected"}),o("unSelect",{type:"geoUnSelect",event:"geounselected"})},function(e,t,n){var o=n(188),i=n(1),r=i.extendComponentView({type:"geo",init:function(e,t){var n=new o(t,!0);this._mapDraw=n,this.group.add(n.group)},render:function(e,t,n,o){if(!o||"geoToggleSelect"!==o.type||o.from!==this.uid){var i=this._mapDraw;e.get("show")?i.draw(e,t,n,this,o):this._mapDraw.group.removeAll(),this.group.silent=e.get("silent")}},dispose:function(){this._mapDraw&&this._mapDraw.remove()}});e.exports=r},function(e,t,n){function o(e,t,n,o){var i=n.type,r=h[i.charAt(0).toUpperCase()+i.slice(1)],a=new r(n);t.add(a),o.set(e,a),a.__ecGraphicId=e}function i(e,t){var n=e&&e.parent;n&&("group"===e.type&&e.traverse(function(e){i(e,t)}),t.removeKey(e.__ecGraphicId),n.remove(e))}function r(e){return e=f.extend({},e),f.each(["id","parentId","$action","hv","bounding"].concat(g.LOCATION_PARAMS),function(t){delete e[t]}),e}function a(e,t){var n;return f.each(t,function(t){null!=e[t]&&"auto"!==e[t]&&(n=!0)}),n}function l(e,t){var n=e.exist;if(t.id=e.keyInfo.id,!t.type&&n&&(t.type=n.type),null==t.parentId){var o=t.parentOption;o?t.parentId=o.id:n&&(t.parentId=n.parentId)}t.parentOption=null}function s(e,t,n){var o=f.extend({},n),i=e[t],r=n.$action||"merge";"merge"===r?i?(f.merge(i,o,!0),g.mergeLayoutParam(i,o,{ignoreSize:!0}),g.copyLayoutParams(n,i)):e[t]=o:"replace"===r?e[t]=o:"remove"===r&&i&&(e[t]=null)}function c(e,t){e&&(e.hv=t.hv=[a(t,["left","right"]),a(t,["top","bottom"])],"group"===e.type&&(null==e.width&&(e.width=t.width=0),null==e.height&&(e.height=t.height=0)))}var u=n(4),d=(u.__DEV__,n(1)),f=n(0),p=n(5),h=n(2),g=n(6);d.registerPreprocessor(function(e){var t=e.graphic;f.isArray(t)?t[0]&&t[0].elements?e.graphic=[e.graphic[0]]:e.graphic=[{elements:t}]:t&&!t.elements&&(e.graphic=[{elements:[t]}])});var m=d.extendComponentModel({type:"graphic",defaultOption:{elements:[],parentId:null},_elOptionsToUpdate:null,mergeOption:function(e){var t=this.option.elements;this.option.elements=null,m.superApply(this,"mergeOption",arguments),this.option.elements=t},optionUpdated:function(e,t){var n=this.option,o=(t?n:e).elements,i=n.elements=t?[]:n.elements,r=[];this._flatten(o,r);var a=p.mappingToExists(i,r);p.makeIdAndName(a);var u=this._elOptionsToUpdate=[];f.each(a,function(e,t){var n=e.option;n&&(u.push(n),l(e,n),s(i,t,n),c(i[t],n))},this);for(var d=i.length-1;d>=0;d--)null==i[d]?i.splice(d,1):delete i[d].$action},_flatten:function(e,t,n){f.each(e,function(e){if(e){n&&(e.parentOption=n),t.push(e);var o=e.children;"group"===e.type&&o&&this._flatten(o,t,e),delete e.children}},this)},useElOptionsToUpdate:function(){var e=this._elOptionsToUpdate;return this._elOptionsToUpdate=null,e}});d.extendComponentView({type:"graphic",init:function(e,t){this._elMap=f.createHashMap(),this._lastGraphicModel},render:function(e,t,n){e!==this._lastGraphicModel&&this._clear(),this._lastGraphicModel=e,this._updateElements(e,n),this._relocate(e,n)},_updateElements:function(e,t){var n=e.useElOptionsToUpdate();if(n){var a=this._elMap,l=this.group;f.each(n,function(e){var t=e.$action,n=e.id,s=a.get(n),c=e.parentId,u=null!=c?a.get(c):l;if("text"===e.type){var d=e.style;e.hv&&e.hv[1]&&(d.textVerticalAlign=d.textBaseline=null),!d.hasOwnProperty("textFill")&&d.fill&&(d.textFill=d.fill),!d.hasOwnProperty("textStroke")&&d.stroke&&(d.textStroke=d.stroke)}var f=r(e);t&&"merge"!==t?"replace"===t?(i(s,a),o(n,u,f,a)):"remove"===t&&i(s,a):s?s.attr(f):o(n,u,f,a);var p=a.get(n);p&&(p.__ecGraphicWidth=e.width,p.__ecGraphicHeight=e.height)})}},_relocate:function(e,t){for(var n=e.option.elements,o=this.group,i=this._elMap,r=n.length-1;r>=0;r--){var a=n[r],l=i.get(a.id);if(l){var s=l.parent,c=s===o?{width:t.getWidth(),height:t.getHeight()}:{width:s.__ecGraphicWidth||0,height:s.__ecGraphicHeight||0};g.positionElement(l,a,c,null,{hv:a.hv,boundingMode:a.bounding})}}},_clear:function(){var e=this._elMap;e.each(function(t){i(t,e)}),this._elMap=f.createHashMap()},dispose:function(){this._clear()}})},function(e,t,n){n(70),n(180),n(67)},function(e,t,n){var o=n(1);n(193),n(508),n(194);var i=n(509),r=n(14);o.registerProcessor(i),r.registerSubTypeDefaulter("legend",function(){return"plain"})},function(e,t,n){function o(e,t,n){var o=e.getOrient(),i=[1,1];i[o.index]=0,a(t,n,{type:"box",ignoreSize:i})}var i=n(193),r=n(6),a=r.mergeLayoutParam,l=r.getLayoutParams,s=i.extend({type:"legend.scroll",setScrollDataIndex:function(e){this.option.scrollDataIndex=e},defaultOption:{scrollDataIndex:0,pageButtonItemGap:5,pageButtonGap:null,pageButtonPosition:"end",pageFormatter:"{current}/{total}",pageIcons:{horizontal:["M0,0L12,-10L12,10z","M0,0L-12,-10L-12,10z"],vertical:["M0,0L20,0L10,-20z","M0,0L20,0L10,20z"]},pageIconColor:"#2f4554",pageIconInactiveColor:"#aaa",pageIconSize:15,pageTextStyle:{color:"#333"},animationDurationUpdate:800},init:function(e,t,n,i){var r=l(e);s.superCall(this,"init",e,t,n,i),o(this,e,r)},mergeOption:function(e,t){s.superCall(this,"mergeOption",e,t),o(this,this.option,e)},getOrient:function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}}}),c=s;e.exports=c},function(e,t,n){var o=n(0),i=n(2),r=n(6),a=n(194),l=i.Group,s=["width","height"],c=["x","y"],u=a.extend({type:"legend.scroll",newlineDisabled:!0,init:function(){u.superCall(this,"init"),this._currentIndex=0,this.group.add(this._containerGroup=new l),this._containerGroup.add(this.getContentGroup()),this.group.add(this._controllerGroup=new l),this._showController},resetInner:function(){u.superCall(this,"resetInner"),this._controllerGroup.removeAll(),this._containerGroup.removeClipPath(),this._containerGroup.__rectSize=null},renderInner:function(e,t,n,r){function a(e,n){var a=e+"DataIndex",u=i.createIcon(t.get("pageIcons",!0)[t.getOrient().name][n],{onclick:o.bind(l._pageGo,l,a,t,r)},{x:-c[0]/2,y:-c[1]/2,width:c[0],height:c[1]});u.name=e,s.add(u)}var l=this;u.superCall(this,"renderInner",e,t,n,r);var s=this._controllerGroup,c=t.get("pageIconSize",!0);o.isArray(c)||(c=[c,c]),a("pagePrev",0);var d=t.getModel("pageTextStyle");s.add(new i.Text({name:"pageText",style:{textFill:d.getTextColor(),font:d.getFont(),textVerticalAlign:"middle",textAlign:"center"},silent:!0})),a("pageNext",1)},layoutInner:function(e,t,n){var a=this.getContentGroup(),l=this._containerGroup,u=this._controllerGroup,d=e.getOrient().index,f=s[d],p=s[1-d],h=c[1-d];r.box(e.get("orient"),a,e.get("itemGap"),d?n.width:null,d?null:n.height),r.box("horizontal",u,e.get("pageButtonItemGap",!0));var g=a.getBoundingRect(),m=u.getBoundingRect(),v=this._showController=g[f]>n[f],b=[-g.x,-g.y];b[d]=a.position[d];var x=[0,0],y=[-m.x,-m.y],_=o.retrieve2(e.get("pageButtonGap",!0),e.get("itemGap",!0));v&&("end"===e.get("pageButtonPosition",!0)?y[d]+=n[f]-m[f]:x[d]+=m[f]+_),y[1-d]+=g[p]/2-m[p]/2,a.attr("position",b),l.attr("position",x),u.attr("position",y);var w=this.group.getBoundingRect(),w={x:0,y:0};if(w[f]=v?n[f]:g[f],w[p]=Math.max(g[p],m[p]),w[h]=Math.min(0,m[h]+y[1-d]),l.__rectSize=n[f],v){var k={x:0,y:0};k[f]=Math.max(n[f]-m[f]-_,0),k[p]=w[p],l.setClipPath(new i.Rect({shape:k})),l.__rectSize=k[f]}else u.eachChild(function(e){e.attr({invisible:!0,silent:!0})});var S=this._getPageInfo(e);return null!=S.pageIndex&&i.updateProps(a,{position:S.contentPosition},!!v&&e),this._updatePageInfoView(e,S),w},_pageGo:function(e,t,n){var o=this._getPageInfo(t)[e];null!=o&&n.dispatchAction({type:"legendScroll",scrollDataIndex:o,legendId:t.id})},_updatePageInfoView:function(e,t){var n=this._controllerGroup;o.each(["pagePrev","pageNext"],function(o){var i=null!=t[o+"DataIndex"],r=n.childOfName(o);r&&(r.setStyle("fill",i?e.get("pageIconColor",!0):e.get("pageIconInactiveColor",!0)),r.cursor=i?"pointer":"default")});var i=n.childOfName("pageText"),r=e.get("pageFormatter"),a=t.pageIndex,l=null!=a?a+1:0,s=t.pageCount;i&&r&&i.setStyle("text",o.isString(r)?r.replace("{current}",l).replace("{total}",s):r({current:l,total:s}))},_getPageInfo:function(e){function t(e){var t=e.getBoundingRect().clone();return t[g]+=e.position[f],t}var n,o,i,r,a=e.get("scrollDataIndex",!0),l=this.getContentGroup(),u=l.getBoundingRect(),d=this._containerGroup.__rectSize,f=e.getOrient().index,p=s[f],h=s[1-f],g=c[f],m=l.position.slice();this._showController?l.eachChild(function(e){e.__legendDataIndex===a&&(r=e)}):r=l.childAt(0);var v=d?Math.ceil(u[p]/d):0;if(r){var b=r.getBoundingRect(),x=r.position[f]+b[g];m[f]=-x-u[g],n=Math.floor(v*(x+b[g]+d/2)/u[p]),n=u[p]&&v?Math.max(0,Math.min(v-1,n)):-1;var y={x:0,y:0};y[p]=d,y[h]=u[h],y[g]=-m[f]-u[g];var _,w=l.children();if(l.eachChild(function(e,n){var o=t(e);o.intersect(y)&&(null==_&&(_=n),i=e.__legendDataIndex),n===w.length-1&&o[g]+o[p]<=y[g]+y[p]&&(i=null)}),null!=_){var k=w[_],S=t(k);if(y[g]=S[g]+S[p]-y[p],_<=0&&S[g]>=y[g])o=null;else{for(;_>0&&t(w[_-1]).intersect(y);)_--;o=w[_].__legendDataIndex}}}return{contentPosition:m,pageIndex:n,pageCount:v,pagePrevDataIndex:o,pageNextDataIndex:i}}}),d=u;e.exports=d},function(e,t,n){function o(e,t,n){var o,i={},a="toggleSelected"===e;return n.eachComponent("legend",function(n){a&&null!=o?n[o?"select":"unSelect"](t.name):(n[e](t.name),o=n.isSelected(t.name));var l=n.getData();r.each(l,function(e){var t=e.get("name");if("\n"!==t&&""!==t){var o=n.isSelected(t);i.hasOwnProperty(t)?i[t]=i[t]&&o:i[t]=o}})}),{name:t.name,selected:i}}var i=n(1),r=n(0);i.registerAction("legendToggleSelect","legendselectchanged",r.curry(o,"toggleSelected")),i.registerAction("legendSelect","legendselected",r.curry(o,"select")),i.registerAction("legendUnSelect","legendunselected",r.curry(o,"unSelect"))},function(e,t){function n(e){var t=e.findComponents({mainType:"legend"});t&&t.length&&e.filterSeries(function(e){for(var n=0;n=0&&"number"==typeof l&&(l=+l.toFixed(Math.min(g,20))),p.coord[u]=h.coord[u]=l,o=[p,h,{type:r,valueIndex:o.valueIndex,value:l}]}return o=[d.dataTransform(e,o[0]),d.dataTransform(e,o[1]),s.extend({},o[2])],o[2].type=o[2].type||"",s.merge(o[2],o[0]),s.merge(o[2],o[1]),o},g=p.extend({type:"markLine",updateLayout:function(e,t,n){t.eachSeries(function(e){var t=e.markLineModel;if(t){var o=t.getData(),i=t.__from,r=t.__to;i.each(function(t){a(i,t,!0,e,n),a(r,t,!1,e,n)}),o.each(function(e){o.setItemLayout(e,[i.getItemLayout(e),r.getItemLayout(e)])}),this.markerGroupMap.get(e.id).updateLayout()}},this)},renderSeries:function(e,t,n,o){function i(t,n,i){var r=t.getItemModel(n);a(t,n,i,e,o),t.setItemVisual(n,{symbolSize:r.get("symbolSize")||x[i?0:1],symbol:r.get("symbol",!0)||b[i?0:1],color:r.get("itemStyle.normal.color")||u.getVisual("color")})}var r=e.coordinateSystem,c=e.id,u=e.getData(),d=this.markerGroupMap,p=d.get(c)||d.set(c,new f);this.group.add(p.group);var h=l(r,e,t),g=h.from,m=h.to,v=h.line;t.__from=g,t.__to=m,t.setData(v);var b=t.get("symbol"),x=t.get("symbolSize");s.isArray(b)||(b=[b,b]),"number"==typeof x&&(x=[x,x]),h.from.each(function(e){i(g,e,!0),i(m,e,!1)}),v.each(function(e){var t=v.getItemModel(e).get("lineStyle.normal.color");v.setItemVisual(e,{color:t||g.getItemVisual(e,"color")}),v.setItemLayout(e,[g.getItemLayout(e),m.getItemLayout(e)]),v.setItemVisual(e,{fromSymbolSize:g.getItemVisual(e,"symbolSize"),fromSymbol:g.getItemVisual(e,"symbol"),toSymbolSize:m.getItemVisual(e,"symbolSize"),toSymbol:m.getItemVisual(e,"symbol")})}),p.updateData(v),h.line.eachItemGraphicEl(function(e,n){e.traverse(function(e){e.dataModel=t})}),p.__keep=!0,p.group.silent=t.get("silent")||e.get("silent")}});e.exports=g},function(e,t,n){var o=n(121),i=o.extend({type:"markPoint",defaultOption:{zlevel:0,z:5,symbol:"pin",symbolSize:50,tooltip:{trigger:"item"},label:{normal:{show:!0,position:"inside"},emphasis:{show:!0}},itemStyle:{normal:{borderWidth:2}}}});e.exports=i},function(e,t,n){function o(e,t,n){var o=t.coordinateSystem;e.each(function(i){var r,a=e.getItemModel(i),s=l.parsePercent(a.get("x"),n.getWidth()),c=l.parsePercent(a.get("y"),n.getHeight());if(isNaN(s)||isNaN(c)){if(t.getMarkerPosition)r=t.getMarkerPosition(e.getValues(e.dimensions,i));else if(o){var u=e.get(o.dimensions[0],i),d=e.get(o.dimensions[1],i);r=o.dataToPoint([u,d])}}else r=[s,c];isNaN(s)||(r[0]=s),isNaN(c)||(r[1]=c),e.setItemLayout(i,r)})}function i(e,t,n){var o;o=e?r.map(e&&e.dimensions,function(e){var n=t.getData().getDimensionInfo(t.coordDimToDataDim(e)[0])||{};return n.name=e,n}):[{name:"value",type:"float"}];var i=new s(o,n),a=r.map(n.get("data"),r.curry(c.dataTransform,t));return e&&(a=r.filter(a,r.curry(c.dataFilter,e))),i.initData(a,null,e?c.dimValueGetter:function(e){return e.value}),i}var r=n(0),a=n(65),l=n(3),s=n(13),c=n(123),u=n(122),d=u.extend({type:"markPoint",updateLayout:function(e,t,n){t.eachSeries(function(e){var t=e.markPointModel;t&&(o(t.getData(),e,n),this.markerGroupMap.get(e.id).updateLayout(t))},this)},renderSeries:function(e,t,n,r){var l=e.coordinateSystem,s=e.id,c=e.getData(),u=this.markerGroupMap,d=u.get(s)||u.set(s,new a),f=i(l,e,t);t.setData(f),o(t.getData(),e,r),f.each(function(e){var n=f.getItemModel(e),o=n.getShallow("symbolSize");"function"==typeof o&&(o=o(t.getRawValue(e),t.getDataParams(e))),f.setItemVisual(e,{symbolSize:o,color:n.get("itemStyle.normal.color")||c.getVisual("color"),symbol:n.getShallow("symbol")})}),d.updateData(f),this.group.add(d.group),f.eachItemGraphicEl(function(e){e.traverse(function(e){e.dataModel=t})}),d.__keep=!0,d.group.silent=t.get("silent")||e.get("silent")}});e.exports=d},function(e,t,n){n(211),n(476),n(473)},function(e,t,n){var o=n(1),i=n(0),r=n(590);n(125),n(469),n(525),n(67),n(479),o.registerLayout(i.curry(r,"bar")),o.extendComponentView({type:"polar"})},function(e,t,n){n(581),n(582),n(524)},function(e,t,n){var o=n(4),i=(o.__DEV__,n(1)),r=n(0),a=n(42),l=n(2),s=["axisLine","axisTickLabel","axisName"],c=i.extendComponentView({type:"radar",render:function(e,t,n){this.group.removeAll(),this._buildAxes(e),this._buildSplitLineAndArea(e)},_buildAxes:function(e){var t=e.coordinateSystem,n=t.getIndicatorAxes(),o=r.map(n,function(e){return new a(e.model,{position:[t.cx,t.cy],rotation:e.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})});r.each(o,function(e){r.each(s,e.add,e),this.group.add(e.getGroup())},this)},_buildSplitLineAndArea:function(e){function t(e,t,n){var o=n%t.length;return e[o]=e[o]||[],o}var n=e.coordinateSystem,o=n.getIndicatorAxes();if(o.length){var i=e.get("shape"),a=e.getModel("splitLine"),s=e.getModel("splitArea"),c=a.getModel("lineStyle"),u=s.getModel("areaStyle"),d=a.get("show"),f=s.get("show"),p=c.get("color"),h=u.get("color");p=r.isArray(p)?p:[p],h=r.isArray(h)?h:[h];var g=[],m=[];if("circle"===i)for(var v=o[0].getTicksCoords(),b=n.cx,x=n.cy,y=0;y=0||"+"===n?"left":"right"},l={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},s={horizontal:0,vertical:w/2},c="vertical"===i?r.height:r.width,u=e.getModel("controlStyle"),d=u.get("show"),f=d?u.get("itemSize"):0,p=d?u.get("itemGap"):0,h=f+p,g=e.get("label.normal.rotate")||0;g=g*w/180;var m,v,b,x,y=u.get("position",!0),d=u.get("show",!0),_=d&&u.get("showPlayBtn",!0),k=d&&u.get("showPrevBtn",!0),S=d&&u.get("showNextBtn",!0),M=0,C=c;return"left"===y||"bottom"===y?(_&&(m=[0,0],M+=h),k&&(v=[M,0],M+=h),S&&(b=[C-f,0],C-=h)):(_&&(m=[C-f,0],C-=h),k&&(v=[0,0],M+=h),S&&(b=[C-f,0],C-=h)),x=[M,C],e.get("inverse")&&x.reverse(),{viewRect:r,mainLength:c,orient:i,rotation:s[i],labelRotation:g,labelPosOpt:n,labelAlign:e.get("label.normal.align")||a[i],labelBaseline:e.get("label.normal.verticalAlign")||e.get("label.normal.baseline")||l[i],playPosition:m,prevBtnPosition:v,nextBtnPosition:b,axisExtent:x,controlSize:f,controlGap:p}},_position:function(e,t){function n(e){var t=e.position;e.origin=[f[0][0]-t[0],f[1][0]-t[1]]}function o(e){return[[e.x,e.x+e.width],[e.y,e.y+e.height]]}function i(e,t,n,o,i){e[o]+=n[o][i]-t[o][i]}var r=this._mainGroup,a=this._labelGroup,l=e.viewRect;if("vertical"===e.orient){var s=c.create(),u=l.x,d=l.y+l.height;c.translate(s,s,[-u,-d]),c.rotate(s,s,-w/2),c.translate(s,s,[u,d]),l=l.clone(),l.applyTransform(s)}var f=o(l),p=o(r.getBoundingRect()),h=o(a.getBoundingRect()),g=r.position,m=a.position;m[0]=g[0]=f[0][0];var v=e.labelPosOpt;if(isNaN(v)){var b="+"===v?0:1;i(g,p,f,1,b),i(m,h,f,1,1-b)}else{var b=v>=0?0:1;i(g,p,f,1,b),m[1]=g[1]+v}r.attr("position",g),a.attr("position",m),r.rotation=a.rotation=e.rotation,n(r),n(a)},_createAxis:function(e,t){var n=t.getData(),o=t.get("axisType"),i=m.createScaleByModel(t,o),r=n.getDataExtent("value");i.setExtent(r[0],r[1]),this._customizeScale(i,n),i.niceTicks();var a=new p("value",i,e.axisExtent,o);return a.model=t,a},_customizeScale:function(e,t){e.getTicks=function(){return t.mapArray(["value"],function(e){return e})},e.getTicksLabels=function(){return l.map(this.getTicks(),e.getLabel,e)}},_createGroup:function(e){var t=this["_"+e]=new u.Group;return this.group.add(t),t},_renderAxisLine:function(e,t,n,o){var i=n.getExtent();o.get("lineStyle.show")&&t.add(new u.Line({shape:{x1:i[0],y1:0,x2:i[1],y2:0},style:l.extend({lineCap:"round"},o.getModel("lineStyle").getLineStyle()),silent:!0,z2:1}))},_renderAxisTick:function(e,t,n,o){var i=o.getData(),a=n.scale.getTicks();_(a,function(e,a){var l=n.dataToCoord(e),s=i.getItemModel(a),c=s.getModel("itemStyle.normal"),d=s.getModel("itemStyle.emphasis"),f={position:[l,0],onclick:y(this._changeTimeline,this,a)},p=r(s,c,t,f);u.setHoverStyle(p,d.getItemStyle()),s.get("tooltip")?(p.dataIndex=a,p.dataModel=o):p.dataIndex=p.dataModel=null},this)},_renderAxisLabel:function(e,t,n,o){var i=o.getModel("label.normal");if(i.get("show")){var r=o.getData(),a=n.scale.getTicks(),l=m.getFormattedLabels(n,i.get("formatter")),s=n.getLabelInterval();_(a,function(o,i){if(!n.isLabelIgnored(i,s)){var a=r.getItemModel(i),c=a.getModel("label.normal"),d=a.getModel("label.emphasis"),f=n.dataToCoord(o),p=new u.Text({position:[f,0],rotation:e.labelRotation-e.rotation,onclick:y(this._changeTimeline,this,i),silent:!1});u.setTextStyle(p.style,c,{text:l[i],textAlign:e.labelAlign,textVerticalAlign:e.labelBaseline}),t.add(p),u.setHoverStyle(p,u.setTextStyle({},d))}},this)}},_renderControl:function(e,t,n,o){function r(e,n,r,f){if(e){var p={position:e,origin:[a/2,0],rotation:f?-l:0,rectHover:!0,style:s,onclick:r},h=i(o,n,d,p);t.add(h),u.setHoverStyle(h,c)}}var a=e.controlSize,l=e.rotation,s=o.getModel("controlStyle.normal").getItemStyle(),c=o.getModel("controlStyle.emphasis").getItemStyle(),d=[0,-a/2,a,a],f=o.getPlayState(),p=o.get("inverse",!0);r(e.nextBtnPosition,"controlStyle.nextIcon",y(this._changeTimeline,this,p?"-":"+")),r(e.prevBtnPosition,"controlStyle.prevIcon",y(this._changeTimeline,this,p?"+":"-")),r(e.playPosition,"controlStyle."+(f?"stopIcon":"playIcon"),y(this._handlePlayClick,this,!f),!0)},_renderCurrentPointer:function(e,t,n,o){var i=o.getData(),l=o.getCurrentIndex(),s=i.getItemModel(l).getModel("checkpointStyle"),c=this,u={onCreate:function(e){e.draggable=!0,e.drift=y(c._handlePointerDrag,c),e.ondragend=y(c._handlePointerDragend,c),a(e,l,n,o,!0)},onUpdate:function(e){a(e,l,n,o)}};this._currentPointer=r(s,s,this._mainGroup,{},this._currentPointer,u)},_handlePlayClick:function(e){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:e,from:this.uid})},_handlePointerDrag:function(e,t,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},_handlePointerDragend:function(e){this._pointerChangeTimeline([e.offsetX,e.offsetY],!0)},_pointerChangeTimeline:function(e,t){var n=this._toAxisCoord(e)[0],o=this._axis,i=v.asc(o.getExtent().slice());n>i[1]&&(n=i[1]),n=t&&(e=t-1),e<0&&(e=0)),this.option.currentIndex=e},getCurrentIndex:function(){return this.option.currentIndex},isIndexMax:function(){return this.getCurrentIndex()>=this._data.count()-1},setPlayState:function(e){this.option.autoPlay=!!e},getPlayState:function(){return!!this.option.autoPlay},_initData:function(){var e=this.option,t=e.data||[],n=e.axisType,i=this._names=[];if("category"===n){var l=[];o.each(t,function(e,t){var n,r=a.getDataItemValue(e);o.isObject(e)?(n=o.clone(e),n.value=t):n=t,l.push(n),o.isString(r)||null!=r&&!isNaN(r)||(r=""),i.push(r+"")}),t=l}var s={category:"ordinal",time:"time"}[n]||"number";(this._data=new r([{name:"value",type:s}],this)).initData(t,i)},getData:function(){return this._data},getCategories:function(){if("category"===this.get("axisType"))return this._names.slice()}}),s=l;e.exports=s},function(e,t,n){var o=n(129),i=o.extend({type:"timeline"});e.exports=i},function(e,t,n){function o(e){var t=e&&e.timeline;l.isArray(t)||(t=t?[t]:[]),l.each(t,function(e){e&&i(e)})}function i(e){var t=e.type,n={number:"value",time:"time"};if(n[t]&&(e.axisType=n[t],delete e.type),r(e),a(e,"controlPosition")){var o=e.controlStyle||(e.controlStyle={});a(o,"position")||(o.position=e.controlPosition),"none"!==o.position||a(o,"show")||(o.show=!1,delete o.position),delete e.controlPosition}l.each(e.data||[],function(e){l.isObject(e)&&!l.isArray(e)&&(!a(e,"value")&&a(e,"name")&&(e.value=e.name),r(e))})}function r(e){var t=e.itemStyle||(e.itemStyle={}),n=t.emphasis||(t.emphasis={}),o=e.label||e.label||{},i=o.normal||(o.normal={}),r={normal:1,emphasis:1};l.each(o,function(e,t){r[t]||a(i,t)||(i[t]=e)}),n.label&&!a(o,"emphasis")&&(o.emphasis=n.label,delete n.label)}function a(e,t){return e.hasOwnProperty(t)}var l=n(0);e.exports=o},function(e,t,n){var o=n(1),i=n(0);o.registerAction({type:"timelineChange",event:"timelineChanged",update:"prepareAndUpdate"},function(e,t){var n=t.getComponent("timeline");return n&&null!=e.currentIndex&&(n.setCurrentIndex(e.currentIndex),!n.get("loop",!0)&&n.isIndexMax()&&n.setPlayState(!1)),t.resetOption("timeline"),i.defaults({currentIndex:n.option.currentIndex},e)}),o.registerAction({type:"timelinePlayChange",event:"timelinePlayChanged",update:"update"},function(e,t){var n=t.getComponent("timeline");n&&null!=e.playState&&n.setPlayState(e.playState)})},function(e,t,n){n(14).registerSubTypeDefaulter("timeline",function(){return"slider"})},function(e,t,n){n(536),n(537),n(543),n(541),n(539),n(540),n(542)},function(e,t,n){var o=n(1),i=n(0),r=n(35),a=o.extendComponentModel({type:"toolbox",layoutMode:{type:"box",ignoreSize:!0},mergeDefaultAndTheme:function(e){a.superApply(this,"mergeDefaultAndTheme",arguments),i.each(this.option.feature,function(e,t){var n=r.get(t);n&&i.merge(e,n.defaultOption)})},defaultOption:{show:!0,z:6,zlevel:0,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{normal:{borderColor:"#666",color:"none"},emphasis:{borderColor:"#3E98C5"}}}}),l=a;e.exports=l},function(e,t,n){function o(e){return 0===e.indexOf("my")}var i=n(1),r=n(0),a=n(27),l=n(35),s=n(2),c=n(12),u=n(56),d=n(191),f=i.extendComponentView({type:"toolbox",render:function(e,t,n,i){function f(r,a){var s,u=b[r],d=b[a],f=m[u],h=new c(f,e,e.ecModel);if(u&&!d){if(o(u))s={model:h,onclick:h.option.onclick,featureName:u};else{var g=l.get(u);if(!g)return;s=new g(h,t,n)}v[u]=s}else{if(!(s=v[d]))return;s.model=h,s.ecModel=t,s.api=n}return!u&&d?void(s.dispose&&s.dispose(t,n)):!h.get("show")||s.unusable?void(s.remove&&s.remove(t,n)):(p(h,s,u),h.setIconStatus=function(e,t){var n=this.option,o=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[e]=t,o[e]&&o[e].trigger(t)},void(s.render&&s.render(h,t,n,i)))}function p(o,i,a){var l=o.getModel("iconStyle"),c=i.getIcons?i.getIcons():o.get("icon"),u=o.get("title")||{};if("string"==typeof c){var d=c,f=u;c={},u={},c[a]=d,u[a]=f}var p=o.iconPaths={};r.each(c,function(a,c){var d=s.createIcon(a,{},{x:-g/2,y:-g/2,width:g,height:g});d.setStyle(l.getModel("normal").getItemStyle()),d.hoverStyle=l.getModel("emphasis").getItemStyle(),s.setHoverStyle(d),e.get("showTitle")&&(d.__title=u[c],d.on("mouseover",function(){var e=l.getModel("emphasis").getItemStyle();d.setStyle({text:u[c],textPosition:e.textPosition||"bottom",textFill:e.fill||e.stroke||"#000",textAlign:e.textAlign||"center"})}).on("mouseout",function(){d.setStyle({textFill:null})})),d.trigger(o.get("iconStatus."+c)||"normal"),h.add(d),d.on("click",r.bind(i.onclick,i,t,n,c)),p[c]=d})}var h=this.group;if(h.removeAll(),e.get("show")){var g=+e.get("itemSize"),m=e.get("feature")||{},v=this._features||(this._features={}),b=[];r.each(m,function(e,t){b.push(t)}),new u(this._featureNames||[],b).add(f).update(f).remove(r.curry(f,null)).execute(),this._featureNames=b,d.layout(h,e,n),h.add(d.makeBackground(h.getBoundingRect(),e)),h.eachChild(function(e){var t=e.__title,o=e.hoverStyle;if(o&&t){var i=a.getBoundingRect(t,a.makeFont(o)),r=e.position[0]+h.position[0],l=e.position[1]+h.position[1]+g,s=!1;l+i.height>n.getHeight()&&(o.textPosition="top",s=!0);var c=s?-5-i.height:g+8;r+i.width/2>n.getWidth()?(o.textPosition=["100%",c],o.textAlign="right"):r-i.width/2<0&&(o.textPosition=[0,c],o.textAlign="left")}})}},updateView:function(e,t,n,o){r.each(this._features,function(e){e.updateView&&e.updateView(e.model,t,n,o)})},updateLayout:function(e,t,n,o){r.each(this._features,function(e){e.updateLayout&&e.updateLayout(e.model,t,n,o)})},remove:function(e,t){r.each(this._features,function(n){n.remove&&n.remove(e,t)}),this.group.removeAll()},dispose:function(e,t){r.each(this._features,function(n){n.dispose&&n.dispose(e,t)})}});e.exports=f},function(e,t,n){function o(e,t,n){this.model=e,this.ecModel=t,this.api=n,this._brushType,this._brushMode}var i=n(0),r=n(35),a=n(57),l=a.toolbox.brush;o.defaultOption={show:!0,type:["rect","polygon","lineX","lineY","keep","clear"],icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:i.clone(l.title)};var s=o.prototype;s.render=s.updateView=s.updateLayout=function(e,t,n){var o,r,a;t.eachComponent({mainType:"brush"},function(e){o=e.brushType,r=e.brushOption.brushMode||"single",a|=e.areas.length}),this._brushType=o,this._brushMode=r,i.each(e.get("type",!0),function(t){e.setIconStatus(t,("keep"===t?"multiple"===r:"clear"===t?a:t===o)?"emphasis":"normal")})},s.getIcons=function(){var e=this.model,t=e.get("icon",!0),n={};return i.each(e.get("type",!0),function(e){t[e]&&(n[e]=t[e])}),n},s.onclick=function(e,t,n){var o=this._brushType,i=this._brushMode;"clear"===n?(t.dispatchAction({type:"axisAreaSelect",intervals:[]}),t.dispatchAction({type:"brush",command:"clear",areas:[]})):t.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:"keep"===n?o:o!==n&&n,brushMode:"keep"===n?"multiple"===i?"single":"multiple":i}})},r.register("brush",o);var c=o;e.exports=c},function(e,t,n){function o(e){var t={},n=[],o=[];return e.eachRawSeries(function(e){var i=e.coordinateSystem;if(!i||"cartesian2d"!==i.type&&"polar"!==i.type)n.push(e);else{var r=i.getBaseAxis();if("category"===r.type){var a=r.dim+"_"+r.index;t[a]||(t[a]={categoryAxis:r,valueAxis:i.getOtherAxis(r),series:[]},o.push({axisDim:r.dim,axisIndex:r.index})),t[a].series.push(e)}else n.push(e)}}),{seriesGroupByCategoryAxis:t,other:n,meta:o}}function i(e){var t=[];return g.each(e,function(e,n){var o=e.categoryAxis,i=e.valueAxis,r=i.dim,a=[" "].concat(g.map(e.series,function(e){return e.name})),l=[o.model.getCategories()];g.each(e.series,function(e){l.push(e.getRawData().mapArray(r,function(e){return e}))});for(var s=[a.join(_)],c=0;c=0)return!0}function c(e){for(var t=e.split(/\n+/g),n=l(t.shift()).split(w),o=[],i=g.map(n,function(e){return{name:e,data:[]}}),r=0;r1?"emphasis":"normal")}function a(e,t,n,o,r){var a=n._isZoomActive;o&&"takeGlobalCursor"===o.type&&(a="dataZoomSelect"===o.key&&o.dataZoomSelectActive),n._isZoomActive=a,e.setIconStatus("zoom",a?"emphasis":"normal");var l=new u(i(e.option),t,{include:["grid"]});n._brushController.setPanels(l.makePanelOpts(r,function(e){return e.xAxisDeclared&&!e.yAxisDeclared?"lineX":!e.xAxisDeclared&&e.yAxisDeclared?"lineY":"rect"})).enableBrush(!!a&&{brushType:"auto",brushStyle:{lineWidth:0,fill:"rgba(0,0,0,0.2)"}})}var l=n(1),s=n(0),c=n(118),u=n(187),d=n(185),f=n(71),p=n(57),h=n(35);n(500);var g=p.toolbox.dataZoom,m=s.each,v="\0_ec_\0toolbox-dataZoom_";o.defaultOption={show:!0,icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:s.clone(g.title)};var b=o.prototype;b.render=function(e,t,n,o){this.model=e,this.ecModel=t,this.api=n,a(e,t,this,o,n),r(e,t)},b.onclick=function(e,t,n){x[n].call(this)},b.remove=function(e,t){this._brushController.unmount()},b.dispose=function(e,t){this._brushController.dispose()};var x={zoom:function(){var e=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:e})},back:function(){this._dispatchZoomAction(d.pop(this.ecModel))}};b._onBrush=function(e,t){function n(e,t,n){var i=t.getAxis(e),l=i.model,s=o(e,l,a),c=s.findRepresentativeAxisProxy(l).getMinMaxSpan();null==c.minValueSpan&&null==c.maxValueSpan||(n=f(0,n.slice(),i.scale.getExtent(),0,c.minValueSpan,c.maxValueSpan)),s&&(r[s.id]={dataZoomId:s.id,startValue:n[0],endValue:n[1]})}function o(e,t,n){var o;return n.eachComponent({mainType:"dataZoom",subType:"select"},function(n){n.getAxisModel(e,t.componentIndex)&&(o=n)}),o}if(t.isEnd&&e.length){var r={},a=this.ecModel;this._brushController.updateCovers([]),new u(i(this.model.option),a,{include:["grid"]}).matchOutputRanges(e,a,function(e,t,o){if("cartesian2d"===o.type){var i=e.brushType;"rect"===i?(n("x",o,t[0]),n("y",o,t[1])):n({lineX:"x",lineY:"y"}[i],o,t)}}),d.push(a,r),this._dispatchZoomAction(r)}},b._dispatchZoomAction=function(e){var t=[];m(e,function(e,n){t.push(s.clone(e))}),t.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:t})},h.register("dataZoom",o),l.registerPreprocessor(function(e){function t(e,t){if(t){var i=e+"Index",r=t[i];null==r||"all"==r||s.isArray(r)||(r=!1===r||"none"===r?[]:[r]),n(e,function(t,n){if(null==r||"all"==r||-1!==s.indexOf(r,n)){var a={type:"select",$fromToolbox:!0,id:v+e+n};a[i]=n,o.push(a)}})}}function n(t,n){var o=e[t];s.isArray(o)||(o=o?[o]:[]),m(o,n)}if(e){var o=e.dataZoom||(e.dataZoom=[]);s.isArray(o)||(e.dataZoom=o=[o]);var i=e.toolbox;if(i&&(s.isArray(i)&&(i=i[0]),i&&i.feature)){var r=i.feature.dataZoom;t("xAxis",r),t("yAxis",r)}}});var y=o;e.exports=y},function(e,t,n){function o(e){this.model=e}var i=n(1),r=n(0),a=n(57),l=n(35),s=a.toolbox.magicType;o.defaultOption={show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z",tiled:"M2.3,2.2h22.8V25H2.3V2.2z M35,2.2h22.8V25H35V2.2zM2.3,35h22.8v22.8H2.3V35z M35,35h22.8v22.8H35V35z"},title:r.clone(s.title),option:{},seriesIndex:{}};var c=o.prototype;c.getIcons=function(){var e=this.model,t=e.get("icon"),n={};return r.each(e.get("type"),function(e){t[e]&&(n[e]=t[e])}),n};var u={line:function(e,t,n,o){if("bar"===e)return r.merge({id:t,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},o.get("option.line")||{},!0)},bar:function(e,t,n,o){if("line"===e)return r.merge({id:t,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},o.get("option.bar")||{},!0)},stack:function(e,t,n,o){if("line"===e||"bar"===e)return r.merge({id:t,stack:"__ec_magicType_stack__"},o.get("option.stack")||{},!0)},tiled:function(e,t,n,o){if("line"===e||"bar"===e)return r.merge({id:t,stack:""},o.get("option.tiled")||{},!0)}},d=[["line","bar"],["stack","tiled"]];c.onclick=function(e,t,n){var o=this.model,i=o.get("seriesIndex."+n);if(u[n]){var a={series:[]},l=function(t){var i=t.subType,l=t.id,s=u[n](i,l,t,o);s&&(r.defaults(s,t.option),a.series.push(s));var c=t.coordinateSystem;if(c&&"cartesian2d"===c.type&&("line"===n||"bar"===n)){var d=c.getAxesByScale("ordinal")[0];if(d){var f=d.dim,p=f+"Axis",h=e.queryComponents({mainType:p,index:t.get(name+"Index"),id:t.get(name+"Id")})[0],g=h.componentIndex;a[p]=a[p]||[];for(var m=0;m<=g;m++)a[p][g]=a[p][g]||{};a[p][g].boundaryGap="bar"===n}}};r.each(d,function(e){r.indexOf(e,n)>=0&&r.each(e,function(e){o.setIconStatus(e,"normal")})}),o.setIconStatus(n,"emphasis"),e.eachComponent({mainType:"series",query:null==i?null:{seriesIndex:i}},l),t.dispatchAction({type:"changeMagicType",currentType:n,newOption:a})}},i.registerAction({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},function(e,t){t.mergeOption(e.newOption)}),l.register("magicType",o);var f=o;e.exports=f},function(e,t,n){function o(e){this.model=e}var i=n(1),r=n(185),a=n(57),l=n(35),s=a.toolbox.restore;o.defaultOption={show:!0,icon:"M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5",title:s.title},o.prototype.onclick=function(e,t,n){r.clear(e),t.dispatchAction({type:"restore",from:this.uid})},l.register("restore",o),i.registerAction({type:"restore",event:"restore",update:"prepareAndUpdate"},function(e,t){t.resetOption("recreate")});var c=o;e.exports=c},function(e,t,n){function o(e){this.model=e}var i=n(15),r=n(57),a=n(35),l=r.toolbox.saveAsImage;o.defaultOption={show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:l.title,type:"png",name:"",excludeComponents:["toolbox"],pixelRatio:1,lang:l.lang.slice()},o.prototype.unusable=!i.canvasSupported,o.prototype.onclick=function(e,t){var n=this.model,o=n.get("name")||e.get("title.0.text")||"echarts",r=document.createElement("a"),a=n.get("type",!0)||"png";r.download=o+"."+a,r.target="_blank";var l=t.getConnectedDataURL({type:a,backgroundColor:n.get("backgroundColor",!0)||e.get("backgroundColor")||"#fff",excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")});if(r.href=l,"function"!=typeof MouseEvent||i.browser.ie||i.browser.edge)if(window.navigator.msSaveOrOpenBlob){for(var s=atob(l.split(",")[1]),c=s.length,u=new Uint8Array(c);c--;)u[c]=s.charCodeAt(c);var d=new Blob([u]);window.navigator.msSaveOrOpenBlob(d,o+"."+a)}else{var f=n.get("lang"),p='',h=window.open();h.document.write(p)}else{var g=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!1});r.dispatchEvent(g)}},a.register("saveAsImage",o);var s=o;e.exports=s},function(e,t,n){function o(e){var t="left "+e+"s cubic-bezier(0.23, 1, 0.32, 1),top "+e+"s cubic-bezier(0.23, 1, 0.32, 1)";return l.map(h,function(e){return e+"transition:"+t}).join(";")}function i(e){var t=[],n=e.get("fontSize"),o=e.getTextColor();return o&&t.push("color:"+o),t.push("font:"+e.getFont()),n&&t.push("line-height:"+Math.round(3*n/2)+"px"),f(["decoration","align"],function(n){var o=e.get(n);o&&t.push("text-"+n+":"+o)}),t.join(";")}function r(e){var t=[],n=e.get("transitionDuration"),r=e.get("backgroundColor"),a=e.getModel("textStyle"),l=e.get("padding");return n&&t.push(o(n)),r&&(u.canvasSupported?t.push("background-Color:"+r):(t.push("background-Color:#"+s.toHex(r)),t.push("filter:alpha(opacity=70)"))),f(["width","color","radius"],function(n){var o="border-"+n,i=p(o),r=e.get(i);null!=r&&t.push(o+":"+r+("color"===n?"":"px"))}),t.push(i(a)),null!=l&&t.push("padding:"+d.normalizeCssArray(l).join("px ")+"px"),t.join(";")+";"}function a(e,t){var n=document.createElement("div"),o=this._zr=t.getZr();this.el=n,this._x=t.getWidth()/2,this._y=t.getHeight()/2,e.appendChild(n),this._container=e,this._show=!1,this._hideTimeout;var i=this;n.onmouseenter=function(){i._enterable&&(clearTimeout(i._hideTimeout),i._show=!0),i._inContent=!0},n.onmousemove=function(t){if(t=t||window.event,!i._enterable){var n=o.handler;c.normalizeEvent(e,t,!0),n.dispatch("mousemove",t)}},n.onmouseleave=function(){i._enterable&&i._show&&i.hideLater(i._hideDelay),i._inContent=!1}}var l=n(0),s=n(32),c=n(31),u=n(15),d=n(8),f=l.each,p=d.toCamelCase,h=["","-webkit-","-moz-","-o-"];a.prototype={constructor:a,_enterable:!0,update:function(){var e=this._container,t=e.currentStyle||document.defaultView.getComputedStyle(e),n=e.style;"absolute"!==n.position&&"absolute"!==t.position&&(n.position="relative")},show:function(e){clearTimeout(this._hideTimeout);var t=this.el;t.style.cssText="position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;"+r(e)+";left:"+this._x+"px;top:"+this._y+"px;"+(e.get("extraCssText")||""),t.style.display=t.innerHTML?"block":"none",this._show=!0},setContent:function(e){this.el.innerHTML=null==e?"":e},setEnterable:function(e){this._enterable=e},getSize:function(){var e=this.el;return[e.clientWidth,e.clientHeight]},moveTo:function(e,t){var n,o=this._zr;o&&o.painter&&(n=o.painter.getViewportRootOffset())&&(e+=n.offsetLeft,t+=n.offsetTop);var i=this.el.style;i.left=e+"px",i.top=t+"px",this._x=e,this._y=t},hide:function(){this.el.style.display="none",this._show=!1},hideLater:function(e){!this._show||this._inContent&&this._enterable||(e?(this._hideDelay=e,this._show=!1,this._hideTimeout=setTimeout(l.bind(this.hide,this),e)):this.hide())},isShow:function(){return this._show}};var g=a;e.exports=g},function(e,t,n){var o=n(1),i=o.extendComponentModel({type:"tooltip",dependencies:["axisPointer"],defaultOption:{zlevel:0,z:8,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",confine:!1,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"rgba(50,50,50,0.7)",borderColor:"#333",borderRadius:4,borderWidth:0,padding:5,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#fff",fontSize:14}}});e.exports=i},function(e,t,n){function o(e){for(var t=e.pop();e.length;){var n=e.pop();n&&(n instanceof x&&(n=n.get("tooltip",!0)),"string"==typeof n&&(n={formatter:n}),t=new x(n,t,t.ecModel))}return t}function i(e,t){return e.dispatchAction||d.bind(t.dispatchAction,t)}function r(e,t,n,o,i,r,a){var s=l(n),c=s.width,u=s.height;return null!=r&&(e+c+r>o?e-=c+r:e+=r),null!=a&&(t+u+a>i?t-=u+a:t+=a),[e,t]}function a(e,t,n,o,i){var r=l(n),a=r.width,s=r.height;return e=Math.min(e+a,o)-a,t=Math.min(t+s,i)-s,e=Math.max(e,0),t=Math.max(t,0),[e,t]}function l(e){var t=e.clientWidth,n=e.clientHeight;if(document.defaultView&&document.defaultView.getComputedStyle){var o=document.defaultView.getComputedStyle(e);o&&(t+=parseInt(o.paddingLeft,10)+parseInt(o.paddingRight,10)+parseInt(o.borderLeftWidth,10)+parseInt(o.borderRightWidth,10),n+=parseInt(o.paddingTop,10)+parseInt(o.paddingBottom,10)+parseInt(o.borderTopWidth,10)+parseInt(o.borderBottomWidth,10))}return{width:t,height:n}}function s(e,t,n){var o=n[0],i=n[1],r=0,a=0,l=t.width,s=t.height;switch(e){case"inside":r=t.x+l/2-o/2,a=t.y+s/2-i/2;break;case"top":r=t.x+l/2-o/2,a=t.y-i-5;break;case"bottom":r=t.x+l/2-o/2,a=t.y+s+5;break;case"left":r=t.x-o-5,a=t.y+s/2-i/2;break;case"right":r=t.x+l+5,a=t.y+s/2-i/2}return[r,a]}function c(e){return"center"===e||"middle"===e}var u=n(1),d=n(0),f=n(15),p=n(544),h=n(8),g=n(3),m=n(2),v=n(181),b=n(6),x=n(12),y=n(182),_=n(22),w=n(85),k=d.bind,S=d.each,M=g.parsePercent,C=new m.Rect({shape:{x:-1,y:-1,width:2,height:2}}),A=u.extendComponentView({type:"tooltip",init:function(e,t){if(!f.node){var n=new p(t.getDom(),t);this._tooltipContent=n}},render:function(e,t,n){if(!f.node){this.group.removeAll(),this._tooltipModel=e,this._ecModel=t,this._api=n,this._lastDataByCoordSys=null,this._alwaysShowContent=e.get("alwaysShowContent");var o=this._tooltipContent;o.update(),o.setEnterable(e.get("enterable")),this._initGlobalListener(),this._keepShow()}},_initGlobalListener:function(){var e=this._tooltipModel,t=e.get("triggerOn");y.register("itemTooltip",this._api,k(function(e,n,o){"none"!==t&&(t.indexOf(e)>=0?this._tryShow(n,o):"leave"===e&&this._hide(o))},this))},_keepShow:function(){var e=this._tooltipModel,t=this._ecModel,n=this._api;if(null!=this._lastX&&null!=this._lastY&&"none"!==e.get("triggerOn")){var o=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout(function(){o.manuallyShowTip(e,t,n,{x:o._lastX,y:o._lastY})})}},manuallyShowTip:function(e,t,n,o){if(o.from!==this.uid&&!f.node){var r=i(o,n);this._ticket="";var a=o.dataByCoordSys;if(o.tooltip&&null!=o.x&&null!=o.y){var l=C;l.position=[o.x,o.y],l.update(),l.tooltip=o.tooltip,this._tryShow({offsetX:o.x,offsetY:o.y,target:l},r)}else if(a)this._tryShow({offsetX:o.x,offsetY:o.y,position:o.position,event:{},dataByCoordSys:o.dataByCoordSys,tooltipOption:o.tooltipOption},r);else if(null!=o.seriesIndex){if(this._manuallyAxisShowTip(e,t,n,o))return;var s=v(o,t),c=s.point[0],u=s.point[1];null!=c&&null!=u&&this._tryShow({offsetX:c,offsetY:u,position:o.position,target:s.el,event:{}},r)}else null!=o.x&&null!=o.y&&(n.dispatchAction({type:"updateAxisPointer",x:o.x,y:o.y}),this._tryShow({offsetX:o.x,offsetY:o.y,position:o.position,target:n.getZr().findHover(o.x,o.y).target,event:{}},r))}},manuallyHideTip:function(e,t,n,o){var r=this._tooltipContent;this._alwaysShowContent||r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=null,o.from!==this.uid&&this._hide(i(o,n))},_manuallyAxisShowTip:function(e,t,n,i){var r=i.seriesIndex,a=i.dataIndex,l=t.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=a&&null!=l){var s=t.getSeriesByIndex(r);if(s){var c=s.getData(),e=o([c.getItemModel(a),s,(s.coordinateSystem||{}).model,e]);if("axis"===e.get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:a,position:i.position}),!0}}},_tryShow:function(e,t){var n=e.target;if(this._tooltipModel){this._lastX=e.offsetX,this._lastY=e.offsetY;var o=e.dataByCoordSys;o&&o.length?this._showAxisTooltip(o,e):n&&null!=n.dataIndex?(this._lastDataByCoordSys=null,this._showSeriesItemTooltip(e,n,t)):n&&n.tooltip?(this._lastDataByCoordSys=null,this._showComponentItemTooltip(e,n,t)):(this._lastDataByCoordSys=null,this._hide(t))}},_showOrMove:function(e,t){var n=e.get("showDelay");t=d.bind(t,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(t,n):t()},_showAxisTooltip:function(e,t){var n=this._ecModel,i=this._tooltipModel,r=[t.offsetX,t.offsetY],a=[],l=[],s=o([t.tooltipOption,i]);S(e,function(e){S(e.dataByAxis,function(e){var t=n.getComponent(e.axisDim+"Axis",e.axisIndex),o=e.value,i=[];if(t&&null!=o){var r=w.getValueLabel(o,t.axis,n,e.seriesDataIndices,e.valueLabelOpt);d.each(e.seriesDataIndices,function(a){var s=n.getSeriesByIndex(a.seriesIndex),c=a.dataIndexInside,u=s&&s.getDataParams(c);u.axisDim=e.axisDim,u.axisIndex=e.axisIndex,u.axisType=e.axisType,u.axisId=e.axisId,u.axisValue=_.getAxisRawValue(t.axis,o),u.axisValueLabel=r,u&&(l.push(u),i.push(s.formatTooltip(c,!0)))});var s=r;a.push((s?h.encodeHTML(s)+"
":"")+i.join("
"))}})},this),a.reverse(),a=a.join("

");var c=t.position;this._showOrMove(s,function(){this._updateContentNotChangedOnAxis(e)?this._updatePosition(s,c,r[0],r[1],this._tooltipContent,l):this._showTooltipContent(s,a,l,Math.random(),r[0],r[1],c)})},_showSeriesItemTooltip:function(e,t,n){var i=this._ecModel,r=t.seriesIndex,a=i.getSeriesByIndex(r),l=t.dataModel||a,s=t.dataIndex,c=t.dataType,u=l.getData(),d=o([u.getItemModel(s),l,a&&(a.coordinateSystem||{}).model,this._tooltipModel]),f=d.get("trigger");if(null==f||"item"===f){var p=l.getDataParams(s,c),h=l.formatTooltip(s,!1,c),g="item_"+l.name+"_"+s;this._showOrMove(d,function(){this._showTooltipContent(d,h,p,g,e.offsetX,e.offsetY,e.position,e.target)}),n({type:"showTip",dataIndexInside:s,dataIndex:u.getRawIndex(s),seriesIndex:r,from:this.uid})}},_showComponentItemTooltip:function(e,t,n){var o=t.tooltip;if("string"==typeof o){var i=o;o={content:i,formatter:i}}var r=new x(o,this._tooltipModel,this._ecModel),a=r.get("content"),l=Math.random();this._showOrMove(r,function(){this._showTooltipContent(r,a,r.get("formatterParams")||{},l,e.offsetX,e.offsetY,e.position,t)}),n({type:"showTip",from:this.uid})},_showTooltipContent:function(e,t,n,o,i,r,a,l){if(this._ticket="",e.get("showContent")&&e.get("show")){var s=this._tooltipContent,c=e.get("formatter");a=a||e.get("position");var u=t;if(c&&"string"==typeof c)u=h.formatTpl(c,n,!0);else if("function"==typeof c){var d=k(function(t,o){t===this._ticket&&(s.setContent(o),this._updatePosition(e,a,i,r,s,n,l))},this);this._ticket=o,u=c(n,o,d)}s.setContent(u),s.show(e),this._updatePosition(e,a,i,r,s,n,l)}},_updatePosition:function(e,t,n,o,i,l,u){var f=this._api.getWidth(),p=this._api.getHeight();t=t||e.get("position");var h=i.getSize(),g=e.get("align"),m=e.get("verticalAlign"),v=u&&u.getBoundingRect().clone();if(u&&v.applyTransform(u.transform),"function"==typeof t&&(t=t([n,o],l,i.el,v,{viewSize:[f,p],contentSize:h.slice()})),d.isArray(t))n=M(t[0],f),o=M(t[1],p);else if(d.isObject(t)){t.width=h[0],t.height=h[1];var x=b.getLayoutRect(t,{width:f,height:p});n=x.x,o=x.y,g=null,m=null}else if("string"==typeof t&&u){var y=s(t,v,h);n=y[0],o=y[1]}else{var y=r(n,o,i.el,f,p,g?null:20,m?null:20);n=y[0],o=y[1]}if(g&&(n-=c(g)?h[0]/2:"right"===g?h[0]:0),m&&(o-=c(m)?h[1]/2:"bottom"===m?h[1]:0),e.get("confine")){var y=a(n,o,i.el,f,p);n=y[0],o=y[1]}i.moveTo(n,o)},_updateContentNotChangedOnAxis:function(e){var t=this._lastDataByCoordSys,n=!!t&&t.length===e.length;return n&&S(t,function(t,o){var i=t.dataByAxis||{},r=e[o]||{},a=r.dataByAxis||[];(n&=i.length===a.length)&&S(i,function(e,t){var o=a[t]||{},i=e.seriesDataIndices||[],r=o.seriesDataIndices||[];(n&=e.value===o.value&&e.axisType===o.axisType&&e.axisId===o.axisId&&i.length===r.length)&&S(i,function(e,t){var o=r[t];n&=e.seriesIndex===o.seriesIndex&&e.dataIndex===o.dataIndex})})}),this._lastDataByCoordSys=e,!!n},_hide:function(e){this._lastDataByCoordSys=null,e({type:"hideTip",from:this.uid})},dispose:function(e,t){f.node||(this._tooltipContent.hide(),y.unregister("itemTooltip",t))}});e.exports=A},function(e,t,n){n(552),n(553)},function(e,t,n){function o(e,t,n){if(n[0]===n[1])return n.slice();for(var o=(n[1]-n[0])/200,i=n[0],r=[],a=0;a<=200&&it[1]&&t.reverse(),t[0]=Math.max(t[0],e[0]),t[1]=Math.min(t[1],e[1]))},completeVisualOption:function(){r.prototype.completeVisualOption.apply(this,arguments),i.each(this.stateList,function(e){var t=this.option.controller[e].symbolSize;t&&t[0]!==t[1]&&(t[0]=0)},this)},setSelected:function(e){this.option.range=e.slice(),this._resetRange()},getSelected:function(){var e=this.getExtent(),t=a.asc((this.get("range")||[]).slice());return t[0]>e[1]&&(t[0]=e[1]),t[1]>e[1]&&(t[1]=e[1]),t[0]=n[1]||e<=t[1])?"inRange":"outOfRange"},findTargetDataIndices:function(e){var t=[];return this.eachTargetSeries(function(n){var o=[],i=n.getData();i.each(this.getDataDimension(i),function(t,n){e[0]<=t&&t<=e[1]&&o.push(n)},!0,this),t.push({seriesId:n.id,dataIndex:o})},this),t},getVisualMeta:function(e){function t(t,n){r.push({value:t,color:e(t,n)})}for(var n=o(this,"outOfRange",this.getExtent()),i=o(this,"inRange",this.option.range.slice()),r=[],a=0,l=0,s=i.length,c=n.length;le[1])break;n.push({color:this.getControllerVisual(r,"color",t),offset:i/100})}return n.push({color:this.getControllerVisual(e[1],"color",t),offset:1}),n},_createBarPoints:function(e,t){var n=this.visualMapModel.itemSize;return[[n[0]-t[0],e[0]],[n[0],e[0]],[n[0],e[1]],[n[0]-t[1],e[1]]]},_createBarGroup:function(e){var t=this._orient,n=this.visualMapModel.get("inverse");return new p.Group("horizontal"!==t||n?"horizontal"===t&&n?{scale:"bottom"===e?[-1,1]:[1,1],rotation:-Math.PI/2}:"vertical"!==t||n?{scale:"left"===e?[1,1]:[-1,1]}:{scale:"left"===e?[1,-1]:[-1,-1]}:{scale:"bottom"===e?[1,1]:[-1,1],rotation:Math.PI/2})},_updateHandle:function(e,t){if(this._useHandle){var n=this._shapes,o=this.visualMapModel,i=n.handleThumbs,r=n.handleLabels;x([0,1],function(a){var l=i[a];l.setStyle("fill",t.handlesColor[a]),l.position[1]=e[a];var s=p.applyTransform(n.handleLabelPoints[a],p.getTransform(l,this.group));r[a].setStyle({x:s[0],y:s[1],text:o.formatValueText(this._dataInterval[a]),textVerticalAlign:"middle",textAlign:this._applyTransform("horizontal"===this._orient?0===a?"bottom":"top":"left",n.barGroup)})},this)}},_showIndicator:function(e,t,n,o){var i=this.visualMapModel,a=i.getExtent(),l=i.itemSize,s=[0,l[1]],c=b(e,a,s,!0),u=this._shapes,d=u.indicator;if(d){d.position[1]=c,d.attr("invisible",!1),d.setShape("points",r(!!n,o,c,l[1]));var f={convertOpacityToAlpha:!0},h=this.getControllerVisual(e,"color",f);d.setStyle("fill",h);var g=p.applyTransform(u.indicatorLabelPoint,p.getTransform(d,this.group)),m=u.indicatorLabel;m.attr("invisible",!1);var v=this._applyTransform("left",u.barGroup),x=this._orient;m.setStyle({text:(n||"")+i.formatValueText(t),textVerticalAlign:"horizontal"===x?v:"middle",textAlign:"horizontal"===x?"center":v,x:g[0],y:g[1]})}},_enableHoverLinkToSeries:function(){var e=this;this._shapes.barGroup.on("mousemove",function(t){if(e._hovering=!0,!e._dragging){var n=e.visualMapModel.itemSize,o=e._applyTransform([t.offsetX,t.offsetY],e._shapes.barGroup,!0,!0);o[1]=y(_(0,o[1]),n[1]),e._doHoverLinkToSeries(o[1],0<=o[0]&&o[0]<=n[0])}}).on("mouseout",function(){e._hovering=!1,!e._dragging&&e._clearHoverLinkToSeries()})},_enableHoverLinkFromSeries:function(){var e=this.api.getZr();this.visualMapModel.option.hoverLink?(e.on("mouseover",this._hoverLinkFromSeriesMouseOver,this),e.on("mouseout",this._hideIndicator,this)):this._clearHoverLinkFromSeries()},_doHoverLinkToSeries:function(e,t){var n=this.visualMapModel,o=n.itemSize;if(n.option.hoverLink){var i=[0,o[1]],r=n.getExtent();e=y(_(i[0],e),i[1]);var s=a(n,r,i),c=[e-s,e+s],u=b(e,i,r,!0),d=[b(c[0],i,r,!0),b(c[1],i,r,!0)];c[0]i[1]&&(d[1]=1/0),t&&(d[0]===-1/0?this._showIndicator(u,d[1],"< ",s):d[1]===1/0?this._showIndicator(u,d[0],"> ",s):this._showIndicator(u,u,"≈ ",s));var f=this._hoverLinkDataIndices,p=[];(t||l(n))&&(p=this._hoverLinkDataIndices=n.findTargetDataIndices(d));var h=v.compressBatches(f,p);this._dispatchHighDown("downplay",m.convertDataIndex(h[0])),this._dispatchHighDown("highlight",m.convertDataIndex(h[1]))}},_hoverLinkFromSeriesMouseOver:function(e){var t=e.target,n=this.visualMapModel;if(t&&null!=t.dataIndex){var o=this.ecModel.getSeriesByIndex(t.seriesIndex);if(n.isTargetSeries(o)){var i=o.getData(t.dataType),r=i.getDimension(n.getDataDimension(i)),a=i.get(r,t.dataIndex,!0);isNaN(a)||this._showIndicator(a,a)}}},_hideIndicator:function(){var e=this._shapes;e.indicator&&e.indicator.attr("invisible",!0),e.indicatorLabel&&e.indicatorLabel.attr("invisible",!0)},_clearHoverLinkToSeries:function(){this._hideIndicator();var e=this._hoverLinkDataIndices;this._dispatchHighDown("downplay",m.convertDataIndex(e)),e.length=0},_clearHoverLinkFromSeries:function(){this._hideIndicator();var e=this.api.getZr();e.off("mouseover",this._hoverLinkFromSeriesMouseOver),e.off("mouseout",this._hideIndicator)},_applyTransform:function(e,t,n,o){var i=p.getTransform(t,o?null:this.group);return p[c.isArray(e)?"applyTransform":"transformDirection"](e,i,n)},_dispatchHighDown:function(e,t){t&&t.length&&this.api.dispatchAction({type:e,batch:t})},dispose:function(){this._clearHoverLinkFromSeries(),this._clearHoverLinkToSeries()},remove:function(){this._clearHoverLinkFromSeries(),this._clearHoverLinkToSeries()}}),M=S;e.exports=M},function(e,t,n){function o(e,t){var n=e.inverse;("vertical"===e.orient?!n:n)&&t.reverse()}var i=n(4),r=(i.__DEV__,n(0)),a=n(199),l=n(45),s=n(218),c=n(3),u=c.reformIntervals,d=a.extend({type:"visualMap.piecewise",defaultOption:{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieceList:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0,showLabel:null},optionUpdated:function(e,t){d.superApply(this,"optionUpdated",arguments),this._pieceList=[],this.resetExtent();var n=this._mode=this._determineMode();f[this._mode].call(this),this._resetSelected(e,t);var o=this.option.categories;this.resetVisual(function(e,t){"categories"===n?(e.mappingMethod="category",e.categories=r.clone(o)):(e.dataExtent=this.getExtent(),e.mappingMethod="piecewise",e.pieceList=r.map(this._pieceList,function(e){var e=r.clone(e);return"inRange"!==t&&(e.visual=null),e}))})},completeVisualOption:function(){function e(e,t,n){return e&&e[t]&&(r.isObject(e[t])?e[t].hasOwnProperty(n):e[t]===n)}var t=this.option,n={},o=l.listVisualTypes(),i=this.isCategory();r.each(t.pieces,function(e){r.each(o,function(t){e.hasOwnProperty(t)&&(n[t]=1)})}),r.each(n,function(n,o){var a=0;r.each(this.stateList,function(n){a|=e(t,n,o)||e(t.target,n,o)},this),!a&&r.each(this.stateList,function(e){(t[e]||(t[e]={}))[o]=s.get(o,"inRange"===e?"active":"inactive",i)})},this),a.prototype.completeVisualOption.apply(this,arguments)},_resetSelected:function(e,t){var n=this.option,o=this._pieceList,i=(t?n:e).selected||{};if(n.selected=i,r.each(o,function(e,t){var n=this.getSelectedMapKey(e);i.hasOwnProperty(n)||(i[n]=!0)},this),"single"===n.selectedMode){var a=!1;r.each(o,function(e,t){var n=this.getSelectedMapKey(e);i[n]&&(a?i[n]=!1:a=!0)},this)}},getSelectedMapKey:function(e){return"categories"===this._mode?e.value+"":e.index+""},getPieceList:function(){return this._pieceList},_determineMode:function(){var e=this.option;return e.pieces&&e.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},setSelected:function(e){this.option.selected=r.clone(e)},getValueState:function(e){var t=l.findPieceIndex(e,this._pieceList);return null!=t&&this.option.selected[this.getSelectedMapKey(this._pieceList[t])]?"inRange":"outOfRange"},findTargetDataIndices:function(e){var t=[];return this.eachTargetSeries(function(n){var o=[],i=n.getData();i.each(this.getDataDimension(i),function(t,n){l.findPieceIndex(t,this._pieceList)===e&&o.push(n)},!0,this),t.push({seriesId:n.id,dataIndex:o})},this),t},getRepresentValue:function(e){var t;if(this.isCategory())t=e.value;else if(null!=e.value)t=e.value;else{var n=e.interval||[];t=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return t},getVisualMeta:function(e){function t(t,r){var a=i.getRepresentValue({interval:t});r||(r=i.getValueState(a));var l=e(a,r);t[0]===-1/0?o[0]=l:t[1]===1/0?o[1]=l:n.push({value:t[0],color:l},{value:t[1],color:l})}if(!this.isCategory()){var n=[],o=[],i=this,a=this._pieceList.slice();if(a.length){var l=a[0].interval[0];l!==-1/0&&a.unshift({interval:[-1/0,l]}),(l=a[a.length-1].interval[1])!==1/0&&a.push({interval:[l,1/0]})}else a.push({interval:[-1/0,1/0]});var s=-1/0;return r.each(a,function(e){var n=e.interval;n&&(n[0]>s&&t([s,n[0]],"outOfRange"),t(n.slice()),s=n[1])},this),{stops:n,outerColors:o}}}}),f={splitNumber:function(){var e=this.option,t=this._pieceList,n=Math.min(e.precision,20),o=this.getExtent(),i=e.splitNumber;i=Math.max(parseInt(i,10),1),e.splitNumber=i;for(var a=(o[1]-o[0])/i;+a.toFixed(n)!==a&&n<5;)n++;e.precision=n,a=+a.toFixed(n);var l=0;e.minOpen&&t.push({index:l++,interval:[-1/0,o[0]],close:[0,0]});for(var s=o[0],c=l+i;l","≥"][t[0]]];e.text=e.text||this.formatValueText(null!=e.value?e.value:e.interval,!1,n)},this)}},p=d;e.exports=p},function(e,t,n){var o=n(0),i=n(200),r=n(2),a=n(23),l=a.createSymbol,s=n(6),c=n(201),u=i.extend({type:"visualMap.piecewise",doRender:function(){function e(e){var a=e.piece,s=new r.Group;s.onclick=o.bind(this._onItemClick,this,a),this._enableHoverLink(s,e.indexInModelPieceList);var f=n.getRepresentValue(a);if(this._createItemSymbol(s,f,[0,0,d[0],d[1]]),h){var p=this.visualMapModel.getValueState(f);s.add(new r.Text({style:{x:"right"===u?-i:d[0]+i,y:d[1]/2,text:a.text,textVerticalAlign:"middle",textAlign:u,textFont:l,textFill:c,opacity:"outOfRange"===p?.5:1}}))}t.add(s)}var t=this.group;t.removeAll();var n=this.visualMapModel,i=n.get("textGap"),a=n.textStyleModel,l=a.getFont(),c=a.getTextColor(),u=this._getItemAlign(),d=n.itemSize,f=this._getViewData(),p=f.endsText,h=o.retrieve(n.get("showLabel",!0),!p);p&&this._renderEndsText(t,p[0],d,h,u),o.each(f.viewPieceList,e,this),p&&this._renderEndsText(t,p[1],d,h,u),s.box(n.get("orient"),t,n.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},_enableHoverLink:function(e,t){function n(e){var n=this.visualMapModel;n.option.hoverLink&&this.api.dispatchAction({type:e,batch:c.convertDataIndex(n.findTargetDataIndices(t))})}e.on("mouseover",o.bind(n,this,"highlight")).on("mouseout",o.bind(n,this,"downplay"))},_getItemAlign:function(){var e=this.visualMapModel,t=e.option;if("vertical"===t.orient)return c.getItemAlign(e,this.api,e.itemSize);var n=t.align;return n&&"auto"!==n||(n="left"),n},_renderEndsText:function(e,t,n,o,i){if(t){var a=new r.Group,l=this.visualMapModel.textStyleModel;a.add(new r.Text({style:{x:o?"right"===i?n[0]:0:n[0]/2,y:n[1]/2,textVerticalAlign:"middle",textAlign:o?i:"center",text:t,textFont:l.getFont(),textFill:l.getTextColor()}})),e.add(a)}},_getViewData:function(){var e=this.visualMapModel,t=o.map(e.getPieceList(),function(e,t){return{piece:e,indexInModelPieceList:t}}),n=e.get("text"),i=e.get("orient"),r=e.get("inverse");return("horizontal"===i?r:!r)?t.reverse():n&&(n=n.slice().reverse()),{viewPieceList:t,endsText:n}},_createItemSymbol:function(e,t,n){e.add(l(this.getControllerVisual(t,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(t,"color")))},_onItemClick:function(e){var t=this.visualMapModel,n=t.option,i=o.clone(n.selected),r=t.getSelectedMapKey(e);"single"===n.selectedMode?(i[r]=!0,o.each(i,function(e,t){i[t]=t===r})):i[r]=!i[r],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:i})}}),d=u;e.exports=d},function(e,t,n){var o=n(1),i=n(202);n(203),n(204),n(548),n(549),n(205),o.registerPreprocessor(i)},function(e,t,n){var o=n(1),i=n(202);n(203),n(204),n(550),n(551),n(205),o.registerPreprocessor(i)},function(e,t,n){function o(e,t,n){this._model=e}function i(e,t,n,o){var i=n.calendarModel,r=n.seriesModel,a=i?i.coordinateSystem:r?r.coordinateSystem:null;return a===this?a[e](o):null}var r=n(0),a=n(6),l=n(3),s=n(26);o.prototype={constructor:o,type:"calendar",dimensions:["time","value"],getDimensionsInfo:function(){return[{name:"time",type:"time"}]},getRangeInfo:function(){return this._rangeInfo},getModel:function(){return this._model},getRect:function(){return this._rect},getCellWidth:function(){return this._sw},getCellHeight:function(){return this._sh},getOrient:function(){return this._orient},getFirstDayOfWeek:function(){return this._firstDayOfWeek},getDateInfo:function(e){e=l.parseDate(e);var t=e.getFullYear(),n=e.getMonth()+1;n=n<10?"0"+n:n;var o=e.getDate();o=o<10?"0"+o:o;var i=e.getDay();return i=Math.abs((i+7-this.getFirstDayOfWeek())%7),{y:t,m:n,d:o,day:i,time:e.getTime(),formatedDate:t+"-"+n+"-"+o,date:e}},getNextNDay:function(e,t){return 0===(t=t||0)?this.getDateInfo(e):(e=new Date(this.getDateInfo(e).time),e.setDate(e.getDate()+t),this.getDateInfo(e))},update:function(e,t){function n(e,t){return null!=e[t]&&"auto"!==e[t]}this._firstDayOfWeek=+this._model.getModel("dayLabel").get("firstDay"),this._orient=this._model.get("orient"),this._lineWidth=this._model.getModel("itemStyle.normal").getItemStyle().lineWidth||0,this._rangeInfo=this._getRangeInfo(this._initRangeOption());var o=this._rangeInfo.weeks||1,i=["width","height"],l=this._model.get("cellSize").slice(),s=this._model.getBoxLayoutParams(),c="horizontal"===this._orient?[o,7]:[7,o];r.each([0,1],function(e){n(l,e)&&(s[i[e]]=l[e]*c[e])});var u={width:t.getWidth(),height:t.getHeight()},d=this._rect=a.getLayoutRect(s,u);r.each([0,1],function(e){n(l,e)||(l[e]=d[i[e]]/c[e])}),this._sw=l[0],this._sh=l[1]},dataToPoint:function(e,t){r.isArray(e)&&(e=e[0]),null==t&&(t=!0);var n=this.getDateInfo(e),o=this._rangeInfo,i=n.formatedDate;if(t&&!(n.time>=o.start.time&&n.time<=o.end.time))return[NaN,NaN];var a=n.day,l=this._getRangeInfo([o.start.time,i]).nthWeek;return"vertical"===this._orient?[this._rect.x+a*this._sw+this._sw/2,this._rect.y+l*this._sh+this._sh/2]:[this._rect.x+l*this._sw+this._sw/2,this._rect.y+a*this._sh+this._sh/2]},pointToData:function(e){var t=this.pointToDate(e);return t&&t.time},dataToRect:function(e,t){var n=this.dataToPoint(e,t);return{contentShape:{x:n[0]-(this._sw-this._lineWidth)/2,y:n[1]-(this._sh-this._lineWidth)/2,width:this._sw-this._lineWidth,height:this._sh-this._lineWidth},center:n,tl:[n[0]-this._sw/2,n[1]-this._sh/2],tr:[n[0]+this._sw/2,n[1]-this._sh/2],br:[n[0]+this._sw/2,n[1]+this._sh/2],bl:[n[0]-this._sw/2,n[1]+this._sh/2]}},pointToDate:function(e){var t=Math.floor((e[0]-this._rect.x)/this._sw)+1,n=Math.floor((e[1]-this._rect.y)/this._sh)+1,o=this._rangeInfo.range;return"vertical"===this._orient?this._getDateByWeeksAndDay(n,t-1,o):this._getDateByWeeksAndDay(t,n-1,o)},convertToPixel:r.curry(i,"dataToPoint"),convertFromPixel:r.curry(i,"pointToData"),_initRangeOption:function(){var e=this._model.get("range"),t=e;if(r.isArray(t)&&1===t.length&&(t=t[0]),/^\d{4}$/.test(t)&&(e=[t+"-01-01",t+"-12-31"]),/^\d{4}[\/|-]\d{1,2}$/.test(t)){var n=this.getDateInfo(t),o=n.date;o.setMonth(o.getMonth()+1);var i=this.getNextNDay(o,-1);e=[n.formatedDate,i.formatedDate]}/^\d{4}[\/|-]\d{1,2}[\/|-]\d{1,2}$/.test(t)&&(e=[t,t]);var a=this._getRangeInfo(e);return a.start.time>a.end.time&&e.reverse(),e},_getRangeInfo:function(e){e=[this.getDateInfo(e[0]),this.getDateInfo(e[1])];var t;e[0].time>e[1].time&&(t=!0,e.reverse());var n=Math.floor(e[1].time/864e5)-Math.floor(e[0].time/864e5)+1,o=new Date(e[0].time),i=o.getDate(),r=e[1].date.getDate();if(o.setDate(i+n-1),o.getDate()!==r)for(var a=o.getTime()-e[1].time>0?1:-1;o.getDate()!==r&&(o.getTime()-e[1].time)*a>0;)n-=a,o.setDate(i+n-1);var l=Math.floor((n+e[0].day+6)/7),s=t?1-l:l-1;return t&&e.reverse(),{range:[e[0].formatedDate,e[1].formatedDate],start:e[0],end:e[1],allDay:n,weeks:l,nthWeek:s,fweek:e[0].day,lweek:e[1].day}},_getDateByWeeksAndDay:function(e,t,n){var o=this._getRangeInfo(n);if(e>o.weeks||0===e&&to.lweek)return!1;var i=7*(e-1)-o.fweek+t,r=new Date(o.start.time);return r.setDate(o.start.d+i),this.getDateInfo(r)}},o.dimensions=o.prototype.dimensions,o.getDimensionsInfo=o.prototype.getDimensionsInfo,o.create=function(e,t){var n=[];return e.eachComponent("calendar",function(i){var r=new o(i,e,t);n.push(r),i.coordinateSystem=r}),e.eachSeries(function(e){"calendar"===e.get("coordinateSystem")&&(e.coordinateSystem=n[e.get("calendarIndex")||0])}),n},s.register("calendar",o);var c=o;e.exports=c},function(e,t,n){function o(e,t){var n=e.cellSize;i.isArray(n)?1===n.length&&(n[1]=n[0]):n=e.cellSize=[n,n];var o=i.map([0,1],function(e){return s(t,e)&&(n[e]="auto"),null!=n[e]&&"auto"!==n[e]});c(e,t,{type:"box",ignoreSize:o})}var i=n(0),r=n(14),a=n(6),l=a.getLayoutParams,s=a.sizeCalculable,c=a.mergeLayoutParam,u=r.extend({type:"calendar",coordinateSystem:null,defaultOption:{zlevel:0,z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{normal:{color:"#fff",borderWidth:1,borderColor:"#ccc"}},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",nameMap:"en",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",nameMap:"en",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},init:function(e,t,n,i){var r=l(e);u.superApply(this,"init",arguments),o(e,r)},mergeOption:function(e,t){u.superApply(this,"mergeOption",arguments),o(this.option,e)}}),d=u;e.exports=d},function(e,t,n){function o(e){var t=e.getRect(),n=e.getRangeInfo();return{coordSys:{type:"calendar",x:t.x,y:t.y,width:t.width,height:t.height,cellWidth:e.getCellWidth(),cellHeight:e.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:i.bind(e.dataToPoint,e)}}}var i=n(0);e.exports=o},function(e,t,n){var o=n(0),i=n(36),r=function(e,t,n,o,r){i.call(this,e,t,n),this.type=o||"value",this.position=r||"bottom"};r.prototype={constructor:r,index:0,onZero:!1,model:null,isHorizontal:function(){var e=this.position;return"top"===e||"bottom"===e},getGlobalExtent:function(e){var t=this.getExtent();return t[0]=this.toGlobalCoord(t[0]),t[1]=this.toGlobalCoord(t[1]),e&&t[0]>t[1]&&t.reverse(),t},getOtherAxis:function(){this.grid.getOtherAxis()},isLabelIgnored:function(e){if("category"===this.type){var t=this.getLabelInterval();return"function"==typeof t&&!t(e,this.scale.getLabel(e))||e%(t+1)}},pointToData:function(e,t){return this.coordToData(this.toLocalCoord(e["x"===this.dim?0:1]),t)},toLocalCoord:null,toGlobalCoord:null},o.inherits(r,i);var a=r;e.exports=a},function(e,t,n){function o(e){return this._axes[e]}var i=n(0),r=function(e){this._axes={},this._dimList=[],this.name=e||""};r.prototype={constructor:r,type:"cartesian",getAxis:function(e){return this._axes[e]},getAxes:function(){return i.map(this._dimList,o,this)},getAxesByScale:function(e){return e=e.toLowerCase(),i.filter(this.getAxes(),function(t){return t.scale.type===e})},addAxis:function(e){var t=e.dim;this._axes[t]=e,this._dimList.push(t)},dataToCoord:function(e){return this._dataCoordConvert(e,"dataToCoord")},coordToData:function(e){return this._dataCoordConvert(e,"coordToData")},_dataCoordConvert:function(e,t){for(var n=this._dimList,o=e instanceof Array?[]:{},i=0;i=0;n--)s.asc(t[n])},getActiveState:function(e){var t=this.activeIntervals;if(!t.length)return"normal";if(null==e)return"inactive";for(var n=0,o=t.length;n=n&&r<=n+t.axisLength&&a>=o&&a<=o+t.layoutLength},getModel:function(){return this._model},_updateAxesFromSeries:function(e,t){t.eachSeries(function(n){if(e.contains(n,t)){var o=n.getData();g(this.dimensions,function(e){var t=this._axesMap.get(e);t.scale.unionExtentFromData(o,e),u.niceScaleExtent(t.scale,t.model)},this)}},this)},resize:function(e,t){this._rect=c.getLayoutRect(e.getBoxLayoutParams(),{width:t.getWidth(),height:t.getHeight()}),this._layoutAxes()},getRect:function(){return this._rect},_makeLayoutInfo:function(){var e,t=this._model,n=this._rect,o=["x","y"],r=["width","height"],a=t.get("layout"),l="horizontal"===a?0:1,s=n[r[l]],c=[0,s],u=this.dimensions.length,d=i(t.get("axisExpandWidth"),c),f=i(t.get("axisExpandCount")||0,[0,u]),p=t.get("axisExpandable")&&u>3&&u>f&&f>1&&d>0&&s>0,h=t.get("axisExpandWindow");h?(e=i(h[1]-h[0],c),h[1]=h[0]+e):(e=i(d*(f-1),c),h=[d*(t.get("axisExpandCenter")||b(u/2))-e/2],h[1]=h[0]+e);var g=(s-e)/(u-f);g<3&&(g=0);var m=[b(y(h[0]/d,1))+1,x(y(h[1]/d,1))-1],v=g/d*h[0];return{layout:a,pixelDimIndex:l,layoutBase:n[o[l]],layoutLength:s,axisBase:n[o[1-l]],axisLength:n[r[1-l]],axisExpandable:p,axisExpandWidth:d,axisCollapseWidth:g,axisExpandWindow:h,axisCount:u,winInnerIndices:m,axisExpandWindow0Pos:v}},_layoutAxes:function(){var e=this._rect,t=this._axesMap,n=this.dimensions,o=this._makeLayoutInfo(),i=o.layout;t.each(function(e){var t=[0,o.axisLength],n=e.inverse?1:0;e.setExtent(t[n],t[1-n])}),g(n,function(n,l){var c=(o.axisExpandable?a:r)(l,o),u={horizontal:{x:c.position,y:o.axisLength},vertical:{x:0,y:c.position}},d={horizontal:_/2,vertical:0},f=[u[i].x+e.x,u[i].y+e.y],p=d[i],h=s.create();s.rotate(h,h,p),s.translate(h,h,f),this._axesLayout[n]={position:f,rotation:p,transform:h,axisNameAvailableWidth:c.axisNameAvailableWidth,axisLabelShow:c.axisLabelShow,nameTruncateMaxWidth:c.nameTruncateMaxWidth,tickDirection:1,labelDirection:1,labelInterval:t.get(n).getLabelInterval()}},this)},getAxis:function(e){return this._axesMap.get(e)},dataToPoint:function(e,t){return this.axisCoordToPoint(this._axesMap.get(t).dataToCoord(e),t)},eachActiveState:function(e,t,n){for(var o=this.dimensions,i=this._axesMap,r=this.hasAxisBrushed(),a=0,l=e.count();ai*(1-u[0])?(s="jump",a=l-i*(1-u[2])):(a=l-i*u[1])>=0&&(a=l-i*(1-u[1]))<=0&&(a=0),a*=t.axisExpandWidth/c,a?h(a,o,r,"all"):s="none";else{var i=o[1]-o[0];o=[v(0,r[1]*l/i-i/2)],o[1]=m(r[1],o[0]+i),o[0]=o[1]-i}return{axisExpandWindow:o,behavior:s}}};var w=o;e.exports=w},function(e,t,n){var o=n(0),i=n(36),r=function(e,t,n,o,r){i.call(this,e,t,n),this.type=o||"value",this.axisIndex=r};r.prototype={constructor:r,model:null,isHorizontal:function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")}},o.inherits(r,i);var a=r;e.exports=a},function(e,t,n){var o=n(0),i=n(14);n(569);var r=i.extend({type:"parallel",dependencies:["parallelAxis"],coordinateSystem:null,dimensions:null,parallelAxisIndex:null,layoutMode:"box",defaultOption:{zlevel:0,z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},init:function(){i.prototype.init.apply(this,arguments),this.mergeOption({})},mergeOption:function(e){var t=this.option;e&&o.merge(t,e,!0),this._initDimensions()},contains:function(e,t){var n=e.get("parallelIndex");return null!=n&&t.getComponent("parallel",n)===this},setAxisExpand:function(e){o.each(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],function(t){e.hasOwnProperty(t)&&(this.option[t]=e[t])},this)},_initDimensions:function(){var e=this.dimensions=[],t=this.parallelAxisIndex=[],n=o.filter(this.dependentModels.parallelAxis,function(e){return(e.get("parallelIndex")||0)===this.componentIndex},this);o.each(n,function(n){e.push("dim"+n.get("dim")),t.push(n.componentIndex)})}});e.exports=r},function(e,t,n){function o(e){i(e),r(e)}function i(e){if(!e.parallel){var t=!1;a.each(e.series,function(e){e&&"parallel"===e.type&&(t=!0)}),t&&(e.parallel=[{}])}}function r(e){var t=l.normalizeToArray(e.parallelAxis);a.each(t,function(t){if(a.isObject(t)){var n=t.parallelIndex||0,o=l.normalizeToArray(e.parallel)[n];o&&o.parallelAxisDefault&&a.merge(t,o.parallelAxisDefault,!1)}})}var a=n(0),l=n(5);e.exports=o},function(e,t,n){function o(e,t){t=t||[0,360],r.call(this,"angle",e,t),this.type="category"}var i=n(0),r=n(36);o.prototype={constructor:o,pointToData:function(e,t){return this.polar.pointToData(e,t)["radius"===this.dim?0:1]},dataToAngle:r.prototype.dataToCoord,angleToData:r.prototype.coordToData},i.inherits(o,r);var a=o;e.exports=a},function(e,t,n){function o(e,t){return t.type||(t.data?"category":"value")}var i=n(0),r=n(14),a=n(87),l=n(55),s=r.extend({type:"polarAxis",axis:null,getCoordSysModel:function(){return this.ecModel.queryComponents({mainType:"polar",index:this.option.polarIndex,id:this.option.polarId})[0]}});i.merge(s.prototype,l);var c={angle:{startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:!1}},radius:{splitNumber:5}};a("angle",s,o,c.angle),a("radius",s,o,c.radius)},function(e,t,n){var o=n(578),i=n(574),r=function(e){this.name=e||"",this.cx=0,this.cy=0,this._radiusAxis=new o,this._angleAxis=new i,this._radiusAxis.polar=this._angleAxis.polar=this};r.prototype={type:"polar",axisPointerEnabled:!0,constructor:r,dimensions:["radius","angle"],model:null,containPoint:function(e){var t=this.pointToCoord(e);return this._radiusAxis.contain(t[0])&&this._angleAxis.contain(t[1])},containData:function(e){return this._radiusAxis.containData(e[0])&&this._angleAxis.containData(e[1])},getAxis:function(e){return this["_"+e+"Axis"]},getAxes:function(){return[this._radiusAxis,this._angleAxis]},getAxesByScale:function(e){var t=[],n=this._angleAxis,o=this._radiusAxis;return n.scale.type===e&&t.push(n),o.scale.type===e&&t.push(o),t},getAngleAxis:function(){return this._angleAxis},getRadiusAxis:function(){return this._radiusAxis},getOtherAxis:function(e){var t=this._angleAxis;return e===t?this._radiusAxis:t},getBaseAxis:function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},getTooltipAxes:function(e){var t=null!=e&&"auto"!==e?this.getAxis(e):this.getBaseAxis();return{baseAxes:[t],otherAxes:[this.getOtherAxis(t)]}},dataToPoint:function(e,t){return this.coordToPoint([this._radiusAxis.dataToRadius(e[0],t),this._angleAxis.dataToAngle(e[1],t)])},pointToData:function(e,t){var n=this.pointToCoord(e);return[this._radiusAxis.radiusToData(n[0],t),this._angleAxis.angleToData(n[1],t)]},pointToCoord:function(e){var t=e[0]-this.cx,n=e[1]-this.cy,o=this.getAngleAxis(),i=o.getExtent(),r=Math.min(i[0],i[1]),a=Math.max(i[0],i[1]);o.inverse?r=a-360:a=r+360;var l=Math.sqrt(t*t+n*n);t/=l,n/=l;for(var s=Math.atan2(-n,t)/Math.PI*180,c=sa;)s+=360*c;return[l,s]},coordToPoint:function(e){var t=e[0],n=e[1]/180*Math.PI;return[Math.cos(n)*t+this.cx,-Math.sin(n)*t+this.cy]}};var a=r;e.exports=a},function(e,t,n){var o=n(1);n(575);var i=o.extendComponentModel({type:"polar",dependencies:["polarAxis","angleAxis"],coordinateSystem:null,findAxisModel:function(e){var t;return this.ecModel.eachComponent(e,function(e){e.getCoordSysModel()===this&&(t=e)},this),t},defaultOption:{zlevel:0,z:0,center:["50%","50%"],radius:"80%"}});e.exports=i},function(e,t,n){function o(e,t){r.call(this,"radius",e,t),this.type="category"}var i=n(0),r=n(36);o.prototype={constructor:o,pointToData:function(e,t){return this.polar.pointToData(e,t)["radius"===this.dim?0:1]},dataToRadius:r.prototype.dataToCoord,radiusToData:r.prototype.coordToData},i.inherits(o,r);var a=o;e.exports=a},function(e,t,n){function o(e,t){return r.map(["Radius","Angle"],function(n,o){var i=this["get"+n+"Axis"](),r=t[o],a=e[o]/2,l="dataTo"+n,s="category"===i.type?i.getBandWidth():Math.abs(i[l](r-a)-i[l](r+a));return"Angle"===n&&(s=s*Math.PI/180),s},this)}function i(e){var t=e.getRadiusAxis(),n=e.getAngleAxis(),i=t.getExtent();return i[0]>i[1]&&i.reverse(),{coordSys:{type:"polar",cx:e.cx,cy:e.cy,r:i[1],r0:i[0]},api:{coord:r.bind(function(o){var i=t.dataToRadius(o[0]),r=n.dataToAngle(o[1]),a=e.coordToPoint([i,r]);return a.push(i,r*Math.PI/180),a}),size:r.bind(o,e)}}}var r=n(0);e.exports=i},function(e,t,n){function o(e,t,n){r.call(this,e,t,n),this.type="value",this.angle=0,this.name="",this.model}var i=n(0),r=n(36);i.inherits(o,r);var a=o;e.exports=a},function(e,t,n){function o(e,t,n){this._model=e,this.dimensions=[],this._indicatorAxes=i.map(e.getIndicatorModels(),function(e,t){var n="indicator_"+t,o=new r(n,new a);return o.name=e.get("name"),o.model=e,e.axis=o,this.dimensions.push(n),o},this),this.resize(e,n),this.cx,this.cy,this.r,this.startAngle}var i=n(0),r=n(580),a=n(90),l=n(3),s=n(22),c=s.getScaleExtent,u=s.niceScaleExtent,d=n(26);o.prototype.getIndicatorAxes=function(){return this._indicatorAxes},o.prototype.dataToPoint=function(e,t){var n=this._indicatorAxes[t];return this.coordToPoint(n.dataToCoord(e),t)},o.prototype.coordToPoint=function(e,t){var n=this._indicatorAxes[t],o=n.angle;return[this.cx+e*Math.cos(o),this.cy-e*Math.sin(o)]},o.prototype.pointToData=function(e){var t=e[0]-this.cx,n=e[1]-this.cy,o=Math.sqrt(t*t+n*n);t/=o,n/=o;for(var i,r=Math.atan2(-n,t),a=1/0,l=-1,s=0;so[0]&&isFinite(h)&&isFinite(o[0]))}else{var g=r.getTicks().length-1;g>a&&(f=n(f));var m=Math.round((o[0]+o[1])/2/f)*f,v=Math.round(a/2);r.setExtent(l.round(m-v*f),l.round(m+(a-v)*f)),r.setInterval(f)}})},o.dimensions=[],o.create=function(e,t){var n=[];return e.eachComponent("radar",function(i){var r=new o(i,e,t);n.push(r),i.coordinateSystem=r}),e.eachSeriesByType("radar",function(e){"radar"===e.get("coordinateSystem")&&(e.coordinateSystem=n[e.get("radarIndex")||0])}),n},d.register("radar",o);var f=o;e.exports=f},function(e,t,n){function o(e,t){return r.defaults({show:t},e)}var i=n(1),r=n(0),a=n(207),l=n(12),s=n(55),c=a.valueAxis,u=i.extendComponentModel({type:"radar",optionUpdated:function(){var e=this.get("boundaryGap"),t=this.get("splitNumber"),n=this.get("scale"),o=this.get("axisLine"),i=this.get("axisTick"),a=this.get("axisLabel"),c=this.get("name"),u=this.get("name.show"),d=this.get("name.formatter"),f=this.get("nameGap"),p=this.get("triggerEvent"),h=r.map(this.get("indicator")||[],function(h){null!=h.max&&h.max>0&&!h.min?h.min=0:null!=h.min&&h.min<0&&!h.max&&(h.max=0);var g=c;if(null!=h.color&&(g=r.defaults({color:h.color},c)),h=r.merge(r.clone(h),{boundaryGap:e,splitNumber:t,scale:n,axisLine:o,axisTick:i,axisLabel:a,name:h.text,nameLocation:"end",nameGap:f,nameTextStyle:g,triggerEvent:p},!1),u||(h.name=""),"string"==typeof d){var m=h.name;h.name=d.replace("{value}",null!=m?m:"")}else"function"==typeof d&&(h.name=d(h.name,h));var v=r.extend(new l(h,null,this.ecModel),s);return v.mainType="radar",v.componentIndex=this.componentIndex,v},this);this.getIndicatorModels=function(){return h}},defaultOption:{zlevel:0,z:0,center:["50%","50%"],radius:"75%",startAngle:90,name:{show:!0},boundaryGap:[0,0],splitNumber:5,nameGap:15,scale:!1,shape:"polygon",axisLine:r.merge({lineStyle:{color:"#bbb"}},c.axisLine),axisLabel:o(c.axisLabel,!1),axisTick:o(c.axisTick,!1),splitLine:o(c.splitLine,!0),splitArea:o(c.splitArea,!0),indicator:[]}}),d=u;e.exports=d},function(e,t,n){function o(e,t){return t.type||(t.data?"category":"value")}var i=n(0),r=n(14),a=n(87),l=n(55),s=r.extend({type:"singleAxis",layoutMode:"box",axis:null,coordinateSystem:null,getCoordSysModel:function(){return this}}),c={left:"5%",top:"5%",right:"5%",bottom:"5%",type:"value",position:"bottom",orient:"horizontal",axisLine:{show:!0,lineStyle:{width:2,type:"solid"}},tooltip:{show:!0},axisTick:{show:!0,length:6,lineStyle:{width:2}},axisLabel:{show:!0,interval:"auto"},splitLine:{show:!0,lineStyle:{type:"dashed",opacity:.2}}};i.merge(s.prototype,l),a("single",s,o,c);var u=s;e.exports=u},function(e,t,n){function o(e,t,n){this.dimension="single",this.dimensions=["single"],this._axis=null,this._rect,this._init(e,t,n),this.model=e}var i=n(585),r=n(22),a=n(6),l=a.getLayoutRect;o.prototype={type:"singleAxis",axisPointerEnabled:!0,constructor:o,_init:function(e,t,n){var o=this.dimension,a=new i(o,r.createScaleByModel(e),[0,0],e.get("type"),e.get("position")),l="category"===a.type;a.onBand=l&&e.get("boundaryGap"),a.inverse=e.get("inverse"),a.orient=e.get("orient"),e.axis=a,a.model=e,a.coordinateSystem=this,this._axis=a},update:function(e,t){e.eachSeries(function(e){if(e.coordinateSystem===this){var t=e.getData(),n=this.dimension;this._axis.scale.unionExtentFromData(t,e.coordDimToDataDim(n)),r.niceScaleExtent(this._axis.scale,this._axis.model)}},this)},resize:function(e,t){this._rect=l({left:e.get("left"),top:e.get("top"),right:e.get("right"),bottom:e.get("bottom"),width:e.get("width"),height:e.get("height")},{width:t.getWidth(),height:t.getHeight()}),this._adjustAxis()},getRect:function(){return this._rect},_adjustAxis:function(){var e=this._rect,t=this._axis,n=t.isHorizontal(),o=n?[0,e.width]:[0,e.height],i=t.reverse?1:0;t.setExtent(o[i],o[1-i]),this._updateAxisTransform(t,n?e.x:e.y)},_updateAxisTransform:function(e,t){var n=e.getExtent(),o=n[0]+n[1],i=e.isHorizontal();e.toGlobalCoord=i?function(e){return e+t}:function(e){return o-e+t},e.toLocalCoord=i?function(e){return e-t}:function(e){return o-e+t}},getAxis:function(){return this._axis},getBaseAxis:function(){return this._axis},getAxes:function(){return[this._axis]},getTooltipAxes:function(){return{baseAxes:[this.getAxis()]}},containPoint:function(e){var t=this.getRect(),n=this.getAxis();return"horizontal"===n.orient?n.contain(n.toLocalCoord(e[0]))&&e[1]>=t.y&&e[1]<=t.y+t.height:n.contain(n.toLocalCoord(e[1]))&&e[0]>=t.y&&e[0]<=t.y+t.height},pointToData:function(e){var t=this.getAxis();return[t.coordToData(t.toLocalCoord(e["horizontal"===t.orient?0:1]))]},dataToPoint:function(e){var t=this.getAxis(),n=this.getRect(),o=[],i="horizontal"===t.orient?0:1;return e instanceof Array&&(e=e[0]),o[i]=t.toGlobalCoord(t.dataToCoord(+e)),o[1-i]=0===i?n.y+n.height/2:n.x+n.width/2,o}};var s=o;e.exports=s},function(e,t,n){var o=n(0),i=n(36),r=function(e,t,n,o,r){i.call(this,e,t,n),this.type=o||"value",this.position=r||"bottom",this.orient=null,this._labelInterval=null};r.prototype={constructor:r,model:null,isHorizontal:function(){var e=this.position;return"top"===e||"bottom"===e},pointToData:function(e,t){return this.coordinateSystem.pointToData(e,t)[0]},toGlobalCoord:null,toLocalCoord:null},o.inherits(r,i);var a=r;e.exports=a},function(e,t,n){function o(e,t){var n=this.getAxis(),o=t instanceof Array?t[0]:t,i=(e instanceof Array?e[0]:e)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(o-i)-n.dataToCoord(o+i))}function i(e){var t=e.getRect();return{coordSys:{type:"singleAxis",x:t.x,y:t.y,width:t.width,height:t.height},api:{coord:r.bind(e.dataToPoint,e),size:r.bind(o,e)}}}var r=n(0);e.exports=i},function(e,t,n){function o(e,t){var n=[];return e.eachComponent("singleAxis",function(o,r){var a=new i(o,e,t);a.name="single_"+r,a.resize(o,t),o.coordinateSystem=a,n.push(a)}),e.eachSeries(function(t){if("singleAxis"===t.get("coordinateSystem")){var n=e.queryComponents({mainType:"singleAxis",index:t.get("singleAxisIndex"),id:t.get("singleAxisId")})[0];t.coordinateSystem=n&&n.coordinateSystem}}),n}var i=n(584);n(26).register("single",{create:o,dimensions:i.prototype.dimensions})},function(e,t,n){function o(e){return"_EC_"+e}function i(e,t){this.id=null==e?"":e,this.inEdges=[],this.outEdges=[],this.edges=[],this.hostGraph,this.dataIndex=null==t?-1:t}function r(e,t,n){this.node1=e,this.node2=t,this.dataIndex=null==n?-1:n}var a=n(4),l=(a.__DEV__,n(0)),s=function(e){this._directed=e||!1,this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this.data,this.edgeData},c=s.prototype;c.type="graph",c.isDirected=function(){return this._directed},c.addNode=function(e,t){e=e||""+t;var n=this._nodesMap;if(!n[o(e)]){var r=new i(e,t);return r.hostGraph=this,this.nodes.push(r),n[o(e)]=r,r}},c.getNodeByIndex=function(e){var t=this.data.getRawIndex(e);return this.nodes[t]},c.getNodeById=function(e){return this._nodesMap[o(e)]},c.addEdge=function(e,t,n){var a=this._nodesMap,l=this._edgesMap;if("number"==typeof e&&(e=this.nodes[e]),"number"==typeof t&&(t=this.nodes[t]),e instanceof i||(e=a[o(e)]),t instanceof i||(t=a[o(t)]),e&&t){var s=e.id+"-"+t.id;if(!l[s]){var c=new r(e,t,n);return c.hostGraph=this,this._directed&&(e.outEdges.push(c),t.inEdges.push(c)),e.edges.push(c),e!==t&&t.edges.push(c),this.edges.push(c),l[s]=c,c}}},c.getEdgeByIndex=function(e){var t=this.edgeData.getRawIndex(e);return this.edges[t]},c.getEdge=function(e,t){e instanceof i&&(e=e.id),t instanceof i&&(t=t.id);var n=this._edgesMap;return this._directed?n[e+"-"+t]:n[e+"-"+t]||n[t+"-"+e]},c.eachNode=function(e,t){for(var n=this.nodes,o=n.length,i=0;i=0&&e.call(t,n[i],i)},c.eachEdge=function(e,t){for(var n=this.edges,o=n.length,i=0;i=0&&n[i].node1.dataIndex>=0&&n[i].node2.dataIndex>=0&&e.call(t,n[i],i)},c.breadthFirstTraverse=function(e,t,n,r){if(t instanceof i||(t=this._nodesMap[o(t)]),t){for(var a="out"===n?"outEdges":"in"===n?"inEdges":"edges",l=0;l=0&&n.node2.dataIndex>=0});for(var i=0,r=o.length;i=0&&this[e][t].setItemVisual(this.dataIndex,n,o)},getVisual:function(n,o){return this[e][t].getItemVisual(this.dataIndex,n,o)},setLayout:function(n,o){this.dataIndex>=0&&this[e][t].setItemLayout(this.dataIndex,n,o)},getLayout:function(){return this[e][t].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[e][t].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[e][t].getRawIndex(this.dataIndex)}}};l.mixin(i,u("hostGraph","data")),l.mixin(r,u("hostGraph","edgeData")),s.Node=i,s.Edge=r;var d=s;e.exports=d},function(e,t,n){function o(e){var t=e.get("data");return l(t,e,e.ecModel)}function i(e,t){var n=t;t instanceof u||(n=new u(t),a.mixin(n,c));var o=s.createScaleByModel(n);return o.setExtent(e[0],e[1]),s.niceScaleExtent(o,n),o}function r(e){a.mixin(e,c)}var a=n(0),l=n(34),s=n(22),c=n(55),u=n(12),d=n(25);t.completeDimensions=d;var f=n(23);t.createSymbol=f.createSymbol,t.createList=o,t.createScale=i,t.mixinAxisModelCommonMethods=r},function(e,t,n){function o(e){return e.get("stack")||"__ec_stack_"+e.seriesIndex}function i(e){return e.dim}function r(e,t,n){var r=n.getWidth(),s=n.getHeight(),u={},d={},f=a(l.filter(t.getSeriesByType(e),function(e){return!t.isSeriesFiltered(e)&&e.coordinateSystem&&"polar"===e.coordinateSystem.type}));t.eachSeriesByType(e,function(e){if("polar"===e.coordinateSystem.type){var t=e.getData(),n=e.coordinateSystem,a=n.getAngleAxis(),l=n.getBaseAxis(),p=o(e),h=f[i(l)][p],g=h.offset,m=h.width,v=n.getOtherAxis(l),b=e.get("center")||["50%","50%"],x=c(b[0],r),y=c(b[1],s),_=e.get("barMinHeight")||0,w=e.get("barMinAngle")||0,k=v.getExtent()[0],S=v.model.get("max"),M=v.model.get("min"),C=[e.coordDimToDataDim("radius")[0],e.coordDimToDataDim("angle")[0]],A=t.mapArray(C,function(e,t){return n.dataToPoint([e,t])},!0);u[p]=u[p]||[],d[p]=d[p]||[],t.each(e.coordDimToDataDim(v.dim)[0],function(e,o){if(!isNaN(e)){u[p][o]||(u[p][o]={p:k,n:k},d[p][o]={p:k,n:k});var i,r,l,s,c=e>=0?"p":"n",f=n.pointToCoord(A[o]),h=d[p][o][c];if("radius"===v.dim)i=h,r=f[0],l=(-f[1]+g)*Math.PI/180,s=l+m*Math.PI/180,Math.abs(r)<_&&(r=i+(r<0?-1:1)*_),d[p][o][c]=r;else{i=f[0]+g,r=i+m,null!=S&&(e=Math.min(e,S)),null!=M&&(e=Math.max(e,M));var b=a.dataToAngle(e);Math.abs(b-h)0?T=C[1]:T===C[1]&&e<0&&(T=C[0]),d[p][o][c]=T}t.setItemLayout(o,{cx:x,cy:y,r0:i,r:r,startAngle:l,endAngle:s})}},!0)}},this)}function a(e,t){var n={};l.each(e,function(e,t){var r=e.getData(),a=e.coordinateSystem,l=a.getBaseAxis(),s=l.getExtent(),u="category"===l.type?l.getBandWidth():Math.abs(s[1]-s[0])/r.count(),d=n[i(l)]||{bandWidth:u,remainedWidth:u,autoWidthCount:0,categoryGap:"20%",gap:"30%",stacks:{}},f=d.stacks;n[i(l)]=d;var p=o(e);f[p]||d.autoWidthCount++,f[p]=f[p]||{width:0,maxWidth:0};var h=c(e.get("barWidth"),u),g=c(e.get("barMaxWidth"),u),m=e.get("barGap"),v=e.get("barCategoryGap");h&&!f[p].width&&(h=Math.min(d.remainedWidth,h),f[p].width=h,d.remainedWidth-=h),g&&(f[p].maxWidth=g),null!=m&&(d.gap=m),null!=v&&(d.categoryGap=v)});var r={};return l.each(n,function(e,t){r[t]={};var n=e.stacks,o=e.bandWidth,i=c(e.categoryGap,o),a=c(e.gap,1),s=e.remainedWidth,u=e.autoWidthCount,d=(s-i)/(u+(u-1)*a);d=Math.max(d,0),l.each(n,function(e,t){var n=e.maxWidth;n&&n=0;o--)d.isIdInner(t[o])&&t.splice(o,1);e[n]=t}}),delete e[w],e},getTheme:function(){return this._theme},getComponent:function(e,t){var n=this._componentsMap.get(e);if(n)return n[t||0]},queryComponents:function(e){var t=e.mainType;if(!t)return[];var n=e.index,o=e.id,i=e.name,r=this._componentsMap.get(t);if(!r||!r.length)return[];var a;if(null!=n)x(n)||(n=[n]),a=v(b(n,function(e){return r[e]}),function(e){return!!e});else if(null!=o){var l=x(o);a=v(r,function(e){return l&&y(o,e.id)>=0||!l&&e.id===o})}else if(null!=i){var c=x(i);a=v(r,function(e){return c&&y(i,e.name)>=0||!c&&e.name===i})}else a=r.slice();return s(a,e)},findComponents:function(e){var t=e.query,n=e.mainType,o=function(e){var t=n+"Index",o=n+"Id",i=n+"Name";return!e||null==e[t]&&null==e[o]&&null==e[i]?null:{mainType:n,index:e[t],id:e[o],name:e[i]}}(t),i=o?this.queryComponents(o):this._componentsMap.get(n);return function(t){return e.filter?v(t,e.filter):t}(s(i,e))},eachComponent:function(e,t,n){var o=this._componentsMap;if("function"==typeof e)n=t,t=e,o.each(function(e,o){m(e,function(e,i){t.call(n,o,e,i)})});else if(u.isString(e))m(o.get(e),t,n);else if(_(e)){var i=this.findComponents(e);m(i,t,n)}},getSeriesByName:function(e){var t=this._componentsMap.get("series");return v(t,function(t){return t.name===e})},getSeriesByIndex:function(e){return this._componentsMap.get("series")[e]},getSeriesByType:function(e){var t=this._componentsMap.get("series");return v(t,function(t){return t.subType===e})},getSeries:function(){return this._componentsMap.get("series").slice()},eachSeries:function(e,t){m(this._seriesIndices,function(n){var o=this._componentsMap.get("series")[n];e.call(t,o,n)},this)},eachRawSeries:function(e,t){m(this._componentsMap.get("series"),e,t)},eachSeriesByType:function(e,t,n){m(this._seriesIndices,function(o){var i=this._componentsMap.get("series")[o];i.subType===e&&t.call(n,i,o)},this)},eachRawSeriesByType:function(e,t,n){return m(this.getSeriesByType(e),t,n)},isSeriesFiltered:function(e){return u.indexOf(this._seriesIndices,e.componentIndex)<0},getCurrentSeriesIndices:function(){return(this._seriesIndices||[]).slice()},filterSeries:function(e,t){var n=v(this._componentsMap.get("series"),e,t);this._seriesIndices=l(n)},restoreData:function(){var e=this._componentsMap;this._seriesIndices=l(e.get("series"));var t=[];e.each(function(e,n){t.push(n)}),p.topologicalTravel(t,p.getAllClassMainTypes(),function(t,n){m(e.get(t),function(e){e.restoreData()})})}});u.mixin(k,g);var S=k;e.exports=S},function(e,t,n){function o(e){this._api=e,this._timelineOptions=[],this._mediaList=[],this._mediaDefault,this._currentMediaIndices=[],this._optionBackup,this._newBaseOption}function i(e,t,n){var o,i,r=[],a=[],l=e.timeline;if(e.baseOption&&(i=e.baseOption),(l||e.options)&&(i=i||{},r=(e.options||[]).slice()),e.media){i=i||{};var s=e.media;f(s,function(e){e&&e.option&&(e.query?a.push(e):o||(o=e))})}return i||(i=e),i.timeline||(i.timeline=l),f([i].concat(r).concat(c.map(a,function(e){return e.option})),function(e){f(t,function(t){t(e,n)})}),{baseOption:i,timelineOptions:r,mediaDefault:o,mediaList:a}}function r(e,t,n){var o={width:t,height:n,aspectratio:t/n},i=!0;return c.each(e,function(e,t){var n=t.match(m);if(n&&n[1]&&n[2]){var r=n[1],l=n[2].toLowerCase();a(o[l],e,r)||(i=!1)}}),i}function a(e,t,n){return"min"===n?e>=t:"max"===n?e<=t:e===t}function l(e,t){return e.join(",")===t.join(",")}function s(e,t){t=t||{},f(t,function(t,n){if(null!=t){var o=e[n];if(d.hasClass(n)){t=u.normalizeToArray(t),o=u.normalizeToArray(o);var i=u.mappingToExists(o,t);e[n]=h(i,function(e){return e.option&&e.exist?g(e.exist,e.option,!0):e.exist||e.option})}else e[n]=g(o,t,!0)}})}var c=n(0),u=n(5),d=n(14),f=c.each,p=c.clone,h=c.map,g=c.merge,m=/^(min|max)?(.+)$/;o.prototype={constructor:o,setOption:function(e,t){e=p(e,!0);var n=this._optionBackup,o=i.call(this,e,t,!n);this._newBaseOption=o.baseOption,n?(s(n.baseOption,o.baseOption),o.timelineOptions.length&&(n.timelineOptions=o.timelineOptions),o.mediaList.length&&(n.mediaList=o.mediaList),o.mediaDefault&&(n.mediaDefault=o.mediaDefault)):this._optionBackup=o},mountOption:function(e){var t=this._optionBackup;return this._timelineOptions=h(t.timelineOptions,p),this._mediaList=h(t.mediaList,p),this._mediaDefault=p(t.mediaDefault),this._currentMediaIndices=[],p(e?t.baseOption:this._newBaseOption)},getTimelineOption:function(e){var t,n=this._timelineOptions;if(n.length){var o=e.getComponent("timeline");o&&(t=p(n[o.getCurrentIndex()],!0))}return t},getMediaOption:function(e){var t=this._api.getWidth(),n=this._api.getHeight(),o=this._mediaList,i=this._mediaDefault,a=[],s=[];if(!o.length&&!i)return s;for(var c=0,u=o.length;c1){var d;"string"==typeof n?d=o[n]:"function"==typeof n&&(d=n),d&&(t=t.downSample(l.dim,1/u,d,i),e.setData(t))}}},this)}var o={average:function(e){for(var t=0,n=0,o=0;ot&&(t=e[n]);return t},min:function(e){for(var t=1/0,n=0;n0}))},niceTicks:function(e){e=e||10;var t=this._extent,n=t[1]-t[0];if(!(n===1/0||n<=0)){var o=a.quantity(n),i=e/n*o;for(i<=.5&&(o*=10);!isNaN(o)&&Math.abs(o)<1&&Math.abs(o)>0;)o*=10;var r=[a.round(p(t[0]/o)*o),a.round(f(t[1]/o)*o)];this._interval=o,this._niceExtent=r}},niceExtent:function(e){c.niceExtent.call(this,e);var t=this._originalScale;t.__fixMin=e.fixMin,t.__fixMax=e.fixMax}});i.each(["contain","normalize"],function(e){m.prototype[e]=function(t){return t=g(t)/g(this.base),s[e].call(this,t)}}),m.create=function(){return new m};var v=m;e.exports=v},function(e,t,n){var o=n(0),i=n(91),r=i.prototype,a=i.extend({type:"ordinal",init:function(e,t){this._data=e,this._extent=t||[0,e.length-1]},parse:function(e){return"string"==typeof e?o.indexOf(this._data,e):Math.round(e)},contain:function(e){return e=this.parse(e),r.contain.call(this,e)&&null!=this._data[e]},normalize:function(e){return r.normalize.call(this,this.parse(e))},scale:function(e){return Math.round(r.scale.call(this,e))},getTicks:function(){for(var e=[],t=this._extent,n=t[0];n<=t[1];)e.push(n),n++;return e},getLabel:function(e){return this._data[e]},count:function(){return this._extent[1]-this._extent[0]+1},unionExtentFromData:function(e,t){this.unionExtent(e.getDataExtent(t,!1))},niceTicks:o.noop,niceExtent:o.noop});a.create=function(){return new a};var l=a;e.exports=l},function(e,t,n){var o=n(0),i=n(3),r=n(8),a=n(216),l=n(90),s=l.prototype,c=Math.ceil,u=Math.floor,d=function(e,t,n,o){for(;n>>1;e[i][1]n&&(l=n);var s=p.length,f=d(p,l,0,s),h=p[Math.min(f,s-1)],g=h[1];if("year"===h[0]){var m=r/g;g*=i.nice(m/e,!0)}var v=this.getSetting("useUTC")?0:60*new Date(+o[0]||+o[1]).getTimezoneOffset()*1e3,b=[Math.round(c((o[0]-v)/g)*g+v),Math.round(u((o[1]-v)/g)*g+v)];a.fixExtent(b,o),this._stepLvl=h,this._interval=g,this._niceExtent=b},parse:function(e){return+i.parseDate(e)}});o.each(["contain","normalize"],function(e){f.prototype[e]=function(t){return s[e].call(this,this.parse(t))}});var p=[["hh:mm:ss",1e3],["hh:mm:ss",5e3],["hh:mm:ss",1e4],["hh:mm:ss",15e3],["hh:mm:ss",3e4],["hh:mm\nMM-dd",6e4],["hh:mm\nMM-dd",3e5],["hh:mm\nMM-dd",6e5],["hh:mm\nMM-dd",9e5],["hh:mm\nMM-dd",18e5],["hh:mm\nMM-dd",36e5],["hh:mm\nMM-dd",72e5],["hh:mm\nMM-dd",216e5],["hh:mm\nMM-dd",432e5],["MM-dd\nyyyy",864e5],["MM-dd\nyyyy",1728e5],["MM-dd\nyyyy",2592e5],["MM-dd\nyyyy",3456e5],["MM-dd\nyyyy",432e6],["MM-dd\nyyyy",5184e5],["week",6048e5],["MM-dd\nyyyy",864e6],["week",12096e5],["week",18144e5],["month",26784e5],["week",36288e5],["month",53568e5],["week",36288e5],["quarter",8208e6],["month",107136e5],["month",13392e6],["half-year",16416e6],["month",214272e5],["month",26784e6],["year",32832e6]];f.create=function(e){return new f({useUTC:e.ecModel.get("useUTC")})};var h=f;e.exports=h},function(e,t,n){function o(){var e,t=[],n={};return{add:function(e,o,r,a,l){return i.isString(a)&&(l=a,a=0),!n[e.id]&&(n[e.id]=1,t.push({el:e,target:o,time:r,delay:a,easing:l}),!0)},done:function(t){return e=t,this},start:function(){function o(){--i||(t.length=0,n={},e&&e())}for(var i=t.length,r=0,a=t.length;r1?t-1:0),a=1;a0&&void 0!==arguments[0]?arguments[0]:{},i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};!(n&&n.context&&o.target&&i.target)||e.contains(o.target)||e.contains(i.target)||e===o.target||n.context.popperElm&&(n.context.popperElm.contains(o.target)||n.context.popperElm.contains(i.target))||(t.expression&&e[s].methodName&&n.context[e[s].methodName]?n.context[e[s].methodName]():e[s].bindingFn&&e[s].bindingFn())}}t.__esModule=!0;var i=n(21),r=function(e){return e&&e.__esModule?e:{default:e}}(i),a=n(29),l=[],s="@@clickoutsideContext",c=void 0,u=0;!r.default.prototype.$isServer&&(0,a.on)(document,"mousedown",function(e){return c=e}),!r.default.prototype.$isServer&&(0,a.on)(document,"mouseup",function(e){l.forEach(function(t){return t[s].documentHandler(e,c)})}),t.default={bind:function(e,t,n){l.push(e);var i=u++;e[s]={id:i,documentHandler:o(e,t,n),methodName:t.expression,bindingFn:t.value}},update:function(e,t,n){e[s].documentHandler=o(e,t,n),e[s].methodName=t.expression,e[s].bindingFn=t.value},unbind:function(e){for(var t=l.length,n=0;n1&&console.warn("WARNING: the given `parent` query("+e.parent+") matched more than one element, the first one will be used"),0===l.length)throw"ERROR: the given `parent` doesn't exists!";l=l[0]}return l.length>1&&l instanceof Element==0&&(console.warn("WARNING: you have passed as parent a list of elements, the first one will be used"),l=l[0]),l.appendChild(r),r},e.prototype._getPosition=function(e,t){var n=a(t);return this._options.forceAbsolute?"absolute":s(t,n)?"fixed":"absolute"},e.prototype._getOffsets=function(e,n,o){o=o.split("-")[0];var i={};i.position=this.state.position;var r="fixed"===i.position,l=p(n,a(e),r),s=t(e);return-1!==["right","left"].indexOf(o)?(i.top=l.top+l.height/2-s.height/2,i.left="left"===o?l.left-s.width:l.right):(i.left=l.left+l.width/2-s.width/2,i.top="top"===o?l.top-s.height:l.bottom),i.width=s.width,i.height=s.height,{popper:i,reference:l}},e.prototype._setupEventListeners=function(){if(this.state.updateBound=this.update.bind(this),g.addEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement){var e=l(this._reference);e!==g.document.body&&e!==g.document.documentElement||(e=g),e.addEventListener("scroll",this.state.updateBound),this.state.scrollTarget=e}},e.prototype._removeEventListeners=function(){g.removeEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement&&this.state.scrollTarget&&(this.state.scrollTarget.removeEventListener("scroll",this.state.updateBound),this.state.scrollTarget=null),this.state.updateBound=null},e.prototype._getBoundaries=function(e,t,n){var o,i,r={};if("window"===n){var s=g.document.body,c=g.document.documentElement;i=Math.max(s.scrollHeight,s.offsetHeight,c.clientHeight,c.scrollHeight,c.offsetHeight),o=Math.max(s.scrollWidth,s.offsetWidth,c.clientWidth,c.scrollWidth,c.offsetWidth),r={top:0,right:o,bottom:i,left:0}}else if("viewport"===n){var u=a(this._popper),f=l(this._popper),p=d(u),h="fixed"===e.offsets.popper.position?0:function(e){return e==document.body?Math.max(document.documentElement.scrollTop,document.body.scrollTop):e.scrollTop}(f),m="fixed"===e.offsets.popper.position?0:function(e){return e==document.body?Math.max(document.documentElement.scrollLeft,document.body.scrollLeft):e.scrollLeft}(f);r={top:0-(p.top-h),right:g.document.documentElement.clientWidth-(p.left-m),bottom:g.document.documentElement.clientHeight-(p.top-h),left:0-(p.left-m)}}else r=a(this._popper)===n?{top:0,left:0,right:n.clientWidth,bottom:n.clientHeight}:d(n);return r.left+=t,r.right-=t,r.top=r.top+t,r.bottom=r.bottom-t,r},e.prototype.runModifiers=function(e,t,n){var o=t.slice();return void 0!==n&&(o=this._options.modifiers.slice(0,i(this._options.modifiers,n))),o.forEach(function(t){u(t)&&(e=t.call(this,e))}.bind(this)),e},e.prototype.isModifierRequired=function(e,t){var n=i(this._options.modifiers,e);return!!this._options.modifiers.slice(0,n).filter(function(e){return e===t}).length},e.prototype.modifiers={},e.prototype.modifiers.applyStyle=function(e){var t,n={position:e.offsets.popper.position},o=Math.round(e.offsets.popper.left),i=Math.round(e.offsets.popper.top);return this._options.gpuAcceleration&&(t=h("transform"))?(n[t]="translate3d("+o+"px, "+i+"px, 0)",n.top=0,n.left=0):(n.left=o,n.top=i),Object.assign(n,e.styles),c(this._popper,n),this._popper.setAttribute("x-placement",e.placement),this.isModifierRequired(this.modifiers.applyStyle,this.modifiers.arrow)&&e.offsets.arrow&&c(e.arrowElement,e.offsets.arrow),e},e.prototype.modifiers.shift=function(e){var t=e.placement,n=t.split("-")[0],i=t.split("-")[1];if(i){var r=e.offsets.reference,a=o(e.offsets.popper),l={y:{start:{top:r.top},end:{top:r.top+r.height-a.height}},x:{start:{left:r.left},end:{left:r.left+r.width-a.width}}},s=-1!==["bottom","top"].indexOf(n)?"x":"y";e.offsets.popper=Object.assign(a,l[s][i])}return e},e.prototype.modifiers.preventOverflow=function(e){var t=this._options.preventOverflowOrder,n=o(e.offsets.popper),i={left:function(){var t=n.left;return n.lefte.boundaries.right&&(t=Math.min(n.left,e.boundaries.right-n.width)),{left:t}},top:function(){var t=n.top;return n.tope.boundaries.bottom&&(t=Math.min(n.top,e.boundaries.bottom-n.height)),{top:t}}};return t.forEach(function(t){e.offsets.popper=Object.assign(n,i[t]())}),e},e.prototype.modifiers.keepTogether=function(e){var t=o(e.offsets.popper),n=e.offsets.reference,i=Math.floor;return t.righti(n.right)&&(e.offsets.popper.left=i(n.right)),t.bottomi(n.bottom)&&(e.offsets.popper.top=i(n.bottom)),e},e.prototype.modifiers.flip=function(e){if(!this.isModifierRequired(this.modifiers.flip,this.modifiers.preventOverflow))return console.warn("WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!"),e;if(e.flipped&&e.placement===e._originalPlacement)return e;var t=e.placement.split("-")[0],i=n(t),r=e.placement.split("-")[1]||"",a=[];return a="flip"===this._options.flipBehavior?[t,i]:this._options.flipBehavior,a.forEach(function(l,s){if(t===l&&a.length!==s+1){t=e.placement.split("-")[0],i=n(t);var c=o(e.offsets.popper),u=-1!==["right","bottom"].indexOf(t);(u&&Math.floor(e.offsets.reference[t])>Math.floor(c[i])||!u&&Math.floor(e.offsets.reference[t])l[p]&&(e.offsets.popper[d]+=s[d]+h-l[p]);var g=s[d]+(i||s[u]/2-h/2),m=g-l[d];return m=Math.max(Math.min(l[u]-h-8,m),8),r[d]=m,r[f]="",e.offsets.arrow=r,e.arrowElement=n,e},Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(e){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var t=Object(e),n=1;n0){var o=t[t.length-1];if(o.id===e)o.modalClass&&o.modalClass.trim().split(/\s+/).forEach(function(e){return(0,r.removeClass)(n,e)}),t.pop(),t.length>0&&(n.style.zIndex=t[t.length-1].zIndex);else for(var i=t.length-1;i>=0;i--)if(t[i].id===e){t.splice(i,1);break}}0===t.length&&(this.modalFade&&(0,r.addClass)(n,"v-modal-leave"),setTimeout(function(){0===t.length&&(n.parentNode&&n.parentNode.removeChild(n),n.style.display="none",d.modalDom=void 0),(0,r.removeClass)(n,"v-modal-leave")},200))}};Object.defineProperty(d,"zIndex",{configurable:!0,get:function(){return l||(s=s||(i.default.prototype.$ELEMENT||{}).zIndex||2e3,l=!0),s},set:function(e){s=e}});var f=function(){if(!i.default.prototype.$isServer&&d.modalStack.length>0){var e=d.modalStack[d.modalStack.length-1];if(!e)return;return d.getInstance(e.id)}};i.default.prototype.$isServer||window.addEventListener("keydown",function(e){if(27===e.keyCode){var t=f();t&&t.closeOnPressEscape&&(t.handleClose?t.handleClose():t.handleAction?t.handleAction("cancel"):t.close())}}),t.default=d},function(e,t,n){"use strict";function o(e){return"[object String]"===Object.prototype.toString.call(e)}function i(e){return"[object Object]"===Object.prototype.toString.call(e)}function r(e){return e&&e.nodeType===Node.ELEMENT_NODE}t.__esModule=!0,t.isString=o,t.isObject=i,t.isHtmlElement=r,t.isFunction=function(e){var t={};return e&&"[object Function]"===t.toString.call(e)},t.isUndefined=function(e){return void 0===e},t.isDefined=function(e){return void 0!==e&&null!==e}},function(e,t){t.read=function(e,t,n,o,i){var r,a,l=8*i-o-1,s=(1<>1,u=-7,d=n?i-1:0,f=n?-1:1,p=e[t+d];for(d+=f,r=p&(1<<-u)-1,p>>=-u,u+=l;u>0;r=256*r+e[t+d],d+=f,u-=8);for(a=r&(1<<-u)-1,r>>=-u,u+=o;u>0;a=256*a+e[t+d],d+=f,u-=8);if(0===r)r=1-c;else{if(r===s)return a?NaN:1/0*(p?-1:1);a+=Math.pow(2,o),r-=c}return(p?-1:1)*a*Math.pow(2,r-o)},t.write=function(e,t,n,o,i,r){var a,l,s,c=8*r-i-1,u=(1<>1,f=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=o?0:r-1,h=o?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(l=isNaN(t)?1:0,a=u):(a=Math.floor(Math.log(t)/Math.LN2),t*(s=Math.pow(2,-a))<1&&(a--,s*=2),t+=a+d>=1?f/s:f*Math.pow(2,1-d),t*s>=2&&(a++,s/=2),a+d>=u?(l=0,a=u):a+d>=1?(l=(t*s-1)*Math.pow(2,i),a+=d):(l=t*Math.pow(2,d-1)*Math.pow(2,i),a=0));i>=8;e[n+p]=255&l,p+=h,l/=256,i-=8);for(a=a<0;e[n+p]=255&a,p+=h,a/=256,c-=8);e[n+p-h]|=128*g}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){e.exports=n(624)},function(e,t,n){"use strict";var o=!("undefined"==typeof window||!window.document||!window.document.createElement),i={canUseDOM:o,canUseWorkers:"undefined"!=typeof Worker,canUseEventListeners:o&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:o&&!!window.screen,isInWorker:!o};e.exports=i},function(e,t){function n(){if(!b){b=!0;var e=navigator.userAgent,t=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(e),n=/(Mac OS X)|(Windows)|(Linux)/.exec(e);if(h=/\b(iPhone|iP[ao]d)/.exec(e),g=/\b(iP[ao]d)/.exec(e),f=/Android/i.exec(e),m=/FBAN\/\w+;/i.exec(e),v=/Mobile/i.exec(e),p=!!/Win64/.exec(e),t){(o=t[1]?parseFloat(t[1]):t[5]?parseFloat(t[5]):NaN)&&document&&document.documentMode&&(o=document.documentMode);var x=/(?:Trident\/(\d+.\d+))/.exec(e);s=x?parseFloat(x[1])+4:o,i=t[2]?parseFloat(t[2]):NaN,r=t[3]?parseFloat(t[3]):NaN,a=t[4]?parseFloat(t[4]):NaN,a?(t=/(?:Chrome\/(\d+\.\d+))/.exec(e),l=t&&t[1]?parseFloat(t[1]):NaN):l=NaN}else o=i=r=l=a=NaN;if(n){if(n[1]){var y=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(e);c=!y||parseFloat(y[1].replace("_","."))}else c=!1;u=!!n[2],d=!!n[3]}else c=u=d=!1}}var o,i,r,a,l,s,c,u,d,f,p,h,g,m,v,b=!1,x={ie:function(){return n()||o},ieCompatibilityMode:function(){return n()||s>o},ie64:function(){return x.ie()&&p},firefox:function(){return n()||i},opera:function(){return n()||r},webkit:function(){return n()||a},safari:function(){return x.webkit()},chrome:function(){return n()||l},windows:function(){return n()||u},osx:function(){return n()||c},linux:function(){return n()||d},iphone:function(){return n()||h},mobile:function(){return n()||h||g||f||v},nativeApp:function(){return n()||m},android:function(){return n()||f},ipad:function(){return n()||g}};e.exports=x},function(e,t,n){"use strict";function o(e,t){if(!r.canUseDOM||t&&!("addEventListener"in document))return!1;var n="on"+e,o=n in document;if(!o){var a=document.createElement("div");a.setAttribute(n,"return;"),o="function"==typeof a[n]}return!o&&i&&"wheel"===e&&(o=document.implementation.hasFeature("Events.wheel","3.0")),o}var i,r=n(621);r.canUseDOM&&(i=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("","")),e.exports=o},function(e,t,n){"use strict";function o(e){var t=0,n=0,o=0,i=0;return"detail"in e&&(n=e.detail),"wheelDelta"in e&&(n=-e.wheelDelta/120),"wheelDeltaY"in e&&(n=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=n,n=0),o=t*a,i=n*a,"deltaY"in e&&(i=e.deltaY),"deltaX"in e&&(o=e.deltaX),(o||i)&&e.deltaMode&&(1==e.deltaMode?(o*=l,i*=l):(o*=s,i*=s)),o&&!t&&(t=o<1?-1:1),i&&!n&&(n=i<1?-1:1),{spinX:t,spinY:n,pixelX:o,pixelY:i}}var i=n(622),r=n(623),a=10,l=40,s=800;o.getEventType=function(){return i.firefox()?"DOMMouseScroll":r("wheel")?"wheel":"mousewheel"},e.exports=o},function(e,t){function n(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function i(e){if(u===setTimeout)return setTimeout(e,0);if((u===n||!u)&&setTimeout)return u=setTimeout,setTimeout(e,0);try{return u(e,0)}catch(t){try{return u.call(null,e,0)}catch(t){return u.call(this,e,0)}}}function r(e){if(d===clearTimeout)return clearTimeout(e);if((d===o||!d)&&clearTimeout)return d=clearTimeout,clearTimeout(e);try{return d(e)}catch(t){try{return d.call(null,e)}catch(t){return d.call(this,e)}}}function a(){g&&p&&(g=!1,p.length?h=p.concat(h):m=-1,h.length&&l())}function l(){if(!g){var e=i(a);g=!0;for(var t=h.length;t;){for(p=h,h=[];++m1)for(var n=1;n0},e.prototype.connect_=function(){p&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),x?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){p&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,n=void 0===t?"":t;b.some(function(e){return!!~n.indexOf(e)})&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),_=function(e,t){for(var n=0,o=Object.keys(t);n0},e}(),T="undefined"!=typeof WeakMap?new WeakMap:new f,E=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=y.getInstance(),o=new A(t,n,this);T.set(this,o)}return e}();["observe","unobserve","disconnect"].forEach(function(e){E.prototype[e]=function(){var t;return(t=T.get(this))[e].apply(t,arguments)}});var I=function(){return void 0!==h.ResizeObserver?h.ResizeObserver:E}();t.default=I}.call(t,n(48))},function(e,t,n){(function(e,t){!function(e,n){"use strict";function o(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(627),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(t,n(48))},function(e,t,n){"use strict";var o=n(642),i=n(228),r=(n(640),n(30)),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(643),i=n(229),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(644),i=n(230),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(645),i=n(231),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(646),i=n(232),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(647),i=n(233),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(648),i=n(234),r=n(30),a=n.i(r.a)(i.a,o.a,o.b,!1,null,null,null);t.a=a.exports},function(e,t,n){"use strict";var o=n(628);n.n(o)},function(e,t,n){"use strict";var o=n(629);n.n(o)},function(e,t,n){"use strict";var o=n(650);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(651);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(652);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(653);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(654);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(655);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(656);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(657);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";var o=n(658);n.d(t,"a",function(){return o.a}),n.d(t,"b",function(){return o.b})},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{attrs:{id:"app"}},[n("header",{staticClass:"grid-content header-color"},[n("el-row",[n("a",{staticClass:"brand",attrs:{href:"#"}},[e._v("frp")])])],1),e._v(" "),n("section",[n("el-row",[n("el-col",{attrs:{id:"side-nav",xs:24,md:4}},[n("el-menu",{attrs:{"default-active":"1",mode:"vertical",theme:"light",router:"false"},on:{select:e.handleSelect}},[n("el-menu-item",{attrs:{index:"/"}},[e._v("Overview")]),e._v(" "),n("el-submenu",{attrs:{index:"/proxies"}},[n("template",{slot:"title"},[e._v("Proxies")]),e._v(" "),n("el-menu-item",{attrs:{index:"/proxies/tcp"}},[e._v("TCP")]),e._v(" "),n("el-menu-item",{attrs:{index:"/proxies/udp"}},[e._v("UDP")]),e._v(" "),n("el-menu-item",{attrs:{index:"/proxies/http"}},[e._v("HTTP")]),e._v(" "),n("el-menu-item",{attrs:{index:"/proxies/https"}},[e._v("HTTPS")]),e._v(" "),n("el-menu-item",{attrs:{index:"/proxies/stcp"}},[e._v("STCP")]),e._v(" "),n("el-menu-item",{attrs:{index:"/proxies/sudp"}},[e._v("SUDP")])],2),e._v(" "),n("el-menu-item",{attrs:{index:""}},[e._v("Help")])],1)],1),e._v(" "),n("el-col",{attrs:{xs:24,md:20}},[n("div",{attrs:{id:"content"}},[n("router-view")],1)])],1)],1),e._v(" "),n("footer")])},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-row",[n("el-col",{attrs:{md:12}},[n("div",{staticClass:"source"},[n("el-form",{staticClass:"server_info",attrs:{"label-position":"left"}},[n("el-form-item",{attrs:{label:"Version"}},[n("span",[e._v(e._s(e.version))])]),e._v(" "),n("el-form-item",{attrs:{label:"BindPort"}},[n("span",[e._v(e._s(e.bind_port))])]),e._v(" "),n("el-form-item",{attrs:{label:"BindUdpPort"}},[n("span",[e._v(e._s(e.bind_udp_port))])]),e._v(" "),n("el-form-item",{attrs:{label:"Http Port"}},[n("span",[e._v(e._s(e.vhost_http_port))])]),e._v(" "),n("el-form-item",{attrs:{label:"Https Port"}},[n("span",[e._v(e._s(e.vhost_https_port))])]),e._v(" "),n("el-form-item",{attrs:{label:"Subdomain Host"}},[n("span",[e._v(e._s(e.subdomain_host))])]),e._v(" "),n("el-form-item",{attrs:{label:"Max PoolCount"}},[n("span",[e._v(e._s(e.max_pool_count))])]),e._v(" "),n("el-form-item",{attrs:{label:"Max Ports Per Client"}},[n("span",[e._v(e._s(e.max_ports_per_client))])]),e._v(" "),n("el-form-item",{attrs:{label:"HeartBeat Timeout"}},[n("span",[e._v(e._s(e.heart_beat_timeout))])]),e._v(" "),n("el-form-item",{attrs:{label:"Client Counts"}},[n("span",[e._v(e._s(e.client_counts))])]),e._v(" "),n("el-form-item",{attrs:{label:"Current Connections"}},[n("span",[e._v(e._s(e.cur_conns))])]),e._v(" "),n("el-form-item",{attrs:{label:"Proxy Counts"}},[n("span",[e._v(e._s(e.proxy_counts))])])],1)],1)]),e._v(" "),n("el-col",{attrs:{md:12}},[n("div",{staticStyle:{width:"400px",height:"250px","margin-bottom":"30px"},attrs:{id:"traffic"}}),e._v(" "),n("div",{staticStyle:{width:"400px",height:"250px"},attrs:{id:"proxies"}})])],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-table",{staticStyle:{width:"100%"},attrs:{data:e.proxies,"default-sort":{prop:"name",order:"ascending"}}},[n("el-table-column",{attrs:{type:"expand"},scopedSlots:e._u([{key:"default",fn:function(t){return[n("el-popover",{ref:"popover4",staticStyle:{"margin-left":"0px"},attrs:{placement:"right",width:"600",trigger:"click"}},[n("my-traffic-chart",{attrs:{proxy_name:t.row.name}})],1),e._v(" "),n("el-button",{directives:[{name:"popover",rawName:"v-popover:popover4",arg:"popover4"}],staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"small",icon:"view"}},[e._v("Traffic Statistics")]),e._v(" "),n("el-form",{staticClass:"demo-table-expand",attrs:{"label-position":"left",inline:""}},[n("el-form-item",{attrs:{label:"Name"}},[n("span",[e._v(e._s(t.row.name))])]),e._v(" "),n("el-form-item",{attrs:{label:"Type"}},[n("span",[e._v(e._s(t.row.type))])]),e._v(" "),n("el-form-item",{attrs:{label:"Domains"}},[n("span",[e._v(e._s(t.row.custom_domains))])]),e._v(" "),n("el-form-item",{attrs:{label:"SubDomain"}},[n("span",[e._v(e._s(t.row.subdomain))])]),e._v(" "),n("el-form-item",{attrs:{label:"locations"}},[n("span",[e._v(e._s(t.row.locations))])]),e._v(" "),n("el-form-item",{attrs:{label:"HostRewrite"}},[n("span",[e._v(e._s(t.row.host_header_rewrite))])]),e._v(" "),n("el-form-item",{attrs:{label:"Encryption"}},[n("span",[e._v(e._s(t.row.encryption))])]),e._v(" "),n("el-form-item",{attrs:{label:"Compression"}},[n("span",[e._v(e._s(t.row.compression))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Start"}},[n("span",[e._v(e._s(t.row.last_start_time))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Close"}},[n("span",[e._v(e._s(t.row.last_close_time))])])],1)]}}])}),e._v(" "),n("el-table-column",{attrs:{label:"Name",prop:"name",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Port",prop:"port",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Connections",prop:"conns",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic In",prop:"traffic_in",formatter:e.formatTrafficIn,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic Out",prop:"traffic_out",formatter:e.formatTrafficOut,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"status",prop:"status",sortable:""},scopedSlots:e._u([{key:"default",fn:function(t){return["online"===t.row.status?n("el-tag",{attrs:{type:"success"}},[e._v(e._s(t.row.status))]):n("el-tag",{attrs:{type:"danger"}},[e._v(e._s(t.row.status))])]}}])})],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-table",{staticStyle:{width:"100%"},attrs:{data:e.proxies,"default-sort":{prop:"name",order:"ascending"}}},[n("el-table-column",{attrs:{type:"expand"},scopedSlots:e._u([{key:"default",fn:function(t){return[n("el-popover",{ref:"popover4",staticStyle:{"margin-left":"0px"},attrs:{placement:"right",width:"600",trigger:"click"}},[n("my-traffic-chart",{attrs:{proxy_name:t.row.name}})],1),e._v(" "),n("el-button",{directives:[{name:"popover",rawName:"v-popover:popover4",arg:"popover4"}],staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"small",icon:"view"}},[e._v("Traffic Statistics")]),e._v(" "),n("el-form",{staticClass:"demo-table-expand",attrs:{"label-position":"left",inline:""}},[n("el-form-item",{attrs:{label:"Name"}},[n("span",[e._v(e._s(t.row.name))])]),e._v(" "),n("el-form-item",{attrs:{label:"Type"}},[n("span",[e._v(e._s(t.row.type))])]),e._v(" "),n("el-form-item",{attrs:{label:"Domains"}},[n("span",[e._v(e._s(t.row.custom_domains))])]),e._v(" "),n("el-form-item",{attrs:{label:"SubDomain"}},[n("span",[e._v(e._s(t.row.subdomain))])]),e._v(" "),n("el-form-item",{attrs:{label:"Encryption"}},[n("span",[e._v(e._s(t.row.encryption))])]),e._v(" "),n("el-form-item",{attrs:{label:"Compression"}},[n("span",[e._v(e._s(t.row.compression))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Start"}},[n("span",[e._v(e._s(t.row.last_start_time))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Close"}},[n("span",[e._v(e._s(t.row.last_close_time))])])],1)]}}])}),e._v(" "),n("el-table-column",{attrs:{label:"Name",prop:"name",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Port",prop:"port",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Connections",prop:"conns",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic In",prop:"traffic_in",formatter:e.formatTrafficIn,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic Out",prop:"traffic_out",formatter:e.formatTrafficOut,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"status",prop:"status",sortable:""},scopedSlots:e._u([{key:"default",fn:function(t){return["online"===t.row.status?n("el-tag",{attrs:{type:"success"}},[e._v(e._s(t.row.status))]):n("el-tag",{attrs:{type:"danger"}},[e._v(e._s(t.row.status))])]}}])})],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-table",{staticStyle:{width:"100%"},attrs:{data:e.proxies,"default-sort":{prop:"name",order:"ascending"}}},[n("el-table-column",{attrs:{type:"expand"},scopedSlots:e._u([{key:"default",fn:function(t){return[n("el-popover",{ref:"popover4",staticStyle:{"margin-left":"0px"},attrs:{placement:"right",width:"600",trigger:"click"}},[n("my-traffic-chart",{attrs:{proxy_name:t.row.name}})],1),e._v(" "),n("el-button",{directives:[{name:"popover",rawName:"v-popover:popover4",arg:"popover4"}],staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"small",icon:"view",name:t.row.name},on:{click:e.fetchData2}},[e._v("Traffic Statistics")]),e._v(" "),n("el-form",{staticClass:"demo-table-expand",attrs:{"label-position":"left",inline:""}},[n("el-form-item",{attrs:{label:"Name"}},[n("span",[e._v(e._s(t.row.name))])]),e._v(" "),n("el-form-item",{attrs:{label:"Type"}},[n("span",[e._v(e._s(t.row.type))])]),e._v(" "),n("el-form-item",{attrs:{label:"Encryption"}},[n("span",[e._v(e._s(t.row.encryption))])]),e._v(" "),n("el-form-item",{attrs:{label:"Compression"}},[n("span",[e._v(e._s(t.row.compression))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Start"}},[n("span",[e._v(e._s(t.row.last_start_time))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Close"}},[n("span",[e._v(e._s(t.row.last_close_time))])])],1)]}}])}),e._v(" "),n("el-table-column",{attrs:{label:"Name",prop:"name",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Connections",prop:"conns",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic In",prop:"traffic_in",formatter:e.formatTrafficIn,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic Out",prop:"traffic_out",formatter:e.formatTrafficOut,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"status",prop:"status",sortable:""},scopedSlots:e._u([{key:"default",fn:function(t){return["online"===t.row.status?n("el-tag",{attrs:{type:"success"}},[e._v(e._s(t.row.status))]):n("el-tag",{attrs:{type:"danger"}},[e._v(e._s(t.row.status))])]}}])})],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-table",{staticStyle:{width:"100%"},attrs:{data:e.proxies,"default-sort":{prop:"name",order:"ascending"}}},[n("el-table-column",{attrs:{type:"expand"},scopedSlots:e._u([{key:"default",fn:function(t){return[n("el-popover",{ref:"popover4",staticStyle:{"margin-left":"0px"},attrs:{placement:"right",width:"600",trigger:"click"}},[n("my-traffic-chart",{attrs:{proxy_name:t.row.name}})],1),e._v(" "),n("el-button",{directives:[{name:"popover",rawName:"v-popover:popover4",arg:"popover4"}],staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"small",icon:"view",name:t.row.name},on:{click:e.fetchData2}},[e._v("Traffic Statistics")]),e._v(" "),n("el-form",{staticClass:"demo-table-expand",attrs:{"label-position":"left",inline:""}},[n("el-form-item",{attrs:{label:"Name"}},[n("span",[e._v(e._s(t.row.name))])]),e._v(" "),n("el-form-item",{attrs:{label:"Type"}},[n("span",[e._v(e._s(t.row.type))])]),e._v(" "),n("el-form-item",{attrs:{label:"Encryption"}},[n("span",[e._v(e._s(t.row.encryption))])]),e._v(" "),n("el-form-item",{attrs:{label:"Compression"}},[n("span",[e._v(e._s(t.row.compression))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Start"}},[n("span",[e._v(e._s(t.row.last_start_time))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Close"}},[n("span",[e._v(e._s(t.row.last_close_time))])])],1)]}}])}),e._v(" "),n("el-table-column",{attrs:{label:"Name",prop:"name",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Connections",prop:"conns",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic In",prop:"traffic_in",formatter:e.formatTrafficIn,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic Out",prop:"traffic_out",formatter:e.formatTrafficOut,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"status",prop:"status",sortable:""},scopedSlots:e._u([{key:"default",fn:function(t){return["online"===t.row.status?n("el-tag",{attrs:{type:"success"}},[e._v(e._s(t.row.status))]):n("el-tag",{attrs:{type:"danger"}},[e._v(e._s(t.row.status))])]}}])})],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-table",{staticStyle:{width:"100%"},attrs:{data:e.proxies,"default-sort":{prop:"name",order:"ascending"}}},[n("el-table-column",{attrs:{type:"expand"},scopedSlots:e._u([{key:"default",fn:function(t){return[n("el-popover",{ref:"popover4",staticStyle:{"margin-left":"0px"},attrs:{placement:"right",width:"600",trigger:"click"}},[n("my-traffic-chart",{attrs:{proxy_name:t.row.name}})],1),e._v(" "),n("el-button",{directives:[{name:"popover",rawName:"v-popover:popover4",arg:"popover4"}],staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"small",icon:"view",name:t.row.name},on:{click:e.fetchData2}},[e._v("Traffic Statistics")]),e._v(" "),n("el-form",{staticClass:"demo-table-expand",attrs:{"label-position":"left",inline:""}},[n("el-form-item",{attrs:{label:"Name"}},[n("span",[e._v(e._s(t.row.name))])]),e._v(" "),n("el-form-item",{attrs:{label:"Type"}},[n("span",[e._v(e._s(t.row.type))])]),e._v(" "),n("el-form-item",{attrs:{label:"Addr"}},[n("span",[e._v(e._s(t.row.addr))])]),e._v(" "),n("el-form-item",{attrs:{label:"Encryption"}},[n("span",[e._v(e._s(t.row.encryption))])]),e._v(" "),n("el-form-item",{attrs:{label:"Compression"}},[n("span",[e._v(e._s(t.row.compression))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Start"}},[n("span",[e._v(e._s(t.row.last_start_time))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Close"}},[n("span",[e._v(e._s(t.row.last_close_time))])])],1)]}}])}),e._v(" "),n("el-table-column",{attrs:{label:"Name",prop:"name",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Port",prop:"port",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Connections",prop:"conns",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic In",prop:"traffic_in",formatter:e.formatTrafficIn,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic Out",prop:"traffic_out",formatter:e.formatTrafficOut,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"status",prop:"status",sortable:""},scopedSlots:e._u([{key:"default",fn:function(t){return["online"===t.row.status?n("el-tag",{attrs:{type:"success"}},[e._v(e._s(t.row.status))]):n("el-tag",{attrs:{type:"danger"}},[e._v(e._s(t.row.status))])]}}])})],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-table",{staticStyle:{width:"100%"},attrs:{data:e.proxies,"default-sort":{prop:"name",order:"ascending"}}},[n("el-table-column",{attrs:{type:"expand"},scopedSlots:e._u([{key:"default",fn:function(t){return[n("el-popover",{ref:"popover4",staticStyle:{"margin-left":"0px"},attrs:{placement:"right",width:"600",trigger:"click"}},[n("my-traffic-chart",{attrs:{proxy_name:t.row.name}})],1),e._v(" "),n("el-button",{directives:[{name:"popover",rawName:"v-popover:popover4",arg:"popover4"}],staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"small",icon:"view"}},[e._v("Traffic Statistics")]),e._v(" "),n("el-form",{staticClass:"demo-table-expand",attrs:{"label-position":"left",inline:""}},[n("el-form-item",{attrs:{label:"Name"}},[n("span",[e._v(e._s(t.row.name))])]),e._v(" "),n("el-form-item",{attrs:{label:"Type"}},[n("span",[e._v(e._s(t.row.type))])]),e._v(" "),n("el-form-item",{attrs:{label:"Addr"}},[n("span",[e._v(e._s(t.row.addr))])]),e._v(" "),n("el-form-item",{attrs:{label:"Encryption"}},[n("span",[e._v(e._s(t.row.encryption))])]),e._v(" "),n("el-form-item",{attrs:{label:"Compression"}},[n("span",[e._v(e._s(t.row.compression))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Start"}},[n("span",[e._v(e._s(t.row.last_start_time))])]),e._v(" "),n("el-form-item",{attrs:{label:"Last Close"}},[n("span",[e._v(e._s(t.row.last_close_time))])])],1)]}}])}),e._v(" "),n("el-table-column",{attrs:{label:"Name",prop:"name",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Port",prop:"port",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Connections",prop:"conns",sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic In",prop:"traffic_in",formatter:e.formatTrafficIn,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"Traffic Out",prop:"traffic_out",formatter:e.formatTrafficOut,sortable:""}}),e._v(" "),n("el-table-column",{attrs:{label:"status",prop:"status",sortable:""},scopedSlots:e._u([{key:"default",fn:function(t){return["online"===t.row.status?n("el-tag",{attrs:{type:"success"}},[e._v(e._s(t.row.status))]):n("el-tag",{attrs:{type:"danger"}},[e._v(e._s(t.row.status))])]}}])})],1)],1)},i=[]},function(e,t,n){"use strict";n.d(t,"a",function(){return o}),n.d(t,"b",function(){return i});var o=function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{staticStyle:{width:"600px",height:"400px"},attrs:{id:e.proxy_name}})},i=[]},function(e,t,n){"use strict";function o(e){return Object.prototype.toString.call(e).indexOf("Error")>-1}function i(e,t){switch(typeof t){case"undefined":return;case"object":return t;case"function":return t(e);case"boolean":return t?e.params:void 0}}function r(e,t){for(var n in t)e[n]=t[n];return e}function a(e,t,n){void 0===t&&(t={});var o,i=n||l;try{o=i(e||"")}catch(e){o={}}for(var r in t)o[r]=t[r];return o}function l(e){var t={};return(e=e.trim().replace(/^(\?|#|&)/,""))?(e.split("&").forEach(function(e){var n=e.replace(/\+/g," ").split("="),o=Fe(n.shift()),i=n.length>0?Fe(n.join("=")):null;void 0===t[o]?t[o]=i:Array.isArray(t[o])?t[o].push(i):t[o]=[t[o],i]}),t):t}function s(e){var t=e?Object.keys(e).map(function(t){var n=e[t];if(void 0===n)return"";if(null===n)return Be(t);if(Array.isArray(n)){var o=[];return n.forEach(function(e){void 0!==e&&(null===e?o.push(Be(t)):o.push(Be(t)+"="+Be(e)))}),o.join("&")}return Be(t)+"="+Be(n)}).filter(function(e){return e.length>0}).join("&"):null;return t?"?"+t:""}function c(e,t,n,o){var i=o&&o.options.stringifyQuery,r=t.query||{};try{r=u(r)}catch(e){}var a={name:t.name||e&&e.name,meta:e&&e.meta||{},path:t.path||"/",hash:t.hash||"",query:r,params:t.params||{},fullPath:f(t,i),matched:e?d(e):[]};return n&&(a.redirectedFrom=f(n,i)),Object.freeze(a)}function u(e){if(Array.isArray(e))return e.map(u);if(e&&"object"==typeof e){var t={};for(var n in e)t[n]=u(e[n]);return t}return e}function d(e){for(var t=[];e;)t.unshift(e),e=e.parent;return t}function f(e,t){var n=e.path,o=e.query;void 0===o&&(o={});var i=e.hash;void 0===i&&(i="");var r=t||s;return(n||"/")+r(o)+i}function p(e,t){return t===je?e===t:!!t&&(e.path&&t.path?e.path.replace(Ve,"")===t.path.replace(Ve,"")&&e.hash===t.hash&&h(e.query,t.query):!(!e.name||!t.name)&&e.name===t.name&&e.hash===t.hash&&h(e.query,t.query)&&h(e.params,t.params))}function h(e,t){if(void 0===e&&(e={}),void 0===t&&(t={}),!e||!t)return e===t;var n=Object.keys(e),o=Object.keys(t);return n.length===o.length&&n.every(function(n){var o=e[n],i=t[n];return"object"==typeof o&&"object"==typeof i?h(o,i):String(o)===String(i)})}function g(e,t){return 0===e.path.replace(Ve,"/").indexOf(t.path.replace(Ve,"/"))&&(!t.hash||e.hash===t.hash)&&m(e.query,t.query)}function m(e,t){for(var n in t)if(!(n in e))return!1;return!0}function v(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey||e.defaultPrevented||void 0!==e.button&&0!==e.button)){if(e.currentTarget&&e.currentTarget.getAttribute&&/\b_blank\b/i.test(e.currentTarget.getAttribute("target")))return;return e.preventDefault&&e.preventDefault(),!0}}function b(e){if(e)for(var t,n=0;n=0&&(t=e.slice(o),e=e.slice(0,o));var i=e.indexOf("?");return i>=0&&(n=e.slice(i+1),e=e.slice(0,i)),{path:e,query:n,hash:t}}function w(e){return e.replace(/\/\//g,"/")}function k(e,t){for(var n,o=[],i=0,r=0,a="",l=t&&t.delimiter||"/";null!=(n=Je.exec(e));){var s=n[0],c=n[1],u=n.index;if(a+=e.slice(r,u),r=u+s.length,c)a+=c[1];else{var d=e[r],f=n[2],p=n[3],h=n[4],g=n[5],m=n[6],v=n[7];a&&(o.push(a),a="");var b=null!=f&&null!=d&&d!==f,x="+"===m||"*"===m,y="?"===m||"*"===m,_=n[2]||l,w=h||g;o.push({name:p||i++,prefix:f||"",delimiter:_,optional:y,repeat:x,partial:b,asterisk:!!v,pattern:w?E(w):v?".*":"[^"+T(_)+"]+?"})}}return r-1&&(i.params[f]=n.params[f]);if(l)return i.path=N(l.path,i.params,'named route "'+r+'"'),a(l,i,o)}else if(i.path){i.params={};for(var p=0;p=e.length?n():e[i]?t(e[i],function(){o(i+1)}):o(i+1)};o(0)}function ce(e){return function(t,n,i){var r=!1,a=0,l=null;ue(e,function(e,t,n,s){if("function"==typeof e&&void 0===e.cid){r=!0,a++;var c,u=pe(function(t){fe(t)&&(t=t.default),e.resolved="function"==typeof t?t:Pe.extend(t),n.components[s]=t,--a<=0&&i()}),d=pe(function(e){var t="Failed to resolve async component "+s+": "+e;l||(l=o(e)?e:new Error(t),i(l))});try{c=e(u,d)}catch(e){d(e)}if(c)if("function"==typeof c.then)c.then(u,d);else{var f=c.component;f&&"function"==typeof f.then&&f.then(u,d)}}}),r||i()}}function ue(e,t){return de(e.map(function(e){return Object.keys(e.components).map(function(n){return t(e.components[n],e.instances[n],e,n)})}))}function de(e){return Array.prototype.concat.apply([],e)}function fe(e){return e.__esModule||it&&"Module"===e[Symbol.toStringTag]}function pe(e){var t=!1;return function(){for(var n=[],o=arguments.length;o--;)n[o]=arguments[o];if(!t)return t=!0,e.apply(this,n)}}function he(e){if(!e)if(Ge){var t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^https?:\/\/[^\/]+/,"")}else e="/";return"/"!==e.charAt(0)&&(e="/"+e),e.replace(/\/$/,"")}function ge(e,t){var n,o=Math.max(e.length,t.length);for(n=0;n=0?t.slice(0,n):t)+"#"+e}function Ee(e){tt?ae(Te(e)):window.location.hash=e}function Ie(e){tt?le(Te(e)):window.location.replace(Te(e))}function Oe(e,t){return e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function Le(e,t,n){var o="hash"===n?"#"+t:t;return e?w(e+"/"+o):o}var Pe,De={name:"router-view",functional:!0,props:{name:{type:String,default:"default"}},render:function(e,t){var n=t.props,o=t.children,a=t.parent,l=t.data;l.routerView=!0;for(var s=a.$createElement,c=n.name,u=a.$route,d=a._routerViewCache||(a._routerViewCache={}),f=0,p=!1;a&&a._routerRoot!==a;)a.$vnode&&a.$vnode.data.routerView&&f++,a._inactive&&(p=!0),a=a.$parent;if(l.routerViewDepth=f,p)return s(d[c],l,o);var h=u.matched[f];if(!h)return d[c]=null,s();var g=d[c]=h.components[c];l.registerRouteInstance=function(e,t){var n=h.instances[c];(t&&n!==e||!t&&n===e)&&(h.instances[c]=t)},(l.hook||(l.hook={})).prepatch=function(e,t){h.instances[c]=t.componentInstance};var m=l.props=i(u,h.props&&h.props[c]);if(m){m=l.props=r({},m);var v=l.attrs=l.attrs||{};for(var b in m)g.props&&b in g.props||(v[b]=m[b],delete m[b])}return s(g,l,o)}},ze=/[!'()*]/g,Re=function(e){return"%"+e.charCodeAt(0).toString(16)},Ne=/%2C/g,Be=function(e){return encodeURIComponent(e).replace(ze,Re).replace(Ne,",")},Fe=decodeURIComponent,Ve=/\/?$/,je=c(null,{path:"/"}),$e=[String,Object],He=[String,Array],We={name:"router-link",props:{to:{type:$e,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,event:{type:He,default:"click"}},render:function(e){var t=this,n=this.$router,o=this.$route,i=n.resolve(this.to,o,this.append),r=i.location,a=i.route,l=i.href,s={},u=n.options.linkActiveClass,d=n.options.linkExactActiveClass,f=null==u?"router-link-active":u,h=null==d?"router-link-exact-active":d,m=null==this.activeClass?f:this.activeClass,x=null==this.exactActiveClass?h:this.exactActiveClass,y=r.path?c(null,r,null,n):a;s[x]=p(o,y),s[m]=this.exact?s[x]:g(o,y);var _=function(e){v(e)&&(t.replace?n.replace(r):n.push(r))},w={click:v};Array.isArray(this.event)?this.event.forEach(function(e){w[e]=_}):w[this.event]=_;var k={class:s};if("a"===this.tag)k.on=w,k.attrs={href:l};else{var S=b(this.$slots.default);if(S){S.isStatic=!1;var M=Pe.util.extend;(S.data=M({},S.data)).on=w,(S.data.attrs=M({},S.data.attrs)).href=l}else k.on=w}return e(this.tag,k,this.$slots.default)}},Ge="undefined"!=typeof window,Ue=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)},qe=R,Ye=k,Ze=S,Xe=A,Ke=z,Je=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");qe.parse=Ye,qe.compile=Ze,qe.tokensToFunction=Xe,qe.tokensToRegExp=Ke;var Qe=Object.create(null),et=Object.create(null),tt=Ge&&function(){var e=window.navigator.userAgent;return(-1===e.indexOf("Android 2.")&&-1===e.indexOf("Android 4.0")||-1===e.indexOf("Mobile Safari")||-1!==e.indexOf("Chrome")||-1!==e.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history}(),nt=Ge&&window.performance&&window.performance.now?window.performance:Date,ot=oe(),it="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag,rt=function(e,t){this.router=e,this.base=he(t),this.current=je,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]};rt.prototype.listen=function(e){this.cb=e},rt.prototype.onReady=function(e,t){this.ready?e():(this.readyCbs.push(e),t&&this.readyErrorCbs.push(t))},rt.prototype.onError=function(e){this.errorCbs.push(e)},rt.prototype.transitionTo=function(e,t,n){var o=this,i=this.router.match(e,this.current);this.confirmTransition(i,function(){o.updateRoute(i),t&&t(i),o.ensureURL(),o.ready||(o.ready=!0,o.readyCbs.forEach(function(e){e(i)}))},function(e){n&&n(e),e&&!o.ready&&(o.ready=!0,o.readyErrorCbs.forEach(function(t){t(e)}))})},rt.prototype.confirmTransition=function(e,t,n){var i=this,r=this.current,a=function(e){o(e)&&(i.errorCbs.length?i.errorCbs.forEach(function(t){t(e)}):console.error(e)),n&&n(e)};if(p(e,r)&&e.matched.length===r.matched.length)return this.ensureURL(),a();var l=ge(this.current.matched,e.matched),s=l.updated,c=l.deactivated,u=l.activated,d=[].concat(be(c),this.router.beforeHooks,xe(s),u.map(function(e){return e.beforeEnter}),ce(u));this.pending=e;var f=function(t,n){if(i.pending!==e)return a();try{t(e,r,function(e){!1===e||o(e)?(i.ensureURL(!0),a(e)):"string"==typeof e||"object"==typeof e&&("string"==typeof e.path||"string"==typeof e.name)?(a(),"object"==typeof e&&e.replace?i.replace(e):i.push(e)):n(e)})}catch(e){a(e)}};se(d,f,function(){var n=[];se(_e(u,n,function(){return i.current===e}).concat(i.router.resolveHooks),f,function(){if(i.pending!==e)return a();i.pending=null,t(e),i.router.app&&i.router.app.$nextTick(function(){n.forEach(function(e){e()})})})})},rt.prototype.updateRoute=function(e){var t=this.current;this.current=e,this.cb&&this.cb(e),this.router.afterHooks.forEach(function(n){n&&n(e,t)})};var at=function(e){function t(t,n){var o=this;e.call(this,t,n);var i=t.options.scrollBehavior;i&&q();var r=Se(this.base);window.addEventListener("popstate",function(e){var n=o.current,a=Se(o.base);o.current===je&&a===r||o.transitionTo(a,function(e){i&&Y(t,e,n,!0)})})}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.go=function(e){window.history.go(e)},t.prototype.push=function(e,t,n){var o=this,i=this,r=i.current;this.transitionTo(e,function(e){ae(w(o.base+e.fullPath)),Y(o.router,e,r,!1),t&&t(e)},n)},t.prototype.replace=function(e,t,n){var o=this,i=this,r=i.current;this.transitionTo(e,function(e){le(w(o.base+e.fullPath)),Y(o.router,e,r,!1),t&&t(e)},n)},t.prototype.ensureURL=function(e){if(Se(this.base)!==this.current.fullPath){var t=w(this.base+this.current.fullPath);e?ae(t):le(t)}},t.prototype.getCurrentLocation=function(){return Se(this.base)},t}(rt),lt=function(e){function t(t,n,o){e.call(this,t,n),o&&Me(this.base)||Ce()}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.setupListeners=function(){var e=this,t=this.router,n=t.options.scrollBehavior,o=tt&&n;o&&q(),window.addEventListener(tt?"popstate":"hashchange",function(){var t=e.current;Ce()&&e.transitionTo(Ae(),function(n){o&&Y(e.router,n,t,!0),tt||Ie(n.fullPath)})})},t.prototype.push=function(e,t,n){var o=this,i=this,r=i.current;this.transitionTo(e,function(e){Ee(e.fullPath),Y(o.router,e,r,!1),t&&t(e)},n)},t.prototype.replace=function(e,t,n){var o=this,i=this,r=i.current;this.transitionTo(e,function(e){Ie(e.fullPath),Y(o.router,e,r,!1),t&&t(e)},n)},t.prototype.go=function(e){window.history.go(e)},t.prototype.ensureURL=function(e){var t=this.current.fullPath;Ae()!==t&&(e?Ee(t):Ie(t))},t.prototype.getCurrentLocation=function(){return Ae()},t}(rt),st=function(e){function t(t,n){e.call(this,t,n),this.stack=[],this.index=-1}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.push=function(e,t,n){var o=this;this.transitionTo(e,function(e){o.stack=o.stack.slice(0,o.index+1).concat(e),o.index++,t&&t(e)},n)},t.prototype.replace=function(e,t,n){var o=this;this.transitionTo(e,function(e){o.stack=o.stack.slice(0,o.index).concat(e),t&&t(e)},n)},t.prototype.go=function(e){var t=this,n=this.index+e;if(!(n<0||n>=this.stack.length)){var o=this.stack[n];this.confirmTransition(o,function(){t.index=n,t.updateRoute(o)})}},t.prototype.getCurrentLocation=function(){var e=this.stack[this.stack.length-1];return e?e.fullPath:"/"},t.prototype.ensureURL=function(){},t}(rt),ct=function(e){void 0===e&&(e={}),this.app=null,this.apps=[],this.options=e,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=W(e.routes||[],this);var t=e.mode||"hash";switch(this.fallback="history"===t&&!tt&&!1!==e.fallback,this.fallback&&(t="hash"),Ge||(t="abstract"),this.mode=t,t){case"history":this.history=new at(this,e.base);break;case"hash":this.history=new lt(this,e.base,this.fallback);break;case"abstract":this.history=new st(this,e.base)}},ut={currentRoute:{configurable:!0}};ct.prototype.match=function(e,t,n){return this.matcher.match(e,t,n)},ut.currentRoute.get=function(){return this.history&&this.history.current},ct.prototype.init=function(e){var t=this;if(this.apps.push(e),!this.app){this.app=e;var n=this.history;if(n instanceof at)n.transitionTo(n.getCurrentLocation());else if(n instanceof lt){var o=function(){n.setupListeners()};n.transitionTo(n.getCurrentLocation(),o,o)}n.listen(function(e){t.apps.forEach(function(t){t._route=e})})}},ct.prototype.beforeEach=function(e){return Oe(this.beforeHooks,e)},ct.prototype.beforeResolve=function(e){return Oe(this.resolveHooks,e)},ct.prototype.afterEach=function(e){return Oe(this.afterHooks,e)},ct.prototype.onReady=function(e,t){this.history.onReady(e,t)},ct.prototype.onError=function(e){this.history.onError(e)},ct.prototype.push=function(e,t,n){this.history.push(e,t,n)},ct.prototype.replace=function(e,t,n){this.history.replace(e,t,n)},ct.prototype.go=function(e){this.history.go(e)},ct.prototype.back=function(){this.go(-1)},ct.prototype.forward=function(){this.go(1)},ct.prototype.getMatchedComponents=function(e){var t=e?e.matched?e:this.resolve(e).route:this.currentRoute;return t?[].concat.apply([],t.matched.map(function(e){return Object.keys(e.components).map(function(t){return e.components[t]})})):[]},ct.prototype.resolve=function(e,t,n){var o=$(e,t||this.history.current,n,this),i=this.match(o,t),r=i.redirectedFrom||i.fullPath;return{location:o,route:i,href:Le(this.history.base,r,this.mode),normalizedTo:o,resolved:i}},ct.prototype.addRoutes=function(e){this.matcher.addRoutes(e),this.history.current!==je&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(ct.prototype,ut),ct.install=x,ct.version="2.8.1",Ge&&window.Vue&&window.Vue.use(ct),t.a=ct},function(e,t,n){function o(e,t,n){return{type:e,event:n,target:t.target,topTarget:t.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which}}function i(){}function r(e,t,n){if(e[e.rectHover?"rectContain":"contain"](t,n)){for(var o,i=e;i;){if(i.clipPath&&!i.clipPath.contain(t,n))return!1;i.silent&&(o=!0),i=i.parent}return!o||u}return!1}var a=n(0),l=n(7),s=n(687),c=n(49),u="silent";i.prototype.dispose=function(){};var d=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],f=function(e,t,n,o){c.call(this),this.storage=e,this.painter=t,this.painterRoot=o,n=n||new i,this.proxy=n,n.handler=this,this._hovered={},this._lastTouchMoment,this._lastX,this._lastY,s.call(this),a.each(d,function(e){n.on&&n.on(e,this[e],this)},this)};f.prototype={constructor:f,mousemove:function(e){var t=e.zrX,n=e.zrY,o=this._hovered,i=o.target;i&&!i.__zr&&(o=this.findHover(o.x,o.y),i=o.target);var r=this._hovered=this.findHover(t,n),a=r.target,l=this.proxy;l.setCursor&&l.setCursor(a?a.cursor:"default"),i&&a!==i&&this.dispatchToElement(o,"mouseout",e),this.dispatchToElement(r,"mousemove",e),a&&a!==i&&this.dispatchToElement(r,"mouseover",e)},mouseout:function(e){this.dispatchToElement(this._hovered,"mouseout",e);var t,n=e.toElement||e.relatedTarget;do{n=n&&n.parentNode}while(n&&9!=n.nodeType&&!(t=n===this.painterRoot));!t&&this.trigger("globalout",{event:e})},resize:function(e){this._hovered={}},dispatch:function(e,t){var n=this[e];n&&n.call(this,t)},dispose:function(){this.proxy.dispose(),this.storage=this.proxy=this.painter=null},setCursorStyle:function(e){var t=this.proxy;t.setCursor&&t.setCursor(e)},dispatchToElement:function(e,t,n){e=e||{};var i=e.target;if(!i||!i.silent){for(var r="on"+t,a=o(t,e,n);i&&(i[r]&&(a.cancelBubble=i[r].call(i,a)),i.trigger(t,a),i=i.parent,!a.cancelBubble););a.cancelBubble||(this.trigger(t,a),this.painter&&this.painter.eachOtherLayer(function(e){"function"==typeof e[r]&&e[r].call(e,a),e.trigger&&e.trigger(t,a)}))}},findHover:function(e,t,n){for(var o=this.storage.getDisplayList(),i={x:e,y:t},a=o.length-1;a>=0;a--){var l;if(o[a]!==n&&!o[a].ignore&&(l=r(o[a],e,t))&&(!i.topTarget&&(i.topTarget=o[a]),l!==u)){i.target=o[a];break}}return i}},a.each(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],function(e){f.prototype[e]=function(t){var n=this.findHover(t.zrX,t.zrY),o=n.target;if("mousedown"===e)this._downEl=o,this._downPoint=[t.zrX,t.zrY],this._upEl=o;else if("mosueup"===e)this._upEl=o;else if("click"===e){if(this._downEl!==this._upEl||!this._downPoint||l.dist(this._downPoint,[t.zrX,t.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,e,t)}}),a.mixin(f,c),a.mixin(f,s);var p=f;e.exports=p},function(e,t,n){function o(){return!1}function i(e,t,n){var o=r.createCanvas(),i=t.getWidth(),a=t.getHeight(),l=o.style;return l.position="absolute",l.left=0,l.top=0,l.width=i+"px",l.height=a+"px",o.width=i*n,o.height=a*n,o.setAttribute("data-zr-dom-id",e),o}var r=n(0),a=n(95),l=a.devicePixelRatio,s=n(248),c=n(247),u=function(e,t,n){var a;n=n||l,"string"==typeof e?a=i(e,t,n):r.isObject(e)&&(a=e,e=a.id),this.id=e,this.dom=a;var s=a.style;s&&(a.onselectstart=o,s["-webkit-user-select"]="none",s["user-select"]="none",s["-webkit-touch-callout"]="none",s["-webkit-tap-highlight-color"]="rgba(0,0,0,0)",s.padding=0,s.margin=0,s["border-width"]=0),this.domBack=null,this.ctxBack=null,this.painter=t,this.config=null,this.clearColor=0,this.motionBlur=!1,this.lastFrameAlpha=.7,this.dpr=n};u.prototype={constructor:u,elCount:0,__dirty:!0,initContext:function(){this.ctx=this.dom.getContext("2d"),this.ctx.__currentValues={},this.ctx.dpr=this.dpr},createBackBuffer:function(){var e=this.dpr;this.domBack=i("back-"+this.id,this.painter,e),this.ctxBack=this.domBack.getContext("2d"),this.ctxBack.__currentValues={},1!=e&&this.ctxBack.scale(e,e)},resize:function(e,t){var n=this.dpr,o=this.dom,i=o.style,r=this.domBack;i.width=e+"px",i.height=t+"px",o.width=e*n,o.height=t*n,r&&(r.width=e*n,r.height=t*n,1!=n&&this.ctxBack.scale(n,n))},clear:function(e){var t=this.dom,n=this.ctx,o=t.width,i=t.height,r=this.clearColor,a=this.motionBlur&&!e,l=this.lastFrameAlpha,u=this.dpr;if(a&&(this.domBack||this.createBackBuffer(),this.ctxBack.globalCompositeOperation="copy",this.ctxBack.drawImage(t,0,0,o/u,i/u)),n.clearRect(0,0,o,i),r){var d;r.colorStops?(d=r.__canvasGradient||s.getGradient(n,r,{x:0,y:0,width:o,height:i}),r.__canvasGradient=d):r.image&&(d=c.prototype.getCanvasPattern.call(r,n)),n.save(),n.fillStyle=d||r,n.fillRect(0,0,o,i),n.restore()}if(a){var f=this.domBack;n.save(),n.globalAlpha=l,n.drawImage(f,0,0,o,i),n.restore()}}};var d=u;e.exports=d},function(e,t,n){function o(e){return parseInt(e,10)}function i(e){return!!e&&(!!e.__builtin__||"function"==typeof e.resize&&"function"==typeof e.refresh)}function r(e){e.__unusedCount++}function a(e){1==e.__unusedCount&&e.clear()}function l(e,t,n){return y.copy(e.getBoundingRect()),e.transform&&y.applyTransform(e.transform),_.width=t,_.height=n,!y.intersect(_)}function s(e,t){if(e==t)return!1;if(!e||!t||e.length!==t.length)return!0;for(var n=0;n=0&&n.splice(o,1),e.__hoverMir=null},clearHover:function(e){for(var t=this._hoverElements,n=0;n=0){if(!l){if(l=this._progressiveLayers[Math.min(c++,4)],l.ctx.save(),l.renderScope={},l&&l.__progress>l.__maxProgress){g=l.__nextIdxNotProg-1;continue}s=l.__progress,l.__dirty||(f=s),l.__progress=f+1}x===f&&this._doPaintEl(v,l,!0,l.renderScope)}else this._doPaintEl(v,o,t,a);v.__dirty=!1}}l&&n(l),r&&r.restore(),this._furtherProgressive=!1,p.each(this._progressiveLayers,function(e){e.__maxProgress>=e.__progress&&(this._furtherProgressive=!0)},this)},_doPaintEl:function(e,t,n,o){var i=t.ctx,r=e.transform;if((t.__dirty||n)&&!e.invisible&&0!==e.style.opacity&&(!r||r[0]||r[3])&&(!e.culling||!l(e,this._width,this._height))){var a=e.__clipPaths;(o.prevClipLayer!==t||s(a,o.prevElClipPaths))&&(o.prevElClipPaths&&(o.prevClipLayer.ctx.restore(),o.prevClipLayer=o.prevElClipPaths=null,o.prevEl=null),a&&(i.save(),c(a,i),o.prevClipLayer=t,o.prevElClipPaths=a)),e.beforeBrush&&e.beforeBrush(i),e.brush(i,o.prevEl||null),o.prevEl=e,e.afterBrush&&e.afterBrush(i)}},getLayer:function(e){if(this._singleCanvas)return this._layers[0];var t=this._layers[e];return t||(t=new v("zr_"+e,this,this.dpr),t.__builtin__=!0,this._layerConfig[e]&&p.merge(t,this._layerConfig[e],!0),this.insertLayer(e,t),t.initContext()),t},insertLayer:function(e,t){var n=this._layers,o=this._zlevelList,r=o.length,a=null,l=-1,s=this._domRoot;if(n[e])return void h("ZLevel "+e+" has been used already");if(!i(t))return void h("Layer of zlevel "+e+" is not valid");if(r>0&&e>o[0]){for(l=0;le);l++);a=n[o[l]]}if(o.splice(l+1,0,e),n[e]=t,!t.virtual)if(a){var c=a.dom;c.nextSibling?s.insertBefore(t.dom,c.nextSibling):s.appendChild(t.dom)}else s.firstChild?s.insertBefore(t.dom,s.firstChild):s.appendChild(t.dom)},eachLayer:function(e,t){var n,o,i=this._zlevelList;for(o=0;o=0){a!==g&&(a=g,s++);var m=d.__frame=s-1;if(!r){var b=Math.min(l,4);r=n[b],r||(r=n[b]=new v("progressive",this,this.dpr),r.initContext()),r.__maxProgress=0}r.__dirty=r.__dirty||d.__dirty,r.elCount++,r.__maxProgress=Math.max(r.__maxProgress,m),r.__maxProgress>=r.__progress&&(h.__dirty=!0)}else d.__frame=-1,r&&(r.__nextIdxNotProg=c,l++,r=null)}r&&(l++,r.__nextIdxNotProg=c),this.eachBuiltinLayer(function(e,t){o[t]!==e.elCount&&(e.__dirty=!0)}),n.length=Math.min(l,5),p.each(n,function(e,t){i[t]!==e.elCount&&(d.__dirty=!0),e.__dirty&&(e.__progress=0)})},clear:function(){return this.eachBuiltinLayer(this._clearLayer),this},_clearLayer:function(e){e.clear()},configLayer:function(e,t){if(t){var n=this._layerConfig;n[e]?p.merge(n[e],t,!0):n[e]=t;var o=this._layers[e];o&&p.merge(o,n[e],!0)}},delLayer:function(e){var t=this._layers,n=this._zlevelList,o=t[e];o&&(o.dom.parentNode.removeChild(o.dom),delete t[e],n.splice(p.indexOf(n,e),1))},resize:function(e,t){var n=this._domRoot;n.style.display="none";var o=this._opts;if(null!=e&&(o.width=e),null!=t&&(o.height=t),e=this._getSize(0),t=this._getSize(1),n.style.display="",this._width!=e||t!=this._height){n.style.width=e+"px",n.style.height=t+"px";for(var i in this._layers)this._layers.hasOwnProperty(i)&&this._layers[i].resize(e,t);p.each(this._progressiveLayers,function(n){n.resize(e,t)}),this.refresh(!0)}return this._width=e,this._height=t,this},clearLayer:function(e){var t=this._layers[e];t&&t.clear()},dispose:function(){this.root.innerHTML="",this.root=this.storage=this._domRoot=this._layers=null},getRenderedCanvas:function(e){function t(e,t){var o=a._zlevelList;null==e&&(e=-1/0);for(var i,r=0;re&&l=0&&(this.delFromStorage(e),this._roots.splice(r,1),e instanceof a&&e.delChildrenFromStorage(this))}},addToStorage:function(e){return e.__storage=this,e.dirty(!1),this},delFromStorage:function(e){return e&&(e.__storage=null),this},dispose:function(){this._renderList=this._roots=null},displayableSortFunc:o};var c=s;e.exports=c},function(e,t,n){var o=n(0),i=n(31),r=i.Dispatcher,a=n(238),l=n(237),s=function(e){e=e||{},this.stage=e.stage||{},this.onframe=e.onframe||function(){},this._clips=[],this._running=!1,this._time,this._pausedTime,this._pauseStart,this._paused=!1,r.call(this)};s.prototype={constructor:s,addClip:function(e){this._clips.push(e)},addAnimator:function(e){e.animation=this;for(var t=e.getClips(),n=0;n=0&&this._clips.splice(t,1)},removeAnimator:function(e){for(var t=e.getClips(),n=0;nn||f+di&&(i+=a);var h=Math.atan2(u,c);return h<0&&(h+=a),h>=o&&h<=i||h+a>=o&&h+a<=i}var i=n(242),r=i.normalizeRadian,a=2*Math.PI;t.containStroke=o},function(e,t,n){function o(e,t,n,o,r,a,l,s,c,u,d){if(0===c)return!1;var f=c;return!(d>t+f&&d>o+f&&d>a+f&&d>s+f||de+f&&u>n+f&&u>r+f&&u>l+f||ut&&u>o&&u>a&&u>s||u1&&i(),f=b.cubicAt(t,o,a,s,S[0]),g>1&&(p=b.cubicAt(t,o,a,s,S[1]))),2==g?vt&&l>o&&l>r||l=0&&c<=1){for(var u=0,d=b.quadraticAt(t,o,r,c),f=0;fn||l<-n)return 0;var s=Math.sqrt(n*n-l*l);k[0]=-s,k[1]=s;var c=Math.abs(o-i);if(c<1e-4)return 0;if(c%_<1e-4){o=0,i=_;var u=r?1:-1;return a>=k[0]+e&&a<=k[1]+e?u:0}if(r){var s=o;o=v(i),i=v(s)}else o=v(o),i=v(i);o>i&&(i+=_);for(var d=0,f=0;f<2;f++){var p=k[f];if(p+e>a){var h=Math.atan2(l,p),u=r?1:-1;h<0&&(h=_+h),(h>=o&&h<=i||h+_>=o&&h+_<=i)&&(h>Math.PI/2&&h<1.5*Math.PI&&(u=-u),d+=u)}}return d}function s(e,t,n,i,s){for(var c=0,u=0,d=0,m=0,v=0,b=0;b1&&(n||(c+=x(u,d,m,v,i,s))),1==b&&(u=e[b],d=e[b+1],m=u,v=d),_){case y.M:m=e[b++],v=e[b++],u=m,d=v;break;case y.L:if(n){if(f.containStroke(u,d,e[b],e[b+1],t,i,s))return!0}else c+=x(u,d,e[b],e[b+1],i,s)||0;u=e[b++],d=e[b++];break;case y.C:if(n){if(p.containStroke(u,d,e[b++],e[b++],e[b++],e[b++],e[b],e[b+1],t,i,s))return!0}else c+=r(u,d,e[b++],e[b++],e[b++],e[b++],e[b],e[b+1],i,s)||0;u=e[b++],d=e[b++];break;case y.Q:if(n){if(h.containStroke(u,d,e[b++],e[b++],e[b],e[b+1],t,i,s))return!0}else c+=a(u,d,e[b++],e[b++],e[b],e[b+1],i,s)||0;u=e[b++],d=e[b++];break;case y.A:var w=e[b++],k=e[b++],S=e[b++],M=e[b++],C=e[b++],A=e[b++],T=(e[b++],1-e[b++]),E=Math.cos(C)*S+w,I=Math.sin(C)*M+k;b>1?c+=x(u,d,E,I,i,s):(m=E,v=I);var O=(i-w)*M/S+w;if(n){if(g.containStroke(w,k,M,C,C+A,T,t,O,s))return!0}else c+=l(w,k,M,C,C+A,T,O,s);u=Math.cos(C+A)*S+w,d=Math.sin(C+A)*M+k;break;case y.R:m=u=e[b++],v=d=e[b++];var L=e[b++],P=e[b++],E=m+L,I=v+P;if(n){if(f.containStroke(m,v,E,v,t,i,s)||f.containStroke(E,v,E,I,t,i,s)||f.containStroke(E,I,m,I,t,i,s)||f.containStroke(m,I,m,v,t,i,s))return!0}else c+=x(E,v,E,I,i,s),c+=x(m,I,m,v,i,s);break;case y.Z:if(n){if(f.containStroke(u,d,m,v,t,i,s))return!0}else c+=x(u,d,m,v,i,s);u=m,d=v}}return n||o(d,v)||(c+=x(u,d,m,v,i,s)||0),0!==c}function c(e,t,n){return s(e,0,!1,t,n)}function u(e,t,n,o){return s(e,t,!0,n,o)}var d=n(61),f=n(239),p=n(668),h=n(241),g=n(667),m=n(242),v=m.normalizeRadian,b=n(39),x=n(243),y=d.CMD,_=2*Math.PI,w=1e-4,k=[-1,-1,-1],S=[-1,-1];t.contain=c,t.containStroke=u},function(e,t,n){function o(e){var t=e[1][0]-e[0][0],n=e[1][1]-e[0][1];return Math.sqrt(t*t+n*n)}function i(e){return[(e[0][0]+e[1][0])/2,(e[0][1]+e[1][1])/2]}var r=n(31),a=function(){this._track=[]};a.prototype={constructor:a,recognize:function(e,t,n){return this._doTrack(e,t,n),this._recognize(e)},clear:function(){return this._track.length=0,this},_doTrack:function(e,t,n){var o=e.touches;if(o){for(var i={points:[],touches:[],target:t,event:e},a=0,l=o.length;a1&&r&&r.length>1){var l=o(r)/o(a);!isFinite(l)&&(l=1),t.pinchScale=l;var s=i(r);return t.pinchX=s[0],t.pinchY=s[1],{type:"pinch",target:e[0].target,event:t}}}}},s=a;e.exports=s},function(e,t){function n(){}function o(e,t,n,o){for(var i=0,r=t.length,a=0,l=0;i=a&&d+1>=l){for(var f=[],p=0;p=a&&p+1>=l)return o(r,c.components,t,e);u[n]=c}else u[n]=void 0}s++}();if(h)return h}},pushComponent:function(e,t,n){var o=e[e.length-1];o&&o.added===t&&o.removed===n?e[e.length-1]={count:o.count+1,added:t,removed:n}:e.push({count:1,added:t,removed:n})},extractCommon:function(e,t,n,o){for(var i=t.length,r=n.length,a=e.newPos,l=a-o,s=0;a+1n-2?n-1:p+1],d=e[p>n-3?n-1:p+2]);var m=h*h,v=h*m;i.push([o(c[0],g[0],u[0],d[0],h,m,v),o(c[1],g[1],u[1],d[1],h,m,v)])}return i}var r=n(7),a=r.distance;e.exports=i},function(e,t,n){var o=n(16),i=o.extend({type:"arc",shape:{cx:0,cy:0,r:0,startAngle:0,endAngle:2*Math.PI,clockwise:!0},style:{stroke:"#000",fill:null},buildPath:function(e,t){var n=t.cx,o=t.cy,i=Math.max(t.r,0),r=t.startAngle,a=t.endAngle,l=t.clockwise,s=Math.cos(r),c=Math.sin(r);e.moveTo(s*i+n,c*i+o),e.arc(n,o,i,r,a,!l)}});e.exports=i},function(e,t,n){function o(e,t,n){var o=e.cpx2,i=e.cpy2;return null===o||null===i?[(n?f:u)(e.x1,e.cpx1,e.cpx2,e.x2,t),(n?f:u)(e.y1,e.cpy1,e.cpy2,e.y2,t)]:[(n?d:c)(e.x1,e.cpx1,e.x2,t),(n?d:c)(e.y1,e.cpy1,e.y2,t)]}var i=n(16),r=n(7),a=n(39),l=a.quadraticSubdivide,s=a.cubicSubdivide,c=a.quadraticAt,u=a.cubicAt,d=a.quadraticDerivativeAt,f=a.cubicDerivativeAt,p=[],h=i.extend({type:"bezier-curve",shape:{x1:0,y1:0,x2:0,y2:0,cpx1:0,cpy1:0,percent:1},style:{stroke:"#000",fill:null},buildPath:function(e,t){var n=t.x1,o=t.y1,i=t.x2,r=t.y2,a=t.cpx1,c=t.cpy1,u=t.cpx2,d=t.cpy2,f=t.percent;0!==f&&(e.moveTo(n,o),null==u||null==d?(f<1&&(l(n,a,i,f,p),a=p[1],i=p[2],l(o,c,r,f,p),c=p[1],r=p[2]),e.quadraticCurveTo(a,c,i,r)):(f<1&&(s(n,a,u,i,f,p),a=p[1],u=p[2],i=p[3],s(o,c,d,r,f,p),c=p[1],d=p[2],r=p[3]),e.bezierCurveTo(a,c,u,d,i,r)))},pointAt:function(e){return o(this.shape,e,!1)},tangentAt:function(e){var t=o(this.shape,e,!0);return r.normalize(t,t)}});e.exports=h},function(e,t,n){var o=n(16),i=o.extend({type:"circle",shape:{cx:0,cy:0,r:0},buildPath:function(e,t,n){n&&e.moveTo(t.cx+t.r,t.cy),e.arc(t.cx,t.cy,t.r,0,2*Math.PI,!0)}});e.exports=i},function(e,t,n){var o=n(16),i=o.extend({type:"line",shape:{x1:0,y1:0,x2:0,y2:0,percent:1},style:{stroke:"#000",fill:null},buildPath:function(e,t){var n=t.x1,o=t.y1,i=t.x2,r=t.y2,a=t.percent;0!==a&&(e.moveTo(n,o),a<1&&(i=n*(1-a)+i*a,r=o*(1-a)+r*a),e.lineTo(i,r))},pointAt:function(e){var t=this.shape;return[t.x1*(1-e)+t.x2*e,t.y1*(1-e)+t.y2*e]}});e.exports=i},function(e,t,n){var o=n(16),i=n(250),r=o.extend({type:"polygon",shape:{points:null,smooth:!1,smoothConstraint:null},buildPath:function(e,t){i.buildPath(e,t,!0)}});e.exports=r},function(e,t,n){var o=n(16),i=n(250),r=o.extend({type:"polyline",shape:{points:null,smooth:!1,smoothConstraint:null},style:{stroke:"#000",fill:null},buildPath:function(e,t){i.buildPath(e,t,!1)}});e.exports=r},function(e,t,n){var o=n(16),i=n(251),r=o.extend({type:"rect",shape:{r:0,x:0,y:0,width:0,height:0},buildPath:function(e,t){var n=t.x,o=t.y,r=t.width,a=t.height;t.r?i.buildPath(e,t):e.rect(n,o,r,a),e.closePath()}});e.exports=r},function(e,t,n){var o=n(16),i=o.extend({type:"ring",shape:{cx:0,cy:0,r:0,r0:0},buildPath:function(e,t){var n=t.cx,o=t.cy,i=2*Math.PI;e.moveTo(n+t.r,o),e.arc(n,o,t.r,0,i,!1),e.moveTo(n+t.r0,o),e.arc(n,o,t.r0,0,i,!0)}});e.exports=i},function(e,t,n){var o=n(16),i=n(249),r=o.extend({type:"sector",shape:{cx:0,cy:0,r0:0,r:0,startAngle:0,endAngle:2*Math.PI,clockwise:!0},brush:i(o.prototype.brush),buildPath:function(e,t){var n=t.cx,o=t.cy,i=Math.max(t.r0||0,0),r=Math.max(t.r,0),a=t.startAngle,l=t.endAngle,s=t.clockwise,c=Math.cos(a),u=Math.sin(a);e.moveTo(c*i+n,u*i+o),e.lineTo(c*r+n,u*r+o),e.arc(n,o,r,a,l,!s),e.lineTo(Math.cos(l)*i+n,Math.sin(l)*i+o),0!==i&&e.arc(n,o,i,l,a,s),e.closePath()}});e.exports=r},function(e,t,n){var o=n(237),i=n(72),r=n(0),a=r.isString,l=r.isFunction,s=r.isObject,c=r.isArrayLike,u=r.indexOf,d=function(){this.animators=[]};d.prototype={constructor:d,animate:function(e,t){var n,r=!1,a=this,l=this.__zr;if(e){var s=e.split("."),c=a;r="shape"===s[0];for(var d=0,f=s.length;d0&&this.animate(e,!1).when(null==o?500:o,r).delay(i||0),this}};var f=d;e.exports=f},function(e,t){function n(){this.on("mousedown",this._dragStart,this),this.on("mousemove",this._drag,this),this.on("mouseup",this._dragEnd,this),this.on("globalout",this._dragEnd,this)}function o(e,t){return{target:e,topTarget:t&&t.topTarget}}n.prototype={constructor:n,_dragStart:function(e){var t=e.target;t&&t.draggable&&(this._draggingTarget=t,t.dragging=!0,this._x=e.offsetX,this._y=e.offsetY,this.dispatchToElement(o(t,e),"dragstart",e.event))},_drag:function(e){var t=this._draggingTarget;if(t){var n=e.offsetX,i=e.offsetY,r=n-this._x,a=i-this._y;this._x=n,this._y=i,t.drift(r,a,e),this.dispatchToElement(o(t,e),"drag",e.event);var l=this.findHover(n,i,t).target,s=this._dropTarget;this._dropTarget=l,t!==l&&(s&&l!==s&&this.dispatchToElement(o(s,e),"dragleave",e.event),l&&l!==s&&this.dispatchToElement(o(l,e),"dragenter",e.event))}},_dragEnd:function(e){var t=this._draggingTarget;t&&(t.dragging=!1),this.dispatchToElement(o(t,e),"dragend",e.event),this._dropTarget&&this.dispatchToElement(o(this._dropTarget,e),"drop",e.event),this._draggingTarget=null,this._dropTarget=null}};var i=n;e.exports=i},function(e,t,n){function o(e){return parseInt(e,10)}function i(e){return e instanceof v?S:e instanceof b?M:e instanceof x?C:S}function r(e,t){return t&&e&&t.parentNode!==e}function a(e,t,n){if(r(e,t)&&n){var o=n.nextSibling;o?e.insertBefore(t,o):e.appendChild(t)}}function l(e,t){if(r(e,t)){var n=e.firstChild;n?e.insertBefore(t,n):e.appendChild(t)}}function s(e,t){t&&e&&t.parentNode===e&&e.removeChild(t)}function c(e){return e.__textSvgEl}function u(e){return e.__svgEl}function d(e){return function(){m('In SVG mode painter not support method "'+e+'"')}}var f=n(137),p=f.createElement,h=n(0),g=h.each,m=n(72),v=n(16),b=n(73),x=n(74),y=n(671),_=n(690),w=n(689),k=n(138),S=k.path,M=k.image,C=k.text,A=function(e,t,n){this.root=e,this.storage=t,this._opts=n=h.extend({},n||{});var o=p("svg");o.setAttribute("xmlns","http://www.w3.org/2000/svg"),o.setAttribute("version","1.1"),o.setAttribute("baseProfile","full"),o.style["user-select"]="none",o.style.cssText="position:absolute;left:0;top:0;",this.gradientManager=new _(o),this.clipPathManager=new w(o);var i=document.createElement("div");i.style.cssText="overflow:hidden;position:relative",this._svgRoot=o,this._viewport=i,e.appendChild(i),i.appendChild(o),this.resize(n.width,n.height),this._visibleList=[]};A.prototype={constructor:A,getType:function(){return"svg"},getViewportRoot:function(){return this._viewport},getViewportRootOffset:function(){var e=this.getViewportRoot();if(e)return{offsetLeft:e.offsetLeft||0,offsetTop:e.offsetTop||0}},refresh:function(){var e=this.storage.getDisplayList(!0);this._paintList(e)},_paintList:function(e){this.gradientManager.markAllUnused(),this.clipPathManager.markAllUnused();var t,n=this._svgRoot,o=this._visibleList,r=e.length,d=[];for(t=0;t=0;--o)if(t[o]===e)return!0;return!1}),n}return null}return n[0]},resize:function(e,t){var n=this._viewport;n.style.display="none";var o=this._opts;if(null!=e&&(o.width=e),null!=t&&(o.height=t),e=this._getSize(0),t=this._getSize(1),n.style.display="",this._width!==e&&this._height!==t){this._width=e,this._height=t;var i=n.style;i.width=e+"px",i.height=t+"px";var r=this._svgRoot;r.setAttribute("width",e),r.setAttribute("height",t)}},getWidth:function(){return this._width},getHeight:function(){return this._height},_getSize:function(e){var t=this._opts,n=["width","height"][e],i=["clientWidth","clientHeight"][e],r=["paddingLeft","paddingTop"][e],a=["paddingRight","paddingBottom"][e];if(null!=t[n]&&"auto"!==t[n])return parseFloat(t[n]);var l=this.root,s=document.defaultView.getComputedStyle(l);return(l[i]||o(s[n])||o(l.style[n]))-(o(s[r])||0)-(o(s[a])||0)|0},dispose:function(){this.root.innerHTML="",this._svgRoot=this._viewport=this.storage=null},clear:function(){this._viewport&&this.root.removeChild(this._viewport)},pathToSvg:function(){this.refresh();var e=this._svgRoot.outerHTML;return"data:img/svg+xml;utf-8,"+unescape(e)}},g(["getLayer","insertLayer","eachLayer","eachBuiltinLayer","eachOtherLayer","getLayers","modLayer","delLayer","clearLayer","toDataURL","pathToImage"],function(e){A.prototype[e]=d(e)});var T=A;e.exports=T},function(e,t,n){function o(e){i.call(this,e,"clipPath","__clippath_in_use__")}var i=n(253),r=n(0),a=n(24);r.inherits(o,i),o.prototype.update=function(e){var t=this.getSvgElement(e);t&&this.updateDom(t,e.__clipPaths,!1);var n=this.getTextSvgElement(e);n&&this.updateDom(n,e.__clipPaths,!0),this.markUsed(e)},o.prototype.updateDom=function(e,t,n){if(t&&t.length>0){var o,i,r=this.getDefs(!0),l=t[0],s=n?"_textDom":"_dom";l[s]?(i=l[s].getAttribute("id"),o=l[s],r.contains(o)||r.appendChild(o)):(i="zr-clip-"+this.nextId,++this.nextId,o=this.createElement("clipPath"),o.setAttribute("id",i),r.appendChild(o),l[s]=o);var c=this.getSvgProxy(l);if(l.transform&&l.parent.invTransform&&!n){var u=Array.prototype.slice.call(l.transform);a.mul(l.transform,l.parent.invTransform,l.transform),c.brush(l),l.transform=u}else c.brush(l);var d=this.getSvgElement(l);o.appendChild(d.cloneNode()),e.setAttribute("clip-path","url(#"+i+")"),t.length>1&&this.updateDom(o,t.slice(1),n)}else e&&e.setAttribute("clip-path","none")},o.prototype.markUsed=function(e){var t=this;e.__clipPaths&&e.__clipPaths.length>0&&r.each(e.__clipPaths,function(e){e._dom&&i.prototype.markUsed.call(t,e._dom),e._textDom&&i.prototype.markUsed.call(t,e._textDom)})};var l=o;e.exports=l},function(e,t,n){function o(e){i.call(this,e,["linearGradient","radialGradient"],"__gradient_in_use__")}var i=n(253),r=n(0),a=n(72);r.inherits(o,i),o.prototype.addWithoutUpdate=function(e,t){if(t&&t.style){var n=this;r.each(["fill","stroke"],function(o){if(t.style[o]&&("linear"===t.style[o].type||"radial"===t.style[o].type)){var i,r=t.style[o],a=n.getDefs(!0);r._dom?(i=r._dom,a.contains(r._dom)||n.addDom(i)):i=n.add(r),n.markUsed(t);var l=i.getAttribute("id");e.setAttribute(o,"url(#"+l+")")}})}},o.prototype.add=function(e){var t;if("linear"===e.type)t=this.createElement("linearGradient");else{if("radial"!==e.type)return a("Illegal gradient type."),null;t=this.createElement("radialGradient")}return e.id=e.id||this.nextId++,t.setAttribute("id","zr-gradient-"+e.id),this.updateDom(e,t),this.addDom(t),t},o.prototype.update=function(e){var t=this;i.prototype.update.call(this,e,function(){var n=e.type,o=e._dom.tagName;"linear"===n&&"linearGradient"===o||"radial"===n&&"radialGradient"===o?t.updateDom(e,e._dom):(t.removeDom(e),t.add(e))})},o.prototype.updateDom=function(e,t){if("linear"===e.type)t.setAttribute("x1",e.x),t.setAttribute("y1",e.y),t.setAttribute("x2",e.x2),t.setAttribute("y2",e.y2);else{if("radial"!==e.type)return void a("Illegal gradient type.");t.setAttribute("cx",e.x),t.setAttribute("cy",e.y),t.setAttribute("r",e.r)}e.global?t.setAttribute("gradientUnits","userSpaceOnUse"):t.setAttribute("gradientUnits","objectBoundingBox"),t.innerHTML="";for(var n=e.colorStops,o=0,i=n.length;o1&&(a*=p(y),l*=p(y));var _=(i===r?-1:1)*p((a*a*(l*l)-a*a*(v*v)-l*l*(f*f))/(a*a*(v*v)+l*l*(f*f)))||0,w=_*a*v/l,k=_*-l*f/a,S=(e+n)/2+g(d)*w-h(d)*k,M=(t+o)/2+h(d)*w+g(d)*k,C=x([1,0],[(f-w)/a,(v-k)/l]),A=[(f-w)/a,(v-k)/l],T=[(-1*f-w)/a,(-1*v-k)/l],E=x(A,T);b(A,T)<=-1&&(E=m),b(A,T)>=1&&(E=0),0===r&&E>0&&(E-=2*m),1===r&&E<0&&(E+=2*m),u.addData(c,S,M,a,l,C,E,d,r)}function i(e){if(!e)return[];var t,n=e.replace(/-/g," -").replace(/ /g," ").replace(/ /g,",").replace(/,,/g,",");for(t=0;t0&&""===m[0]&&m.shift();for(var v=0;v=0?parseFloat(e)/100*t:parseFloat(e):e},D=function(e,t,n){var o=l.parse(t);n=+n,isNaN(n)&&(n=1),o&&(e.color=E(o[0],o[1],o[2]),e.opacity=n*o[3])},z=function(e){var t=l.parse(e);return[E(t[0],t[1],t[2]),t[3]]},R=function(e,t,n){var o=t.fill;if(null!=o)if(o instanceof m){var i,a=0,l=[0,0],s=0,c=1,u=n.getBoundingRect(),d=u.width,f=u.height;if("linear"===o.type){i="gradient";var p=n.transform,h=[o.x*d,o.y*f],g=[o.x2*d,o.y2*f];p&&(r(h,h,p),r(g,g,p));var v=g[0]-h[0],b=g[1]-h[1];a=180*Math.atan2(v,b)/Math.PI,a<0&&(a+=360),a<1e-6&&(a=0)}else{i="gradientradial";var h=[o.x*d,o.y*f],p=n.transform,x=n.scale,y=d,_=f;l=[(h[0]-u.x)/y,(h[1]-u.y)/_],p&&r(h,h,p),y/=x[0]*M,_/=x[1]*M;var w=S(y,_);s=0/w,c=2*o.r/w-s}var k=o.colorStops.slice();k.sort(function(e,t){return e.offset-t.offset});for(var C=k.length,A=[],T=[],E=0;E=2){var L=A[0][0],P=A[1][0],R=A[0][1]*t.opacity,N=A[1][1]*t.opacity;e.type=i,e.method="none",e.focus="100%",e.angle=a,e.color=L,e.color2=P,e.colors=T.join(","),e.opacity=N,e.opacity2=R}"radial"===i&&(e.focusposition=l.join(","))}else D(e,o,t.opacity)},N=function(e,t){null!=t.lineDash&&(e.dashstyle=t.lineDash.join(" ")),null==t.stroke||t.stroke instanceof m||D(e,t.stroke,t.opacity)},B=function(e,t,n,o){var i="fill"==t,r=e.getElementsByTagName(t)[0];null!=n[t]&&"none"!==n[t]&&(i||!i&&n.lineWidth)?(e[i?"filled":"stroked"]="true",n[t]instanceof m&&O(e,r),r||(r=v.createNode(t)),i?R(r,n,o):N(r,n),I(e,r)):(e[i?"filled":"stroked"]="false",O(e,r))},F=[[],[],[]],V=function(e,t){var n,o,i,a,l,s,c=b.M,u=b.C,d=b.L,f=b.A,p=b.Q,h=[],g=e.data,m=e.len();for(a=0;a.01?$&&(H+=.0125):Math.abs(W-R)<1e-4?$&&Hz?A-=.0125:A+=.0125:$&&WR?S+=.0125:S-=.0125),h.push(G,x(((z-N)*L+I)*M-C),",",x(((R-B)*P+O)*M-C),",",x(((z+N)*L+I)*M-C),",",x(((R+B)*P+O)*M-C),",",x((H*L+I)*M-C),",",x((W*P+O)*M-C),",",x((S*L+I)*M-C),",",x((A*P+O)*M-C)),l=S,s=A;break;case b.R:var U=F[0],q=F[1];U[0]=g[a++],U[1]=g[a++],q[0]=U[0]+g[a++],q[1]=U[1]+g[a++],t&&(r(U,U,t),r(q,q,t)),U[0]=x(U[0]*M-C),q[0]=x(q[0]*M-C),U[1]=x(U[1]*M-C),q[1]=x(q[1]*M-C),h.push(" m ",U[0],",",U[1]," l ",q[0],",",U[1]," l ",q[0],",",q[1]," l ",U[0],",",q[1]);break;case b.Z:h.push(" x ")}if(n>0){h.push(o);for(var Y=0;Y100&&(W=0,H={});var n,o=G.style;try{o.font=e,n=o.fontFamily.split(",")[0]}catch(e){}t={style:o.fontStyle||"normal",variant:o.fontVariant||"normal",weight:o.fontWeight||"normal",size:0|parseFloat(o.fontSize||12),family:n||"Microsoft YaHei"},H[e]=t,W++}return t};s.$override("measureText",function(e,t){var n=v.doc;$||($=n.createElement("div"),$.style.cssText="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;",v.doc.body.appendChild($));try{$.style.font=t}catch(e){}return $.innerHTML="",$.appendChild(n.createTextNode(e)),{width:$.offsetWidth}});for(var q=new a,Y=function(e,t,n,o){var i=this.style;this.__dirty&&c.normalizeTextStyle(i,!0);var a=i.text;if(null!=a&&(a+=""),a){if(i.rich){var l=s.parseRichText(a,i);a=[];for(var u=0;u 0 { - w.Write([]byte(res.Msg)) - } - }() - - _, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile) - if err != nil { - res.Code = 400 - res.Msg = err.Error() - log.Warn("reload frpc proxy config error: %s", res.Msg) - return - } - - if err = svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil { - res.Code = 500 - res.Msg = err.Error() - log.Warn("reload frpc proxy config error: %s", res.Msg) - return - } - log.Info("success reload conf") - return -} - -type StatusResp struct { - TCP []ProxyStatusResp `json:"tcp"` - UDP []ProxyStatusResp `json:"udp"` - HTTP []ProxyStatusResp `json:"http"` - HTTPS []ProxyStatusResp `json:"https"` - STCP []ProxyStatusResp `json:"stcp"` - XTCP []ProxyStatusResp `json:"xtcp"` - SUDP []ProxyStatusResp `json:"sudp"` -} - -type ProxyStatusResp struct { - Name string `json:"name"` - Type string `json:"type"` - Status string `json:"status"` - Err string `json:"err"` - LocalAddr string `json:"local_addr"` - Plugin string `json:"plugin"` - RemoteAddr string `json:"remote_addr"` -} - -type ByProxyStatusResp []ProxyStatusResp - -func (a ByProxyStatusResp) Len() int { return len(a) } -func (a ByProxyStatusResp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByProxyStatusResp) Less(i, j int) bool { return strings.Compare(a[i].Name, a[j].Name) < 0 } - -func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp { - psr := ProxyStatusResp{ - Name: status.Name, - Type: status.Type, - Status: status.Phase, - Err: status.Err, - } - switch cfg := status.Cfg.(type) { - case *config.TCPProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - psr.Plugin = cfg.Plugin - if status.Err != "" { - psr.RemoteAddr = net.JoinHostPort(serverAddr, strconv.Itoa(cfg.RemotePort)) - } else { - psr.RemoteAddr = serverAddr + status.RemoteAddr - } - case *config.UDPProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - if status.Err != "" { - psr.RemoteAddr = net.JoinHostPort(serverAddr, strconv.Itoa(cfg.RemotePort)) - } else { - psr.RemoteAddr = serverAddr + status.RemoteAddr - } - case *config.HTTPProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - psr.Plugin = cfg.Plugin - psr.RemoteAddr = status.RemoteAddr - case *config.HTTPSProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - psr.Plugin = cfg.Plugin - psr.RemoteAddr = status.RemoteAddr - case *config.STCPProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - psr.Plugin = cfg.Plugin - case *config.XTCPProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - psr.Plugin = cfg.Plugin - case *config.SUDPProxyConf: - if cfg.LocalPort != 0 { - psr.LocalAddr = net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - } - psr.Plugin = cfg.Plugin - } - return psr -} - -// GET api/status -func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) { - var ( - buf []byte - res StatusResp - ) - res.TCP = make([]ProxyStatusResp, 0) - res.UDP = make([]ProxyStatusResp, 0) - res.HTTP = make([]ProxyStatusResp, 0) - res.HTTPS = make([]ProxyStatusResp, 0) - res.STCP = make([]ProxyStatusResp, 0) - res.XTCP = make([]ProxyStatusResp, 0) - res.SUDP = make([]ProxyStatusResp, 0) - - log.Info("Http request [/api/status]") - defer func() { - log.Info("Http response [/api/status]") - buf, _ = json.Marshal(&res) - w.Write(buf) - }() - - ps := svr.ctl.pm.GetAllProxyStatus() - for _, status := range ps { - switch status.Type { - case "tcp": - res.TCP = append(res.TCP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - case "udp": - res.UDP = append(res.UDP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - case "http": - res.HTTP = append(res.HTTP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - case "https": - res.HTTPS = append(res.HTTPS, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - case "stcp": - res.STCP = append(res.STCP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - case "xtcp": - res.XTCP = append(res.XTCP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - case "sudp": - res.SUDP = append(res.SUDP, NewProxyStatusResp(status, svr.cfg.ServerAddr)) - } - } - sort.Sort(ByProxyStatusResp(res.TCP)) - sort.Sort(ByProxyStatusResp(res.UDP)) - sort.Sort(ByProxyStatusResp(res.HTTP)) - sort.Sort(ByProxyStatusResp(res.HTTPS)) - sort.Sort(ByProxyStatusResp(res.STCP)) - sort.Sort(ByProxyStatusResp(res.XTCP)) - sort.Sort(ByProxyStatusResp(res.SUDP)) - return -} - -// GET api/config -func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) { - res := GeneralResponse{Code: 200} - - log.Info("Http get request [/api/config]") - defer func() { - log.Info("Http get response [/api/config], code [%d]", res.Code) - w.WriteHeader(res.Code) - if len(res.Msg) > 0 { - w.Write([]byte(res.Msg)) - } - }() - - if svr.cfgFile == "" { - res.Code = 400 - res.Msg = "frpc has no config file path" - log.Warn("%s", res.Msg) - return - } - - content, err := config.GetRenderedConfFromFile(svr.cfgFile) - if err != nil { - res.Code = 400 - res.Msg = err.Error() - log.Warn("load frpc config file error: %s", res.Msg) - return - } - - rows := strings.Split(string(content), "\n") - newRows := make([]string, 0, len(rows)) - for _, row := range rows { - row = strings.TrimSpace(row) - if strings.HasPrefix(row, "token") { - continue - } - newRows = append(newRows, row) - } - res.Msg = strings.Join(newRows, "\n") -} - -// PUT api/config -func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) { - res := GeneralResponse{Code: 200} - - log.Info("Http put request [/api/config]") - defer func() { - log.Info("Http put response [/api/config], code [%d]", res.Code) - w.WriteHeader(res.Code) - if len(res.Msg) > 0 { - w.Write([]byte(res.Msg)) - } - }() - - // get new config content - body, err := io.ReadAll(r.Body) - if err != nil { - res.Code = 400 - res.Msg = fmt.Sprintf("read request body error: %v", err) - log.Warn("%s", res.Msg) - return - } - - if len(body) == 0 { - res.Code = 400 - res.Msg = "body can't be empty" - log.Warn("%s", res.Msg) - return - } - - // get token from origin content - token := "" - b, err := os.ReadFile(svr.cfgFile) - if err != nil { - res.Code = 400 - res.Msg = err.Error() - log.Warn("load frpc config file error: %s", res.Msg) - return - } - content := string(b) - - for _, row := range strings.Split(content, "\n") { - row = strings.TrimSpace(row) - if strings.HasPrefix(row, "token") { - token = row - break - } - } - - tmpRows := make([]string, 0) - for _, row := range strings.Split(string(body), "\n") { - row = strings.TrimSpace(row) - if strings.HasPrefix(row, "token") { - continue - } - tmpRows = append(tmpRows, row) - } - - newRows := make([]string, 0) - if token != "" { - for _, row := range tmpRows { - newRows = append(newRows, row) - if strings.HasPrefix(row, "[common]") { - newRows = append(newRows, token) - } - } - } else { - newRows = tmpRows - } - content = strings.Join(newRows, "\n") - - err = os.WriteFile(svr.cfgFile, []byte(content), 0644) - if err != nil { - res.Code = 500 - res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err) - log.Warn("%s", res.Msg) - return - } -} diff --git a/client/api_router.go b/client/api_router.go new file mode 100644 index 00000000000..82b730460e9 --- /dev/null +++ b/client/api_router.go @@ -0,0 +1,87 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 client + +import ( + "net/http" + + adminapi "github.com/fatedier/frp/client/http" + "github.com/fatedier/frp/client/proxy" + httppkg "github.com/fatedier/frp/pkg/util/http" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) { + apiController := newAPIController(svr) + + // Healthz endpoint without auth + helper.Router.HandleFunc("/healthz", healthz) + + // API routes and static files with auth + subRouter := helper.Router.NewRoute().Subrouter() + subRouter.Use(helper.AuthMiddleware) + subRouter.Use(httppkg.NewRequestLogger) + subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost) + subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut) + subRouter.HandleFunc("/api/proxy/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetProxyConfig)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/visitor/{name}/config", httppkg.MakeHTTPHandlerFunc(apiController.GetVisitorConfig)).Methods(http.MethodGet) + + if svr.storeSource != nil { + subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost) + subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut) + subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete) + subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost) + subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet) + subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut) + subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete) + } + + subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") + subRouter.PathPrefix("/static/").Handler( + netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), + ).Methods("GET") + subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/", http.StatusMovedPermanently) + }) +} + +func healthz(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func newAPIController(svr *Service) *adminapi.Controller { + manager := newServiceConfigManager(svr) + return adminapi.NewController(adminapi.ControllerParams{ + ServerAddr: svr.common.ServerAddr, + Manager: manager, + }) +} + +// getAllProxyStatus returns all proxy statuses. +func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus { + svr.ctlMu.RLock() + ctl := svr.ctl + svr.ctlMu.RUnlock() + if ctl == nil { + return nil + } + return ctl.pm.GetAllProxyStatus() +} diff --git a/client/config_manager.go b/client/config_manager.go new file mode 100644 index 00000000000..24512e9fbb9 --- /dev/null +++ b/client/config_manager.go @@ -0,0 +1,464 @@ +package client + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/proxy" + "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/util/log" +) + +type serviceConfigManager struct { + svr *Service +} + +func newServiceConfigManager(svr *Service) configmgmt.ConfigManager { + return &serviceConfigManager{svr: svr} +} + +func (m *serviceConfigManager) ReloadFromFile(strict bool) error { + if m.svr.configFilePath == "" { + return fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument) + } + + result, err := config.LoadClientConfigResult(m.svr.configFilePath, strict) + if err != nil { + return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err) + } + + proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers( + result.Common, + result.Proxies, + result.Visitors, + ) + proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation) + visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation) + + if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, m.svr.unsafeFeatures); err != nil { + return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err) + } + + if err := m.svr.UpdateConfigSource(result.Common, result.Proxies, result.Visitors); err != nil { + return fmt.Errorf("%w: %v", configmgmt.ErrApplyConfig, err) + } + + log.Infof("success reload conf") + return nil +} + +func (m *serviceConfigManager) ReadConfigFile() (string, error) { + if m.svr.configFilePath == "" { + return "", fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument) + } + + content, err := os.ReadFile(m.svr.configFilePath) + if err != nil { + return "", fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err) + } + return string(content), nil +} + +func (m *serviceConfigManager) WriteConfigFile(content []byte) error { + if len(content) == 0 { + return fmt.Errorf("%w: body can't be empty", configmgmt.ErrInvalidArgument) + } + + if err := os.WriteFile(m.svr.configFilePath, content, 0o600); err != nil { + return err + } + return nil +} + +func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus { + return m.svr.getAllProxyStatus() +} + +func (m *serviceConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) { + // Try running proxy manager first + ws, ok := m.svr.getProxyStatus(name) + if ok { + return ws.Cfg, true + } + + // Fallback to store + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource != nil { + cfg := storeSource.GetProxy(name) + if cfg != nil { + return cfg, true + } + } + return nil, false +} + +func (m *serviceConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) { + // Try running visitor manager first + cfg, ok := m.svr.getVisitorCfg(name) + if ok { + return cfg, true + } + + // Fallback to store + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource != nil { + vcfg := storeSource.GetVisitor(name) + if vcfg != nil { + return vcfg, true + } + } + return nil, false +} + +func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool { + if name == "" { + return false + } + + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource == nil { + return false + } + + cfg := storeSource.GetProxy(name) + if cfg == nil { + return false + } + enabled := cfg.GetBaseConfig().Enabled + return enabled == nil || *enabled +} + +func (m *serviceConfigManager) StoreEnabled() bool { + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + return storeSource != nil +} + +func (m *serviceConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) { + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + return storeSource.GetAllProxies() +} + +func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + + cfg := storeSource.GetProxy(name) + if cfg == nil { + return nil, fmt.Errorf("%w: proxy %q", configmgmt.ErrNotFound, name) + } + return cfg, nil +} + +func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + if err := m.validateStoreProxyConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + name := cfg.GetBaseConfig().Name + persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error { + if err := storeSource.AddProxy(cfg); err != nil { + if errors.Is(err, source.ErrAlreadyExists) { + return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + log.Infof("store: created proxy %q", name) + return persisted, nil +} + +func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + if cfg == nil { + return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument) + } + bodyName := cfg.GetBaseConfig().Name + if bodyName != name { + return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument) + } + if err := m.validateStoreProxyConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error { + if err := storeSource.UpdateProxy(cfg); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Infof("store: updated proxy %q", name) + return persisted, nil +} + +func (m *serviceConfigManager) DeleteStoreProxy(name string) error { + if name == "" { + return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.RemoveProxy(name); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: deleted proxy %q", name) + return nil +} + +func (m *serviceConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) { + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + return storeSource.GetAllVisitors() +} + +func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + + storeSource, err := m.storeSourceOrError() + if err != nil { + return nil, err + } + + cfg := storeSource.GetVisitor(name) + if cfg == nil { + return nil, fmt.Errorf("%w: visitor %q", configmgmt.ErrNotFound, name) + } + return cfg, nil +} + +func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + if err := m.validateStoreVisitorConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + name := cfg.GetBaseConfig().Name + persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error { + if err := storeSource.AddVisitor(cfg); err != nil { + if errors.Is(err, source.ErrAlreadyExists) { + return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Infof("store: created visitor %q", name) + return persisted, nil +} + +func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + if name == "" { + return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + if cfg == nil { + return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument) + } + bodyName := cfg.GetBaseConfig().Name + if bodyName != name { + return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument) + } + if err := m.validateStoreVisitorConfigurer(cfg); err != nil { + return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err) + } + + persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error { + if err := storeSource.UpdateVisitor(cfg); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + log.Infof("store: updated visitor %q", name) + return persisted, nil +} + +func (m *serviceConfigManager) DeleteStoreVisitor(name string) error { + if name == "" { + return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument) + } + + if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error { + if err := storeSource.RemoveVisitor(name); err != nil { + if errors.Is(err, source.ErrNotFound) { + return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err) + } + return err + } + return nil + }); err != nil { + return err + } + + log.Infof("store: deleted visitor %q", name) + return nil +} + +func (m *serviceConfigManager) GracefulClose(d time.Duration) { + m.svr.GracefulClose(d) +} + +func (m *serviceConfigManager) storeSourceOrError() (*source.StoreSource, error) { + m.svr.reloadMu.Lock() + storeSource := m.svr.storeSource + m.svr.reloadMu.Unlock() + + if storeSource == nil { + return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + return storeSource, nil +} + +func (m *serviceConfigManager) withStoreMutationAndReload( + fn func(storeSource *source.StoreSource) error, +) error { + m.svr.reloadMu.Lock() + defer m.svr.reloadMu.Unlock() + + storeSource := m.svr.storeSource + if storeSource == nil { + return fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + + if err := fn(storeSource); err != nil { + return err + } + + if err := m.svr.reloadConfigFromSourcesLocked(); err != nil { + return fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err) + } + return nil +} + +func (m *serviceConfigManager) withStoreProxyMutationAndReload( + name string, + fn func(storeSource *source.StoreSource) error, +) (v1.ProxyConfigurer, error) { + m.svr.reloadMu.Lock() + defer m.svr.reloadMu.Unlock() + + storeSource := m.svr.storeSource + if storeSource == nil { + return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + + if err := fn(storeSource); err != nil { + return nil, err + } + if err := m.svr.reloadConfigFromSourcesLocked(); err != nil { + return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err) + } + + persisted := storeSource.GetProxy(name) + if persisted == nil { + return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name) + } + return persisted.Clone(), nil +} + +func (m *serviceConfigManager) withStoreVisitorMutationAndReload( + name string, + fn func(storeSource *source.StoreSource) error, +) (v1.VisitorConfigurer, error) { + m.svr.reloadMu.Lock() + defer m.svr.reloadMu.Unlock() + + storeSource := m.svr.storeSource + if storeSource == nil { + return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled) + } + + if err := fn(storeSource); err != nil { + return nil, err + } + if err := m.svr.reloadConfigFromSourcesLocked(); err != nil { + return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err) + } + + persisted := storeSource.GetVisitor(name) + if persisted == nil { + return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name) + } + return persisted.Clone(), nil +} + +func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error { + if cfg == nil { + return fmt.Errorf("invalid proxy config") + } + runtimeCfg := cfg.Clone() + if runtimeCfg == nil { + return fmt.Errorf("invalid proxy config") + } + runtimeCfg.Complete() + return validation.ValidateProxyConfigurerForClient(runtimeCfg) +} + +func (m *serviceConfigManager) validateStoreVisitorConfigurer(cfg v1.VisitorConfigurer) error { + if cfg == nil { + return fmt.Errorf("invalid visitor config") + } + runtimeCfg := cfg.Clone() + if runtimeCfg == nil { + return fmt.Errorf("invalid visitor config") + } + runtimeCfg.Complete() + return validation.ValidateVisitorConfigurer(runtimeCfg) +} diff --git a/client/config_manager_test.go b/client/config_manager_test.go new file mode 100644 index 00000000000..2758b2d754d --- /dev/null +++ b/client/config_manager_test.go @@ -0,0 +1,391 @@ +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 { + return &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: name, + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 10080, + }, + }, + } +} + +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"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + if err := storeSource.AddProxy(newTestRawTCPProxyConfig("p1")); err != nil { + t.Fatalf("seed proxy: %v", err) + } + + agg := source.NewAggregator(source.NewConfigSource()) + agg.SetStoreSource(storeSource) + + mgr := &serviceConfigManager{ + svr: &Service{ + aggregator: agg, + configSource: agg.ConfigSource(), + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + _, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + if err == nil { + t.Fatal("expected conflict error") + } + if !errors.Is(err, configmgmt.ErrConflict) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + + mgr := &serviceConfigManager{ + svr: &Service{ + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + _, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + if err == nil { + t.Fatal("expected apply config error") + } + if !errors.Is(err, configmgmt.ErrApplyConfig) { + t.Fatalf("unexpected error: %v", err) + } + if storeSource.GetProxy("p1") == nil { + t.Fatal("proxy should remain in store after reload failure") + } +} + +func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) { + mgr := &serviceConfigManager{ + svr: &Service{ + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + _, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1")) + if err == nil { + t.Fatal("expected store disabled error") + } + if !errors.Is(err, configmgmt.ErrStoreDisabled) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + agg := source.NewAggregator(source.NewConfigSource()) + agg.SetStoreSource(storeSource) + + mgr := &serviceConfigManager{ + svr: &Service{ + aggregator: agg, + configSource: agg.ConfigSource(), + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + }, + } + + persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy")) + if err != nil { + t.Fatalf("create store proxy: %v", err) + } + if persisted == nil { + t.Fatal("expected persisted proxy to be returned") + } + + got := storeSource.GetProxy("raw-proxy") + if got == nil { + t.Fatal("proxy not found in store") + } + if got.GetBaseConfig().LocalIP != "" { + t.Fatalf("localIP was persisted with runtime default: %q", got.GetBaseConfig().LocalIP) + } + if got.GetBaseConfig().Transport.BandwidthLimitMode != "" { + t.Fatalf("bandwidthLimitMode was persisted with runtime default: %q", got.GetBaseConfig().Transport.BandwidthLimitMode) + } +} diff --git a/client/configmgmt/types.go b/client/configmgmt/types.go new file mode 100644 index 00000000000..5a51a5e5f0f --- /dev/null +++ b/client/configmgmt/types.go @@ -0,0 +1,45 @@ +package configmgmt + +import ( + "errors" + "time" + + "github.com/fatedier/frp/client/proxy" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +var ( + ErrInvalidArgument = errors.New("invalid argument") + ErrNotFound = errors.New("not found") + ErrConflict = errors.New("conflict") + ErrStoreDisabled = errors.New("store disabled") + ErrApplyConfig = errors.New("apply config failed") +) + +type ConfigManager interface { + ReloadFromFile(strict bool) error + + ReadConfigFile() (string, error) + WriteConfigFile(content []byte) error + + GetProxyStatus() []*proxy.WorkingStatus + IsStoreProxyEnabled(name string) bool + StoreEnabled() bool + + GetProxyConfig(name string) (v1.ProxyConfigurer, bool) + GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) + + ListStoreProxies() ([]v1.ProxyConfigurer, error) + GetStoreProxy(name string) (v1.ProxyConfigurer, error) + CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) + UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) + DeleteStoreProxy(name string) error + + ListStoreVisitors() ([]v1.VisitorConfigurer, error) + GetStoreVisitor(name string) (v1.VisitorConfigurer, error) + CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) + UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) + DeleteStoreVisitor(name string) error + + GracefulClose(d time.Duration) +} diff --git a/client/connector.go b/client/connector.go new file mode 100644 index 00000000000..34b24f0d2f4 --- /dev/null +++ b/client/connector.go @@ -0,0 +1,263 @@ +// Copyright 2023 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 client + +import ( + "context" + "crypto/tls" + "net" + "strconv" + "strings" + "sync" + "time" + + libnet "github.com/fatedier/golib/net" + fmux "github.com/hashicorp/yamux" + quic "github.com/quic-go/quic-go" + "github.com/samber/lo" + + 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/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/xlog" +) + +// Connector is an interface for establishing connections to the server. +type Connector interface { + Open() error + Connect() (net.Conn, error) + Close() error +} + +type MessageConnector interface { + Connect() (*msg.Conn, error) + Close() error +} + +type messageConnector struct { + connector Connector + wireProtocol string +} + +func newMessageConnector(connector Connector, wireProtocol string) *messageConnector { + return &messageConnector{ + connector: connector, + wireProtocol: wireProtocol, + } +} + +func (c *messageConnector) Connect() (*msg.Conn, error) { + conn, err := c.connector.Connect() + if err != nil { + return nil, err + } + if err = wire.WriteMagicIfV2(conn, c.wireProtocol); err != nil { + conn.Close() + return nil, err + } + return msg.NewConn(conn, msg.NewReadWriter(conn, c.wireProtocol)), nil +} + +func (c *messageConnector) Close() error { + return c.connector.Close() +} + +// defaultConnectorImpl is the default implementation of Connector for normal frpc. +type defaultConnectorImpl struct { + ctx context.Context + cfg *v1.ClientCommonConfig + + muxSession *fmux.Session + quicConn *quic.Conn + closeOnce sync.Once +} + +func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector { + return &defaultConnectorImpl{ + ctx: ctx, + cfg: cfg, + } +} + +// Open opens an underlying connection to the server. +// The underlying connection is either a TCP connection or a QUIC connection. +// After the underlying connection is established, you can call Connect() to get a stream. +// If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect(). +func (c *defaultConnectorImpl) Open() error { + xl := xlog.FromContextSafe(c.ctx) + + // special for quic + if strings.EqualFold(c.cfg.Transport.Protocol, "quic") { + var tlsConfig *tls.Config + var err error + sn := c.cfg.Transport.TLS.ServerName + if sn == "" { + sn = c.cfg.ServerAddr + } + if lo.FromPtr(c.cfg.Transport.TLS.Enable) { + tlsConfig, err = transport.NewClientTLSConfig( + c.cfg.Transport.TLS.CertFile, + c.cfg.Transport.TLS.KeyFile, + c.cfg.Transport.TLS.TrustedCaFile, + sn) + } else { + tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn) + } + if err != nil { + xl.Warnf("fail to build tls configuration, err: %v", err) + return err + } + tlsConfig.NextProtos = []string{"frp"} + + conn, err := quic.DialAddr( + c.ctx, + net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)), + tlsConfig, &quic.Config{ + MaxIdleTimeout: time.Duration(c.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(c.cfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(c.cfg.Transport.QUIC.KeepalivePeriod) * time.Second, + }) + if err != nil { + return err + } + c.quicConn = conn + return nil + } + + if !lo.FromPtr(c.cfg.Transport.TCPMux) { + return nil + } + + conn, err := c.realConnect() + if err != nil { + return err + } + + fmuxCfg := fmux.DefaultConfig() + fmuxCfg.KeepAliveInterval = time.Duration(c.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second + // Use trace level for yamux logs + fmuxCfg.LogOutput = xlog.NewTraceWriter(xl) + fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 + session, err := fmux.Client(conn, fmuxCfg) + if err != nil { + conn.Close() + return err + } + c.muxSession = session + return nil +} + +// Connect returns a stream from the underlying connection, or a new TCP connection if TCPMux isn't enabled. +func (c *defaultConnectorImpl) Connect() (net.Conn, error) { + if c.quicConn != nil { + stream, err := c.quicConn.OpenStreamSync(context.Background()) + if err != nil { + return nil, err + } + return netpkg.QuicStreamToNetConn(stream, c.quicConn), nil + } else if c.muxSession != nil { + stream, err := c.muxSession.OpenStream() + if err != nil { + return nil, err + } + return stream, nil + } + + return c.realConnect() +} + +func (c *defaultConnectorImpl) realConnect() (net.Conn, error) { + xl := xlog.FromContextSafe(c.ctx) + var tlsConfig *tls.Config + var err error + tlsEnable := lo.FromPtr(c.cfg.Transport.TLS.Enable) + if c.cfg.Transport.Protocol == "wss" { + tlsEnable = true + } + if tlsEnable { + sn := c.cfg.Transport.TLS.ServerName + if sn == "" { + sn = c.cfg.ServerAddr + } + + tlsConfig, err = transport.NewClientTLSConfig( + c.cfg.Transport.TLS.CertFile, + c.cfg.Transport.TLS.KeyFile, + c.cfg.Transport.TLS.TrustedCaFile, + sn) + if err != nil { + xl.Warnf("fail to build tls configuration, err: %v", err) + return nil, err + } + } + + proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL) + if err != nil { + xl.Errorf("fail to parse proxy url") + return nil, err + } + dialOptions := []libnet.DialOption{} + protocol := c.cfg.Transport.Protocol + switch protocol { + case "websocket": + protocol = "tcp" + dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")})) + dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{ + Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)), + })) + dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig)) + case "wss": + protocol = "tcp" + dialOptions = append(dialOptions, libnet.WithTLSConfigAndPriority(100, tlsConfig)) + // Make sure that if it is wss, the websocket hook is executed after the tls hook. + dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110})) + default: + dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{ + Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)), + })) + dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig)) + } + + if c.cfg.Transport.ConnectServerLocalIP != "" { + dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP)) + } + dialOptions = append(dialOptions, + libnet.WithProtocol(protocol), + libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second), + libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second), + libnet.WithProxy(proxyType, addr), + libnet.WithProxyAuth(auth), + ) + conn, err := libnet.DialContext( + c.ctx, + net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)), + dialOptions..., + ) + return conn, err +} + +func (c *defaultConnectorImpl) Close() error { + c.closeOnce.Do(func() { + if c.quicConn != nil { + _ = c.quicConn.CloseWithError(0, "") + } + if c.muxSession != nil { + _ = c.muxSession.Close() + } + }) + return nil +} diff --git a/client/control.go b/client/control.go index ccec2feda7e..4a354ff6211 100644 --- a/client/control.go +++ b/client/control.go @@ -16,173 +16,195 @@ package client import ( "context" - "crypto/tls" - "io" - "math" "net" - "runtime/debug" - "strconv" - "sync" + "sync/atomic" "time" "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/client/visitor" "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" "github.com/fatedier/frp/pkg/transport" - frpNet "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" - - "github.com/fatedier/golib/control/shutdown" - "github.com/fatedier/golib/crypto" - libdial "github.com/fatedier/golib/net/dial" - fmux "github.com/hashicorp/yamux" + "github.com/fatedier/frp/pkg/vnet" ) +type SessionContext struct { + // The client common configuration. + Common *v1.ClientCommonConfig + + // Unique ID obtained from frps. + // It should be attached to the login message when reconnecting. + RunID string + // Underlying control connection. Once conn is closed, the msgDispatcher and the entire Control will exit. + Conn *msg.Conn + // Auth runtime used for login, heartbeats, and encryption. + Auth *auth.ClientAuth + // Connector is used to create message connections to frps. + Connector MessageConnector + // Virtual net controller + VnetController *vnet.Controller + // UDPPacketCodec is immutable for the lifetime of this negotiated session. + UDPPacketCodec string +} + type Control struct { - // uniq id got from frps, attach it in loginMsg - runID string + // service context + ctx context.Context + xl *xlog.Logger + + // session context + sessionCtx *SessionContext // manage all proxies - pxyCfgs map[string]config.ProxyConf - pm *proxy.Manager + pm *proxy.Manager // manage all visitors - vm *VisitorManager - - // control connection - conn net.Conn - - // tcp stream multiplexing, if enabled - session *fmux.Session - - // put a message in this channel to send it over control connection to server - sendCh chan (msg.Message) - - // read from this channel to get the next message sent by server - readCh chan (msg.Message) - - // goroutines can block by reading from this channel, it will be closed only in reader() when control connection is closed - closedCh chan struct{} - - closedDoneCh chan struct{} + vm *visitor.Manager - // last time got the Pong message - lastPong time.Time + doneCh chan struct{} - // The client configuration - clientCfg config.ClientCommonConf + // of time.Time, last time got the Pong message + lastPong atomic.Value - readerShutdown *shutdown.Shutdown - writerShutdown *shutdown.Shutdown - msgHandlerShutdown *shutdown.Shutdown + // The role of msgTransporter is similar to HTTP2. + // It allows multiple messages to be sent simultaneously on the same control connection. + // The server's response messages will be dispatched to the corresponding waiting goroutines based on the laneKey and message type. + msgTransporter transport.MessageTransporter - // The UDP port that the server is listening on - serverUDPPort int - - mu sync.RWMutex - - xl *xlog.Logger - - // service context - ctx context.Context - - // sets authentication based on selected method - authSetter auth.Setter + // msgDispatcher is a wrapper for control connection. + // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types. + msgDispatcher *msg.Dispatcher } -func NewControl(ctx context.Context, runID string, conn net.Conn, session *fmux.Session, - clientCfg config.ClientCommonConf, - pxyCfgs map[string]config.ProxyConf, - visitorCfgs map[string]config.VisitorConf, - serverUDPPort int, - authSetter auth.Setter) *Control { - +func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { // new xlog instance ctl := &Control{ - runID: runID, - conn: conn, - session: session, - pxyCfgs: pxyCfgs, - sendCh: make(chan msg.Message, 100), - readCh: make(chan msg.Message, 100), - closedCh: make(chan struct{}), - closedDoneCh: make(chan struct{}), - clientCfg: clientCfg, - readerShutdown: shutdown.New(), - writerShutdown: shutdown.New(), - msgHandlerShutdown: shutdown.New(), - serverUDPPort: serverUDPPort, - xl: xlog.FromContextSafe(ctx), - ctx: ctx, - authSetter: authSetter, + ctx: ctx, + xl: xlog.FromContextSafe(ctx), + sessionCtx: sessionCtx, + doneCh: make(chan struct{}), } - ctl.pm = proxy.NewManager(ctl.ctx, ctl.sendCh, clientCfg, serverUDPPort) - - ctl.vm = NewVisitorManager(ctl.ctx, ctl) - ctl.vm.Reload(visitorCfgs) - return ctl + ctl.lastPong.Store(time.Now()) + + ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn) + ctl.registerMsgHandlers() + ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher) + + 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, sessionCtx.UDPPacketCodec) + return ctl, nil } -func (ctl *Control) Run() { +func (ctl *Control) Run(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) { go ctl.worker() // start all proxies - ctl.pm.Reload(ctl.pxyCfgs) + ctl.pm.UpdateAll(proxyCfgs) // start all visitors - go ctl.vm.Run() - return + ctl.vm.UpdateAll(visitorCfgs) +} + +func (ctl *Control) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + ctl.pm.SetInWorkConnCallback(cb) } -func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) { +func (ctl *Control) handleReqWorkConn(_ msg.Message) { xl := ctl.xl workConn, err := ctl.connectServer() if err != nil { + xl.Warnf("start new connection to server error: %v", err) return } m := &msg.NewWorkConn{ - RunID: ctl.runID, + RunID: ctl.sessionCtx.RunID, } - if err = ctl.authSetter.SetNewWorkConn(m); err != nil { - xl.Warn("error during NewWorkConn authentication: %v", err) + if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil { + xl.Warnf("error during NewWorkConn authentication: %v", err) + workConn.Close() return } - if err = msg.WriteMsg(workConn, m); err != nil { - xl.Warn("work connection write to server error: %v", err) + if err = workConn.WriteMsg(m); err != nil { + xl.Warnf("work connection write to server error: %v", err) workConn.Close() return } var startMsg msg.StartWorkConn - if err = msg.ReadMsgInto(workConn, &startMsg); err != nil { - xl.Error("work connection closed before response StartWorkConn message: %v", err) + if err = workConn.ReadMsgInto(&startMsg); err != nil { + xl.Tracef("work connection closed before response StartWorkConn message: %v", err) workConn.Close() return } if startMsg.Error != "" { - xl.Error("StartWorkConn contains error: %s", startMsg.Error) + xl.Errorf("StartWorkConn contains error: %s", startMsg.Error) workConn.Close() return } + startMsg.ProxyName = naming.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName) + // dispatch this work connection to related proxy ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) } -func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { +func (ctl *Control) handleNewProxyResp(m msg.Message) { xl := ctl.xl + inMsg := m.(*msg.NewProxyResp) // Server will return NewProxyResp message to each NewProxy message. // Start a new proxy handler if no error got - err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) + proxyName := naming.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName) + err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error) if err != nil { - xl.Warn("[%s] start error: %v", inMsg.ProxyName, err) + xl.Warnf("[%s] start error: %v", proxyName, err) } else { - xl.Info("[%s] start proxy success", inMsg.ProxyName) + xl.Infof("[%s] start proxy success", proxyName) } } +func (ctl *Control) handleNatHoleResp(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.NatHoleResp) + + // Dispatch the NatHoleResp message to the related proxy. + ok := ctl.msgTransporter.DispatchWithType(inMsg, msg.TypeNameNatHoleResp, inMsg.TransactionID) + if !ok { + xl.Tracef("dispatch NatHoleResp message to related proxy error") + } +} + +func (ctl *Control) handlePong(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.Pong) + + if inMsg.Error != "" { + xl.Errorf("pong message contains error: %s", inMsg.Error) + ctl.closeSession() + return + } + ctl.lastPong.Store(time.Now()) + xl.Debugf("receive heartbeat from server") +} + +// closeSession closes the control connection. +func (ctl *Control) closeSession() { + ctl.sessionCtx.Conn.Close() + ctl.sessionCtx.Connector.Close() +} + func (ctl *Control) Close() error { return ctl.GracefulClose(0) } @@ -193,256 +215,84 @@ func (ctl *Control) GracefulClose(d time.Duration) error { time.Sleep(d) - ctl.conn.Close() - if ctl.session != nil { - ctl.session.Close() - } + ctl.closeSession() return nil } -// ClosedDoneCh returns a channel which will be closed after all resources are released -func (ctl *Control) ClosedDoneCh() <-chan struct{} { - return ctl.closedDoneCh +// Done returns a channel that will be closed after all resources are released +func (ctl *Control) Done() <-chan struct{} { + return ctl.doneCh } // connectServer return a new connection to frps -func (ctl *Control) connectServer() (conn net.Conn, err error) { - xl := ctl.xl - if ctl.clientCfg.TCPMux { - stream, errRet := ctl.session.OpenStream() - if errRet != nil { - err = errRet - xl.Warn("start new connection to server error: %v", err) - return - } - conn = stream - } else { - var tlsConfig *tls.Config - sn := ctl.clientCfg.TLSServerName - if sn == "" { - sn = ctl.clientCfg.ServerAddr - } +func (ctl *Control) connectServer() (*msg.Conn, error) { + return ctl.sessionCtx.Connector.Connect() +} - if ctl.clientCfg.TLSEnable { - tlsConfig, err = transport.NewClientTLSConfig( - ctl.clientCfg.TLSCertFile, - ctl.clientCfg.TLSKeyFile, - ctl.clientCfg.TLSTrustedCaFile, - sn) +func (ctl *Control) registerMsgHandlers() { + ctl.msgDispatcher.RegisterHandler(&msg.ReqWorkConn{}, msg.AsyncHandler(ctl.handleReqWorkConn)) + ctl.msgDispatcher.RegisterHandler(&msg.NewProxyResp{}, ctl.handleNewProxyResp) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleResp{}, ctl.handleNatHoleResp) + ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong) +} - if err != nil { - xl.Warn("fail to build tls configuration when connecting to server, err: %v", err) - return - } - } +// heartbeatWorker sends heartbeat to server and check heartbeat timeout. +func (ctl *Control) heartbeatWorker() { + xl := ctl.xl - proxyType, addr, auth, err := libdial.ParseProxyURL(ctl.clientCfg.HTTPProxy) - if err != nil { - xl.Error("fail to parse proxy url") - return nil, err - } - dialOptions := []libdial.DialOption{} - protocol := ctl.clientCfg.Protocol - var websocketAfterHook *libdial.AfterHook - if protocol == "websocket" || protocol == "wss" { - if protocol == "wss" { - websocketAfterHook = &libdial.AfterHook{ - Priority: math.MaxUint64, // in case of wss, we first want to make the TLS handshake and then switch protocols from https to wss - Hook: frpNet.DialHookWebsocket(true), - } - } else { - websocketAfterHook = &libdial.AfterHook{ - Priority: 0, // in case of ws, we first want to switch protocols from http to ws, and only then make the TLS handshake in case TLS is enabled - Hook: frpNet.DialHookWebsocket(false), - } + if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 { + // Send heartbeat to server. + sendHeartBeat := func() (bool, error) { + xl.Debugf("send heartbeat to server") + pingMsg := &msg.Ping{} + if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil { + xl.Warnf("error during ping authentication: %v, skip sending ping message", err) + return false, err } - - protocol = "tcp" - } - if ctl.clientCfg.ConnectServerLocalIP != "" { - dialOptions = append(dialOptions, libdial.WithLocalAddr(ctl.clientCfg.ConnectServerLocalIP)) - } - dialOptions = append(dialOptions, - libdial.WithProtocol(protocol), - libdial.WithTimeout(time.Duration(ctl.clientCfg.DialServerTimeout)*time.Second), - libdial.WithKeepAlive(time.Duration(ctl.clientCfg.DialServerKeepAlive)*time.Second), - libdial.WithProxy(proxyType, addr), - libdial.WithProxyAuth(auth), - libdial.WithTLSConfig(tlsConfig), // TLS AfterHook has math.MaxUint64 priority - libdial.WithAfterHook(libdial.AfterHook{ - Priority: 1, // should be executed before TLS AfterHook but after the rest of the AfterHooks (except for wss) - Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, ctl.clientCfg.DisableCustomTLSFirstByte), - }), - ) - if websocketAfterHook != nil { - // websocketAfterHook must be appended after TLS AfterHook because they both might have the - // same priority of math.MaxUint64 in case of wss but TLS AfterHook must be executed first - dialOptions = append(dialOptions, libdial.WithAfterHook(*websocketAfterHook)) + _ = ctl.msgDispatcher.Send(pingMsg) + return false, nil } - conn, err = libdial.Dial( - net.JoinHostPort(ctl.clientCfg.ServerAddr, strconv.Itoa(ctl.clientCfg.ServerPort)), - dialOptions..., + go wait.BackoffUntil(sendHeartBeat, + wait.NewFastBackoffManager(wait.FastBackoffOptions{ + Duration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second, + InitDurationIfFail: time.Second, + Factor: 2.0, + Jitter: 0.1, + MaxDuration: time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second, + }), + true, ctl.doneCh, ) - if err != nil { - xl.Warn("start new connection to server error: %v", err) - return nil, err - } } - return -} -// reader read all messages from frps and send to readCh -func (ctl *Control) reader() { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - defer ctl.readerShutdown.Done() - defer close(ctl.closedCh) - - encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token)) - for { - m, err := msg.ReadMsg(encReader) - if err != nil { - if err == io.EOF { - xl.Debug("read from control connection EOF") + // Check heartbeat timeout. + if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 { + go wait.Until(func() { + if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second { + xl.Warnf("heartbeat timeout") + ctl.closeSession() return } - xl.Warn("read error: %v", err) - ctl.conn.Close() - return - } - ctl.readCh <- m - } -} - -// writer writes messages got from sendCh to frps -func (ctl *Control) writer() { - xl := ctl.xl - defer ctl.writerShutdown.Done() - encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Token)) - if err != nil { - xl.Error("crypto new writer error: %v", err) - ctl.conn.Close() - return - } - for { - m, ok := <-ctl.sendCh - if !ok { - xl.Info("control writer is closing") - return - } - - if err := msg.WriteMsg(encWriter, m); err != nil { - xl.Warn("write message to control connection error: %v", err) - return - } + }, time.Second, ctl.doneCh) } } -// msgHandler handles all channel events and do corresponding operations. -func (ctl *Control) msgHandler() { +func (ctl *Control) worker() { xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - defer ctl.msgHandlerShutdown.Done() - - var hbSendCh <-chan time.Time - // TODO(fatedier): disable heartbeat if TCPMux is enabled. - // Just keep it here to keep compatible with old version frps. - if ctl.clientCfg.HeartbeatInterval > 0 { - hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second) - defer hbSend.Stop() - hbSendCh = hbSend.C - } + go ctl.heartbeatWorker() + go ctl.msgDispatcher.Run() - var hbCheckCh <-chan time.Time - // Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature. - if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux { - hbCheck := time.NewTicker(time.Second) - defer hbCheck.Stop() - hbCheckCh = hbCheck.C - } - - ctl.lastPong = time.Now() - for { - select { - case <-hbSendCh: - // send heartbeat to server - xl.Debug("send heartbeat to server") - pingMsg := &msg.Ping{} - if err := ctl.authSetter.SetPing(pingMsg); err != nil { - xl.Warn("error during ping authentication: %v", err) - return - } - ctl.sendCh <- pingMsg - case <-hbCheckCh: - if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second { - xl.Warn("heartbeat timeout") - // let reader() stop - ctl.conn.Close() - return - } - case rawMsg, ok := <-ctl.readCh: - if !ok { - return - } + <-ctl.msgDispatcher.Done() + xl.Debugf("control message dispatcher exited") + ctl.closeSession() - switch m := rawMsg.(type) { - case *msg.ReqWorkConn: - go ctl.HandleReqWorkConn(m) - case *msg.NewProxyResp: - ctl.HandleNewProxyResp(m) - case *msg.Pong: - if m.Error != "" { - xl.Error("Pong contains error: %s", m.Error) - ctl.conn.Close() - return - } - ctl.lastPong = time.Now() - xl.Debug("receive heartbeat from server") - } - } - } -} - -// If controler is notified by closedCh, reader and writer and handler will exit -func (ctl *Control) worker() { - go ctl.msgHandler() - go ctl.reader() - go ctl.writer() - - select { - case <-ctl.closedCh: - // close related channels and wait until other goroutines done - close(ctl.readCh) - ctl.readerShutdown.WaitDone() - ctl.msgHandlerShutdown.WaitDone() - - close(ctl.sendCh) - ctl.writerShutdown.WaitDone() - - ctl.pm.Close() - ctl.vm.Close() - - close(ctl.closedDoneCh) - if ctl.session != nil { - ctl.session.Close() - } - return - } + ctl.pm.Close() + ctl.vm.Close() + close(ctl.doneCh) } -func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error { - ctl.vm.Reload(visitorCfgs) - ctl.pm.Reload(pxyCfgs) +func (ctl *Control) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { + ctl.vm.UpdateAll(visitorCfgs) + ctl.pm.UpdateAll(proxyCfgs) return nil } diff --git a/client/control_session.go b/client/control_session.go new file mode 100644 index 00000000000..e3ed27fc502 --- /dev/null +++ b/client/control_session.go @@ -0,0 +1,225 @@ +// 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 client + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "os" + "runtime" + "time" + + "github.com/samber/lo" + + "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" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/version" + "github.com/fatedier/frp/pkg/vnet" +) + +type controlSessionDialer struct { + ctx context.Context + + common *v1.ClientCommonConfig + auth *auth.ClientAuth + clientSpec *msg.ClientSpec + vnetController *vnet.Controller + + connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector +} + +func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, error) { + connector := d.connectorCreator(d.ctx, d.common) + if err := connector.Open(); err != nil { + return nil, err + } + + success := false + defer func() { + if !success { + _ = connector.Close() + } + }() + + conn, err := connector.Connect() + if err != nil { + return nil, err + } + defer func() { + if !success { + _ = conn.Close() + } + }() + + loginMsg, err := d.buildLoginMsg(previousRunID) + if err != nil { + return nil, err + } + + loginResult, err := d.exchangeLogin(conn, loginMsg) + if err != nil { + return nil, err + } + loginRespMsg := loginResult.resp + if loginRespMsg.Error != "" { + return nil, errors.New(loginRespMsg.Error) + } + + var controlRW io.ReadWriter = conn + if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" { + controlRW, err = d.newControlReadWriter(conn, loginResult.crypto) + if err != nil { + return nil, fmt.Errorf("create control crypto read writer: %w", err) + } + } + + success = true + return &SessionContext{ + Common: d.common, + RunID: loginRespMsg.RunID, + Conn: msg.NewConn(conn, msg.NewReadWriter(controlRW, d.common.Transport.WireProtocol)), + Auth: d.auth, + Connector: newMessageConnector(connector, d.common.Transport.WireProtocol), + VnetController: d.vnetController, + UDPPacketCodec: loginResult.udpPacketCodec, + }, nil +} + +func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login, error) { + hostname, _ := os.Hostname() + loginMsg := &msg.Login{ + Arch: runtime.GOARCH, + Os: runtime.GOOS, + Hostname: hostname, + PoolCount: d.common.Transport.PoolCount, + User: d.common.User, + ClientID: d.common.ClientID, + Version: version.Full(), + Timestamp: time.Now().Unix(), + RunID: previousRunID, + Metas: d.common.Metadatas, + } + if d.clientSpec != nil { + loginMsg.ClientSpec = *d.clientSpec + } + + if err := d.auth.Setter.SetLogin(loginMsg); err != nil { + return nil, err + } + return loginMsg, nil +} + +type loginExchangeResult struct { + resp *msg.LoginResp + crypto *wire.CryptoContext + udpPacketCodec string +} + +func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*loginExchangeResult, error) { + rw := msg.NewV1ReadWriter(conn) + var wireConn *wire.Conn + var clientHello wire.ClientHello + var clientHelloPayload []byte + + if d.common.Transport.WireProtocol == wire.ProtocolV2 { + if err := wire.WriteMagic(conn); err != nil { + return nil, err + } + + wireConn = wire.NewConn(conn) + rw = msg.NewV2ReadWriterWithConn(wireConn) + var err error + clientHello, err = wire.NewClientHello(wire.BootstrapInfo{ + Transport: d.common.Transport.Protocol, + TLS: lo.FromPtr(d.common.Transport.TLS.Enable) || d.common.Transport.Protocol == "wss" || d.common.Transport.Protocol == "quic", + TCPMux: lo.FromPtr(d.common.Transport.TCPMux), + }) + if err != nil { + return nil, err + } + clientHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeClientHello, clientHello) + if err != nil { + return nil, err + } + if err := wireConn.WriteFrame(clientHelloFrame); err != nil { + return nil, err + } + clientHelloPayload = clientHelloFrame.Payload + } + if err := rw.WriteMsg(loginMsg); err != nil { + return nil, err + } + + _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + defer func() { + _ = conn.SetReadDeadline(time.Time{}) + }() + + var cryptoContext *wire.CryptoContext + var udpPacketCodec string + if wireConn != nil { + serverHelloFrame, err := wireConn.ReadFrame() + if err != nil { + return nil, err + } + if serverHelloFrame.Type != wire.FrameTypeServerHello { + return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello) + } + var serverHello wire.ServerHello + if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil { + return nil, err + } + if serverHello.Error != "" { + return nil, errors.New(serverHello.Error) + } + cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload) + if err != nil { + return nil, err + } + udpPacketCodec = serverHello.Selected.Message.UDPPacketCodec + } + + var loginRespMsg msg.LoginResp + if err := rw.ReadMsgInto(&loginRespMsg); err != nil { + return nil, err + } + return &loginExchangeResult{ + resp: &loginRespMsg, + crypto: cryptoContext, + udpPacketCodec: udpPacketCodec, + }, nil +} + +func (d *controlSessionDialer) newControlReadWriter(conn net.Conn, cryptoContext *wire.CryptoContext) (io.ReadWriter, error) { + if d.common.Transport.WireProtocol == wire.ProtocolV2 { + if cryptoContext == nil { + return nil, errors.New("missing v2 crypto negotiation") + } + return netpkg.NewAEADCryptoReadWriter( + conn, + d.auth.EncryptionKey(), + netpkg.AEADCryptoRoleClient, + cryptoContext.Algorithm, + cryptoContext.TranscriptHash, + ) + } + return netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey()) +} diff --git a/client/control_session_test.go b/client/control_session_test.go new file mode 100644 index 00000000000..4bbcd453ef1 --- /dev/null +++ b/client/control_session_test.go @@ -0,0 +1,299 @@ +// 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 client + +import ( + "context" + "fmt" + "io" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "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" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +type testConnector struct { + conn net.Conn + closed atomic.Bool +} + +func (c *testConnector) Open() error { + return nil +} + +func (c *testConnector) Connect() (net.Conn, error) { + return c.conn, nil +} + +func (c *testConnector) Close() error { + c.closed.Store(true) + return nil +} + +type trackingConn struct { + net.Conn + closed atomic.Bool +} + +func (c *trackingConn) Close() error { + c.closed.Store(true) + return c.Conn.Close() +} + +func newTestControlSessionDialer(t *testing.T, protocol string, connector Connector, clientSpec *msg.ClientSpec) *controlSessionDialer { + t.Helper() + + authRuntime, err := auth.BuildClientAuth(&v1.AuthClientConfig{ + Method: v1.AuthMethodToken, + Token: "token", + }) + require.NoError(t, err) + + return &controlSessionDialer{ + ctx: context.Background(), + common: &v1.ClientCommonConfig{ + User: "test-user", + Transport: v1.ClientTransportConfig{ + Protocol: "tcp", + WireProtocol: protocol, + }, + }, + auth: authRuntime, + clientSpec: clientSpec, + connectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector { + return connector + }, + } +} + +func TestControlSessionDialerDialV1(t *testing.T) { + clientRaw, serverRaw := net.Pipe() + defer serverRaw.Close() + + connector := &testConnector{conn: &trackingConn{Conn: clientRaw}} + serverErrCh := make(chan error, 1) + go func() { + rw := msg.NewV1ReadWriter(serverRaw) + var loginMsg msg.Login + if err := rw.ReadMsgInto(&loginMsg); err != nil { + serverErrCh <- err + return + } + if loginMsg.RunID != "previous-run-id" { + serverErrCh <- fmt.Errorf("unexpected previous run id: %s", loginMsg.RunID) + return + } + if loginMsg.User != "test-user" { + serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User) + return + } + serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v1"}) + }() + + dialer := newTestControlSessionDialer(t, wire.ProtocolV1, connector, nil) + sessionCtx, err := dialer.Dial("previous-run-id") + require.NoError(t, err) + defer sessionCtx.Conn.Close() + 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()) + require.NoError(t, <-serverErrCh) +} + +func TestControlSessionDialerDialV2(t *testing.T) { + clientRaw, serverRaw := net.Pipe() + defer serverRaw.Close() + + connector := &testConnector{conn: &trackingConn{Conn: clientRaw}} + serverErrCh := make(chan error, 1) + go func() { + magic := make([]byte, len(wire.MagicV2)) + if _, err := io.ReadFull(serverRaw, magic); err != nil { + serverErrCh <- err + return + } + if string(magic) != wire.MagicV2 { + serverErrCh <- fmt.Errorf("unexpected magic: %q", string(magic)) + return + } + + wireConn := wire.NewConn(serverRaw) + clientHelloFrame, err := wireConn.ReadFrame() + if err != nil { + serverErrCh <- err + return + } + if clientHelloFrame.Type != wire.FrameTypeClientHello { + serverErrCh <- fmt.Errorf("unexpected frame type %d, want %d", clientHelloFrame.Type, wire.FrameTypeClientHello) + return + } + var hello wire.ClientHello + if err := wireConn.UnmarshalFrame(clientHelloFrame, &hello); err != nil { + serverErrCh <- err + return + } + if err := wire.ValidateClientHello(hello); err != nil { + serverErrCh <- err + return + } + + rw := msg.NewV2ReadWriterWithConn(wireConn) + var loginMsg msg.Login + if err := rw.ReadMsgInto(&loginMsg); err != nil { + serverErrCh <- err + return + } + if loginMsg.User != "test-user" { + serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User) + return + } + serverHello, err := wire.NewServerHello(hello) + if err != nil { + serverErrCh <- err + return + } + serverHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeServerHello, serverHello) + if err != nil { + serverErrCh <- err + return + } + cryptoContext := wire.NewCryptoContext( + serverHello.Selected.Crypto.Algorithm, + clientHelloFrame.Payload, + serverHelloFrame.Payload, + ) + if err := wireConn.WriteFrame(serverHelloFrame); err != nil { + serverErrCh <- err + return + } + if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"}); err != nil { + serverErrCh <- err + return + } + + controlRW, err := netpkg.NewAEADCryptoReadWriter( + serverRaw, + []byte("token"), + netpkg.AEADCryptoRoleServer, + cryptoContext.Algorithm, + cryptoContext.TranscriptHash, + ) + if err != nil { + serverErrCh <- err + return + } + controlMsgRW := msg.NewReadWriter(controlRW, wire.ProtocolV2) + var ping msg.Ping + if err := controlMsgRW.ReadMsgInto(&ping); err != nil { + serverErrCh <- err + return + } + if ping.PrivilegeKey != "v2-ping" || ping.Timestamp != 12345 { + serverErrCh <- fmt.Errorf("unexpected ping: %+v", ping) + return + } + serverErrCh <- nil + }() + + dialer := newTestControlSessionDialer(t, wire.ProtocolV2, connector, nil) + sessionCtx, err := dialer.Dial("") + require.NoError(t, err) + defer sessionCtx.Conn.Close() + 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()) + require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{PrivilegeKey: "v2-ping", Timestamp: 12345})) + require.NoError(t, <-serverErrCh) +} + +func TestControlSessionDialerDialLoginErrorClosesResources(t *testing.T) { + clientRaw, serverRaw := net.Pipe() + defer serverRaw.Close() + + clientConn := &trackingConn{Conn: clientRaw} + connector := &testConnector{conn: clientConn} + serverErrCh := make(chan error, 1) + go func() { + rw := msg.NewV1ReadWriter(serverRaw) + var loginMsg msg.Login + if err := rw.ReadMsgInto(&loginMsg); err != nil { + serverErrCh <- err + return + } + serverErrCh <- rw.WriteMsg(&msg.LoginResp{Error: "login denied"}) + }() + + dialer := newTestControlSessionDialer(t, wire.ProtocolV1, connector, nil) + sessionCtx, err := dialer.Dial("") + require.Nil(t, sessionCtx) + require.ErrorContains(t, err, "login denied") + require.True(t, clientConn.closed.Load()) + require.True(t, connector.closed.Load()) + require.NoError(t, <-serverErrCh) +} + +func TestControlSessionDialerDialSSHTunnelSkipsControlEncryption(t *testing.T) { + clientRaw, serverRaw := net.Pipe() + defer serverRaw.Close() + + connector := &testConnector{conn: &trackingConn{Conn: clientRaw}} + serverErrCh := make(chan error, 1) + go func() { + rw := msg.NewV1ReadWriter(serverRaw) + var loginMsg msg.Login + if err := rw.ReadMsgInto(&loginMsg); err != nil { + serverErrCh <- err + return + } + if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-ssh-tunnel"}); err != nil { + serverErrCh <- err + return + } + + _ = serverRaw.SetReadDeadline(time.Now().Add(time.Second)) + var ping msg.Ping + if err := rw.ReadMsgInto(&ping); err != nil { + serverErrCh <- err + return + } + serverErrCh <- nil + }() + + dialer := newTestControlSessionDialer(t, wire.ProtocolV1, connector, &msg.ClientSpec{Type: "ssh-tunnel"}) + sessionCtx, err := dialer.Dial("") + require.NoError(t, err) + defer sessionCtx.Conn.Close() + defer sessionCtx.Connector.Close() + + require.Equal(t, "run-ssh-tunnel", sessionCtx.RunID) + require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{})) + require.NoError(t, <-serverErrCh) +} diff --git a/client/control_udp_test.go b/client/control_udp_test.go new file mode 100644 index 00000000000..f3c91232f08 --- /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/event/event.go b/client/event/event.go index f9718448823..acdc96ba2b9 100644 --- a/client/event/event.go +++ b/client/event/event.go @@ -6,18 +6,9 @@ import ( "github.com/fatedier/frp/pkg/msg" ) -type Type int +var ErrPayloadType = errors.New("error payload type") -const ( - EvStartProxy Type = iota - EvCloseProxy -) - -var ( - ErrPayloadType = errors.New("error payload type") -) - -type Handler func(evType Type, payload interface{}) error +type Handler func(payload any) error type StartProxyPayload struct { NewProxyMsg *msg.NewProxy diff --git a/client/health/health.go b/client/health/health.go index 829bee3df9f..d298e61520d 100644 --- a/client/health/health.go +++ b/client/health/health.go @@ -21,14 +21,14 @@ import ( "io" "net" "net/http" + "strings" "time" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/xlog" ) -var ( - ErrHealthCheckType = errors.New("error health check type") -) +var ErrHealthCheckType = errors.New("error health check type") type Monitor struct { checkType string @@ -40,8 +40,8 @@ type Monitor struct { addr string // For http - url string - + url string + header http.Header failedTimes uint64 statusOK bool statusNormalFn func() @@ -51,28 +51,41 @@ type Monitor struct { cancel context.CancelFunc } -func NewMonitor(ctx context.Context, checkType string, - intervalS int, timeoutS int, maxFailedTimes int, - addr string, url string, - statusNormalFn func(), statusFailedFn func()) *Monitor { - - if intervalS <= 0 { - intervalS = 10 +func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string, + statusNormalFn func(), statusFailedFn func(), +) *Monitor { + if cfg.IntervalSeconds <= 0 { + cfg.IntervalSeconds = 10 } - if timeoutS <= 0 { - timeoutS = 3 + if cfg.TimeoutSeconds <= 0 { + cfg.TimeoutSeconds = 3 } - if maxFailedTimes <= 0 { - maxFailedTimes = 1 + if cfg.MaxFailed <= 0 { + cfg.MaxFailed = 1 } newctx, cancel := context.WithCancel(ctx) + + var url string + if cfg.Type == "http" && cfg.Path != "" { + s := "http://" + addr + if !strings.HasPrefix(cfg.Path, "/") { + s += "/" + } + url = s + cfg.Path + } + header := make(http.Header) + for _, h := range cfg.HTTPHeaders { + header.Set(h.Name, h.Value) + } + return &Monitor{ - checkType: checkType, - interval: time.Duration(intervalS) * time.Second, - timeout: time.Duration(timeoutS) * time.Second, - maxFailedTimes: maxFailedTimes, + checkType: cfg.Type, + interval: time.Duration(cfg.IntervalSeconds) * time.Second, + timeout: time.Duration(cfg.TimeoutSeconds) * time.Second, + maxFailedTimes: cfg.MaxFailed, addr: addr, url: url, + header: header, statusOK: false, statusNormalFn: statusNormalFn, statusFailedFn: statusFailedFn, @@ -105,17 +118,17 @@ func (monitor *Monitor) checkWorker() { } if err == nil { - xl.Trace("do one health check success") + xl.Tracef("do one health check success") if !monitor.statusOK && monitor.statusNormalFn != nil { - xl.Info("health check status change to success") + xl.Infof("health check status change to success") monitor.statusOK = true monitor.statusNormalFn() } } else { - xl.Warn("do one health check failed: %v", err) + xl.Warnf("do one health check failed: %v", err) monitor.failedTimes++ if monitor.statusOK && int(monitor.failedTimes) >= monitor.maxFailedTimes && monitor.statusFailedFn != nil { - xl.Warn("health check status change to failed") + xl.Warnf("health check status change to failed") monitor.statusOK = false monitor.statusFailedFn() } @@ -152,16 +165,18 @@ func (monitor *Monitor) doTCPCheck(ctx context.Context) error { } func (monitor *Monitor) doHTTPCheck(ctx context.Context) error { - req, err := http.NewRequest("GET", monitor.url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", monitor.url, nil) if err != nil { return err } + req.Header = monitor.header + req.Host = monitor.header.Get("Host") resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() - io.Copy(io.Discard, resp.Body) + _, _ = io.Copy(io.Discard, resp.Body) if resp.StatusCode/100 != 2 { return fmt.Errorf("do http health check, StatusCode is [%d] not 2xx", resp.StatusCode) diff --git a/client/http/controller.go b/client/http/controller.go new file mode 100644 index 00000000000..57e8165d35c --- /dev/null +++ b/client/http/controller.go @@ -0,0 +1,433 @@ +// Copyright 2025 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 http + +import ( + "cmp" + "errors" + "fmt" + "net" + "net/http" + "slices" + "strconv" + "time" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/http/model" + "github.com/fatedier/frp/client/proxy" + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/pkg/util/jsonx" +) + +// Controller handles HTTP API requests for frpc. +type Controller struct { + serverAddr string + manager configmgmt.ConfigManager +} + +// ControllerParams contains parameters for creating an APIController. +type ControllerParams struct { + ServerAddr string + Manager configmgmt.ConfigManager +} + +func NewController(params ControllerParams) *Controller { + return &Controller{ + serverAddr: params.ServerAddr, + manager: params.Manager, + } +} + +func (c *Controller) toHTTPError(err error) error { + if err == nil { + return nil + } + + code := http.StatusInternalServerError + switch { + case errors.Is(err, configmgmt.ErrInvalidArgument): + code = http.StatusBadRequest + case errors.Is(err, configmgmt.ErrNotFound), errors.Is(err, configmgmt.ErrStoreDisabled): + code = http.StatusNotFound + case errors.Is(err, configmgmt.ErrConflict): + code = http.StatusConflict + } + return httppkg.NewError(code, err.Error()) +} + +// Reload handles GET /api/reload +func (c *Controller) Reload(ctx *httppkg.Context) (any, error) { + strictConfigMode := false + strictStr := ctx.Query("strictConfig") + if strictStr != "" { + strictConfigMode, _ = strconv.ParseBool(strictStr) + } + + if err := c.manager.ReloadFromFile(strictConfigMode); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +// Stop handles POST /api/stop +func (c *Controller) Stop(ctx *httppkg.Context) (any, error) { + go c.manager.GracefulClose(100 * time.Millisecond) + return nil, nil +} + +// Status handles GET /api/status +func (c *Controller) Status(ctx *httppkg.Context) (any, error) { + res := make(model.StatusResp) + ps := c.manager.GetProxyStatus() + if ps == nil { + return res, nil + } + + for _, status := range ps { + res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status)) + } + + for _, arrs := range res { + if len(arrs) <= 1 { + continue + } + slices.SortFunc(arrs, func(a, b model.ProxyStatusResp) int { + return cmp.Compare(a.Name, b.Name) + }) + } + return res, nil +} + +// GetConfig handles GET /api/config +func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) { + content, err := c.manager.ReadConfigFile() + if err != nil { + return nil, c.toHTTPError(err) + } + return content, nil +} + +// PutConfig handles PUT /api/config +func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) { + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err)) + } + + if len(body) == 0 { + return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty") + } + + if err := c.manager.WriteConfigFile(body); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp { + psr := model.ProxyStatusResp{ + Name: status.Name, + Type: status.Type, + Status: status.Phase, + Err: status.Err, + } + baseCfg := status.Cfg.GetBaseConfig() + if baseCfg.LocalPort != 0 { + psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)) + } + psr.Plugin = baseCfg.Plugin.Type + + if status.Err == "" { + psr.RemoteAddr = status.RemoteAddr + if slices.Contains([]string{"tcp", "udp"}, status.Type) { + psr.RemoteAddr = c.serverAddr + psr.RemoteAddr + } + } + + if c.manager.IsStoreProxyEnabled(status.Name) { + psr.Source = model.SourceStore + } + return psr +} + +// GetProxyConfig handles GET /api/proxy/{name}/config +func (c *Controller) GetProxyConfig(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + cfg, ok := c.manager.GetProxyConfig(name) + if !ok { + return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("proxy %q not found", name)) + } + + payload, err := model.ProxyDefinitionFromConfigurer(cfg) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return payload, nil +} + +// GetVisitorConfig handles GET /api/visitor/{name}/config +func (c *Controller) GetVisitorConfig(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + cfg, ok := c.manager.GetVisitorConfig(name) + if !ok { + return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("visitor %q not found", name)) + } + + payload, err := model.VisitorDefinitionFromConfigurer(cfg) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return payload, nil +} + +func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) { + proxies, err := c.manager.ListStoreProxies() + if err != nil { + return nil, c.toHTTPError(err) + } + + resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))} + for _, p := range proxies { + payload, err := model.ProxyDefinitionFromConfigurer(p) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + resp.Proxies = append(resp.Proxies, payload) + } + slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int { + return cmp.Compare(a.Name, b.Name) + }) + return resp, nil +} + +func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + p, err := c.manager.GetStoreProxy(name) + if err != nil { + return nil, c.toHTTPError(err) + } + + payload, err := model.ProxyDefinitionFromConfigurer(p) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + + return payload, nil +} + +func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) { + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var payload model.ProxyDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if err := payload.Validate("", false); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + created, err := c.manager.CreateStoreProxy(cfg) + if err != nil { + return nil, c.toHTTPError(err) + } + + resp, err := model.ProxyDefinitionFromConfigurer(created) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil +} + +func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var payload model.ProxyDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if err := payload.Validate(name, true); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + updated, err := c.manager.UpdateStoreProxy(name, cfg) + if err != nil { + return nil, c.toHTTPError(err) + } + + resp, err := model.ProxyDefinitionFromConfigurer(updated) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil +} + +func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required") + } + + if err := c.manager.DeleteStoreProxy(name); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} + +func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) { + visitors, err := c.manager.ListStoreVisitors() + if err != nil { + return nil, c.toHTTPError(err) + } + + resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))} + for _, v := range visitors { + payload, err := model.VisitorDefinitionFromConfigurer(v) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + resp.Visitors = append(resp.Visitors, payload) + } + slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int { + return cmp.Compare(a.Name, b.Name) + }) + return resp, nil +} + +func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + v, err := c.manager.GetStoreVisitor(name) + if err != nil { + return nil, c.toHTTPError(err) + } + + payload, err := model.VisitorDefinitionFromConfigurer(v) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + + return payload, nil +} + +func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) { + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var payload model.VisitorDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if err := payload.Validate("", false); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + created, err := c.manager.CreateStoreVisitor(cfg) + if err != nil { + return nil, c.toHTTPError(err) + } + + resp, err := model.VisitorDefinitionFromConfigurer(created) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil +} + +func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + body, err := ctx.Body() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err)) + } + + var payload model.VisitorDefinition + if err := jsonx.Unmarshal(body, &payload); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err)) + } + + if err := payload.Validate(name, true); err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + cfg, err := payload.ToConfigurer() + if err != nil { + return nil, httppkg.NewError(http.StatusBadRequest, err.Error()) + } + updated, err := c.manager.UpdateStoreVisitor(name, cfg) + if err != nil { + return nil, c.toHTTPError(err) + } + + resp, err := model.VisitorDefinitionFromConfigurer(updated) + if err != nil { + return nil, httppkg.NewError(http.StatusInternalServerError, err.Error()) + } + return resp, nil +} + +func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + if name == "" { + return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required") + } + + if err := c.manager.DeleteStoreVisitor(name); err != nil { + return nil, c.toHTTPError(err) + } + return nil, nil +} diff --git a/client/http/controller_test.go b/client/http/controller_test.go new file mode 100644 index 00000000000..719fbcd4f7e --- /dev/null +++ b/client/http/controller_test.go @@ -0,0 +1,662 @@ +package http + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/fatedier/frp/client/configmgmt" + "github.com/fatedier/frp/client/http/model" + "github.com/fatedier/frp/client/proxy" + v1 "github.com/fatedier/frp/pkg/config/v1" + httppkg "github.com/fatedier/frp/pkg/util/http" +) + +type fakeConfigManager struct { + reloadFromFileFn func(strict bool) error + readConfigFileFn func() (string, error) + writeConfigFileFn func(content []byte) error + getProxyStatusFn func() []*proxy.WorkingStatus + isStoreProxyEnabledFn func(name string) bool + storeEnabledFn func() bool + getProxyConfigFn func(name string) (v1.ProxyConfigurer, bool) + getVisitorConfigFn func(name string) (v1.VisitorConfigurer, bool) + + listStoreProxiesFn func() ([]v1.ProxyConfigurer, error) + getStoreProxyFn func(name string) (v1.ProxyConfigurer, error) + createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) + updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) + deleteStoreProxyFn func(name string) error + listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error) + getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error) + createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) + updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) + deleteStoreVisitFn func(name string) error + gracefulCloseFn func(d time.Duration) +} + +func (m *fakeConfigManager) ReloadFromFile(strict bool) error { + if m.reloadFromFileFn != nil { + return m.reloadFromFileFn(strict) + } + return nil +} + +func (m *fakeConfigManager) ReadConfigFile() (string, error) { + if m.readConfigFileFn != nil { + return m.readConfigFileFn() + } + return "", nil +} + +func (m *fakeConfigManager) WriteConfigFile(content []byte) error { + if m.writeConfigFileFn != nil { + return m.writeConfigFileFn(content) + } + return nil +} + +func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus { + if m.getProxyStatusFn != nil { + return m.getProxyStatusFn() + } + return nil +} + +func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool { + if m.isStoreProxyEnabledFn != nil { + return m.isStoreProxyEnabledFn(name) + } + return false +} + +func (m *fakeConfigManager) StoreEnabled() bool { + if m.storeEnabledFn != nil { + return m.storeEnabledFn() + } + return false +} + +func (m *fakeConfigManager) GetProxyConfig(name string) (v1.ProxyConfigurer, bool) { + if m.getProxyConfigFn != nil { + return m.getProxyConfigFn(name) + } + return nil, false +} + +func (m *fakeConfigManager) GetVisitorConfig(name string) (v1.VisitorConfigurer, bool) { + if m.getVisitorConfigFn != nil { + return m.getVisitorConfigFn(name) + } + return nil, false +} + +func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) { + if m.listStoreProxiesFn != nil { + return m.listStoreProxiesFn() + } + return nil, nil +} + +func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) { + if m.getStoreProxyFn != nil { + return m.getStoreProxyFn(name) + } + return nil, nil +} + +func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + if m.createStoreProxyFn != nil { + return m.createStoreProxyFn(cfg) + } + return cfg, nil +} + +func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + if m.updateStoreProxyFn != nil { + return m.updateStoreProxyFn(name, cfg) + } + return cfg, nil +} + +func (m *fakeConfigManager) DeleteStoreProxy(name string) error { + if m.deleteStoreProxyFn != nil { + return m.deleteStoreProxyFn(name) + } + return nil +} + +func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) { + if m.listStoreVisitorsFn != nil { + return m.listStoreVisitorsFn() + } + return nil, nil +} + +func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) { + if m.getStoreVisitorFn != nil { + return m.getStoreVisitorFn(name) + } + return nil, nil +} + +func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + if m.createStoreVisitFn != nil { + return m.createStoreVisitFn(cfg) + } + return cfg, nil +} + +func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + if m.updateStoreVisitFn != nil { + return m.updateStoreVisitFn(name, cfg) + } + return cfg, nil +} + +func (m *fakeConfigManager) DeleteStoreVisitor(name string) error { + if m.deleteStoreVisitFn != nil { + return m.deleteStoreVisitFn(name) + } + return nil +} + +func (m *fakeConfigManager) GracefulClose(d time.Duration) { + if m.gracefulCloseFn != nil { + m.gracefulCloseFn(d) + } +} + +func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig { + return &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: name, + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 10080, + }, + }, + } +} + +func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) { + status := &proxy.WorkingStatus{ + Name: "shared-proxy", + Type: "tcp", + Phase: proxy.ProxyPhaseRunning, + RemoteAddr: ":8080", + Cfg: newRawTCPProxyConfig("shared-proxy"), + } + + controller := &Controller{ + serverAddr: "127.0.0.1", + manager: &fakeConfigManager{ + isStoreProxyEnabledFn: func(name string) bool { + return name == "shared-proxy" + }, + }, + } + + resp := controller.buildProxyStatusResp(status) + if resp.Source != "store" { + t.Fatalf("unexpected source: %q", resp.Source) + } + if resp.RemoteAddr != "127.0.0.1:8080" { + t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr) + } +} + +func TestReloadErrorMapping(t *testing.T) { + tests := []struct { + name string + err error + expectedCode int + }{ + {name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest}, + {name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }}, + } + ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil)) + _, err := controller.Reload(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, tc.expectedCode) + }) + } +} + +func TestStoreProxyErrorMapping(t *testing.T) { + tests := []struct { + name string + err error + expectedCode int + }{ + {name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound}, + {name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict}, + {name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`) + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + controller := &Controller{ + manager: &fakeConfigManager{ + updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + return nil, tc.err + }, + }, + } + + _, err := controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, tc.expectedCode) + }) + } +} + +func TestStoreVisitorErrorMapping(t *testing.T) { + body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`) + req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + controller := &Controller{ + manager: &fakeConfigManager{ + deleteStoreVisitFn: func(string) error { + return fmtError(configmgmt.ErrStoreDisabled, "disabled") + }, + }, + } + + _, err := controller.DeleteStoreVisitor(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusNotFound) +} + +func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) { + var gotName string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + gotName = cfg.GetBaseConfig().Name + return cfg, nil + }, + }, + } + + body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`) + req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.CreateStoreProxy(ctx) + if err != nil { + t.Fatalf("create store proxy: %v", err) + } + if gotName != "raw-proxy" { + t.Fatalf("unexpected proxy name: %q", gotName) + } + + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Type != "tcp" || payload.TCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) { + var gotName string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + gotName = cfg.GetBaseConfig().Name + return cfg, nil + }, + }, + } + + body := []byte(`{ + "name":"raw-visitor","type":"xtcp","unexpected":"value", + "xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"} + }`) + req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.CreateStoreVisitor(ctx) + if err != nil { + t.Fatalf("create store visitor: %v", err) + } + if gotName != "raw-visitor" { + t.Fatalf("unexpected visitor name: %q", gotName) + } + + payload, ok := resp.(model.VisitorDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Type != "xtcp" || payload.XTCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) { + var gotPluginType string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + gotPluginType = cfg.GetBaseConfig().Plugin.Type + return cfg, nil + }, + }, + } + + body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`) + req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.CreateStoreProxy(ctx) + if err != nil { + t.Fatalf("create store proxy: %v", err) + } + if gotPluginType != "http2https" { + t.Fatalf("unexpected plugin type: %q", gotPluginType) + } + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.TCP == nil { + t.Fatalf("unexpected response payload: %#v", payload) + } + pluginType := payload.TCP.Plugin.Type + + if pluginType != "http2https" { + t.Fatalf("unexpected plugin type in response payload: %q", pluginType) + } +} + +func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) { + var gotPluginType string + controller := &Controller{ + manager: &fakeConfigManager{ + createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) { + gotPluginType = cfg.GetBaseConfig().Plugin.Type + return cfg, nil + }, + }, + } + + body := []byte(`{ + "name":"plugin-visitor","type":"stcp", + "stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}} + }`) + req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body)) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.CreateStoreVisitor(ctx) + if err != nil { + t.Fatalf("create store visitor: %v", err) + } + if gotPluginType != "virtual_net" { + t.Fatalf("unexpected plugin type: %q", gotPluginType) + } + payload, ok := resp.(model.VisitorDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.STCP == nil { + t.Fatalf("unexpected response payload: %#v", payload) + } + pluginType := payload.STCP.Plugin.Type + + if pluginType != "virtual_net" { + t.Fatalf("unexpected plugin type in response payload: %q", pluginType) + } +} + +func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) { + controller := &Controller{manager: &fakeConfigManager{}} + body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`) + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "p1"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusBadRequest) +} + +func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) { + controller := &Controller{manager: &fakeConfigManager{}} + body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`) + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{"name": "p1"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.UpdateStoreProxy(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusBadRequest) +} + +func TestListStoreProxiesReturnsSortedPayload(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) { + b := newRawTCPProxyConfig("b") + a := newRawTCPProxyConfig("a") + return []v1.ProxyConfigurer{b, a}, nil + }, + }, + } + ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil)) + + resp, err := controller.ListStoreProxies(ctx) + if err != nil { + t.Fatalf("list store proxies: %v", err) + } + out, ok := resp.(model.ProxyListResp) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if len(out.Proxies) != 2 { + t.Fatalf("unexpected proxy count: %d", len(out.Proxies)) + } + if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" { + t.Fatalf("proxies are not sorted by name: %#v", out.Proxies) + } +} + +func fmtError(sentinel error, msg string) error { + return fmt.Errorf("%w: %s", sentinel, msg) +} + +func assertHTTPCode(t *testing.T, err error, expected int) { + t.Helper() + var httpErr *httppkg.Error + if !errors.As(err, &httpErr) { + t.Fatalf("unexpected error type: %T", err) + } + if httpErr.Code != expected { + t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected) + } +} + +func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) { + return cfg, nil + }, + }, + } + + body := map[string]any{ + "name": "shared-proxy", + "type": "tcp", + "tcp": map[string]any{ + "localPort": 10080, + "remotePort": 7000, + }, + } + data, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + + req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data)) + req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.UpdateStoreProxy(ctx) + if err != nil { + t.Fatalf("update store proxy: %v", err) + } + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.TCP == nil || payload.TCP.RemotePort != 7000 { + t.Fatalf("unexpected response payload: %#v", payload) + } +} + +func TestGetProxyConfigFromManager(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) { + if name == "ssh" { + cfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "ssh", + Type: "tcp", + ProxyBackend: v1.ProxyBackend{ + LocalPort: 22, + }, + }, + } + return cfg, true + } + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/proxy/ssh/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "ssh"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.GetProxyConfig(ctx) + if err != nil { + t.Fatalf("get proxy config: %v", err) + } + payload, ok := resp.(model.ProxyDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Name != "ssh" || payload.Type != "tcp" || payload.TCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestGetProxyConfigNotFound(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getProxyConfigFn: func(name string) (v1.ProxyConfigurer, bool) { + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/proxy/missing/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "missing"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.GetProxyConfig(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusNotFound) +} + +func TestGetVisitorConfigFromManager(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) { + if name == "my-stcp" { + cfg := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: "my-stcp", + Type: "stcp", + ServerName: "server1", + BindPort: 9000, + }, + } + return cfg, true + } + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/visitor/my-stcp/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "my-stcp"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + resp, err := controller.GetVisitorConfig(ctx) + if err != nil { + t.Fatalf("get visitor config: %v", err) + } + payload, ok := resp.(model.VisitorDefinition) + if !ok { + t.Fatalf("unexpected response type: %T", resp) + } + if payload.Name != "my-stcp" || payload.Type != "stcp" || payload.STCP == nil { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestGetVisitorConfigNotFound(t *testing.T) { + controller := &Controller{ + manager: &fakeConfigManager{ + getVisitorConfigFn: func(name string) (v1.VisitorConfigurer, bool) { + return nil, false + }, + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/visitor/missing/config", nil) + req = mux.SetURLVars(req, map[string]string{"name": "missing"}) + ctx := httppkg.NewContext(httptest.NewRecorder(), req) + + _, err := controller.GetVisitorConfig(ctx) + if err == nil { + t.Fatal("expected error") + } + assertHTTPCode(t, err, http.StatusNotFound) +} diff --git a/client/http/model/proxy_definition.go b/client/http/model/proxy_definition.go new file mode 100644 index 00000000000..dae4b4b84d7 --- /dev/null +++ b/client/http/model/proxy_definition.go @@ -0,0 +1,148 @@ +package model + +import ( + "fmt" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type ProxyDefinition struct { + Name string `json:"name"` + Type string `json:"type"` + + TCP *v1.TCPProxyConfig `json:"tcp,omitempty"` + UDP *v1.UDPProxyConfig `json:"udp,omitempty"` + HTTP *v1.HTTPProxyConfig `json:"http,omitempty"` + HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"` + TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"` + STCP *v1.STCPProxyConfig `json:"stcp,omitempty"` + SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"` + XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"` +} + +func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error { + if strings.TrimSpace(p.Name) == "" { + return fmt.Errorf("proxy name is required") + } + if !IsProxyType(p.Type) { + return fmt.Errorf("invalid proxy type: %s", p.Type) + } + if isUpdate && pathName != "" && pathName != p.Name { + return fmt.Errorf("proxy name in URL must match name in body") + } + + _, blockType, blockCount := p.activeBlock() + if blockCount != 1 { + return fmt.Errorf("exactly one proxy type block is required") + } + if blockType != p.Type { + return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type) + } + return nil +} + +func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) { + block, _, _ := p.activeBlock() + if block == nil { + return nil, fmt.Errorf("exactly one proxy type block is required") + } + + cfg := block + cfg.GetBaseConfig().Name = p.Name + cfg.GetBaseConfig().Type = p.Type + return cfg, nil +} + +func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) { + if cfg == nil { + return ProxyDefinition{}, fmt.Errorf("proxy config is nil") + } + + base := cfg.GetBaseConfig() + payload := ProxyDefinition{ + Name: base.Name, + Type: base.Type, + } + + switch c := cfg.(type) { + case *v1.TCPProxyConfig: + payload.TCP = c + case *v1.UDPProxyConfig: + payload.UDP = c + case *v1.HTTPProxyConfig: + payload.HTTP = c + case *v1.HTTPSProxyConfig: + payload.HTTPS = c + case *v1.TCPMuxProxyConfig: + payload.TCPMux = c + case *v1.STCPProxyConfig: + payload.STCP = c + case *v1.SUDPProxyConfig: + payload.SUDP = c + case *v1.XTCPProxyConfig: + payload.XTCP = c + default: + return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg) + } + + return payload, nil +} + +func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) { + count := 0 + var block v1.ProxyConfigurer + var blockType string + + if p.TCP != nil { + count++ + block = p.TCP + blockType = "tcp" + } + if p.UDP != nil { + count++ + block = p.UDP + blockType = "udp" + } + if p.HTTP != nil { + count++ + block = p.HTTP + blockType = "http" + } + if p.HTTPS != nil { + count++ + block = p.HTTPS + blockType = "https" + } + if p.TCPMux != nil { + count++ + block = p.TCPMux + blockType = "tcpmux" + } + if p.STCP != nil { + count++ + block = p.STCP + blockType = "stcp" + } + if p.SUDP != nil { + count++ + block = p.SUDP + blockType = "sudp" + } + if p.XTCP != nil { + count++ + block = p.XTCP + blockType = "xtcp" + } + + return block, blockType, count +} + +func IsProxyType(typ string) bool { + switch typ { + case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp": + return true + default: + return false + } +} diff --git a/client/http/model/types.go b/client/http/model/types.go new file mode 100644 index 00000000000..637048506c1 --- /dev/null +++ b/client/http/model/types.go @@ -0,0 +1,42 @@ +// Copyright 2025 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 model + +const SourceStore = "store" + +// StatusResp is the response for GET /api/status +type StatusResp map[string][]ProxyStatusResp + +// ProxyStatusResp contains proxy status information +type ProxyStatusResp struct { + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` + Err string `json:"err"` + LocalAddr string `json:"local_addr"` + Plugin string `json:"plugin"` + RemoteAddr string `json:"remote_addr"` + Source string `json:"source,omitempty"` // "store" or "config" +} + +// ProxyListResp is the response for GET /api/store/proxies +type ProxyListResp struct { + Proxies []ProxyDefinition `json:"proxies"` +} + +// VisitorListResp is the response for GET /api/store/visitors +type VisitorListResp struct { + Visitors []VisitorDefinition `json:"visitors"` +} diff --git a/client/http/model/visitor_definition.go b/client/http/model/visitor_definition.go new file mode 100644 index 00000000000..a108982d0a4 --- /dev/null +++ b/client/http/model/visitor_definition.go @@ -0,0 +1,107 @@ +package model + +import ( + "fmt" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type VisitorDefinition struct { + Name string `json:"name"` + Type string `json:"type"` + + STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"` + SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"` + XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"` +} + +func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error { + if strings.TrimSpace(p.Name) == "" { + return fmt.Errorf("visitor name is required") + } + if !IsVisitorType(p.Type) { + return fmt.Errorf("invalid visitor type: %s", p.Type) + } + if isUpdate && pathName != "" && pathName != p.Name { + return fmt.Errorf("visitor name in URL must match name in body") + } + + _, blockType, blockCount := p.activeBlock() + if blockCount != 1 { + return fmt.Errorf("exactly one visitor type block is required") + } + if blockType != p.Type { + return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type) + } + return nil +} + +func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) { + block, _, _ := p.activeBlock() + if block == nil { + return nil, fmt.Errorf("exactly one visitor type block is required") + } + + cfg := block + cfg.GetBaseConfig().Name = p.Name + cfg.GetBaseConfig().Type = p.Type + return cfg, nil +} + +func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) { + if cfg == nil { + return VisitorDefinition{}, fmt.Errorf("visitor config is nil") + } + + base := cfg.GetBaseConfig() + payload := VisitorDefinition{ + Name: base.Name, + Type: base.Type, + } + + switch c := cfg.(type) { + case *v1.STCPVisitorConfig: + payload.STCP = c + case *v1.SUDPVisitorConfig: + payload.SUDP = c + case *v1.XTCPVisitorConfig: + payload.XTCP = c + default: + return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg) + } + + return payload, nil +} + +func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) { + count := 0 + var block v1.VisitorConfigurer + var blockType string + + if p.STCP != nil { + count++ + block = p.STCP + blockType = "stcp" + } + if p.SUDP != nil { + count++ + block = p.SUDP + blockType = "sudp" + } + if p.XTCP != nil { + count++ + block = p.XTCP + blockType = "xtcp" + } + return block, blockType, count +} + +func IsVisitorType(typ string) bool { + switch typ { + case "stcp", "sudp", "xtcp": + return true + default: + return false + } +} diff --git a/client/proxy/general_tcp.go b/client/proxy/general_tcp.go new file mode 100644 index 00000000000..c3923085503 --- /dev/null +++ b/client/proxy/general_tcp.go @@ -0,0 +1,47 @@ +// Copyright 2023 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 ( + "reflect" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func init() { + pxyConfs := []v1.ProxyConfigurer{ + &v1.TCPProxyConfig{}, + &v1.HTTPProxyConfig{}, + &v1.HTTPSProxyConfig{}, + &v1.STCPProxyConfig{}, + &v1.TCPMuxProxyConfig{}, + } + for _, cfg := range pxyConfs { + RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy) + } +} + +// GeneralTCPProxy is a general implementation of Proxy interface for TCP protocol. +// If the default GeneralTCPProxy cannot meet the requirements, you can customize +// the implementation of the Proxy interface. +type GeneralTCPProxy struct { + *BaseProxy +} + +func NewGeneralTCPProxy(baseProxy *BaseProxy, _ v1.ProxyConfigurer) Proxy { + return &GeneralTCPProxy{ + BaseProxy: baseProxy, + } +} diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 340da950c30..e982c03b083 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -15,794 +15,244 @@ package proxy import ( - "bytes" "context" + "fmt" "io" "net" + "reflect" "strconv" - "strings" "sync" "time" - "github.com/fatedier/frp/pkg/config" + libio "github.com/fatedier/golib/io" + libnet "github.com/fatedier/golib/net" + "golang.org/x/time/rate" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/client" - "github.com/fatedier/frp/pkg/proto/udp" + "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/limit" - frpNet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" - - "github.com/fatedier/golib/errors" - frpIo "github.com/fatedier/golib/io" - libdial "github.com/fatedier/golib/net/dial" - "github.com/fatedier/golib/pool" - fmux "github.com/hashicorp/yamux" - pp "github.com/pires/go-proxyproto" - "golang.org/x/time/rate" + "github.com/fatedier/frp/pkg/vnet" ) +var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{} + +func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v1.ProxyConfigurer) Proxy) { + proxyFactoryRegistry[proxyConfType] = factory +} + // Proxy defines how to handle work connections for different proxy type. type Proxy interface { Run() error - // InWorkConn accept work connections registered to server. InWorkConn(net.Conn, *msg.StartWorkConn) - + SetInWorkConnCallback(func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool) Close() } -func NewProxy(ctx context.Context, pxyConf config.ProxyConf, clientCfg config.ClientCommonConf, serverUDPPort int) (pxy Proxy) { +func NewProxy( + ctx context.Context, + pxyConf v1.ProxyConfigurer, + clientCfg *v1.ClientCommonConfig, + encryptionKey []byte, + msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, + udpPacketCodec string, +) (pxy Proxy) { var limiter *rate.Limiter - limitBytes := pxyConf.GetBaseInfo().BandwidthLimit.Bytes() - if limitBytes > 0 { - limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes)) + limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() + if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient { + limiter = limit.NewBandwidthLimiter(limitBytes) } baseProxy := BaseProxy{ - clientCfg: clientCfg, - serverUDPPort: serverUDPPort, - limiter: limiter, - xl: xlog.FromContextSafe(ctx), - ctx: ctx, + baseCfg: pxyConf.GetBaseConfig(), + clientCfg: clientCfg, + encryptionKey: encryptionKey, + limiter: limiter, + msgTransporter: msgTransporter, + vnetController: vnetController, + xl: xlog.FromContextSafe(ctx), + ctx: ctx, + udpPacketCodec: udpPacketCodec, } - switch cfg := pxyConf.(type) { - case *config.TCPProxyConf: - pxy = &TCPProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.TCPMuxProxyConf: - pxy = &TCPMuxProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.UDPProxyConf: - pxy = &UDPProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.HTTPProxyConf: - pxy = &HTTPProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.HTTPSProxyConf: - pxy = &HTTPSProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.STCPProxyConf: - pxy = &STCPProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.XTCPProxyConf: - pxy = &XTCPProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - } - case *config.SUDPProxyConf: - pxy = &SUDPProxy{ - BaseProxy: &baseProxy, - cfg: cfg, - closeCh: make(chan struct{}), - } + + factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)] + if factory == nil { + return nil } - return + return factory(&baseProxy, pxyConf) } type BaseProxy struct { - closed bool - clientCfg config.ClientCommonConf - serverUDPPort int - limiter *rate.Limiter - - mu sync.RWMutex - xl *xlog.Logger - ctx context.Context -} - -// TCP -type TCPProxy struct { - *BaseProxy - - cfg *config.TCPProxyConf - proxyPlugin plugin.Plugin -} - -func (pxy *TCPProxy) Run() (err error) { - if pxy.cfg.Plugin != "" { - pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) + baseCfg *v1.ProxyBaseConfig + clientCfg *v1.ClientCommonConfig + encryptionKey []byte + msgTransporter transport.MessageTransporter + vnetController *vnet.Controller + limiter *rate.Limiter + // proxyPlugin is used to handle connections instead of dialing to local service. + // It's only validate for TCP protocol now. + proxyPlugin plugin.Plugin + inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) /* continue */ bool + + mu sync.RWMutex + xl *xlog.Logger + ctx context.Context + udpPacketCodec string +} + +func (pxy *BaseProxy) Run() error { + if pxy.baseCfg.Plugin.Type != "" { + p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{ + Name: pxy.baseCfg.Name, + VnetController: pxy.vnetController, + }, pxy.baseCfg.Plugin.ClientPluginOptions) if err != nil { - return + return err } + pxy.proxyPlugin = p } - return + return nil } -func (pxy *TCPProxy) Close() { +func (pxy *BaseProxy) Close() { if pxy.proxyPlugin != nil { pxy.proxyPlugin.Close() } } -func (pxy *TCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, - conn, []byte(pxy.clientCfg.Token), m) -} - -// TCP Multiplexer -type TCPMuxProxy struct { - *BaseProxy - - cfg *config.TCPMuxProxyConf - proxyPlugin plugin.Plugin -} - -func (pxy *TCPMuxProxy) Run() (err error) { - if pxy.cfg.Plugin != "" { - pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) - if err != nil { - return - } - } - return -} - -func (pxy *TCPMuxProxy) Close() { - if pxy.proxyPlugin != nil { - pxy.proxyPlugin.Close() +// wrapWorkConn applies rate limiting, encryption, and compression +// to a work connection based on the proxy's transport configuration. +// The returned recycle function should be called when the stream is no longer in use +// to return compression resources to the pool. It is safe to not call recycle, +// in which case resources will be garbage collected normally. +func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) { + var rwc io.ReadWriteCloser = conn + if pxy.limiter != nil { + rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error { + return conn.Close() + }) } -} - -func (pxy *TCPMuxProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, - conn, []byte(pxy.clientCfg.Token), m) -} - -// HTTP -type HTTPProxy struct { - *BaseProxy - - cfg *config.HTTPProxyConf - proxyPlugin plugin.Plugin -} - -func (pxy *HTTPProxy) Run() (err error) { - if pxy.cfg.Plugin != "" { - pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) + if pxy.baseCfg.Transport.UseEncryption { + var err error + rwc, err = libio.WithEncryption(rwc, encKey) if err != nil { - return + conn.Close() + return nil, nil, fmt.Errorf("create encryption stream error: %w", err) } } - return -} - -func (pxy *HTTPProxy) Close() { - if pxy.proxyPlugin != nil { - pxy.proxyPlugin.Close() + var recycleFn func() + if pxy.baseCfg.Transport.UseCompression { + rwc, recycleFn = libio.WithCompressionFromPool(rwc) } + return rwc, recycleFn, nil } -func (pxy *HTTPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, - conn, []byte(pxy.clientCfg.Token), m) -} - -// HTTPS -type HTTPSProxy struct { - *BaseProxy - - cfg *config.HTTPSProxyConf - proxyPlugin plugin.Plugin +func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + pxy.inWorkConnCallback = cb } -func (pxy *HTTPSProxy) Run() (err error) { - if pxy.cfg.Plugin != "" { - pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) - if err != nil { +func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { + if pxy.inWorkConnCallback != nil { + if !pxy.inWorkConnCallback(pxy.baseCfg, conn, m) { return } } - return -} - -func (pxy *HTTPSProxy) Close() { - if pxy.proxyPlugin != nil { - pxy.proxyPlugin.Close() - } -} - -func (pxy *HTTPSProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, - conn, []byte(pxy.clientCfg.Token), m) + pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey) } -// STCP -type STCPProxy struct { - *BaseProxy +// Common handler for tcp work connections. +func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) { + xl := pxy.xl + baseCfg := pxy.baseCfg - cfg *config.STCPProxyConf - proxyPlugin plugin.Plugin -} + xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t", + baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression) -func (pxy *STCPProxy) Run() (err error) { - if pxy.cfg.Plugin != "" { - pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) + 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 } - } - return -} - -func (pxy *STCPProxy) Close() { - if pxy.proxyPlugin != nil { - pxy.proxyPlugin.Close() - } -} - -func (pxy *STCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, - conn, []byte(pxy.clientCfg.Token), m) -} - -// XTCP -type XTCPProxy struct { - *BaseProxy - - cfg *config.XTCPProxyConf - proxyPlugin plugin.Plugin -} - -func (pxy *XTCPProxy) Run() (err error) { - if pxy.cfg.Plugin != "" { - pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) + 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 } } - return -} -func (pxy *XTCPProxy) Close() { - if pxy.proxyPlugin != nil { - pxy.proxyPlugin.Close() - } -} - -func (pxy *XTCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - xl := pxy.xl - defer conn.Close() - var natHoleSidMsg msg.NatHoleSid - err := msg.ReadMsgInto(conn, &natHoleSidMsg) + remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey) if err != nil { - xl.Error("xtcp read from workConn error: %v", err) + xl.Errorf("wrap work connection: %v", err) return } - natHoleClientMsg := &msg.NatHoleClient{ - ProxyName: pxy.cfg.ProxyName, - Sid: natHoleSidMsg.Sid, - } - raddr, _ := net.ResolveUDPAddr("udp", - net.JoinHostPort(pxy.clientCfg.ServerAddr, strconv.Itoa(pxy.serverUDPPort))) - clientConn, err := net.DialUDP("udp", nil, raddr) - if err != nil { - xl.Error("dial server udp addr error: %v", err) - return - } - defer clientConn.Close() - - err = msg.WriteMsg(clientConn, natHoleClientMsg) - if err != nil { - xl.Error("send natHoleClientMsg to server error: %v", err) - return - } - - // Wait for client address at most 5 seconds. - var natHoleRespMsg msg.NatHoleResp - clientConn.SetReadDeadline(time.Now().Add(5 * time.Second)) - - buf := pool.GetBuf(1024) - n, err := clientConn.Read(buf) - if err != nil { - xl.Error("get natHoleRespMsg error: %v", err) - return - } - err = msg.ReadMsgInto(bytes.NewReader(buf[:n]), &natHoleRespMsg) - if err != nil { - xl.Error("get natHoleRespMsg error: %v", err) - return - } - clientConn.SetReadDeadline(time.Time{}) - clientConn.Close() - - if natHoleRespMsg.Error != "" { - xl.Error("natHoleRespMsg get error info: %s", natHoleRespMsg.Error) - return - } - - xl.Trace("get natHoleRespMsg, sid [%s], client address [%s] visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr) - - // Send detect message - host, portStr, err := net.SplitHostPort(natHoleRespMsg.VisitorAddr) - if err != nil { - xl.Error("get NatHoleResp visitor address [%s] error: %v", natHoleRespMsg.VisitorAddr, err) - } - laddr, _ := net.ResolveUDPAddr("udp", clientConn.LocalAddr().String()) - - port, err := strconv.ParseInt(portStr, 10, 64) - if err != nil { - xl.Error("get natHoleResp visitor address error: %v", natHoleRespMsg.VisitorAddr) - return - } - pxy.sendDetectMsg(host, int(port), laddr, []byte(natHoleRespMsg.Sid)) - xl.Trace("send all detect msg done") - - msg.WriteMsg(conn, &msg.NatHoleClientDetectOK{}) - - // Listen for clientConn's address and wait for visitor connection - lConn, err := net.ListenUDP("udp", laddr) - if err != nil { - xl.Error("listen on visitorConn's local address error: %v", err) - return - } - defer lConn.Close() - - lConn.SetReadDeadline(time.Now().Add(8 * time.Second)) - sidBuf := pool.GetBuf(1024) - var uAddr *net.UDPAddr - n, uAddr, err = lConn.ReadFromUDP(sidBuf) - if err != nil { - xl.Warn("get sid from visitor error: %v", err) - return - } - lConn.SetReadDeadline(time.Time{}) - if string(sidBuf[:n]) != natHoleRespMsg.Sid { - xl.Warn("incorrect sid from visitor") - return - } - pool.PutBuf(sidBuf) - xl.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid) - - lConn.WriteToUDP(sidBuf[:n], uAddr) - - kcpConn, err := frpNet.NewKCPConnFromUDP(lConn, false, uAddr.String()) - if err != nil { - xl.Error("create kcp connection from udp connection error: %v", err) - return - } - - fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = 5 * time.Second - fmuxCfg.LogOutput = io.Discard - sess, err := fmux.Server(kcpConn, fmuxCfg) - if err != nil { - xl.Error("create yamux server from kcp connection error: %v", err) - return - } - defer sess.Close() - muxConn, err := sess.Accept() - if err != nil { - xl.Error("accept for yamux connection error: %v", err) - return - } - - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, - muxConn, []byte(pxy.cfg.Sk), m) -} - -func (pxy *XTCPProxy) sendDetectMsg(addr string, port int, laddr *net.UDPAddr, content []byte) (err error) { - daddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(addr, strconv.Itoa(port))) - if err != nil { - return err + // check if we need to send proxy protocol info + var connInfo plugin.ConnectionInfo + if m.SrcAddr != "" && m.SrcPort != 0 { + connInfo.SrcAddr = srcAddr + connInfo.DstAddr = dstAddr } - tConn, err := net.DialUDP("udp", laddr, daddr) - if err != nil { - return err + if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 { + header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion) + connInfo.ProxyProtocolHeader = header } + connInfo.Conn = remote + connInfo.UnderlyingConn = workConn - //uConn := ipv4.NewConn(tConn) - //uConn.SetTTL(3) - - tConn.Write(content) - tConn.Close() - return nil -} - -// UDP -type UDPProxy struct { - *BaseProxy - - cfg *config.UDPProxyConf - - localAddr *net.UDPAddr - readCh chan *msg.UDPPacket - - // include msg.UDPPacket and msg.Ping - sendCh chan msg.Message - workConn net.Conn -} - -func (pxy *UDPProxy) Run() (err error) { - pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort))) - if err != nil { + if pxy.proxyPlugin != nil { + // if plugin is set, let plugin handle connection first + // Don't recycle compression resources here because plugins may + // retain the connection after Handle returns. + xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name()) + pxy.proxyPlugin.Handle(pxy.ctx, &connInfo) + xl.Debugf("handle by plugin finished") return } - return -} - -func (pxy *UDPProxy) Close() { - pxy.mu.Lock() - defer pxy.mu.Unlock() - if !pxy.closed { - pxy.closed = true - if pxy.workConn != nil { - pxy.workConn.Close() - } - if pxy.readCh != nil { - close(pxy.readCh) - } - if pxy.sendCh != nil { - close(pxy.sendCh) - } + if recycleFn != nil { + defer recycleFn() } -} -func (pxy *UDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - xl := pxy.xl - xl.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String()) - // close resources releated with old workConn - pxy.Close() - - var rwc io.ReadWriteCloser = conn - var err error - if pxy.limiter != nil { - rwc = frpIo.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error { - return conn.Close() - }) - } - if pxy.cfg.UseEncryption { - rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.clientCfg.Token)) - if err != nil { - conn.Close() - xl.Error("create encryption stream error: %v", err) - return - } - } - if pxy.cfg.UseCompression { - rwc = frpIo.WithCompression(rwc) - } - conn = frpNet.WrapReadWriteCloserToConn(rwc, conn) - - pxy.mu.Lock() - pxy.workConn = conn - pxy.readCh = make(chan *msg.UDPPacket, 1024) - pxy.sendCh = make(chan msg.Message, 1024) - pxy.closed = false - pxy.mu.Unlock() - - workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) { - for { - var udpMsg msg.UDPPacket - if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil { - xl.Warn("read from workConn for udp error: %v", errRet) - return - } - if errRet := errors.PanicToError(func() { - xl.Trace("get udp package from workConn: %s", udpMsg.Content) - readCh <- &udpMsg - }); errRet != nil { - xl.Info("reader goroutine for udp work connection closed: %v", errRet) - return - } - } - } - workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) { - defer func() { - xl.Info("writer goroutine for udp work connection closed") - }() - var errRet error - for rawMsg := range sendCh { - switch m := rawMsg.(type) { - case *msg.UDPPacket: - xl.Trace("send udp package to workConn: %s", m.Content) - case *msg.Ping: - xl.Trace("send ping message to udp workConn") - } - if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil { - xl.Error("udp work write error: %v", errRet) - return - } - } - } - heartbeatFn := func(conn net.Conn, sendCh chan msg.Message) { - var errRet error - for { - time.Sleep(time.Duration(30) * time.Second) - if errRet = errors.PanicToError(func() { - sendCh <- &msg.Ping{} - }); errRet != nil { - xl.Trace("heartbeat goroutine for udp work connection closed") - break - } - } - } - - go workConnSenderFn(pxy.workConn, pxy.sendCh) - go workConnReaderFn(pxy.workConn, pxy.readCh) - go heartbeatFn(pxy.workConn, pxy.sendCh) - udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize)) -} - -type SUDPProxy struct { - *BaseProxy - - cfg *config.SUDPProxyConf - - localAddr *net.UDPAddr - - closeCh chan struct{} -} - -func (pxy *SUDPProxy) Run() (err error) { - pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort))) + localConn, err := libnet.Dial( + net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)), + libnet.WithTimeout(10*time.Second), + ) if err != nil { + workConn.Close() + xl.Errorf("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err) return } - return -} - -func (pxy *SUDPProxy) Close() { - pxy.mu.Lock() - defer pxy.mu.Unlock() - select { - case <-pxy.closeCh: - return - default: - close(pxy.closeCh) - } -} -func (pxy *SUDPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - xl := pxy.xl - xl.Info("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String()) - - var rwc io.ReadWriteCloser = conn - var err error - if pxy.limiter != nil { - rwc = frpIo.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error { - return conn.Close() - }) - } - if pxy.cfg.UseEncryption { - rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.clientCfg.Token)) - if err != nil { - conn.Close() - xl.Error("create encryption stream error: %v", err) - return - } - } - if pxy.cfg.UseCompression { - rwc = frpIo.WithCompression(rwc) - } - conn = frpNet.WrapReadWriteCloserToConn(rwc, conn) - - workConn := conn - readCh := make(chan *msg.UDPPacket, 1024) - sendCh := make(chan msg.Message, 1024) - isClose := false - - mu := &sync.Mutex{} - - closeFn := func() { - mu.Lock() - defer mu.Unlock() - if isClose { - return - } - - isClose = true - if workConn != nil { - workConn.Close() - } - close(readCh) - close(sendCh) - } - - // udp service <- frpc <- frps <- frpc visitor <- user - workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) { - defer closeFn() - - for { - // first to check sudp proxy is closed or not - select { - case <-pxy.closeCh: - xl.Trace("frpc sudp proxy is closed") - return - default: - } - - var udpMsg msg.UDPPacket - if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil { - xl.Warn("read from workConn for sudp error: %v", errRet) - return - } - - if errRet := errors.PanicToError(func() { - readCh <- &udpMsg - }); errRet != nil { - xl.Warn("reader goroutine for sudp work connection closed: %v", errRet) - return - } - } - } - - // udp service -> frpc -> frps -> frpc visitor -> user - workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) { - defer func() { - closeFn() - xl.Info("writer goroutine for sudp work connection closed") - }() - - var errRet error - for rawMsg := range sendCh { - switch m := rawMsg.(type) { - case *msg.UDPPacket: - xl.Trace("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]", - m.LocalAddr.String(), m.RemoteAddr.String(), conn.LocalAddr().String(), conn.RemoteAddr().String()) - case *msg.Ping: - xl.Trace("frpc send ping message to frpc visitor") - } - - if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil { - xl.Error("sudp work write error: %v", errRet) - return - } - } - } - - heartbeatFn := func(conn net.Conn, sendCh chan msg.Message) { - ticker := time.NewTicker(30 * time.Second) - defer func() { - ticker.Stop() - closeFn() - }() - - var errRet error - for { - select { - case <-ticker.C: - if errRet = errors.PanicToError(func() { - sendCh <- &msg.Ping{} - }); errRet != nil { - xl.Warn("heartbeat goroutine for sudp work connection closed") - return - } - case <-pxy.closeCh: - xl.Trace("frpc sudp proxy is closed") - return - } - } - } - - go workConnSenderFn(workConn, sendCh) - go workConnReaderFn(workConn, readCh) - go heartbeatFn(workConn, sendCh) - - udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize)) -} - -// Common handler for tcp work connections. -func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf, proxyPlugin plugin.Plugin, - baseInfo *config.BaseProxyConf, limiter *rate.Limiter, workConn net.Conn, encKey []byte, m *msg.StartWorkConn) { - xl := xlog.FromContextSafe(ctx) - var ( - remote io.ReadWriteCloser - err error - ) - remote = workConn - if limiter != nil { - remote = frpIo.WrapReadWriteCloser(limit.NewReader(workConn, limiter), limit.NewWriter(workConn, limiter), func() error { - return workConn.Close() - }) - } + xl.Debugf("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(), + localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String()) - xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t", - baseInfo.UseEncryption, baseInfo.UseCompression) - if baseInfo.UseEncryption { - remote, err = frpIo.WithEncryption(remote, encKey) - if err != nil { + if connInfo.ProxyProtocolHeader != nil { + if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { workConn.Close() - xl.Error("create encryption stream error: %v", err) + localConn.Close() + xl.Errorf("write proxy protocol header to local conn error: %v", err) return } } - if baseInfo.UseCompression { - remote = frpIo.WithCompression(remote) - } - - // check if we need to send proxy protocol info - var extraInfo []byte - if baseInfo.ProxyProtocolVersion != "" { - 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)))) - h := &pp.Header{ - Command: pp.PROXY, - SourceAddr: srcAddr, - DestinationAddr: dstAddr, - } - - if strings.Contains(m.SrcAddr, ".") { - h.TransportProtocol = pp.TCPv4 - } else { - h.TransportProtocol = pp.TCPv6 - } - if baseInfo.ProxyProtocolVersion == "v1" { - h.Version = 1 - } else if baseInfo.ProxyProtocolVersion == "v2" { - h.Version = 2 - } - - buf := bytes.NewBuffer(nil) - h.WriteTo(buf) - extraInfo = buf.Bytes() - } - } - - if proxyPlugin != nil { - // if plugin is set, let plugin handle connections first - xl.Debug("handle by plugin: %s", proxyPlugin.Name()) - proxyPlugin.Handle(remote, workConn, extraInfo) - xl.Debug("handle by plugin finished") - return - } - - localConn, err := libdial.Dial( - net.JoinHostPort(localInfo.LocalIP, strconv.Itoa(localInfo.LocalPort)), - libdial.WithTimeout(10*time.Second), - ) - if err != nil { - workConn.Close() - xl.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIP, localInfo.LocalPort, err) - return - } - - xl.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(), - localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String()) - - if len(extraInfo) > 0 { - localConn.Write(extraInfo) + _, _, errs := libio.Join(localConn, remote) + xl.Debugf("join connections closed") + if len(errs) > 0 { + xl.Tracef("join connections errors: %v", errs) } - - frpIo.Join(localConn, remote) - xl.Debug("join connections closed") } diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 98c17faa984..6bf8d4a8559 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -1,42 +1,69 @@ +// Copyright 2023 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 ( "context" "fmt" "net" + "reflect" "sync" + "github.com/samber/lo" + "github.com/fatedier/frp/client/event" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" - - "github.com/fatedier/golib/errors" + "github.com/fatedier/frp/pkg/vnet" ) type Manager struct { - sendCh chan (msg.Message) - proxies map[string]*Wrapper + proxies map[string]*Wrapper + msgTransporter transport.MessageTransporter + inWorkConnCallback func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool + vnetController *vnet.Controller closed bool mu sync.RWMutex - clientCfg config.ClientCommonConf - - // The UDP port that the server is listening on - serverUDPPort int + encryptionKey []byte + clientCfg *v1.ClientCommonConfig - ctx context.Context + ctx context.Context + udpPacketCodec string } -func NewManager(ctx context.Context, msgSendCh chan (msg.Message), clientCfg config.ClientCommonConf, serverUDPPort int) *Manager { +func NewManager( + ctx context.Context, + clientCfg *v1.ClientCommonConfig, + encryptionKey []byte, + msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, + udpPacketCodec string, +) *Manager { return &Manager{ - sendCh: msgSendCh, - proxies: make(map[string]*Wrapper), - closed: false, - clientCfg: clientCfg, - serverUDPPort: serverUDPPort, - ctx: ctx, + proxies: make(map[string]*Wrapper), + msgTransporter: msgTransporter, + vnetController: vnetController, + closed: false, + encryptionKey: encryptionKey, + clientCfg: clientCfg, + ctx: ctx, + udpPacketCodec: udpPacketCodec, } } @@ -55,6 +82,10 @@ func (pm *Manager) StartProxy(name string, remoteAddr string, serverRespErr stri return nil } +func (pm *Manager) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + pm.inWorkConnCallback = cb +} + func (pm *Manager) Close() { pm.mu.Lock() defer pm.mu.Unlock() @@ -75,7 +106,7 @@ func (pm *Manager) HandleWorkConn(name string, workConn net.Conn, m *msg.StartWo } } -func (pm *Manager) HandleEvent(evType event.Type, payload interface{}) error { +func (pm *Manager) HandleEvent(payload any) error { var m msg.Message switch e := payload.(type) { case *event.StartProxyPayload: @@ -86,54 +117,62 @@ func (pm *Manager) HandleEvent(evType event.Type, payload interface{}) error { return event.ErrPayloadType } - err := errors.PanicToError(func() { - pm.sendCh <- m - }) - return err + return pm.msgTransporter.Send(m) } func (pm *Manager) GetAllProxyStatus() []*WorkingStatus { - ps := make([]*WorkingStatus, 0) pm.mu.RLock() defer pm.mu.RUnlock() + ps := make([]*WorkingStatus, 0, len(pm.proxies)) for _, pxy := range pm.proxies { ps = append(ps, pxy.GetStatus()) } return ps } -func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) { +func (pm *Manager) GetProxyStatus(name string) (*WorkingStatus, bool) { + pm.mu.RLock() + defer pm.mu.RUnlock() + if pxy, ok := pm.proxies[name]; ok { + return pxy.GetStatus(), true + } + return nil, false +} + +func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) { xl := xlog.FromContextSafe(pm.ctx) + proxyCfgsMap := lo.KeyBy(proxyCfgs, func(c v1.ProxyConfigurer) string { + return c.GetBaseConfig().Name + }) pm.mu.Lock() defer pm.mu.Unlock() delPxyNames := make([]string, 0) for name, pxy := range pm.proxies { del := false - cfg, ok := pxyCfgs[name] - if !ok { + cfg, ok := proxyCfgsMap[name] + if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) { del = true - } else { - if !pxy.Cfg.Compare(cfg) { - del = true - } } if del { delPxyNames = append(delPxyNames, name) delete(pm.proxies, name) - pxy.Stop() } } if len(delPxyNames) > 0 { - xl.Info("proxy removed: %v", delPxyNames) + xl.Infof("proxy removed: %s", delPxyNames) } addPxyNames := make([]string, 0) - for name, cfg := range pxyCfgs { + for _, cfg := range proxyCfgs { + name := cfg.GetBaseConfig().Name if _, ok := pm.proxies[name]; !ok { - pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.serverUDPPort) + 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) + } pm.proxies[name] = pxy addPxyNames = append(addPxyNames, name) @@ -141,6 +180,6 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) { } } if len(addPxyNames) > 0 { - xl.Info("proxy added: %v", addPxyNames) + xl.Infof("proxy added: %s", addPxyNames) } } diff --git a/client/proxy/proxy_test.go b/client/proxy/proxy_test.go new file mode 100644 index 00000000000..c51c69973ac --- /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/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 4d785cc2b8a..37a71277c0b 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -1,20 +1,38 @@ +// Copyright 2023 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 ( "context" "fmt" "net" + "strconv" "sync" "sync/atomic" "time" + "github.com/fatedier/golib/errors" + "github.com/fatedier/frp/client/event" "github.com/fatedier/frp/client/health" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" + "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" - - "github.com/fatedier/golib/errors" + "github.com/fatedier/frp/pkg/vnet" ) const ( @@ -27,17 +45,17 @@ const ( ) var ( - statusCheckInterval time.Duration = 3 * time.Second - waitResponseTimeout = 20 * time.Second - startErrTimeout = 30 * time.Second + statusCheckInterval = 3 * time.Second + waitResponseTimeout = 20 * time.Second + startErrTimeout = 30 * time.Second ) type WorkingStatus struct { - Name string `json:"name"` - Type string `json:"type"` - Phase string `json:"status"` - Err string `json:"err"` - Cfg config.ProxyConf `json:"cfg"` + Name string `json:"name"` + Type string `json:"type"` + Phase string `json:"status"` + Err string `json:"err"` + Cfg v1.ProxyConfigurer `json:"cfg"` // Got from server. RemoteAddr string `json:"remote_addr"` @@ -56,6 +74,10 @@ type Wrapper struct { // event handler handler event.Handler + msgTransporter transport.MessageTransporter + // vnet controller + vnetController *vnet.Controller + health uint32 lastSendStartMsg time.Time lastStartErr time.Time @@ -65,37 +87,55 @@ type Wrapper struct { xl *xlog.Logger ctx context.Context + + wireName string } -func NewWrapper(ctx context.Context, cfg config.ProxyConf, clientCfg config.ClientCommonConf, eventHandler event.Handler, serverUDPPort int) *Wrapper { - baseInfo := cfg.GetBaseInfo() - xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName) +func NewWrapper( + ctx context.Context, + cfg v1.ProxyConfigurer, + clientCfg *v1.ClientCommonConfig, + encryptionKey []byte, + eventHandler event.Handler, + msgTransporter transport.MessageTransporter, + vnetController *vnet.Controller, + udpPacketCodec string, +) *Wrapper { + baseInfo := cfg.GetBaseConfig() + xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name) pw := &Wrapper{ WorkingStatus: WorkingStatus{ - Name: baseInfo.ProxyName, - Type: baseInfo.ProxyType, + Name: baseInfo.Name, + Type: baseInfo.Type, Phase: ProxyPhaseNew, Cfg: cfg, }, closeCh: make(chan struct{}), healthNotifyCh: make(chan struct{}), handler: eventHandler, + msgTransporter: msgTransporter, + vnetController: vnetController, xl: xl, ctx: xlog.NewContext(ctx, xl), + wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name), } - if baseInfo.HealthCheckType != "" { + if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 { pw.health = 1 // means failed - pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheckType, baseInfo.HealthCheckIntervalS, - baseInfo.HealthCheckTimeoutS, baseInfo.HealthCheckMaxFailed, baseInfo.HealthCheckAddr, - baseInfo.HealthCheckURL, pw.statusNormalCallback, pw.statusFailedCallback) - xl.Trace("enable health check monitor") + addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort)) + pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr, + pw.statusNormalCallback, pw.statusFailedCallback) + xl.Tracef("enable health check monitor") } - pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, serverUDPPort) + pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController, udpPacketCodec) return pw } +func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) { + pw.pxy.SetInWorkConnCallback(cb) +} + func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error { pw.mu.Lock() defer pw.mu.Unlock() @@ -145,9 +185,9 @@ func (pw *Wrapper) Stop() { } func (pw *Wrapper) close() { - pw.handler(event.EvCloseProxy, &event.CloseProxyPayload{ + _ = pw.handler(&event.CloseProxyPayload{ CloseProxyMsg: &msg.CloseProxy{ - ProxyName: pw.Name, + ProxyName: pw.wireName, }, }) } @@ -168,13 +208,14 @@ func (pw *Wrapper) checkWorker() { (pw.Phase == ProxyPhaseWaitStart && now.After(pw.lastSendStartMsg.Add(waitResponseTimeout))) || (pw.Phase == ProxyPhaseStartErr && now.After(pw.lastStartErr.Add(startErrTimeout))) { - xl.Trace("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart) + xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart) pw.Phase = ProxyPhaseWaitStart var newProxyMsg msg.NewProxy pw.Cfg.MarshalToMsg(&newProxyMsg) + newProxyMsg.ProxyName = pw.wireName pw.lastSendStartMsg = now - pw.handler(event.EvStartProxy, &event.StartProxyPayload{ + _ = pw.handler(&event.StartProxyPayload{ NewProxyMsg: &newProxyMsg, }) } @@ -183,7 +224,7 @@ func (pw *Wrapper) checkWorker() { pw.mu.Lock() if pw.Phase == ProxyPhaseRunning || pw.Phase == ProxyPhaseWaitStart { pw.close() - xl.Trace("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed) + xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed) pw.Phase = ProxyPhaseCheckFailed } pw.mu.Unlock() @@ -201,25 +242,25 @@ func (pw *Wrapper) checkWorker() { func (pw *Wrapper) statusNormalCallback() { xl := pw.xl atomic.StoreUint32(&pw.health, 0) - errors.PanicToError(func() { + _ = errors.PanicToError(func() { select { case pw.healthNotifyCh <- struct{}{}: default: } }) - xl.Info("health check success") + xl.Infof("health check success") } func (pw *Wrapper) statusFailedCallback() { xl := pw.xl atomic.StoreUint32(&pw.health, 1) - errors.PanicToError(func() { + _ = errors.PanicToError(func() { select { case pw.healthNotifyCh <- struct{}{}: default: } }) - xl.Info("health check failed") + xl.Infof("health check failed") } func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) { @@ -228,7 +269,7 @@ func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) { pxy := pw.pxy pw.mu.RUnlock() if pxy != nil && pw.Phase == ProxyPhaseRunning { - xl.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String()) + xl.Debugf("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String()) go pxy.InWorkConn(workConn, m) } else { workConn.Close() diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go new file mode 100644 index 00000000000..b5613856d6a --- /dev/null +++ b/client/proxy/sudp.go @@ -0,0 +1,199 @@ +// Copyright 2023 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 ( + "net" + "reflect" + "strconv" + "sync" + "time" + + "github.com/fatedier/golib/errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/udp" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) +} + +type SUDPProxy struct { + *BaseProxy + + cfg *v1.SUDPProxyConfig + + localAddr *net.UDPAddr + + closeCh chan struct{} +} + +func NewSUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { + unwrapped, ok := cfg.(*v1.SUDPProxyConfig) + if !ok { + return nil + } + return &SUDPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + closeCh: make(chan struct{}), + } +} + +func (pxy *SUDPProxy) Run() (err error) { + pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort))) + if err != nil { + return + } + return +} + +func (pxy *SUDPProxy) Close() { + pxy.mu.Lock() + defer pxy.mu.Unlock() + select { + case <-pxy.closeCh: + return + default: + close(pxy.closeCh) + } +} + +func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { + xl := pxy.xl + xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String()) + + remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey) + if err != nil { + xl.Errorf("wrap work connection: %v", err) + return + } + + workConn := netpkg.WrapReadWriteCloserToConn(remote, conn) + 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 + + mu := &sync.Mutex{} + + closeFn := func() { + mu.Lock() + defer mu.Unlock() + if isClose { + return + } + + isClose = true + if workConn != nil { + workConn.Close() + } + close(readCh) + close(sendCh) + } + + // udp service <- frpc <- frps <- frpc visitor <- user + workConnReaderFn := func(payloadConn *msg.Conn, readCh chan *msg.UDPPacket) { + defer closeFn() + + for { + // first to check sudp proxy is closed or not + select { + case <-pxy.closeCh: + xl.Tracef("frpc sudp proxy is closed") + return + default: + } + + var udpMsg msg.UDPPacket + if errRet := payloadConn.ReadMsgInto(&udpMsg); errRet != nil { + xl.Warnf("read from workConn for sudp error: %v", errRet) + return + } + + if errRet := errors.PanicToError(func() { + readCh <- &udpMsg + }); errRet != nil { + xl.Warnf("reader goroutine for sudp work connection closed: %v", errRet) + return + } + } + } + + // udp service -> frpc -> frps -> frpc visitor -> user + workConnSenderFn := func(payloadConn *msg.Conn, sendCh chan msg.Message) { + defer func() { + closeFn() + xl.Infof("writer goroutine for sudp work connection closed") + }() + + var errRet error + for rawMsg := range sendCh { + switch m := rawMsg.(type) { + case *msg.UDPPacket: + xl.Tracef("frpc send udp package to frpc visitor, [udp local: %v, remote: %v], [tcp work conn local: %v, remote: %v]", + m.LocalAddr.String(), m.RemoteAddr.String(), payloadConn.LocalAddr().String(), payloadConn.RemoteAddr().String()) + case *msg.Ping: + xl.Tracef("frpc send ping message to frpc visitor") + } + + if errRet = payloadConn.WriteMsg(rawMsg); errRet != nil { + xl.Errorf("sudp work write error: %v", errRet) + return + } + } + } + + heartbeatFn := func(sendCh chan msg.Message) { + ticker := time.NewTicker(30 * time.Second) + defer func() { + ticker.Stop() + closeFn() + }() + + var errRet error + for { + select { + case <-ticker.C: + if errRet = errors.PanicToError(func() { + sendCh <- &msg.Ping{} + }); errRet != nil { + xl.Warnf("heartbeat goroutine for sudp work connection closed") + return + } + case <-pxy.closeCh: + xl.Tracef("frpc sudp proxy is closed") + return + } + } + } + + go workConnSenderFn(payloadConn, sendCh) + go workConnReaderFn(payloadConn, readCh) + go heartbeatFn(sendCh) + + udp.Forwarder(pxy.localAddr, readCh, sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion) +} diff --git a/client/proxy/udp.go b/client/proxy/udp.go new file mode 100644 index 00000000000..d01d2c8e33b --- /dev/null +++ b/client/proxy/udp.go @@ -0,0 +1,169 @@ +// Copyright 2023 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 ( + "net" + "reflect" + "strconv" + "time" + + "github.com/fatedier/golib/errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/udp" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) +} + +type UDPProxy struct { + *BaseProxy + + cfg *v1.UDPProxyConfig + + localAddr *net.UDPAddr + readCh chan *msg.UDPPacket + + // include msg.UDPPacket and msg.Ping + sendCh chan msg.Message + workConn net.Conn + closed bool +} + +func NewUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { + unwrapped, ok := cfg.(*v1.UDPProxyConfig) + if !ok { + return nil + } + return &UDPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } +} + +func (pxy *UDPProxy) Run() (err error) { + pxy.localAddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.cfg.LocalIP, strconv.Itoa(pxy.cfg.LocalPort))) + if err != nil { + return + } + return +} + +func (pxy *UDPProxy) Close() { + pxy.mu.Lock() + defer pxy.mu.Unlock() + + if !pxy.closed { + pxy.closed = true + if pxy.workConn != nil { + pxy.workConn.Close() + } + if pxy.readCh != nil { + close(pxy.readCh) + } + if pxy.sendCh != nil { + close(pxy.sendCh) + } + } +} + +func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { + xl := pxy.xl + xl.Infof("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String()) + // close resources related with old workConn + pxy.Close() + + remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey) + if err != nil { + xl.Errorf("wrap work connection: %v", err) + return + } + + workConn := netpkg.WrapReadWriteCloserToConn(remote, conn) + // Plain UDP payload follows the configured wire protocol for message framing. + 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 + pxy.mu.Unlock() + + workConnReaderFn := func(rw msg.ReadWriter, readCh chan *msg.UDPPacket) { + for { + var udpMsg msg.UDPPacket + if errRet := rw.ReadMsgInto(&udpMsg); errRet != nil { + xl.Warnf("read from workConn for udp error: %v", errRet) + return + } + if errRet := errors.PanicToError(func() { + xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content)) + readCh <- &udpMsg + }); errRet != nil { + xl.Infof("reader goroutine for udp work connection closed: %v", errRet) + return + } + } + } + workConnSenderFn := func(rw msg.ReadWriter, sendCh chan msg.Message) { + defer func() { + xl.Infof("writer goroutine for udp work connection closed") + }() + var errRet error + for rawMsg := range sendCh { + switch m := rawMsg.(type) { + case *msg.UDPPacket: + xl.Tracef("send udp package to workConn, len: %d", len(m.Content)) + case *msg.Ping: + xl.Tracef("send ping message to udp workConn") + } + if errRet = rw.WriteMsg(rawMsg); errRet != nil { + xl.Errorf("udp work write error: %v", errRet) + return + } + } + } + heartbeatFn := func(sendCh chan msg.Message) { + var errRet error + for { + time.Sleep(time.Duration(30) * time.Second) + if errRet = errors.PanicToError(func() { + sendCh <- &msg.Ping{} + }); errRet != nil { + xl.Tracef("heartbeat goroutine for udp work connection closed") + break + } + } + } + + go workConnSenderFn(payloadRW, pxy.sendCh) + go workConnReaderFn(payloadRW, pxy.readCh) + go heartbeatFn(pxy.sendCh) + + // Call Forwarder with proxy protocol version (empty string means no proxy protocol) + udp.Forwarder(pxy.localAddr, pxy.readCh, pxy.sendCh, int(pxy.clientCfg.UDPPacketSize), pxy.cfg.Transport.ProxyProtocolVersion) +} diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go new file mode 100644 index 00000000000..aef66780625 --- /dev/null +++ b/client/proxy/xtcp.go @@ -0,0 +1,216 @@ +// Copyright 2023 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" + "reflect" + "time" + + fmux "github.com/hashicorp/yamux" + "github.com/quic-go/quic-go" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" + "github.com/fatedier/frp/pkg/nathole" + "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) +} + +type XTCPProxy struct { + *BaseProxy + + cfg *v1.XTCPProxyConfig +} + +func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { + unwrapped, ok := cfg.(*v1.XTCPProxyConfig) + if !ok { + return nil + } + return &XTCPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } +} + +func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) { + xl := pxy.xl + defer conn.Close() + natHoleSidMsg, err := readNatHoleSid(conn, pxy.clientCfg.Transport.WireProtocol) + if err != nil { + xl.Errorf("xtcp read from workConn error: %v", err) + return + } + + xl.Tracef("nathole prepare start") + + // Prepare NAT traversal options + var opts nathole.PrepareOptions + if pxy.cfg.NatTraversal != nil && pxy.cfg.NatTraversal.DisableAssistedAddrs { + opts.DisableAssistedAddrs = true + } + + prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, opts) + if err != nil { + xl.Warnf("nathole prepare error: %v", err) + return + } + + xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", + prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) + defer prepareResult.ListenConn.Close() + + // send NatHoleClient msg to server + transactionID := nathole.NewTransactionID() + natHoleClientMsg := &msg.NatHoleClient{ + TransactionID: transactionID, + ProxyName: naming.AddUserPrefix(pxy.clientCfg.User, pxy.cfg.Name), + Sid: natHoleSidMsg.Sid, + MappedAddrs: prepareResult.Addrs, + AssistedAddrs: prepareResult.AssistedAddrs, + } + + xl.Tracef("nathole exchange info start") + natHoleRespMsg, err := nathole.ExchangeInfo(pxy.ctx, pxy.msgTransporter, transactionID, natHoleClientMsg, 5*time.Second) + if err != nil { + xl.Warnf("nathole exchange info error: %v", err) + return + } + + xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v", + natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs, + natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior) + + listenConn := prepareResult.ListenConn + newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey)) + if err != nil { + listenConn.Close() + xl.Warnf("make hole error: %v", err) + _ = pxy.msgTransporter.Send(&msg.NatHoleReport{ + Sid: natHoleRespMsg.Sid, + Success: false, + }) + return + } + listenConn = newListenConn + xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr) + + _ = pxy.msgTransporter.Send(&msg.NatHoleReport{ + Sid: natHoleRespMsg.Sid, + Success: true, + }) + + if natHoleRespMsg.Protocol == "kcp" { + pxy.listenByKCP(listenConn, raddr, startWorkConnMsg) + return + } + + // default is quic + pxy.listenByQUIC(listenConn, raddr, startWorkConnMsg) +} + +func readNatHoleSid(conn net.Conn, wireProtocol string) (*msg.NatHoleSid, error) { + workMsgConn := msg.NewConn(conn, msg.NewReadWriter(conn, wireProtocol)) + var natHoleSidMsg msg.NatHoleSid + if err := workMsgConn.ReadMsgInto(&natHoleSidMsg); err != nil { + return nil, err + } + return &natHoleSidMsg, nil +} + +func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) { + xl := pxy.xl + listenConn.Close() + laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String()) + lConn, err := net.DialUDP("udp", laddr, raddr) + if err != nil { + xl.Warnf("dial udp error: %v", err) + return + } + defer lConn.Close() + + remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String()) + if err != nil { + xl.Warnf("create kcp connection from udp connection error: %v", err) + return + } + + fmuxCfg := fmux.DefaultConfig() + fmuxCfg.KeepAliveInterval = 10 * time.Second + fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 + fmuxCfg.LogOutput = io.Discard + session, err := fmux.Server(remote, fmuxCfg) + if err != nil { + xl.Errorf("create mux session error: %v", err) + return + } + defer session.Close() + + for { + muxConn, err := session.Accept() + if err != nil { + xl.Errorf("accept connection error: %v", err) + return + } + go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey)) + } +} + +func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) { + xl := pxy.xl + defer listenConn.Close() + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + if err != nil { + xl.Warnf("create tls config error: %v", err) + return + } + tlsConfig.NextProtos = []string{"frp"} + quicListener, err := quic.Listen(listenConn, tlsConfig, + &quic.Config{ + MaxIdleTimeout: time.Duration(pxy.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(pxy.clientCfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(pxy.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second, + }, + ) + if err != nil { + xl.Warnf("dial quic error: %v", err) + return + } + // only accept one connection from raddr + c, err := quicListener.Accept(pxy.ctx) + if err != nil { + xl.Errorf("quic accept connection error: %v", err) + return + } + for { + stream, err := c.AcceptStream(pxy.ctx) + if err != nil { + xl.Debugf("quic accept stream error: %v", err) + _ = c.CloseWithError(0, "") + return + } + go pxy.HandleTCPWorkConnection(netpkg.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey)) + } +} diff --git a/client/proxy/xtcp_test.go b/client/proxy/xtcp_test.go new file mode 100644 index 00000000000..bd295a0717e --- /dev/null +++ b/client/proxy/xtcp_test.go @@ -0,0 +1,66 @@ +// 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 ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestReadNatHoleSidUsesSelectedWireProtocol(t *testing.T) { + for _, tc := range []struct { + name string + wireProtocol string + }{ + {name: "v2", wireProtocol: wire.ProtocolV2}, + {name: "v1", wireProtocol: wire.ProtocolV1}, + {name: "default", wireProtocol: ""}, + } { + t.Run(tc.name, func(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + setPipeDeadline(t, client, server) + + errCh := make(chan error, 1) + go func() { + writer := msg.NewConn(server, msg.NewReadWriter(server, tc.wireProtocol)) + errCh <- writer.WriteMsg(&msg.NatHoleSid{Sid: "sid"}) + }() + + out, err := readNatHoleSid(client, tc.wireProtocol) + require.NoError(t, err) + require.Equal(t, "sid", out.Sid) + require.NoError(t, <-errCh) + }) + } +} + +func setPipeDeadline(t *testing.T, conns ...net.Conn) { + t.Helper() + + deadline := time.Now().Add(time.Second) + for _, conn := range conns { + require.NoError(t, conn.SetDeadline(deadline)) + } +} diff --git a/client/service.go b/client/service.go index 8c753ff4d8e..7964050fcf1 100644 --- a/client/service.go +++ b/client/service.go @@ -16,363 +16,359 @@ package client import ( "context" - "crypto/tls" + "errors" "fmt" - "io" - "math" - "math/rand" "net" - "runtime" - "strconv" - "strings" + "net/http" + "os" "sync" "sync/atomic" "time" - "github.com/fatedier/frp/assets" + "github.com/fatedier/golib/crypto" + "github.com/samber/lo" + + "github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/pkg/auth" "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/transport" + "github.com/fatedier/frp/pkg/policy/security" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" - frpNet "github.com/fatedier/frp/pkg/util/net" - "github.com/fatedier/frp/pkg/util/util" - "github.com/fatedier/frp/pkg/util/version" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" - "github.com/fatedier/golib/crypto" - libdial "github.com/fatedier/golib/net/dial" - - fmux "github.com/hashicorp/yamux" + "github.com/fatedier/frp/pkg/vnet" ) func init() { crypto.DefaultSalt = "frp" - rand.Seed(time.Now().UnixNano()) + // Disable quic-go's receive buffer warning. + os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true") + // Disable quic-go's ECN support by default. It may cause issues on certain operating systems. + if os.Getenv("QUIC_GO_DISABLE_ECN") == "" { + os.Setenv("QUIC_GO_DISABLE_ECN", "true") + } } -// Service is a client service. +type cancelErr struct { + Err error +} + +func (e cancelErr) Error() string { + return e.Err.Error() +} + +// ServiceOptions contains options for creating a new client service. +type ServiceOptions struct { + Common *v1.ClientCommonConfig + + // ConfigSourceAggregator manages internal config and optional store sources. + // It is required for creating a Service. + ConfigSourceAggregator *source.Aggregator + + UnsafeFeatures *security.UnsafeFeatures + + // ConfigFilePath is the path to the configuration file used to initialize. + // If it is empty, it means that the configuration file is not used for initialization. + // It may be initialized using command line parameters or called directly. + ConfigFilePath string + + // ClientSpec is the client specification that control the client behavior. + ClientSpec *msg.ClientSpec + + // ConnectorCreator is a function that creates a new connector to make connections to the server. + // The Connector shields the underlying connection details, whether it is through TCP or QUIC connection, + // and regardless of whether multiplexing is used. + // + // If it is not set, the default frpc connector will be used. + // By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps + // through a pipe instead of a real physical connection. + ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector + + // HandleWorkConnCb is a callback function that is called when a new work connection is created. + // + // If it is not set, the default frpc implementation will be used. + HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool +} + +// setServiceOptionsDefault sets the default values for ServiceOptions. +func setServiceOptionsDefault(options *ServiceOptions) error { + if options.Common != nil { + if err := options.Common.Complete(); err != nil { + return err + } + } + if options.ConnectorCreator == nil { + options.ConnectorCreator = NewConnector + } + return nil +} + +// Service is the client service that connects to frps and provides proxy services. type Service struct { - // uniq id got from frps, attach it in loginMsg + ctlMu sync.RWMutex + // Stores gracefulShutdownDuration independently from ctlMu, because the + // graceful shutdown wait may hold ctlMu for an arbitrary duration. + gracefulShutdownDuration atomic.Int64 + // manager control connection with server + ctl *Control + // Uniq id got from frps, it will be attached to loginMsg. runID string - // manager control connection with server - ctl *Control - ctlMu sync.RWMutex + // Auth runtime and encryption materials + auth *auth.ClientAuth - // Sets authentication based on selected method - authSetter auth.Setter + // web server for admin UI and apis + webServer *httppkg.Server - cfg config.ClientCommonConf - pxyCfgs map[string]config.ProxyConf - visitorCfgs map[string]config.VisitorConf - cfgMu sync.RWMutex + vnetController *vnet.Controller - // The configuration file used to initialize this client, or an empty - // string if no configuration file was used. - cfgFile string + cfgMu sync.RWMutex + // reloadMu serializes reload transactions to keep reloadCommon and applied + // config in sync across concurrent API operations. + reloadMu sync.Mutex + common *v1.ClientCommonConfig + // reloadCommon is used for filtering/defaulting during config-source reloads. + // It can be updated by /api/reload without mutating startup-only common behavior. + reloadCommon *v1.ClientCommonConfig + proxyCfgs []v1.ProxyConfigurer + visitorCfgs []v1.VisitorConfigurer + clientSpec *msg.ClientSpec - // This is configured by the login response from frps - serverUDPPort int + // aggregator manages multiple configuration sources. + // When set, the service watches for config changes and reloads automatically. + aggregator *source.Aggregator + configSource *source.ConfigSource + storeSource *source.StoreSource - exit uint32 // 0 means not exit + unsafeFeatures *security.UnsafeFeatures + + // The configuration file used to initialize this client, or an empty + // string if no configuration file was used. + configFilePath string // service context ctx context.Context // call cancel to stop service - cancel context.CancelFunc + cancel context.CancelCauseFunc + + connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector + handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool } -func NewService(cfg config.ClientCommonConf, pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf, cfgFile string) (svr *Service, err error) { - - ctx, cancel := context.WithCancel(context.Background()) - svr = &Service{ - authSetter: auth.NewAuthSetter(cfg.ClientConfig), - cfg: cfg, - cfgFile: cfgFile, - pxyCfgs: pxyCfgs, - visitorCfgs: visitorCfgs, - exit: 0, - ctx: xlog.NewContext(ctx, xlog.New()), - cancel: cancel, +func NewService(options ServiceOptions) (*Service, error) { + if err := setServiceOptionsDefault(&options); err != nil { + return nil, err } - return -} -func (svr *Service) GetController() *Control { - svr.ctlMu.RLock() - defer svr.ctlMu.RUnlock() - return svr.ctl -} + authRuntime, err := auth.BuildClientAuth(&options.Common.Auth) + if err != nil { + return nil, err + } -func (svr *Service) Run() error { - xl := xlog.FromContextSafe(svr.ctx) + if options.ConfigSourceAggregator == nil { + return nil, fmt.Errorf("config source aggregator is required") + } - // set custom DNSServer - if svr.cfg.DNSServer != "" { - dnsAddr := svr.cfg.DNSServer - if !strings.Contains(dnsAddr, ":") { - dnsAddr += ":53" - } - // Change default dns server for frpc - net.DefaultResolver = &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - return net.Dial("udp", dnsAddr) - }, + configSource := options.ConfigSourceAggregator.ConfigSource() + storeSource := options.ConfigSourceAggregator.StoreSource() + + proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load() + if loadErr != nil { + return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr) + } + proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs) + proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs) + visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs) + + // Create the web server after all fallible steps so its listener is not + // leaked when an earlier error causes NewService to return. + var webServer *httppkg.Server + if options.Common.WebServer.Port > 0 { + ws, err := httppkg.NewServer(options.Common.WebServer) + if err != nil { + return nil, err } + webServer = ws } - // login to frps - for { - conn, session, err := svr.login() - if err != nil { - xl.Warn("login to server failed: %v", err) + s := &Service{ + ctx: context.Background(), + auth: authRuntime, + webServer: webServer, + common: options.Common, + reloadCommon: options.Common, + configFilePath: options.ConfigFilePath, + unsafeFeatures: options.UnsafeFeatures, + proxyCfgs: proxyCfgs, + visitorCfgs: visitorCfgs, + clientSpec: options.ClientSpec, + aggregator: options.ConfigSourceAggregator, + configSource: configSource, + storeSource: storeSource, + connectorCreator: options.ConnectorCreator, + handleWorkConnCb: options.HandleWorkConnCb, + } - // if login_fail_exit is true, just exit this program - // otherwise sleep a while and try again to connect to server - if svr.cfg.LoginFailExit { - return err - } - util.RandomSleep(10*time.Second, 0.9, 1.1) - } else { - // login success - ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter) - ctl.Run() - svr.ctlMu.Lock() - svr.ctl = ctl - svr.ctlMu.Unlock() - break - } + if webServer != nil { + webServer.RouteRegister(s.registerRouteHandlers) + } + if options.Common.VirtualNet.Address != "" { + s.vnetController = vnet.NewController(options.Common.VirtualNet) } + return s, nil +} - go svr.keepControllerWorking() +func (svr *Service) Run(ctx context.Context) error { + ctx, cancel := context.WithCancelCause(ctx) + svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx)) + svr.cancel = cancel - if svr.cfg.AdminPort != 0 { - // Init admin server assets - assets.Load(svr.cfg.AssetsDir) + // set custom DNSServer + if svr.common.DNSServer != "" { + netpkg.SetDefaultDNSAddress(svr.common.DNSServer) + } - address := net.JoinHostPort(svr.cfg.AdminAddr, strconv.Itoa(svr.cfg.AdminPort)) - err := svr.RunAdminServer(address) - if err != nil { - log.Warn("run admin server error: %v", err) + if svr.vnetController != nil { + vnetController := svr.vnetController + if err := svr.vnetController.Init(); err != nil { + log.Errorf("init virtual network controller error: %v", err) + svr.stop() + return err } - log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort) + go func() { + log.Infof("virtual network controller start...") + if err := vnetController.Run(); err != nil && !errors.Is(err, net.ErrClosed) { + log.Warnf("virtual network controller exit with error: %v", err) + } + }() + } + + if svr.webServer != nil { + webServer := svr.webServer + go func() { + log.Infof("admin server listen on %s", webServer.Address()) + if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Warnf("admin server exit with error: %v", err) + } + }() + } + + // first login to frps + svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit)) + if svr.ctl == nil { + cancelCause := cancelErr{} + _ = errors.As(context.Cause(svr.ctx), &cancelCause) + svr.stop() + return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err) } + + go svr.keepControllerWorking() + <-svr.ctx.Done() + svr.stop() return nil } func (svr *Service) keepControllerWorking() { - xl := xlog.FromContextSafe(svr.ctx) - maxDelayTime := 20 * time.Second - delayTime := time.Second - - // if frpc reconnect frps, we need to limit retry times in 1min - // current retry logic is sleep 0s, 0s, 0s, 1s, 2s, 4s, 8s, ... - // when exceed 1min, we will reset delay and counts - cutoffTime := time.Now().Add(time.Minute) - reconnectDelay := time.Second - reconnectCounts := 1 - - for { - <-svr.ctl.ClosedDoneCh() - if atomic.LoadUint32(&svr.exit) != 0 { - return - } - - // the first three retry with no delay - if reconnectCounts > 3 { - util.RandomSleep(reconnectDelay, 0.9, 1.1) - xl.Info("wait %v to reconnect", reconnectDelay) - reconnectDelay *= 2 - } else { - util.RandomSleep(time.Second, 0, 0.5) - } - reconnectCounts++ - - now := time.Now() - if now.After(cutoffTime) { - // reset - cutoffTime = now.Add(time.Minute) - reconnectDelay = time.Second - reconnectCounts = 1 - } - - for { - xl.Info("try to reconnect to server...") - conn, session, err := svr.login() - if err != nil { - xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime) - util.RandomSleep(delayTime, 0.9, 1.1) - - delayTime = delayTime * 2 - if delayTime > maxDelayTime { - delayTime = maxDelayTime - } - continue - } - // reconnect success, init delayTime - delayTime = time.Second - - ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter) - ctl.Run() - svr.ctlMu.Lock() - if svr.ctl != nil { - svr.ctl.Close() - } - svr.ctl = ctl - svr.ctlMu.Unlock() - break + <-svr.ctl.Done() + + // There is a situation where the login is successful but due to certain reasons, + // the control immediately exits. It is necessary to limit the frequency of reconnection in this case. + // The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially. + // The maximum interval is 20 seconds. + wait.BackoffUntil(func() (bool, error) { + // loopLoginUntilSuccess is another layer of loop that will continuously attempt to + // login to the server until successful. + svr.loopLoginUntilSuccess(20*time.Second, false) + if svr.ctl != nil { + <-svr.ctl.Done() + return false, errors.New("control is closed and try another loop") } - } + // If the control is nil, it means that the login failed and the service is also closed. + return false, nil + }, wait.NewFastBackoffManager( + wait.FastBackoffOptions{ + Duration: time.Second, + Factor: 2, + Jitter: 0.1, + MaxDuration: 20 * time.Second, + FastRetryCount: 3, + FastRetryDelay: 200 * time.Millisecond, + FastRetryWindow: time.Minute, + FastRetryJitter: 0.5, + }, + ), true, svr.ctx.Done()) } -// login creates a connection to frps and registers it self as a client -// conn: control connection -// session: if it's not nil, using tcp mux -func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) { +func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) { xl := xlog.FromContextSafe(svr.ctx) - var tlsConfig *tls.Config - if svr.cfg.TLSEnable { - sn := svr.cfg.TLSServerName - if sn == "" { - sn = svr.cfg.ServerAddr - } - tlsConfig, err = transport.NewClientTLSConfig( - svr.cfg.TLSCertFile, - svr.cfg.TLSKeyFile, - svr.cfg.TLSTrustedCaFile, - sn) - if err != nil { - xl.Warn("fail to build tls configuration when service login, err: %v", err) - return + loginFunc := func() (bool, error) { + xl.Infof("try to connect to server...") + dialer := &controlSessionDialer{ + ctx: svr.ctx, + common: svr.common, + auth: svr.auth, + clientSpec: svr.clientSpec, + vnetController: svr.vnetController, + connectorCreator: svr.connectorCreator, } - } - - proxyType, addr, auth, err := libdial.ParseProxyURL(svr.cfg.HTTPProxy) - if err != nil { - xl.Error("fail to parse proxy url") - return - } - dialOptions := []libdial.DialOption{} - protocol := svr.cfg.Protocol - var websocketAfterHook *libdial.AfterHook - if protocol == "websocket" || protocol == "wss" { - if protocol == "wss" { - websocketAfterHook = &libdial.AfterHook{ - Priority: math.MaxUint64, // in case of wss, we first want to make the TLS handshake and then switch protocols from https to wss - Hook: frpNet.DialHookWebsocket(true), - } - } else { - websocketAfterHook = &libdial.AfterHook{ - Priority: 0, // in case of ws, we first want to switch protocols from http to ws, and only then make the TLS handshake in case TLS is enabled - Hook: frpNet.DialHookWebsocket(false), + sessionCtx, err := dialer.Dial(svr.runID) + if err != nil { + xl.Warnf("connect to server error: %v", err) + if firstLoginExit { + svr.cancel(cancelErr{Err: err}) } + return false, err } - protocol = "tcp" - } - if svr.cfg.ConnectServerLocalIP != "" { - dialOptions = append(dialOptions, libdial.WithLocalAddr(svr.cfg.ConnectServerLocalIP)) - } - dialOptions = append(dialOptions, - libdial.WithProtocol(protocol), - libdial.WithTimeout(time.Duration(svr.cfg.DialServerTimeout)*time.Second), - libdial.WithKeepAlive(time.Duration(svr.cfg.DialServerKeepAlive)*time.Second), - libdial.WithProxy(proxyType, addr), - libdial.WithProxyAuth(auth), - libdial.WithTLSConfig(tlsConfig), // TLS AfterHook has math.MaxUint64 priority - libdial.WithAfterHook(libdial.AfterHook{ - Priority: 1, // should be executed before TLS AfterHook but after the rest of the AfterHooks (except for wss) - Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, svr.cfg.DisableCustomTLSFirstByte), - }), - ) - if websocketAfterHook != nil { - // websocketAfterHook must be appended after TLS AfterHook because they both might have the - // same priority of math.MaxUint64 in case of wss but TLS AfterHook must be executed first - dialOptions = append(dialOptions, libdial.WithAfterHook(*websocketAfterHook)) - } + svr.runID = sessionCtx.RunID + xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID}) + xl.Infof("login to server success, get run id [%s]", svr.runID) - conn, err = libdial.Dial( - net.JoinHostPort(svr.cfg.ServerAddr, strconv.Itoa(svr.cfg.ServerPort)), - dialOptions..., - ) - if err != nil { - return - } + svr.cfgMu.RLock() + proxyCfgs := svr.proxyCfgs + visitorCfgs := svr.visitorCfgs + svr.cfgMu.RUnlock() - defer func() { + ctl, err := NewControl(svr.ctx, sessionCtx) if err != nil { - conn.Close() - if session != nil { - session.Close() - } + sessionCtx.Conn.Close() + sessionCtx.Connector.Close() + xl.Errorf("new control error: %v", err) + return false, err } - }() + ctl.SetInWorkConnCallback(svr.handleWorkConnCb) - if svr.cfg.TCPMux { - fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard - session, err = fmux.Client(conn, fmuxCfg) - if err != nil { - return - } - stream, errRet := session.OpenStream() - if errRet != nil { - session.Close() - err = errRet - return + ctl.Run(proxyCfgs, visitorCfgs) + // close and replace previous control + svr.ctlMu.Lock() + if svr.ctl != nil { + svr.ctl.Close() } - conn = stream + svr.ctl = ctl + svr.ctlMu.Unlock() + return true, nil } - loginMsg := &msg.Login{ - Arch: runtime.GOARCH, - Os: runtime.GOOS, - PoolCount: svr.cfg.PoolCount, - User: svr.cfg.User, - Version: version.Full(), - Timestamp: time.Now().Unix(), - RunID: svr.runID, - Metas: svr.cfg.Metas, - } - - // Add auth - if err = svr.authSetter.SetLogin(loginMsg); err != nil { - return - } - - if err = msg.WriteMsg(conn, loginMsg); err != nil { - return - } - - var loginRespMsg msg.LoginResp - conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - if err = msg.ReadMsgInto(conn, &loginRespMsg); err != nil { - return - } - conn.SetReadDeadline(time.Time{}) - - if loginRespMsg.Error != "" { - err = fmt.Errorf("%s", loginRespMsg.Error) - xl.Error("%s", loginRespMsg.Error) - return - } - - svr.runID = loginRespMsg.RunID - xl.ResetPrefixes() - xl.AppendPrefix(svr.runID) - - svr.serverUDPPort = loginRespMsg.ServerUDPPort - xl.Info("login to server success, get run id [%s], server udp port [%d]", loginRespMsg.RunID, loginRespMsg.ServerUDPPort) - return + // try to reconnect to server until success + wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager( + wait.FastBackoffOptions{ + Duration: time.Second, + Factor: 2, + Jitter: 0.1, + MaxDuration: maxInterval, + }), true, svr.ctx.Done()) } -func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error { +func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { svr.cfgMu.Lock() - svr.pxyCfgs = pxyCfgs + svr.proxyCfgs = proxyCfgs svr.visitorCfgs = visitorCfgs svr.cfgMu.Unlock() @@ -381,7 +377,36 @@ func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs svr.ctlMu.RUnlock() if ctl != nil { - return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs) + return svr.ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs) + } + return nil +} + +func (svr *Service) UpdateConfigSource( + common *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, +) error { + svr.reloadMu.Lock() + defer svr.reloadMu.Unlock() + + cfgSource := svr.configSource + if cfgSource == nil { + return fmt.Errorf("config source is not available") + } + + if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil { + return err + } + + // Non-atomic update semantics: source has been updated at this point. + // Even if reload fails below, keep this common config for subsequent reloads. + svr.cfgMu.Lock() + svr.reloadCommon = common + svr.cfgMu.Unlock() + + if err := svr.reloadConfigFromSourcesLocked(); err != nil { + return err } return nil } @@ -391,13 +416,112 @@ func (svr *Service) Close() { } func (svr *Service) GracefulClose(d time.Duration) { - atomic.StoreUint32(&svr.exit, 1) + svr.gracefulShutdownDuration.Store(int64(d)) + svr.cancel(nil) +} - svr.ctlMu.RLock() +func (svr *Service) stop() { + // Coordinate shutdown with reload/update paths that read source pointers. + svr.reloadMu.Lock() + if svr.aggregator != nil { + svr.aggregator = nil + } + svr.configSource = nil + svr.storeSource = nil + svr.reloadMu.Unlock() + + svr.ctlMu.Lock() + defer svr.ctlMu.Unlock() if svr.ctl != nil { + d := time.Duration(svr.gracefulShutdownDuration.Load()) svr.ctl.GracefulClose(d) + svr.ctl = nil + } + if svr.webServer != nil { + svr.webServer.Close() + svr.webServer = nil } + if svr.vnetController != nil { + _ = svr.vnetController.Stop() + svr.vnetController = nil + } +} + +func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) { + svr.ctlMu.RLock() + ctl := svr.ctl + svr.ctlMu.RUnlock() + + if ctl == nil { + return nil, false + } + return ctl.pm.GetProxyStatus(name) +} + +func (svr *Service) getVisitorCfg(name string) (v1.VisitorConfigurer, bool) { + svr.ctlMu.RLock() + ctl := svr.ctl svr.ctlMu.RUnlock() - svr.cancel() + if ctl == nil { + return nil, false + } + return ctl.vm.GetVisitorCfg(name) +} + +func (svr *Service) StatusExporter() StatusExporter { + return &statusExporterImpl{ + getProxyStatusFunc: svr.getProxyStatus, + } +} + +type StatusExporter interface { + GetProxyStatus(name string) (*proxy.WorkingStatus, bool) +} + +type statusExporterImpl struct { + getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool) +} + +func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) { + return s.getProxyStatusFunc(name) +} + +func (svr *Service) reloadConfigFromSources() error { + svr.reloadMu.Lock() + defer svr.reloadMu.Unlock() + return svr.reloadConfigFromSourcesLocked() +} + +func (svr *Service) reloadConfigFromSourcesLocked() error { + aggregator := svr.aggregator + if aggregator == nil { + return errors.New("config aggregator is not initialized") + } + + svr.cfgMu.RLock() + reloadCommon := svr.reloadCommon + svr.cfgMu.RUnlock() + + proxies, visitors, err := aggregator.Load() + if err != nil { + return fmt.Errorf("reload config from sources failed: %w", err) + } + + 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 { + return err + } + return nil } diff --git a/client/service_shutdown_test.go b/client/service_shutdown_test.go new file mode 100644 index 00000000000..73c7bd3d20d --- /dev/null +++ b/client/service_shutdown_test.go @@ -0,0 +1,95 @@ +package client + +import ( + "context" + "net" + "sync" + "testing" + "time" + + "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/client/visitor" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" +) + +type gracefulCloseTestConnector struct { + conn net.Conn +} + +func (*gracefulCloseTestConnector) Connect() (*msg.Conn, error) { return nil, net.ErrClosed } +func (c *gracefulCloseTestConnector) Close() error { return c.conn.Close() } + +func newGracefulCloseTestService() *Service { + ctx := context.Background() + common := &v1.ClientCommonConfig{} + serverConn, clientConn := net.Pipe() + ctl := &Control{ + ctx: ctx, + sessionCtx: &SessionContext{ + Common: common, + RunID: "graceful-close-race", + Conn: msg.NewConn(clientConn, msg.NewV1ReadWriter(clientConn)), + Connector: &gracefulCloseTestConnector{conn: serverConn}, + }, + 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, "") + return &Service{ctl: ctl, cancel: context.CancelCauseFunc(func(error) {})} +} + +func TestGracefulCloseAndStopSynchronizeDuration(t *testing.T) { + for i := range 10000 { + svr := newGracefulCloseTestService() + start := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + <-start + svr.GracefulClose(time.Duration(i)) + }() + go func() { + defer wg.Done() + <-start + svr.stop() + }() + close(start) + wg.Wait() + } +} + +func TestGracefulCloseDoesNotBlockDuringStop(t *testing.T) { + const gracefulDuration = 200 * time.Millisecond + + svr := newGracefulCloseTestService() + svr.GracefulClose(gracefulDuration) + stopDone := make(chan struct{}) + go func() { + svr.stop() + close(stopDone) + }() + defer func() { + select { + case <-stopDone: + case <-time.After(time.Second): + t.Error("stop did not finish") + } + }() + + deadline := time.Now().Add(time.Second) + for svr.ctlMu.TryLock() { + svr.ctlMu.Unlock() + if time.Now().After(deadline) { + t.Fatal("stop did not acquire ctlMu") + } + time.Sleep(time.Millisecond) + } + + start := time.Now() + svr.GracefulClose(0) + if elapsed := time.Since(start); elapsed >= gracefulDuration/2 { + t.Fatalf("GracefulClose blocked for %v while stop was waiting", elapsed) + } +} diff --git a/client/service_test.go b/client/service_test.go new file mode 100644 index 00000000000..29f141a1961 --- /dev/null +++ b/client/service_test.go @@ -0,0 +1,246 @@ +package client + +import ( + "context" + "errors" + "net" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/config/source" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type failingConnector struct { + err error +} + +func (c *failingConnector) Open() error { + return c.err +} + +func (c *failingConnector) Connect() (net.Conn, error) { + return nil, c.err +} + +func (c *failingConnector) Close() error { + return nil +} + +func getFreeTCPPort(t *testing.T) int { + t.Helper() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen on ephemeral port: %v", err) + } + defer ln.Close() + + return ln.Addr().(*net.TCPAddr).Port +} + +func TestRunStopsStartedComponentsOnInitialLoginFailure(t *testing.T) { + port := getFreeTCPPort(t) + agg := source.NewAggregator(source.NewConfigSource()) + + svr, err := NewService(ServiceOptions{ + Common: &v1.ClientCommonConfig{ + LoginFailExit: lo.ToPtr(true), + WebServer: v1.WebServerConfig{ + Addr: "127.0.0.1", + Port: port, + }, + }, + ConfigSourceAggregator: agg, + ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) Connector { + return &failingConnector{err: errors.New("login boom")} + }, + }) + if err != nil { + t.Fatalf("new service: %v", err) + } + + err = svr.Run(context.Background()) + if err == nil { + t.Fatal("expected run error, got nil") + } + if !strings.Contains(err.Error(), "login boom") { + t.Fatalf("unexpected error: %v", err) + } + if svr.webServer != nil { + t.Fatal("expected web server to be cleaned up after initial login failure") + } + + ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + t.Fatalf("expected admin port to be released: %v", err) + } + _ = ln.Close() +} + +func TestNewServiceDoesNotLeakAdminListenerOnAuthBuildFailure(t *testing.T) { + port := getFreeTCPPort(t) + agg := source.NewAggregator(source.NewConfigSource()) + + _, err := NewService(ServiceOptions{ + Common: &v1.ClientCommonConfig{ + Auth: v1.AuthClientConfig{ + Method: v1.AuthMethodOIDC, + OIDC: v1.AuthOIDCClientConfig{ + TokenEndpointURL: "://bad", + }, + }, + WebServer: v1.WebServerConfig{ + Addr: "127.0.0.1", + Port: port, + }, + }, + ConfigSourceAggregator: agg, + }) + if err == nil { + t.Fatal("expected new service error, got nil") + } + if !strings.Contains(err.Error(), "auth.oidc.tokenEndpointURL") { + t.Fatalf("unexpected error: %v", err) + } + + ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + t.Fatalf("expected admin port to remain free: %v", err) + } + _ = ln.Close() +} + +func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) { + prevCommon := &v1.ClientCommonConfig{User: "old-user"} + newCommon := &v1.ClientCommonConfig{User: "new-user"} + + svr := &Service{ + configSource: source.NewConfigSource(), + reloadCommon: prevCommon, + } + + invalidProxy := &v1.TCPProxyConfig{} + err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{invalidProxy}, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "proxy name cannot be empty") { + t.Fatalf("unexpected error: %v", err) + } + + if svr.reloadCommon != prevCommon { + t.Fatalf("reloadCommon should roll back on ReplaceAll failure") + } +} + +func TestUpdateConfigSourceKeepsReloadCommonOnReloadFailure(t *testing.T) { + prevCommon := &v1.ClientCommonConfig{User: "old-user"} + newCommon := &v1.ClientCommonConfig{User: "new-user"} + + svr := &Service{ + // Keep configSource valid so ReplaceAll succeeds first. + configSource: source.NewConfigSource(), + reloadCommon: prevCommon, + // Keep aggregator nil to force reload failure. + aggregator: nil, + } + + validProxy := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "p1", + Type: "tcp", + }, + } + err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{validProxy}, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "config aggregator is not initialized") { + t.Fatalf("unexpected error: %v", err) + } + + if svr.reloadCommon != newCommon { + t.Fatalf("reloadCommon should keep new value on reload failure") + } +} + +func TestReloadConfigFromSourcesDoesNotMutateStoreConfigs(t *testing.T) { + storeSource, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: filepath.Join(t.TempDir(), "store.json"), + }) + if err != nil { + t.Fatalf("new store source: %v", err) + } + + proxyCfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "store-proxy", + Type: "tcp", + }, + } + visitorCfg := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: "store-visitor", + Type: "stcp", + }, + } + if err := storeSource.AddProxy(proxyCfg); err != nil { + t.Fatalf("add proxy to store: %v", err) + } + if err := storeSource.AddVisitor(visitorCfg); err != nil { + t.Fatalf("add visitor to store: %v", err) + } + + agg := source.NewAggregator(source.NewConfigSource()) + agg.SetStoreSource(storeSource) + svr := &Service{ + aggregator: agg, + configSource: agg.ConfigSource(), + storeSource: storeSource, + reloadCommon: &v1.ClientCommonConfig{}, + } + + if err := svr.reloadConfigFromSources(); err != nil { + t.Fatalf("reload config from sources: %v", err) + } + + gotProxy := storeSource.GetProxy("store-proxy") + if gotProxy == nil { + t.Fatalf("proxy not found in store") + } + if gotProxy.GetBaseConfig().LocalIP != "" { + t.Fatalf("store proxy localIP should stay empty, got %q", gotProxy.GetBaseConfig().LocalIP) + } + + gotVisitor := storeSource.GetVisitor("store-visitor") + if gotVisitor == nil { + t.Fatalf("visitor not found in store") + } + if gotVisitor.GetBaseConfig().BindAddr != "" { + t.Fatalf("store visitor bindAddr should stay empty, got %q", gotVisitor.GetBaseConfig().BindAddr) + } + + svr.cfgMu.RLock() + defer svr.cfgMu.RUnlock() + + if len(svr.proxyCfgs) != 1 { + t.Fatalf("expected 1 runtime proxy, got %d", len(svr.proxyCfgs)) + } + if svr.proxyCfgs[0].GetBaseConfig().LocalIP != "127.0.0.1" { + t.Fatalf("runtime proxy localIP should be defaulted, got %q", svr.proxyCfgs[0].GetBaseConfig().LocalIP) + } + + if len(svr.visitorCfgs) != 1 { + t.Fatalf("expected 1 runtime visitor, got %d", len(svr.visitorCfgs)) + } + if svr.visitorCfgs[0].GetBaseConfig().BindAddr != "127.0.0.1" { + t.Fatalf("runtime visitor bindAddr should be defaulted, got %q", svr.visitorCfgs[0].GetBaseConfig().BindAddr) + } +} diff --git a/client/visitor.go b/client/visitor.go deleted file mode 100644 index ba1cd7cb98b..00000000000 --- a/client/visitor.go +++ /dev/null @@ -1,567 +0,0 @@ -// Copyright 2017 fatedier, fatedier@gmail.com -// -// 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 client - -import ( - "bytes" - "context" - "fmt" - "io" - "net" - "strconv" - "sync" - "time" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/msg" - "github.com/fatedier/frp/pkg/proto/udp" - frpNet "github.com/fatedier/frp/pkg/util/net" - "github.com/fatedier/frp/pkg/util/util" - "github.com/fatedier/frp/pkg/util/xlog" - - "github.com/fatedier/golib/errors" - frpIo "github.com/fatedier/golib/io" - "github.com/fatedier/golib/pool" - fmux "github.com/hashicorp/yamux" -) - -// Visitor is used for forward traffics from local port tot remote service. -type Visitor interface { - Run() error - Close() -} - -func NewVisitor(ctx context.Context, ctl *Control, cfg config.VisitorConf) (visitor Visitor) { - xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseInfo().ProxyName) - baseVisitor := BaseVisitor{ - ctl: ctl, - ctx: xlog.NewContext(ctx, xl), - } - switch cfg := cfg.(type) { - case *config.STCPVisitorConf: - visitor = &STCPVisitor{ - BaseVisitor: &baseVisitor, - cfg: cfg, - } - case *config.XTCPVisitorConf: - visitor = &XTCPVisitor{ - BaseVisitor: &baseVisitor, - cfg: cfg, - } - case *config.SUDPVisitorConf: - visitor = &SUDPVisitor{ - BaseVisitor: &baseVisitor, - cfg: cfg, - checkCloseCh: make(chan struct{}), - } - } - return -} - -type BaseVisitor struct { - ctl *Control - l net.Listener - closed bool - - mu sync.RWMutex - ctx context.Context -} - -type STCPVisitor struct { - *BaseVisitor - - cfg *config.STCPVisitorConf -} - -func (sv *STCPVisitor) Run() (err error) { - sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort))) - if err != nil { - return - } - - go sv.worker() - return -} - -func (sv *STCPVisitor) Close() { - sv.l.Close() -} - -func (sv *STCPVisitor) worker() { - xl := xlog.FromContextSafe(sv.ctx) - for { - conn, err := sv.l.Accept() - if err != nil { - xl.Warn("stcp local listener closed") - return - } - - go sv.handleConn(conn) - } -} - -func (sv *STCPVisitor) handleConn(userConn net.Conn) { - xl := xlog.FromContextSafe(sv.ctx) - defer userConn.Close() - - xl.Debug("get a new stcp user connection") - visitorConn, err := sv.ctl.connectServer() - if err != nil { - return - } - defer visitorConn.Close() - - now := time.Now().Unix() - newVisitorConnMsg := &msg.NewVisitorConn{ - ProxyName: sv.cfg.ServerName, - SignKey: util.GetAuthKey(sv.cfg.Sk, now), - Timestamp: now, - UseEncryption: sv.cfg.UseEncryption, - UseCompression: sv.cfg.UseCompression, - } - err = msg.WriteMsg(visitorConn, newVisitorConnMsg) - if err != nil { - xl.Warn("send newVisitorConnMsg to server error: %v", err) - return - } - - var newVisitorConnRespMsg msg.NewVisitorConnResp - visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) - if err != nil { - xl.Warn("get newVisitorConnRespMsg error: %v", err) - return - } - visitorConn.SetReadDeadline(time.Time{}) - - if newVisitorConnRespMsg.Error != "" { - xl.Warn("start new visitor connection error: %s", newVisitorConnRespMsg.Error) - return - } - - var remote io.ReadWriteCloser - remote = visitorConn - if sv.cfg.UseEncryption { - remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk)) - if err != nil { - xl.Error("create encryption stream error: %v", err) - return - } - } - - if sv.cfg.UseCompression { - remote = frpIo.WithCompression(remote) - } - - frpIo.Join(userConn, remote) -} - -type XTCPVisitor struct { - *BaseVisitor - - cfg *config.XTCPVisitorConf -} - -func (sv *XTCPVisitor) Run() (err error) { - sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort))) - if err != nil { - return - } - - go sv.worker() - return -} - -func (sv *XTCPVisitor) Close() { - sv.l.Close() -} - -func (sv *XTCPVisitor) worker() { - xl := xlog.FromContextSafe(sv.ctx) - for { - conn, err := sv.l.Accept() - if err != nil { - xl.Warn("xtcp local listener closed") - return - } - - go sv.handleConn(conn) - } -} - -func (sv *XTCPVisitor) handleConn(userConn net.Conn) { - xl := xlog.FromContextSafe(sv.ctx) - defer userConn.Close() - - xl.Debug("get a new xtcp user connection") - if sv.ctl.serverUDPPort == 0 { - xl.Error("xtcp is not supported by server") - return - } - - raddr, err := net.ResolveUDPAddr("udp", - net.JoinHostPort(sv.ctl.clientCfg.ServerAddr, strconv.Itoa(sv.ctl.serverUDPPort))) - if err != nil { - xl.Error("resolve server UDP addr error") - return - } - - visitorConn, err := net.DialUDP("udp", nil, raddr) - if err != nil { - xl.Warn("dial server udp addr error: %v", err) - return - } - defer visitorConn.Close() - - now := time.Now().Unix() - natHoleVisitorMsg := &msg.NatHoleVisitor{ - ProxyName: sv.cfg.ServerName, - SignKey: util.GetAuthKey(sv.cfg.Sk, now), - Timestamp: now, - } - err = msg.WriteMsg(visitorConn, natHoleVisitorMsg) - if err != nil { - xl.Warn("send natHoleVisitorMsg to server error: %v", err) - return - } - - // Wait for client address at most 10 seconds. - var natHoleRespMsg msg.NatHoleResp - visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - buf := pool.GetBuf(1024) - n, err := visitorConn.Read(buf) - if err != nil { - xl.Warn("get natHoleRespMsg error: %v", err) - return - } - - err = msg.ReadMsgInto(bytes.NewReader(buf[:n]), &natHoleRespMsg) - if err != nil { - xl.Warn("get natHoleRespMsg error: %v", err) - return - } - visitorConn.SetReadDeadline(time.Time{}) - pool.PutBuf(buf) - - if natHoleRespMsg.Error != "" { - xl.Error("natHoleRespMsg get error info: %s", natHoleRespMsg.Error) - return - } - - xl.Trace("get natHoleRespMsg, sid [%s], client address [%s], visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr) - - // Close visitorConn, so we can use it's local address. - visitorConn.Close() - - // send sid message to client - laddr, _ := net.ResolveUDPAddr("udp", visitorConn.LocalAddr().String()) - daddr, err := net.ResolveUDPAddr("udp", natHoleRespMsg.ClientAddr) - if err != nil { - xl.Error("resolve client udp address error: %v", err) - return - } - lConn, err := net.DialUDP("udp", laddr, daddr) - if err != nil { - xl.Error("dial client udp address error: %v", err) - return - } - defer lConn.Close() - - lConn.Write([]byte(natHoleRespMsg.Sid)) - - // read ack sid from client - sidBuf := pool.GetBuf(1024) - lConn.SetReadDeadline(time.Now().Add(8 * time.Second)) - n, err = lConn.Read(sidBuf) - if err != nil { - xl.Warn("get sid from client error: %v", err) - return - } - lConn.SetReadDeadline(time.Time{}) - if string(sidBuf[:n]) != natHoleRespMsg.Sid { - xl.Warn("incorrect sid from client") - return - } - pool.PutBuf(sidBuf) - - xl.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid) - - // wrap kcp connection - var remote io.ReadWriteCloser - remote, err = frpNet.NewKCPConnFromUDP(lConn, true, natHoleRespMsg.ClientAddr) - if err != nil { - xl.Error("create kcp connection from udp connection error: %v", err) - return - } - - fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = 5 * time.Second - fmuxCfg.LogOutput = io.Discard - sess, err := fmux.Client(remote, fmuxCfg) - if err != nil { - xl.Error("create yamux session error: %v", err) - return - } - defer sess.Close() - muxConn, err := sess.Open() - if err != nil { - xl.Error("open yamux stream error: %v", err) - return - } - - var muxConnRWCloser io.ReadWriteCloser = muxConn - if sv.cfg.UseEncryption { - muxConnRWCloser, err = frpIo.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk)) - if err != nil { - xl.Error("create encryption stream error: %v", err) - return - } - } - if sv.cfg.UseCompression { - muxConnRWCloser = frpIo.WithCompression(muxConnRWCloser) - } - - frpIo.Join(userConn, muxConnRWCloser) - xl.Debug("join connections closed") -} - -type SUDPVisitor struct { - *BaseVisitor - - checkCloseCh chan struct{} - // udpConn is the listener of udp packet - udpConn *net.UDPConn - readCh chan *msg.UDPPacket - sendCh chan *msg.UDPPacket - - cfg *config.SUDPVisitorConf -} - -// SUDP Run start listen a udp port -func (sv *SUDPVisitor) Run() (err error) { - xl := xlog.FromContextSafe(sv.ctx) - - addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort))) - if err != nil { - return fmt.Errorf("sudp ResolveUDPAddr error: %v", err) - } - - sv.udpConn, err = net.ListenUDP("udp", addr) - if err != nil { - return fmt.Errorf("listen udp port %s error: %v", addr.String(), err) - } - - sv.sendCh = make(chan *msg.UDPPacket, 1024) - sv.readCh = make(chan *msg.UDPPacket, 1024) - - xl.Info("sudp start to work, listen on %s", addr) - - go sv.dispatcher() - go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.ctl.clientCfg.UDPPacketSize)) - - return -} - -func (sv *SUDPVisitor) dispatcher() { - xl := xlog.FromContextSafe(sv.ctx) - - var ( - visitorConn net.Conn - err error - - firstPacket *msg.UDPPacket - ) - - for { - select { - case firstPacket = <-sv.sendCh: - if firstPacket == nil { - xl.Info("frpc sudp visitor proxy is closed") - return - } - case <-sv.checkCloseCh: - xl.Info("frpc sudp visitor proxy is closed") - return - } - - visitorConn, err = sv.getNewVisitorConn() - if err != nil { - xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err) - continue - } - - // visitorConn always be closed when worker done. - sv.worker(visitorConn, firstPacket) - - select { - case <-sv.checkCloseCh: - return - default: - } - } - -} - -func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { - xl := xlog.FromContextSafe(sv.ctx) - xl.Debug("starting sudp proxy worker") - - wg := &sync.WaitGroup{} - wg.Add(2) - closeCh := make(chan struct{}) - - // udp service -> frpc -> frps -> frpc visitor -> user - workConnReaderFn := func(conn net.Conn) { - defer func() { - conn.Close() - close(closeCh) - wg.Done() - }() - - for { - var ( - rawMsg msg.Message - errRet error - ) - - // frpc will send heartbeat in workConn to frpc visitor for keeping alive - conn.SetReadDeadline(time.Now().Add(60 * time.Second)) - if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil { - xl.Warn("read from workconn for user udp conn error: %v", errRet) - return - } - - conn.SetReadDeadline(time.Time{}) - switch m := rawMsg.(type) { - case *msg.Ping: - xl.Debug("frpc visitor get ping message from frpc") - continue - case *msg.UDPPacket: - if errRet := errors.PanicToError(func() { - sv.readCh <- m - xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content) - }); errRet != nil { - xl.Info("reader goroutine for udp work connection closed") - return - } - } - } - } - - // udp service <- frpc <- frps <- frpc visitor <- user - workConnSenderFn := func(conn net.Conn) { - defer func() { - conn.Close() - wg.Done() - }() - - var errRet error - if firstPacket != nil { - if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil { - xl.Warn("sender goroutine for udp work connection closed: %v", errRet) - return - } - xl.Trace("send udp package to workConn: %s", firstPacket.Content) - } - - for { - select { - case udpMsg, ok := <-sv.sendCh: - if !ok { - xl.Info("sender goroutine for udp work connection closed") - return - } - - if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil { - xl.Warn("sender goroutine for udp work connection closed: %v", errRet) - return - } - xl.Trace("send udp package to workConn: %s", udpMsg.Content) - case <-closeCh: - return - } - } - } - - go workConnReaderFn(workConn) - go workConnSenderFn(workConn) - - wg.Wait() - xl.Info("sudp worker is closed") -} - -func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { - xl := xlog.FromContextSafe(sv.ctx) - visitorConn, err := sv.ctl.connectServer() - if err != nil { - return nil, fmt.Errorf("frpc connect frps error: %v", err) - } - - now := time.Now().Unix() - newVisitorConnMsg := &msg.NewVisitorConn{ - ProxyName: sv.cfg.ServerName, - SignKey: util.GetAuthKey(sv.cfg.Sk, now), - Timestamp: now, - UseEncryption: sv.cfg.UseEncryption, - UseCompression: sv.cfg.UseCompression, - } - err = msg.WriteMsg(visitorConn, newVisitorConnMsg) - if err != nil { - return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err) - } - - var newVisitorConnRespMsg msg.NewVisitorConnResp - visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) - if err != nil { - return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err) - } - visitorConn.SetReadDeadline(time.Time{}) - - if newVisitorConnRespMsg.Error != "" { - return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) - } - - var remote io.ReadWriteCloser - remote = visitorConn - if sv.cfg.UseEncryption { - remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk)) - if err != nil { - xl.Error("create encryption stream error: %v", err) - return nil, err - } - } - if sv.cfg.UseCompression { - remote = frpIo.WithCompression(remote) - } - return frpNet.WrapReadWriteCloserToConn(remote, visitorConn), nil -} - -func (sv *SUDPVisitor) Close() { - sv.mu.Lock() - defer sv.mu.Unlock() - - select { - case <-sv.checkCloseCh: - return - default: - close(sv.checkCloseCh) - } - if sv.udpConn != nil { - sv.udpConn.Close() - } - close(sv.readCh) - close(sv.sendCh) -} diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go new file mode 100644 index 00000000000..03ec51feb6e --- /dev/null +++ b/client/visitor/stcp.go @@ -0,0 +1,85 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 visitor + +import ( + "net" + "strconv" + + libio "github.com/fatedier/golib/io" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/xlog" +) + +type STCPVisitor struct { + *BaseVisitor + + cfg *v1.STCPVisitorConfig +} + +func (sv *STCPVisitor) Run() (err error) { + if sv.cfg.BindPort > 0 { + sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort))) + if err != nil { + return + } + go sv.acceptLoop(sv.l, "stcp local", sv.handleConn) + } + + go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn) + + if sv.plugin != nil { + sv.plugin.Start() + } + return +} + +func (sv *STCPVisitor) Close() { + sv.BaseVisitor.Close() +} + +func (sv *STCPVisitor) handleConn(userConn net.Conn) { + xl := xlog.FromContextSafe(sv.ctx) + var tunnelErr error + defer func() { + if tunnelErr != nil { + if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok { + _ = eConn.CloseWithError(tunnelErr) + return + } + } + userConn.Close() + }() + + xl.Debugf("get a new stcp user connection") + visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig()) + if err != nil { + xl.Warnf("dialRawVisitorConn error: %v", err) + tunnelErr = err + return + } + defer visitorConn.Close() + + remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig()) + if err != nil { + xl.Warnf("wrapVisitorConn error: %v", err) + tunnelErr = err + return + } + defer recycleFn() + + libio.Join(userConn, remote) +} diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go new file mode 100644 index 00000000000..f1939bda58a --- /dev/null +++ b/client/visitor/sudp.go @@ -0,0 +1,237 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 visitor + +import ( + "fmt" + "net" + "strconv" + "sync" + "time" + + "github.com/fatedier/golib/errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/udp" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/xlog" +) + +type SUDPVisitor struct { + *BaseVisitor + + checkCloseCh chan struct{} + // udpConn is the listener of udp packet + udpConn *net.UDPConn + readCh chan *msg.UDPPacket + sendCh chan *msg.UDPPacket + + cfg *v1.SUDPVisitorConfig +} + +// SUDP Run start listen a udp port +func (sv *SUDPVisitor) Run() (err error) { + xl := xlog.FromContextSafe(sv.ctx) + + addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort))) + if err != nil { + return fmt.Errorf("sudp ResolveUDPAddr error: %v", err) + } + + sv.udpConn, err = net.ListenUDP("udp", addr) + if err != nil { + return fmt.Errorf("listen udp port %s error: %v", addr.String(), err) + } + + sv.sendCh = make(chan *msg.UDPPacket, 1024) + sv.readCh = make(chan *msg.UDPPacket, 1024) + + xl.Infof("sudp start to work, listen on %s", addr) + + go sv.dispatcher() + go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.clientCfg.UDPPacketSize)) + + return +} + +func (sv *SUDPVisitor) dispatcher() { + xl := xlog.FromContextSafe(sv.ctx) + + var ( + visitorConn net.Conn + recycleFn func() + err error + + firstPacket *msg.UDPPacket + ) + + for { + select { + case firstPacket = <-sv.sendCh: + if firstPacket == nil { + xl.Infof("frpc sudp visitor proxy is closed") + return + } + case <-sv.checkCloseCh: + xl.Infof("frpc sudp visitor proxy is closed") + return + } + + visitorConn, recycleFn, err = sv.getNewVisitorConn() + if err != nil { + xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err) + continue + } + + // visitorConn always be closed when worker done. + func() { + defer recycleFn() + sv.worker(visitorConn, firstPacket) + }() + + select { + case <-sv.checkCloseCh: + return + default: + } + } +} + +func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) { + xl := xlog.FromContextSafe(sv.ctx) + xl.Debugf("starting sudp proxy worker") + 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) + closeCh := make(chan struct{}) + + // udp service -> frpc -> frps -> frpc visitor -> user + workConnReaderFn := func(payloadConn *msg.Conn) { + defer func() { + payloadConn.Close() + close(closeCh) + wg.Done() + }() + + for { + var ( + rawMsg msg.Message + errRet error + ) + + // frpc will send heartbeat in workConn to frpc visitor for keeping alive + _ = payloadConn.SetReadDeadline(time.Now().Add(60 * time.Second)) + if rawMsg, errRet = payloadConn.ReadMsg(); errRet != nil { + xl.Warnf("read from workconn for user udp conn error: %v", errRet) + return + } + + _ = payloadConn.SetReadDeadline(time.Time{}) + switch m := rawMsg.(type) { + case *msg.Ping: + xl.Debugf("frpc visitor get ping message from frpc") + continue + case *msg.UDPPacket: + if errRet := errors.PanicToError(func() { + sv.readCh <- m + xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content)) + }); errRet != nil { + xl.Infof("reader goroutine for udp work connection closed") + return + } + } + } + } + + // udp service <- frpc <- frps <- frpc visitor <- user + workConnSenderFn := func(payloadConn *msg.Conn) { + defer func() { + payloadConn.Close() + wg.Done() + }() + + var errRet error + if firstPacket != nil { + if errRet = payloadConn.WriteMsg(firstPacket); errRet != nil { + xl.Warnf("sender goroutine for udp work connection closed: %v", errRet) + return + } + xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content)) + } + + for { + select { + case udpMsg, ok := <-sv.sendCh: + if !ok { + xl.Infof("sender goroutine for udp work connection closed") + return + } + + if errRet = payloadConn.WriteMsg(udpMsg); errRet != nil { + xl.Warnf("sender goroutine for udp work connection closed: %v", errRet) + return + } + xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content)) + case <-closeCh: + return + } + } + } + + go workConnReaderFn(payloadConn) + go workConnSenderFn(payloadConn) + + wg.Wait() + xl.Infof("sudp worker is closed") +} + +func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) { + rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig()) + if err != nil { + return nil, func() {}, err + } + rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig()) + if err != nil { + rawConn.Close() + return nil, func() {}, err + } + return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil +} + +func (sv *SUDPVisitor) Close() { + sv.mu.Lock() + defer sv.mu.Unlock() + + select { + case <-sv.checkCloseCh: + return + default: + close(sv.checkCloseCh) + } + sv.BaseVisitor.Close() + if sv.udpConn != nil { + sv.udpConn.Close() + } + close(sv.readCh) + close(sv.sendCh) +} diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go new file mode 100644 index 00000000000..eccad013e52 --- /dev/null +++ b/client/visitor/visitor.go @@ -0,0 +1,217 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 visitor + +import ( + "context" + "fmt" + "io" + "net" + "sync" + "time" + + libio "github.com/fatedier/golib/io" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" + plugin "github.com/fatedier/frp/pkg/plugin/visitor" + "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" +) + +// Helper wraps some functions for visitor to use. +type Helper interface { + // ConnectServer directly connects to the frp server. + ConnectServer() (*msg.Conn, error) + // TransferConn transfers the connection to another visitor. + TransferConn(string, net.Conn) error + // MsgTransporter returns the message transporter that is used to send and receive messages + // to the frp server through the controller. + MsgTransporter() transport.MessageTransporter + // VNetController returns the vnet controller that is used to manage the virtual network. + VNetController() *vnet.Controller + // RunID returns the run id of current controller. + 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 + AcceptConn(conn net.Conn) error + Close() +} + +func NewVisitor( + ctx context.Context, + cfg v1.VisitorConfigurer, + clientCfg *v1.ClientCommonConfig, + helper Helper, +) (Visitor, error) { + xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name) + ctx = xlog.NewContext(ctx, xl) + var visitor Visitor + baseVisitor := BaseVisitor{ + clientCfg: clientCfg, + helper: helper, + ctx: ctx, + internalLn: netpkg.NewInternalListener(), + } + if cfg.GetBaseConfig().Plugin.Type != "" { + p, err := plugin.Create( + cfg.GetBaseConfig().Plugin.Type, + plugin.PluginContext{ + Name: cfg.GetBaseConfig().Name, + Ctx: ctx, + VnetController: helper.VNetController(), + SendConnToVisitor: func(conn net.Conn) { + _ = baseVisitor.AcceptConn(conn) + }, + }, + cfg.GetBaseConfig().Plugin.VisitorPluginOptions, + ) + if err != nil { + return nil, err + } + baseVisitor.plugin = p + } + switch cfg := cfg.(type) { + case *v1.STCPVisitorConfig: + visitor = &STCPVisitor{ + BaseVisitor: &baseVisitor, + cfg: cfg, + } + case *v1.XTCPVisitorConfig: + visitor = &XTCPVisitor{ + BaseVisitor: &baseVisitor, + cfg: cfg, + startTunnelCh: make(chan struct{}), + } + case *v1.SUDPVisitorConfig: + visitor = &SUDPVisitor{ + BaseVisitor: &baseVisitor, + cfg: cfg, + checkCloseCh: make(chan struct{}), + } + } + return visitor, nil +} + +type BaseVisitor struct { + clientCfg *v1.ClientCommonConfig + helper Helper + l net.Listener + internalLn *netpkg.InternalListener + plugin plugin.Plugin + + mu sync.RWMutex + ctx context.Context +} + +func (v *BaseVisitor) AcceptConn(conn net.Conn) error { + return v.internalLn.PutConn(conn) +} + +func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) { + xl := xlog.FromContextSafe(v.ctx) + for { + conn, err := l.Accept() + if err != nil { + xl.Warnf("%s listener closed", name) + return + } + go handleConn(conn) + } +} + +func (v *BaseVisitor) Close() { + if v.l != nil { + v.l.Close() + } + if v.internalLn != nil { + v.internalLn.Close() + } + if v.plugin != nil { + v.plugin.Close() + } +} + +func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) { + visitorConn, err := v.helper.ConnectServer() + if err != nil { + return nil, fmt.Errorf("connect to server error: %v", err) + } + + now := time.Now().Unix() + targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName) + newVisitorConnMsg := &msg.NewVisitorConn{ + RunID: v.helper.RunID(), + ProxyName: targetProxyName, + SignKey: util.GetAuthKey(cfg.SecretKey, now), + Timestamp: now, + UseEncryption: cfg.Transport.UseEncryption, + UseCompression: cfg.Transport.UseCompression, + } + err = visitorConn.WriteMsg(newVisitorConnMsg) + if err != nil { + visitorConn.Close() + return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err) + } + + _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + var newVisitorConnRespMsg msg.NewVisitorConnResp + err = visitorConn.ReadMsgInto(&newVisitorConnRespMsg) + if err != nil { + visitorConn.Close() + return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err) + } + _ = visitorConn.SetReadDeadline(time.Time{}) + + if newVisitorConnRespMsg.Error != "" { + visitorConn.Close() + return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) + } + return visitorConn, nil +} + +func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) { + rwc := conn + if cfg.Transport.UseEncryption { + var err error + rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey)) + if err != nil { + return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err) + } + } + recycleFn := func() {} + if cfg.Transport.UseCompression { + rwc, recycleFn = libio.WithCompressionFromPool(rwc) + } + return rwc, recycleFn, nil +} diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go new file mode 100644 index 00000000000..3e614f7a876 --- /dev/null +++ b/client/visitor/visitor_manager.go @@ -0,0 +1,239 @@ +// Copyright 2018 fatedier, fatedier@gmail.com +// +// 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 visitor + +import ( + "context" + "fmt" + "net" + "reflect" + "sync" + "time" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/vnet" +) + +type Manager struct { + clientCfg *v1.ClientCommonConfig + cfgs map[string]v1.VisitorConfigurer + visitors map[string]Visitor + helper Helper + + checkInterval time.Duration + keepVisitorsRunningOnce sync.Once + + mu sync.RWMutex + ctx context.Context + + stopCh chan struct{} +} + +func NewManager( + ctx context.Context, + runID string, + clientCfg *v1.ClientCommonConfig, + 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), + visitors: make(map[string]Visitor), + checkInterval: 10 * time.Second, + ctx: ctx, + stopCh: make(chan struct{}), + } + m.helper = &visitorHelperImpl{ + connectServerFn: connectServer, + msgTransporter: msgTransporter, + vnetController: vnetController, + transferConnFn: m.TransferConn, + runID: runID, + udpPacketCodec: udpPacketCodec, + } + return m +} + +// keepVisitorsRunning checks all visitors' status periodically, if some visitor is not running, start it. +// It will only start after Reload is called and a new visitor is added. +func (vm *Manager) keepVisitorsRunning() { + xl := xlog.FromContextSafe(vm.ctx) + + ticker := time.NewTicker(vm.checkInterval) + defer ticker.Stop() + + for { + select { + case <-vm.stopCh: + xl.Tracef("gracefully shutdown visitor manager") + return + case <-ticker.C: + vm.mu.Lock() + for _, cfg := range vm.cfgs { + name := cfg.GetBaseConfig().Name + if _, exist := vm.visitors[name]; !exist { + xl.Infof("try to start visitor [%s]", name) + _ = vm.startVisitor(cfg) + } + } + vm.mu.Unlock() + } + } +} + +func (vm *Manager) Close() { + vm.mu.Lock() + defer vm.mu.Unlock() + for _, v := range vm.visitors { + v.Close() + } + select { + case <-vm.stopCh: + default: + close(vm.stopCh) + } +} + +// Hold lock before calling this function. +func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) { + xl := xlog.FromContextSafe(vm.ctx) + name := cfg.GetBaseConfig().Name + visitor, err := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper) + if err != nil { + xl.Warnf("new visitor error: %v", err) + return + } + err = visitor.Run() + if err != nil { + xl.Warnf("start error: %v", err) + } else { + vm.visitors[name] = visitor + xl.Infof("start visitor success") + } + return +} + +func (vm *Manager) UpdateAll(cfgs []v1.VisitorConfigurer) { + if len(cfgs) > 0 { + // Only start keepVisitorsRunning goroutine once and only when there is at least one visitor. + vm.keepVisitorsRunningOnce.Do(func() { + go vm.keepVisitorsRunning() + }) + } + + xl := xlog.FromContextSafe(vm.ctx) + cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string { + return c.GetBaseConfig().Name + }) + vm.mu.Lock() + defer vm.mu.Unlock() + + delNames := make([]string, 0) + for name, oldCfg := range vm.cfgs { + del := false + cfg, ok := cfgsMap[name] + if !ok || !reflect.DeepEqual(oldCfg, cfg) { + del = true + } + + if del { + delNames = append(delNames, name) + delete(vm.cfgs, name) + if visitor, ok := vm.visitors[name]; ok { + visitor.Close() + } + delete(vm.visitors, name) + } + } + if len(delNames) > 0 { + xl.Infof("visitor removed: %v", delNames) + } + + addNames := make([]string, 0) + for _, cfg := range cfgs { + name := cfg.GetBaseConfig().Name + if _, ok := vm.cfgs[name]; !ok { + vm.cfgs[name] = cfg + addNames = append(addNames, name) + _ = vm.startVisitor(cfg) + } + } + if len(addNames) > 0 { + xl.Infof("visitor added: %v", addNames) + } +} + +// TransferConn transfers a connection to a visitor. +func (vm *Manager) TransferConn(name string, conn net.Conn) error { + vm.mu.RLock() + defer vm.mu.RUnlock() + v, ok := vm.visitors[name] + if !ok { + return fmt.Errorf("visitor [%s] not found", name) + } + return v.AcceptConn(conn) +} + +func (vm *Manager) GetVisitorCfg(name string) (v1.VisitorConfigurer, bool) { + vm.mu.RLock() + defer vm.mu.RUnlock() + cfg, ok := vm.cfgs[name] + return cfg, ok +} + +type visitorHelperImpl struct { + connectServerFn func() (*msg.Conn, error) + msgTransporter transport.MessageTransporter + vnetController *vnet.Controller + transferConnFn func(name string, conn net.Conn) error + runID string + udpPacketCodec string +} + +func (v *visitorHelperImpl) ConnectServer() (*msg.Conn, error) { + return v.connectServerFn() +} + +func (v *visitorHelperImpl) TransferConn(name string, conn net.Conn) error { + return v.transferConnFn(name, conn) +} + +func (v *visitorHelperImpl) MsgTransporter() transport.MessageTransporter { + return v.msgTransporter +} + +func (v *visitorHelperImpl) VNetController() *vnet.Controller { + return v.vnetController +} + +func (v *visitorHelperImpl) RunID() string { + return v.runID +} + +func (v *visitorHelperImpl) UDPPacketCodec() string { + return v.udpPacketCodec +} diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go new file mode 100644 index 00000000000..e7a60895336 --- /dev/null +++ b/client/visitor/xtcp.go @@ -0,0 +1,450 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 visitor + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "strconv" + "sync" + "time" + + libio "github.com/fatedier/golib/io" + fmux "github.com/hashicorp/yamux" + quic "github.com/quic-go/quic-go" + "golang.org/x/time/rate" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/naming" + "github.com/fatedier/frp/pkg/nathole" + "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/pkg/util/xlog" +) + +var ErrNoTunnelSession = errors.New("no tunnel session") + +type XTCPVisitor struct { + *BaseVisitor + session TunnelSession + startTunnelCh chan struct{} + retryLimiter *rate.Limiter + cancel context.CancelFunc + + cfg *v1.XTCPVisitorConfig +} + +func (sv *XTCPVisitor) Run() (err error) { + sv.ctx, sv.cancel = context.WithCancel(sv.ctx) + + if sv.cfg.Protocol == "kcp" { + sv.session = NewKCPTunnelSession() + } else { + sv.session = NewQUICTunnelSession(sv.clientCfg) + } + + if sv.cfg.BindPort > 0 { + sv.l, err = net.Listen("tcp", net.JoinHostPort(sv.cfg.BindAddr, strconv.Itoa(sv.cfg.BindPort))) + if err != nil { + return + } + go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn) + } + + go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn) + go sv.processTunnelStartEvents() + if sv.cfg.KeepTunnelOpen { + sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour) + go sv.keepTunnelOpenWorker() + } + + if sv.plugin != nil { + sv.plugin.Start() + } + return +} + +func (sv *XTCPVisitor) Close() { + sv.mu.Lock() + defer sv.mu.Unlock() + sv.BaseVisitor.Close() + if sv.cancel != nil { + sv.cancel() + } + if sv.session != nil { + sv.session.Close() + } +} + +func (sv *XTCPVisitor) processTunnelStartEvents() { + for { + select { + case <-sv.ctx.Done(): + return + case <-sv.startTunnelCh: + start := time.Now() + sv.makeNatHole() + duration := time.Since(start) + // avoid too frequently + if duration < 10*time.Second { + time.Sleep(10*time.Second - duration) + } + } + } +} + +func (sv *XTCPVisitor) keepTunnelOpenWorker() { + xl := xlog.FromContextSafe(sv.ctx) + ticker := time.NewTicker(time.Duration(sv.cfg.MinRetryInterval) * time.Second) + defer ticker.Stop() + + sv.startTunnelCh <- struct{}{} + for { + select { + case <-sv.ctx.Done(): + return + case <-ticker.C: + xl.Debugf("keepTunnelOpenWorker try to check tunnel...") + conn, err := sv.getTunnelConn(sv.ctx) + if err != nil { + xl.Warnf("keepTunnelOpenWorker get tunnel connection error: %v", err) + _ = sv.retryLimiter.Wait(sv.ctx) + continue + } + xl.Debugf("keepTunnelOpenWorker check success") + if conn != nil { + conn.Close() + } + } + } +} + +func (sv *XTCPVisitor) handleConn(userConn net.Conn) { + xl := xlog.FromContextSafe(sv.ctx) + isConnTransferred := false + var tunnelErr error + defer func() { + if !isConnTransferred { + // If there was an error and connection supports CloseWithError, use it + if tunnelErr != nil { + if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok { + _ = eConn.CloseWithError(tunnelErr) + return + } + } + userConn.Close() + } + }() + + xl.Debugf("get a new xtcp user connection") + + // Open a tunnel connection to the server. If there is already a successful hole-punching connection, + // it will be reused. Otherwise, it will block and wait for a successful hole-punching connection until timeout. + ctx := sv.ctx + if sv.cfg.FallbackTo != "" { + timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(sv.cfg.FallbackTimeoutMs)*time.Millisecond) + defer cancel() + ctx = timeoutCtx + } + tunnelConn, err := sv.openTunnel(ctx) + if err != nil { + xl.Errorf("open tunnel error: %v", err) + tunnelErr = err + + // no fallback, just return + if sv.cfg.FallbackTo == "" { + return + } + + xl.Debugf("try to transfer connection to visitor: %s", sv.cfg.FallbackTo) + if err := sv.helper.TransferConn(sv.cfg.FallbackTo, userConn); err != nil { + xl.Errorf("transfer connection to visitor %s error: %v", sv.cfg.FallbackTo, err) + return + } + isConnTransferred = true + return + } + + muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig()) + if err != nil { + xl.Errorf("%v", err) + tunnelConn.Close() + tunnelErr = err + return + } + defer recycleFn() + + _, _, errs := libio.Join(userConn, muxConnRWCloser) + xl.Debugf("join connections closed") + if len(errs) > 0 { + xl.Tracef("join connections errors: %v", errs) + } +} + +// openTunnel will open a tunnel connection to the target server. +func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) { + xl := xlog.FromContextSafe(sv.ctx) + ctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + + timer := time.NewTimer(0) + defer timer.Stop() + + for { + select { + case <-sv.ctx.Done(): + return nil, sv.ctx.Err() + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, fmt.Errorf("open tunnel timeout") + } + return nil, ctx.Err() + case <-timer.C: + conn, err = sv.getTunnelConn(ctx) + if err != nil { + if !errors.Is(err, ErrNoTunnelSession) { + xl.Warnf("get tunnel connection error: %v", err) + } + timer.Reset(500 * time.Millisecond) + continue + } + return conn, nil + } + } +} + +func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) { + conn, err := sv.session.OpenConn(ctx) + if err == nil { + return conn, nil + } + sv.session.Close() + + select { + case sv.startTunnelCh <- struct{}{}: + default: + } + return nil, err +} + +// 0. PreCheck +// 1. Prepare +// 2. ExchangeInfo +// 3. MakeNATHole +// 4. Create a tunnel session using an underlying UDP connection. +func (sv *XTCPVisitor) makeNatHole() { + xl := xlog.FromContextSafe(sv.ctx) + targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName) + xl.Tracef("makeNatHole start") + if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil { + xl.Warnf("nathole precheck error: %v", err) + return + } + + xl.Tracef("nathole prepare start") + + // Prepare NAT traversal options + var opts nathole.PrepareOptions + if sv.cfg.NatTraversal != nil && sv.cfg.NatTraversal.DisableAssistedAddrs { + opts.DisableAssistedAddrs = true + } + + prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, opts) + if err != nil { + xl.Warnf("nathole prepare error: %v", err) + return + } + + xl.Infof("nathole prepare success, nat type: %s, behavior: %s, addresses: %v, assistedAddresses: %v", + prepareResult.NatType, prepareResult.Behavior, prepareResult.Addrs, prepareResult.AssistedAddrs) + + listenConn := prepareResult.ListenConn + + // send NatHoleVisitor to server + now := time.Now().Unix() + transactionID := nathole.NewTransactionID() + natHoleVisitorMsg := &msg.NatHoleVisitor{ + TransactionID: transactionID, + ProxyName: targetProxyName, + Protocol: sv.cfg.Protocol, + SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), + Timestamp: now, + MappedAddrs: prepareResult.Addrs, + AssistedAddrs: prepareResult.AssistedAddrs, + } + + xl.Tracef("nathole exchange info start") + natHoleRespMsg, err := nathole.ExchangeInfo(sv.ctx, sv.helper.MsgTransporter(), transactionID, natHoleVisitorMsg, 5*time.Second) + if err != nil { + listenConn.Close() + xl.Warnf("nathole exchange info error: %v", err) + return + } + + xl.Infof("get natHoleRespMsg, sid [%s], protocol [%s], candidate address %v, assisted address %v, detectBehavior: %+v", + natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs, + natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior) + + newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey)) + if err != nil { + listenConn.Close() + xl.Warnf("make hole error: %v", err) + return + } + listenConn = newListenConn + xl.Infof("establishing nat hole connection successful, sid [%s], remoteAddr [%s]", natHoleRespMsg.Sid, raddr) + + if err := sv.session.Init(listenConn, raddr); err != nil { + listenConn.Close() + xl.Warnf("init tunnel session error: %v", err) + return + } +} + +type TunnelSession interface { + Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error + OpenConn(context.Context) (net.Conn, error) + Close() +} + +type KCPTunnelSession struct { + session *fmux.Session + lConn *net.UDPConn + mu sync.RWMutex +} + +func NewKCPTunnelSession() TunnelSession { + return &KCPTunnelSession{} +} + +func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error { + listenConn.Close() + laddr, _ := net.ResolveUDPAddr("udp", listenConn.LocalAddr().String()) + lConn, err := net.DialUDP("udp", laddr, raddr) + if err != nil { + return fmt.Errorf("dial udp error: %v", err) + } + remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String()) + if err != nil { + lConn.Close() + return fmt.Errorf("create kcp connection from udp connection error: %v", err) + } + + fmuxCfg := fmux.DefaultConfig() + fmuxCfg.KeepAliveInterval = 10 * time.Second + fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 + fmuxCfg.LogOutput = io.Discard + session, err := fmux.Client(remote, fmuxCfg) + if err != nil { + remote.Close() + return fmt.Errorf("initial client session error: %v", err) + } + ks.mu.Lock() + ks.session = session + ks.lConn = lConn + ks.mu.Unlock() + return nil +} + +func (ks *KCPTunnelSession) OpenConn(_ context.Context) (net.Conn, error) { + ks.mu.RLock() + defer ks.mu.RUnlock() + session := ks.session + if session == nil { + return nil, ErrNoTunnelSession + } + return session.Open() +} + +func (ks *KCPTunnelSession) Close() { + ks.mu.Lock() + defer ks.mu.Unlock() + if ks.session != nil { + _ = ks.session.Close() + ks.session = nil + } + if ks.lConn != nil { + _ = ks.lConn.Close() + ks.lConn = nil + } +} + +type QUICTunnelSession struct { + session *quic.Conn + listenConn *net.UDPConn + mu sync.RWMutex + + clientCfg *v1.ClientCommonConfig +} + +func NewQUICTunnelSession(clientCfg *v1.ClientCommonConfig) TunnelSession { + return &QUICTunnelSession{ + clientCfg: clientCfg, + } +} + +func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error { + tlsConfig, err := transport.NewClientTLSConfig("", "", "", raddr.String()) + if err != nil { + return fmt.Errorf("create tls config error: %v", err) + } + tlsConfig.NextProtos = []string{"frp"} + quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig, + &quic.Config{ + MaxIdleTimeout: time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second, + }) + if err != nil { + return fmt.Errorf("dial quic error: %v", err) + } + qs.mu.Lock() + qs.session = quicConn + qs.listenConn = listenConn + qs.mu.Unlock() + return nil +} + +func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) { + qs.mu.RLock() + defer qs.mu.RUnlock() + session := qs.session + if session == nil { + return nil, ErrNoTunnelSession + } + stream, err := session.OpenStreamSync(ctx) + if err != nil { + return nil, err + } + return netpkg.QuicStreamToNetConn(stream, session), nil +} + +func (qs *QUICTunnelSession) Close() { + qs.mu.Lock() + defer qs.mu.Unlock() + if qs.session != nil { + _ = qs.session.CloseWithError(0, "") + qs.session = nil + } + if qs.listenConn != nil { + _ = qs.listenConn.Close() + qs.listenConn = nil + } +} diff --git a/client/visitor_manager.go b/client/visitor_manager.go deleted file mode 100644 index 642b21e8f2f..00000000000 --- a/client/visitor_manager.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 client - -import ( - "context" - "sync" - "time" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/util/xlog" -) - -type VisitorManager struct { - ctl *Control - - cfgs map[string]config.VisitorConf - visitors map[string]Visitor - - checkInterval time.Duration - - mu sync.Mutex - ctx context.Context - - stopCh chan struct{} -} - -func NewVisitorManager(ctx context.Context, ctl *Control) *VisitorManager { - return &VisitorManager{ - ctl: ctl, - cfgs: make(map[string]config.VisitorConf), - visitors: make(map[string]Visitor), - checkInterval: 10 * time.Second, - ctx: ctx, - stopCh: make(chan struct{}), - } -} - -func (vm *VisitorManager) Run() { - xl := xlog.FromContextSafe(vm.ctx) - - ticker := time.NewTicker(vm.checkInterval) - defer ticker.Stop() - - for { - select { - case <-vm.stopCh: - xl.Info("gracefully shutdown visitor manager") - return - case <-ticker.C: - vm.mu.Lock() - for _, cfg := range vm.cfgs { - name := cfg.GetBaseInfo().ProxyName - if _, exist := vm.visitors[name]; !exist { - xl.Info("try to start visitor [%s]", name) - vm.startVisitor(cfg) - } - } - vm.mu.Unlock() - } - } -} - -// Hold lock before calling this function. -func (vm *VisitorManager) startVisitor(cfg config.VisitorConf) (err error) { - xl := xlog.FromContextSafe(vm.ctx) - name := cfg.GetBaseInfo().ProxyName - visitor := NewVisitor(vm.ctx, vm.ctl, cfg) - err = visitor.Run() - if err != nil { - xl.Warn("start error: %v", err) - } else { - vm.visitors[name] = visitor - xl.Info("start visitor success") - } - return -} - -func (vm *VisitorManager) Reload(cfgs map[string]config.VisitorConf) { - xl := xlog.FromContextSafe(vm.ctx) - vm.mu.Lock() - defer vm.mu.Unlock() - - delNames := make([]string, 0) - for name, oldCfg := range vm.cfgs { - del := false - cfg, ok := cfgs[name] - if !ok { - del = true - } else { - if !oldCfg.Compare(cfg) { - del = true - } - } - - if del { - delNames = append(delNames, name) - delete(vm.cfgs, name) - if visitor, ok := vm.visitors[name]; ok { - visitor.Close() - } - delete(vm.visitors, name) - } - } - if len(delNames) > 0 { - xl.Info("visitor removed: %v", delNames) - } - - addNames := make([]string, 0) - for name, cfg := range cfgs { - if _, ok := vm.cfgs[name]; !ok { - vm.cfgs[name] = cfg - addNames = append(addNames, name) - vm.startVisitor(cfg) - } - } - if len(addNames) > 0 { - xl.Info("visitor added: %v", addNames) - } - return -} - -func (vm *VisitorManager) Close() { - vm.mu.Lock() - defer vm.mu.Unlock() - for _, v := range vm.visitors { - v.Close() - } - select { - case <-vm.stopCh: - default: - close(vm.stopCh) - } -} diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go index f9a6c252ad1..c44216674e8 100644 --- a/cmd/frpc/main.go +++ b/cmd/frpc/main.go @@ -15,10 +15,12 @@ package main import ( - _ "github.com/fatedier/frp/assets/frpc" "github.com/fatedier/frp/cmd/frpc/sub" + "github.com/fatedier/frp/pkg/util/system" + _ "github.com/fatedier/frp/web/frpc" ) func main() { + system.EnableCompatibilityMode() sub.Execute() } diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go new file mode 100644 index 00000000000..abe8063585c --- /dev/null +++ b/cmd/frpc/sub/admin.go @@ -0,0 +1,125 @@ +// Copyright 2023 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 ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/rodaine/table" + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + clientsdk "github.com/fatedier/frp/pkg/sdk/client" +) + +var adminAPITimeout = 30 * time.Second + +func init() { + commands := []struct { + name string + description string + handler func(*v1.ClientCommonConfig) error + }{ + {"reload", "Hot-Reload frpc configuration", ReloadHandler}, + {"status", "Overview of all proxies status", StatusHandler}, + {"stop", "Stop the running frpc", StopHandler}, + } + + for _, cmdConfig := range commands { + cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler) + cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls") + rootCmd.AddCommand(cmd) + } +} + +func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command { + return &cobra.Command{ + Use: name, + Short: short, + Run: func(cmd *cobra.Command, args []string) { + cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if cfg.WebServer.Port <= 0 { + fmt.Println("web server port should be set if you want to use this feature") + os.Exit(1) + } + + if err := handler(cfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + } +} + +func ReloadHandler(clientCfg *v1.ClientCommonConfig) error { + client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) + client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) + ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout) + defer cancel() + if err := client.Reload(ctx, strictConfigMode); err != nil { + return err + } + fmt.Println("reload success") + return nil +} + +func StatusHandler(clientCfg *v1.ClientCommonConfig) error { + client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) + client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) + ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout) + defer cancel() + res, err := client.GetAllProxyStatus(ctx) + if err != nil { + return err + } + + fmt.Printf("Proxy Status...\n\n") + for _, typ := range proxyTypes { + arrs := res[string(typ)] + if len(arrs) == 0 { + continue + } + + fmt.Println(strings.ToUpper(string(typ))) + tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") + for _, ps := range arrs { + tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) + } + tbl.Print() + fmt.Println("") + } + return nil +} + +func StopHandler(clientCfg *v1.ClientCommonConfig) error { + client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) + client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) + ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout) + defer cancel() + if err := client.Stop(ctx); err != nil { + return err + } + fmt.Println("stop success") + return nil +} diff --git a/cmd/frpc/sub/http.go b/cmd/frpc/sub/http.go deleted file mode 100644 index 824a5e329c7..00000000000 --- a/cmd/frpc/sub/http.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - "strings" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - - "github.com/spf13/cobra" -) - -func init() { - RegisterCommonFlags(httpCmd) - - httpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - httpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - httpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - httpCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") - httpCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") - httpCmd.PersistentFlags().StringVarP(&locations, "locations", "", "", "locations") - httpCmd.PersistentFlags().StringVarP(&httpUser, "http_user", "", "", "http auth user") - httpCmd.PersistentFlags().StringVarP(&httpPwd, "http_pwd", "", "", "http auth password") - httpCmd.PersistentFlags().StringVarP(&ipsAllowList, "ips_allow_list", "", "", "lists the rules to configure which IP addresses and subnet masks can access your client (e.g \"192.168.0.0/16, 255.255.0.0\")- IPv4/IPv6 support") - httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") - httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(httpCmd) -} - -var httpCmd = &cobra.Command{ - Use: "http", - Short: "Run frpc with a single http proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.HTTPProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.HTTPProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.CustomDomains = strings.Split(customDomains, ",") - cfg.SubDomain = subDomain - cfg.Locations = strings.Split(locations, ",") - cfg.HTTPUser = httpUser - cfg.HTTPPwd = httpPwd - cfg.HostHeaderRewrite = hostHeaderRewrite - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - if ipsAllowList != "" { - cfg.IpsAllowList = strings.Split(ipsAllowList, ",") - } - - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/https.go b/cmd/frpc/sub/https.go deleted file mode 100644 index 8a14d39d73c..00000000000 --- a/cmd/frpc/sub/https.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(httpsCmd) - - httpsCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - httpsCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - httpsCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - httpsCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") - httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") - httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(httpsCmd) -} - -var httpsCmd = &cobra.Command{ - Use: "https", - Short: "Run frpc with a single https proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.HTTPSProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.HTTPSProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.CustomDomains = strings.Split(customDomains, ",") - cfg.SubDomain = subDomain - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go new file mode 100644 index 00000000000..b76c8ab70d8 --- /dev/null +++ b/cmd/frpc/sub/nathole.go @@ -0,0 +1,100 @@ +// Copyright 2023 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 ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/nathole" +) + +var ( + natHoleSTUNServer string + natHoleLocalAddr string +) + +func init() { + rootCmd.AddCommand(natholeCmd) + natholeCmd.AddCommand(natholeDiscoveryCmd) + + natholeCmd.PersistentFlags().StringVarP(&natHoleSTUNServer, "nat_hole_stun_server", "", "", "STUN server address for nathole") + natholeCmd.PersistentFlags().StringVarP(&natHoleLocalAddr, "nat_hole_local_addr", "l", "", "local address to connect STUN server") +} + +var natholeCmd = &cobra.Command{ + Use: "nathole", + Short: "Actions about nathole", +} + +var natholeDiscoveryCmd = &cobra.Command{ + Use: "discover", + Short: "Discover nathole information from stun server", + RunE: func(cmd *cobra.Command, args []string) error { + // ignore error here, because we can use command line parameters + cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) + if err != nil { + cfg = &v1.ClientCommonConfig{} + if err := cfg.Complete(); err != nil { + fmt.Printf("failed to complete config: %v\n", err) + os.Exit(1) + } + } + if natHoleSTUNServer != "" { + cfg.NatHoleSTUNServer = natHoleSTUNServer + } + + if err := validateForNatHoleDiscovery(cfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + + addrs, localAddr, err := nathole.Discover([]string{cfg.NatHoleSTUNServer}, natHoleLocalAddr) + if err != nil { + fmt.Println("discover error:", err) + os.Exit(1) + } + if len(addrs) < 2 { + fmt.Printf("discover error: can not get enough addresses, need 2, got: %v\n", addrs) + os.Exit(1) + } + + localIPs, _ := nathole.ListLocalIPsForNatHole(10) + + natFeature, err := nathole.ClassifyNATFeature(addrs, localIPs) + if err != nil { + fmt.Println("classify nat feature error:", err) + os.Exit(1) + } + fmt.Println("STUN server:", cfg.NatHoleSTUNServer) + fmt.Println("Your NAT type is:", natFeature.NatType) + fmt.Println("Behavior is:", natFeature.Behavior) + fmt.Println("External address is:", addrs) + fmt.Println("Local address is:", localAddr.String()) + fmt.Println("Public Network:", natFeature.PublicNetwork) + return nil + }, +} + +func validateForNatHoleDiscovery(cfg *v1.ClientCommonConfig) error { + if cfg.NatHoleSTUNServer == "" { + return fmt.Errorf("nat_hole_stun_server can not be empty") + } + return nil +} diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go new file mode 100644 index 00000000000..8651f0b3bf8 --- /dev/null +++ b/cmd/frpc/sub/proxy.go @@ -0,0 +1,151 @@ +// Copyright 2023 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 ( + "fmt" + "os" + "slices" + + "github.com/spf13/cobra" + + "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/policy/security" +) + +var proxyTypes = []v1.ProxyType{ + v1.ProxyTypeTCP, + v1.ProxyTypeUDP, + v1.ProxyTypeTCPMUX, + v1.ProxyTypeHTTP, + v1.ProxyTypeHTTPS, + v1.ProxyTypeSTCP, + v1.ProxyTypeSUDP, + v1.ProxyTypeXTCP, +} + +var visitorTypes = []v1.VisitorType{ + v1.VisitorTypeSTCP, + v1.VisitorTypeSUDP, + v1.VisitorTypeXTCP, +} + +func init() { + for _, typ := range proxyTypes { + c := v1.NewProxyConfigurerByType(typ) + if c == nil { + panic("proxy type: " + typ + " not support") + } + clientCfg := v1.ClientCommonConfig{} + cmd := NewProxyCommand(string(typ), c, &clientCfg) + config.RegisterClientCommonConfigFlags(cmd, &clientCfg) + config.RegisterProxyFlags(cmd, c) + + // add sub command for visitor + if slices.Contains(visitorTypes, v1.VisitorType(typ)) { + vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ)) + if vc == nil { + panic("visitor type: " + typ + " not support") + } + visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg) + config.RegisterVisitorFlags(visitorCmd, vc) + cmd.AddCommand(visitorCmd) + } + rootCmd.AddCommand(cmd) + } +} + +func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command { + return &cobra.Command{ + Use: name, + Short: fmt.Sprintf("Run frpc with a single %s proxy", name), + Run: func(cmd *cobra.Command, args []string) { + if err := clientCfg.Complete(); err != nil { + fmt.Println(err) + os.Exit(1) + } + + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + + c.GetBaseConfig().Type = name + c.Complete() + proxyCfg := c + if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + } +} + +func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command { + return &cobra.Command{ + Use: "visitor", + Short: fmt.Sprintf("Run frpc with a single %s visitor", name), + Run: func(cmd *cobra.Command, args []string) { + if err := clientCfg.Complete(); err != nil { + fmt.Println(err) + os.Exit(1) + } + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + + c.GetBaseConfig().Type = name + c.Complete() + visitorCfg := c + if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + } +} + +func startService( + cfg *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, + unsafeFeatures *security.UnsafeFeatures, + cfgFile string, +) error { + configSource := source.NewConfigSource() + if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil { + return fmt.Errorf("failed to set config source: %w", err) + } + aggregator := source.NewAggregator(configSource) + return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile) +} diff --git a/cmd/frpc/sub/reload.go b/cmd/frpc/sub/reload.go deleted file mode 100644 index c625daedb43..00000000000 --- a/cmd/frpc/sub/reload.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "encoding/base64" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/fatedier/frp/pkg/config" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(reloadCmd) -} - -var reloadCmd = &cobra.Command{ - Use: "reload", - Short: "Hot-Reload frpc configuration", - RunE: func(cmd *cobra.Command, args []string) error { - cfg, _, _, err := config.ParseClientConfig(cfgFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - err = reload(cfg) - if err != nil { - fmt.Printf("frpc reload error: %v\n", err) - os.Exit(1) - } - fmt.Printf("reload success\n") - return nil - }, -} - -func reload(clientCfg config.ClientCommonConf) error { - if clientCfg.AdminPort == 0 { - return fmt.Errorf("admin_port shoud be set if you want to use reload feature") - } - - req, err := http.NewRequest("GET", "http://"+ - clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/reload", nil) - if err != nil { - return err - } - - authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+ - clientCfg.AdminPwd)) - - req.Header.Add("Authorization", authStr) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - return nil - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body))) -} diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 8e56fa7123c..8a0f1842320 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -15,84 +15,45 @@ package sub import ( + "context" "fmt" "io/fs" - "net" "os" "os/signal" "path/filepath" - "strconv" + "strings" "sync" "syscall" "time" + "github.com/spf13/cobra" + "github.com/fatedier/frp/client" - "github.com/fatedier/frp/pkg/auth" "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/policy/security" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" - - "github.com/spf13/cobra" -) - -const ( - CfgFileTypeIni = iota - CfgFileTypeCmd ) var ( - cfgFile string - cfgDir string - showVersion bool - - serverAddr string - user string - protocol string - token string - logLevel string - logFile string - logMaxDays int - disableLogColor bool - - proxyName string - localIP string - localPort int - remotePort int - useEncryption bool - useCompression bool - customDomains string - subDomain string - httpUser string - httpPwd string - ipsAllowList string - locations string - hostHeaderRewrite string - role string - sk string - multiplexer string - serverName string - bindAddr string - bindPort int - - tlsEnable bool + cfgFile string + cfgDir string + showVersion bool + strictConfigMode bool + allowUnsafe []string ) func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") -} + rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors") -func RegisterCommonFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address") - cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user") - cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp or kcp or websocket or wss") - cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") - cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") - cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path") - cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days") - cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") - cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", false, "enable frpc tls") + rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, + fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", "))) } var rootCmd = &cobra.Command{ @@ -104,34 +65,17 @@ var rootCmd = &cobra.Command{ return nil } + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + // If cfgDir is not empty, run multiple frpc service for each config file in cfgDir. // Note that it's only designed for testing. It's not guaranteed to be stable. if cfgDir != "" { - var wg sync.WaitGroup - filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return nil - } - if d.IsDir() { - return nil - } - wg.Add(1) - time.Sleep(time.Millisecond) - go func() { - defer wg.Done() - err := runClient(path) - if err != nil { - fmt.Printf("frpc service error for config file [%s]\n", path) - } - }() - return nil - }) - wg.Wait() + _ = runMultipleClients(cfgDir, unsafeFeatures) return nil } // Do not show command usage here. - err := runClient(cfgFile) + err := runClient(cfgFile, unsafeFeatures) if err != nil { fmt.Println(err) os.Exit(1) @@ -140,93 +84,129 @@ var rootCmd = &cobra.Command{ }, } +func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error { + var wg sync.WaitGroup + err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + wg.Add(1) + time.Sleep(time.Millisecond) + go func() { + defer wg.Done() + err := runClient(path, unsafeFeatures) + if err != nil { + fmt.Printf("frpc service error for config file [%s]\n", path) + } + }() + return nil + }) + wg.Wait() + return err +} + func Execute() { + rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc) if err := rootCmd.Execute(); err != nil { os.Exit(1) } } -func handleSignal(svr *client.Service, doneCh chan struct{}) { +func handleTermSignal(svr *client.Service) { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) <-ch svr.GracefulClose(500 * time.Millisecond) - close(doneCh) } -func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { - cfg = config.GetDefaultClientConf() - - ipStr, portStr, err := net.SplitHostPort(serverAddr) +func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error { + // Load configuration + result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode) if err != nil { - err = fmt.Errorf("invalid server_addr: %v", err) - return + return err + } + if result.IsLegacyFormat { + fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " + + "please use yaml/json/toml format instead!\n") } - cfg.ServerAddr = ipStr - cfg.ServerPort, err = strconv.Atoi(portStr) - if err != nil { - err = fmt.Errorf("invalid server_addr: %v", err) - return + return runClientWithAggregator(result, unsafeFeatures, cfgFilePath) +} + +// runClientWithAggregator runs the client using the internal source aggregator. +func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error { + configSource := source.NewConfigSource() + if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil { + return fmt.Errorf("failed to set config source: %w", err) } - cfg.User = user - cfg.Protocol = protocol - cfg.LogLevel = logLevel - cfg.LogFile = logFile - cfg.LogMaxDays = int64(logMaxDays) - cfg.DisableLogColor = disableLogColor - - // Only token authentication is supported in cmd mode - cfg.ClientConfig = auth.GetDefaultClientConf() - cfg.Token = token - cfg.TLSEnable = tlsEnable - - cfg.Complete() - if err = cfg.Validate(); err != nil { - err = fmt.Errorf("Parse config error: %v", err) - return + var storeSource *source.StoreSource + + if result.Common.Store.IsEnabled() { + storePath := result.Common.Store.Path + if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) { + storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath) + } + + s, err := source.NewStoreSource(source.StoreSourceConfig{ + Path: storePath, + }) + if err != nil { + return fmt.Errorf("failed to create store source: %w", err) + } + storeSource = s + } + + aggregator := source.NewAggregator(configSource) + if storeSource != nil { + aggregator.SetStoreSource(storeSource) } - return -} -func runClient(cfgFilePath string) error { - cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath) + proxyCfgs, visitorCfgs, err := aggregator.Load() + if err != nil { + return fmt.Errorf("failed to load config from sources: %w", err) + } + + proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs) + proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs) + visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs) + + warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } if err != nil { return err } - return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) + + return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath) } -func startService( - cfg config.ClientCommonConf, - pxyCfgs map[string]config.ProxyConf, - visitorCfgs map[string]config.VisitorConf, +func startServiceWithAggregator( + cfg *v1.ClientCommonConfig, + aggregator *source.Aggregator, + unsafeFeatures *security.UnsafeFeatures, cfgFile string, -) (err error) { - - log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, - cfg.LogMaxDays, cfg.DisableLogColor) +) error { + log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) if cfgFile != "" { - log.Trace("start frpc service for config file [%s]", cfgFile) - defer log.Trace("frpc service for config file [%s] stopped", cfgFile) + log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile) + defer log.Infof("frpc service for config file [%s] stopped", cfgFile) } - svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile) - if errRet != nil { - err = errRet - return - } - - kcpDoneCh := make(chan struct{}) - // Capture the exit signal if we use kcp. - if cfg.Protocol == "kcp" { - go handleSignal(svr, kcpDoneCh) + svr, err := client.NewService(client.ServiceOptions{ + Common: cfg, + ConfigSourceAggregator: aggregator, + UnsafeFeatures: unsafeFeatures, + ConfigFilePath: cfgFile, + }) + if err != nil { + return err } - err = svr.Run() - if err == nil && cfg.Protocol == "kcp" { - <-kcpDoneCh + shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic" + if shouldGracefulClose { + go handleTermSignal(svr) } - return + return svr.Run(context.Background()) } diff --git a/cmd/frpc/sub/status.go b/cmd/frpc/sub/status.go deleted file mode 100644 index 52c1c1954cf..00000000000 --- a/cmd/frpc/sub/status.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/fatedier/frp/client" - "github.com/fatedier/frp/pkg/config" - - "github.com/rodaine/table" - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(statusCmd) -} - -var statusCmd = &cobra.Command{ - Use: "status", - Short: "Overview of all proxies status", - RunE: func(cmd *cobra.Command, args []string) error { - cfg, _, _, err := config.ParseClientConfig(cfgFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err = status(cfg); err != nil { - fmt.Printf("frpc get status error: %v\n", err) - os.Exit(1) - } - return nil - }, -} - -func status(clientCfg config.ClientCommonConf) error { - if clientCfg.AdminPort == 0 { - return fmt.Errorf("admin_port shoud be set if you want to get proxy status") - } - - req, err := http.NewRequest("GET", "http://"+ - clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/status", nil) - if err != nil { - return err - } - - authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+ - clientCfg.AdminPwd)) - - req.Header.Add("Authorization", authStr) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return fmt.Errorf("admin api status code [%d]", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - res := &client.StatusResp{} - err = json.Unmarshal(body, &res) - if err != nil { - return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body))) - } - - fmt.Println("Proxy Status...") - if len(res.TCP) > 0 { - fmt.Println("TCP") - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range res.TCP { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - if len(res.UDP) > 0 { - fmt.Println("UDP") - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range res.UDP { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - if len(res.HTTP) > 0 { - fmt.Println("HTTP") - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range res.HTTP { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - if len(res.HTTPS) > 0 { - fmt.Println("HTTPS") - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range res.HTTPS { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - if len(res.STCP) > 0 { - fmt.Println("STCP") - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range res.STCP { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - if len(res.XTCP) > 0 { - fmt.Println("XTCP") - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range res.XTCP { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - - return nil -} diff --git a/cmd/frpc/sub/stcp.go b/cmd/frpc/sub/stcp.go deleted file mode 100644 index 45f01e59872..00000000000 --- a/cmd/frpc/sub/stcp.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - - "github.com/spf13/cobra" -) - -func init() { - RegisterCommonFlags(stcpCmd) - - stcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - stcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role") - stcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key") - stcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name") - stcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - stcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - stcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr") - stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") - stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(stcpCmd) -} - -var stcpCmd = &cobra.Command{ - Use: "stcp", - Short: "Run frpc with a single stcp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := make(map[string]config.ProxyConf) - visitorConfs := make(map[string]config.VisitorConf) - - var prefix string - if user != "" { - prefix = user + "." - } - - if role == "server" { - cfg := &config.STCPProxyConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.STCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.LocalIP = localIP - cfg.LocalPort = localPort - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - proxyConfs[cfg.ProxyName] = cfg - } else if role == "visitor" { - cfg := &config.STCPVisitorConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.STCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.ServerName = serverName - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - err = cfg.Check() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - visitorConfs[cfg.ProxyName] = cfg - } else { - fmt.Println("invalid role") - os.Exit(1) - } - - err = startService(clientCfg, proxyConfs, visitorConfs, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/sudp.go b/cmd/frpc/sub/sudp.go deleted file mode 100644 index 45c5ad61b2d..00000000000 --- a/cmd/frpc/sub/sudp.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - - "github.com/spf13/cobra" -) - -func init() { - RegisterCommonFlags(sudpCmd) - - sudpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - sudpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role") - sudpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key") - sudpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name") - sudpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - sudpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - sudpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr") - sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") - sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(sudpCmd) -} - -var sudpCmd = &cobra.Command{ - Use: "sudp", - Short: "Run frpc with a single sudp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := make(map[string]config.ProxyConf) - visitorConfs := make(map[string]config.VisitorConf) - - var prefix string - if user != "" { - prefix = user + "." - } - - if role == "server" { - cfg := &config.SUDPProxyConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.SUDPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.LocalIP = localIP - cfg.LocalPort = localPort - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - proxyConfs[cfg.ProxyName] = cfg - } else if role == "visitor" { - cfg := &config.SUDPVisitorConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.SUDPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.ServerName = serverName - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - err = cfg.Check() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - visitorConfs[cfg.ProxyName] = cfg - } else { - fmt.Println("invalid role") - os.Exit(1) - } - - err = startService(clientCfg, proxyConfs, visitorConfs, "") - if err != nil { - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/tcp.go b/cmd/frpc/sub/tcp.go deleted file mode 100644 index 7e867345617..00000000000 --- a/cmd/frpc/sub/tcp.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(tcpCmd) - - tcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - tcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - tcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port") - tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(tcpCmd) -} - -var tcpCmd = &cobra.Command{ - Use: "tcp", - Short: "Run frpc with a single tcp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.TCPProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.TCPProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.RemotePort = remotePort - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/tcpmux.go b/cmd/frpc/sub/tcpmux.go deleted file mode 100644 index cef845d6342..00000000000 --- a/cmd/frpc/sub/tcpmux.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2020 guylewin, guy@lewin.co.il -// -// 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 ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(tcpMuxCmd) - - tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - tcpMuxCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") - tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") - tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer") - tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(tcpMuxCmd) -} - -var tcpMuxCmd = &cobra.Command{ - Use: "tcpmux", - Short: "Run frpc with a single tcpmux proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.TCPMuxProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.TCPMuxProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.CustomDomains = strings.Split(customDomains, ",") - cfg.SubDomain = subDomain - cfg.Multiplexer = multiplexer - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/udp.go b/cmd/frpc/sub/udp.go deleted file mode 100644 index 2ce4327e4dd..00000000000 --- a/cmd/frpc/sub/udp.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - - "github.com/spf13/cobra" -) - -func init() { - RegisterCommonFlags(udpCmd) - - udpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - udpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - udpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port") - udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(udpCmd) -} - -var udpCmd = &cobra.Command{ - Use: "udp", - Short: "Run frpc with a single udp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.UDPProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.UDPProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.RemotePort = remotePort - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index 76872b90b82..d226c473e8e 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -18,20 +18,43 @@ import ( "fmt" "os" - "github.com/fatedier/frp/pkg/config" - "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/policy/security" ) 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", RunE: func(cmd *cobra.Command, args []string) error { - _, _, _, err := config.ParseClientConfig(cfgFile) + if cfgFile == "" { + fmt.Println("frpc: the configuration file is not specified") + return nil + } + + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + warning, err := verifyClientConfig(cfgFile, strictConfigMode, unsafeFeatures) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/verify_test.go b/cmd/frpc/sub/verify_test.go new file mode 100644 index 00000000000..b93db23bc90 --- /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/cmd/frpc/sub/xtcp.go b/cmd/frpc/sub/xtcp.go deleted file mode 100644 index 6c6c7d84bb0..00000000000 --- a/cmd/frpc/sub/xtcp.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 ( - "fmt" - "os" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - - "github.com/spf13/cobra" -) - -func init() { - RegisterCommonFlags(xtcpCmd) - - xtcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - xtcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role") - xtcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key") - xtcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name") - xtcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - xtcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - xtcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr") - xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") - xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - - rootCmd.AddCommand(xtcpCmd) -} - -var xtcpCmd = &cobra.Command{ - Use: "xtcp", - Short: "Run frpc with a single xtcp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := make(map[string]config.ProxyConf) - visitorConfs := make(map[string]config.VisitorConf) - - var prefix string - if user != "" { - prefix = user + "." - } - - if role == "server" { - cfg := &config.XTCPProxyConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.XTCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.LocalIP = localIP - cfg.LocalPort = localPort - err = cfg.CheckForCli() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - proxyConfs[cfg.ProxyName] = cfg - } else if role == "visitor" { - cfg := &config.XTCPVisitorConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.XTCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.ServerName = serverName - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - err = cfg.Check() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - visitorConfs[cfg.ProxyName] = cfg - } else { - fmt.Println("invalid role") - os.Exit(1) - } - - err = startService(clientCfg, proxyConfs, visitorConfs, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frps/main.go b/cmd/frps/main.go index 56477aad139..4eda4f1f3f4 100644 --- a/cmd/frps/main.go +++ b/cmd/frps/main.go @@ -15,18 +15,12 @@ package main import ( - "math/rand" - "time" - - "github.com/fatedier/golib/crypto" - - _ "github.com/fatedier/frp/assets/frps" _ "github.com/fatedier/frp/pkg/metrics" + "github.com/fatedier/frp/pkg/util/system" + _ "github.com/fatedier/frp/web/frps" ) func main() { - crypto.DefaultSalt = "frp" - rand.Seed(time.Now().UnixNano()) - + system.EnableCompatibilityMode() Execute() } diff --git a/cmd/frps/root.go b/cmd/frps/root.go index d31944ac409..e6ab008a343 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -15,88 +15,39 @@ package main import ( + "context" "fmt" "os" + "strings" + + "github.com/spf13/cobra" - "github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/policy/security" "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/server" - - "github.com/spf13/cobra" -) - -const ( - CfgFileTypeIni = iota - CfgFileTypeCmd ) var ( - cfgFile string - showVersion bool - - bindAddr string - bindPort int - bindUDPPort int - kcpBindPort int - proxyBindAddr string - vhostHTTPPort int - vhostHTTPSPort int - vhostHTTPTimeout int64 - dashboardAddr string - dashboardPort int - dashboardUser string - dashboardPwd string - enablePrometheus bool - assetsDir string - logFile string - logLevel string - logMaxDays int64 - disableLogColor bool - token string - subDomainHost string - tcpMux bool - allowPorts string - maxPoolCount int64 - maxPortsPerClient int64 - tlsOnly bool - dashboardTLSMode bool - dashboardTLSCertFile string - dashboardTLSKeyFile string + cfgFile string + showVersion bool + strictConfigMode bool + allowUnsafe []string + + serverCfg v1.ServerConfig ) func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps") + rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors") + rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, + fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", "))) - rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address") - rootCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "p", 7000, "bind port") - rootCmd.PersistentFlags().IntVarP(&bindUDPPort, "bind_udp_port", "", 0, "bind udp port") - rootCmd.PersistentFlags().IntVarP(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port") - rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") - rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") - rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") - rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") - rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dasboard address") - rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port") - rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user") - rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password") - rootCmd.PersistentFlags().BoolVarP(&enablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard") - rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file") - rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") - rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days") - rootCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") - - rootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") - rootCmd.PersistentFlags().StringVarP(&subDomainHost, "subdomain_host", "", "", "subdomain host") - rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports") - rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") - rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only") - rootCmd.PersistentFlags().BoolVarP(&dashboardTLSMode, "dashboard_tls_mode", "", false, "dashboard tls mode") - rootCmd.PersistentFlags().StringVarP(&dashboardTLSCertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file") - rootCmd.PersistentFlags().StringVarP(&dashboardTLSKeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file") + config.RegisterServerConfigFlags(rootCmd, &serverCfg) } var rootCmd = &cobra.Command{ @@ -108,24 +59,41 @@ var rootCmd = &cobra.Command{ return nil } - var cfg config.ServerCommonConf - var err error + var ( + svrCfg *v1.ServerConfig + isLegacyFormat bool + err error + ) if cfgFile != "" { - var content []byte - content, err = config.GetRenderedConfFromFile(cfgFile) + svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile, strictConfigMode) if err != nil { - return err + fmt.Println(err) + os.Exit(1) + } + if isLegacyFormat { + fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " + + "please use yaml/json/toml format instead!\n") } - cfg, err = parseServerCommonCfg(CfgFileTypeIni, content) } else { - cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil) + if err := serverCfg.Complete(); err != nil { + fmt.Printf("failed to complete server config: %v\n", err) + os.Exit(1) + } + svrCfg = &serverCfg + } + + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + warning, err := validator.ValidateServerConfig(svrCfg) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) } if err != nil { - return err + fmt.Println(err) + os.Exit(1) } - err = runServer(cfg) - if err != nil { + if err := runServer(svrCfg); err != nil { fmt.Println(err) os.Exit(1) } @@ -134,88 +102,26 @@ var rootCmd = &cobra.Command{ } func Execute() { + rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc) if err := rootCmd.Execute(); err != nil { os.Exit(1) } } -func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonConf, err error) { - if fileType == CfgFileTypeIni { - cfg, err = config.UnmarshalServerConfFromIni(source) - } else if fileType == CfgFileTypeCmd { - cfg, err = parseServerCommonCfgFromCmd() - } - if err != nil { - return - } - cfg.Complete() - err = cfg.Validate() - if err != nil { - err = fmt.Errorf("Parse config error: %v", err) - return - } - return -} - -func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) { - cfg = config.GetDefaultServerConf() - - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - cfg.BindUDPPort = bindUDPPort - cfg.KCPBindPort = kcpBindPort - cfg.ProxyBindAddr = proxyBindAddr - cfg.VhostHTTPPort = vhostHTTPPort - cfg.VhostHTTPSPort = vhostHTTPSPort - cfg.VhostHTTPTimeout = vhostHTTPTimeout - cfg.DashboardAddr = dashboardAddr - cfg.DashboardPort = dashboardPort - cfg.DashboardUser = dashboardUser - cfg.DashboardPwd = dashboardPwd - cfg.EnablePrometheus = enablePrometheus - cfg.DashboardTLSCertFile = dashboardTLSCertFile - cfg.DashboardTLSKeyFile = dashboardTLSKeyFile - cfg.DashboardTLSMode = dashboardTLSMode - cfg.LogFile = logFile - cfg.LogLevel = logLevel - cfg.LogMaxDays = logMaxDays - cfg.SubDomainHost = subDomainHost - cfg.TLSOnly = tlsOnly - - // Only token authentication is supported in cmd mode - cfg.ServerConfig = auth.GetDefaultServerConf() - cfg.Token = token - if len(allowPorts) > 0 { - // e.g. 1000-2000,2001,2002,3000-4000 - ports, errRet := util.ParseRangeNumbers(allowPorts) - if errRet != nil { - err = fmt.Errorf("Parse conf error: allow_ports: %v", errRet) - return - } - - for _, port := range ports { - cfg.AllowPorts[int(port)] = struct{}{} - } - } - cfg.MaxPortsPerClient = maxPortsPerClient - cfg.DisableLogColor = disableLogColor - return -} - -func runServer(cfg config.ServerCommonConf) (err error) { - log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLogColor) +func runServer(cfg *v1.ServerConfig) (err error) { + log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) if cfgFile != "" { - log.Info("frps uses config file: %s", cfgFile) + log.Infof("frps uses config file: %s", cfgFile) } else { - log.Info("frps uses command line arguments for config") + log.Infof("frps uses command line arguments for config") } svr, err := server.NewService(cfg) if err != nil { return err } - log.Info("frps started successfully") - svr.Run() + log.Infof("frps started successfully") + svr.Run(context.Background()) return } diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go index 35b2acdf181..7ddef1abcf6 100644 --- a/cmd/frps/verify.go +++ b/cmd/frps/verify.go @@ -18,9 +18,11 @@ import ( "fmt" "os" - "github.com/fatedier/frp/pkg/config" - "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/policy/security" ) func init() { @@ -32,21 +34,25 @@ var verifyCmd = &cobra.Command{ Short: "Verify that the configures is valid", RunE: func(cmd *cobra.Command, args []string) error { if cfgFile == "" { - fmt.Println("no config file is specified") + fmt.Println("frps: the configuration file is not specified") return nil } - iniContent, err := config.GetRenderedConfFromFile(cfgFile) + svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode) if err != nil { fmt.Println(err) os.Exit(1) } - _, err = parseServerCommonCfg(CfgFileTypeIni, iniContent) + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + warning, err := validator.ValidateServerConfig(svrCfg) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } if err != nil { fmt.Println(err) os.Exit(1) } - fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile) return nil }, diff --git a/conf/frpc.ini b/conf/frpc.ini deleted file mode 100644 index 13a8e5f6fc9..00000000000 --- a/conf/frpc.ini +++ /dev/null @@ -1,9 +0,0 @@ -[common] -server_addr = 127.0.0.1 -server_port = 7000 - -[ssh] -type = tcp -local_ip = 127.0.0.1 -local_port = 22 -remote_port = 6000 diff --git a/conf/frpc.toml b/conf/frpc.toml new file mode 100644 index 00000000000..6438f040e9b --- /dev/null +++ b/conf/frpc.toml @@ -0,0 +1,9 @@ +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "test-tcp" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +remotePort = 6000 diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml new file mode 100644 index 00000000000..37ee9352e2c --- /dev/null +++ b/conf/frpc_full_example.toml @@ -0,0 +1,472 @@ +# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues. + +# Optional unique identifier for this frpc instance. +clientID = "your_client_id" +# your proxy name will be changed to {user}.{proxy} +user = "your_name" + +# A literal address or host name for IPv6 must be enclosed +# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" +# For single serverAddr field, no need square brackets, like serverAddr = "::". +serverAddr = "0.0.0.0" +serverPort = 7000 + +# STUN server to help penetrate NAT hole. +# natHoleStunServer = "stun.easyvoip.com:3478" + +# Decide if exit program when first login failed, otherwise continuous relogin to frps +# default is true +loginFailExit = true + +# console or real logFile path like ./frpc.log +log.to = "./frpc.log" +# trace, debug, info, warn, error +log.level = "info" +log.maxDays = 3 +# disable log colors when log.to is console, default is false +log.disablePrintColor = false + +auth.method = "token" +# auth.additionalScopes specifies additional scopes to include authentication information. +# Optional values are HeartBeats, NewWorkConns. +# auth.additionalScopes = ["HeartBeats", "NewWorkConns"] + +# auth token +auth.token = "12345678" + +# alternatively, you can use tokenSource to load the token from a file +# this is mutually exclusive with auth.token +# auth.tokenSource.type = "file" +# auth.tokenSource.file.path = "/etc/frp/token" + +# oidc.clientID specifies the client ID to use to get a token in OIDC authentication. +# auth.oidc.clientID = "" +# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication. +# auth.oidc.clientSecret = "" +# oidc.audience specifies the audience of the token in OIDC authentication. +# auth.oidc.audience = "" +# oidc.scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". +# auth.oidc.scope = "" +# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint. +# It will be used to get an OIDC token. +# auth.oidc.tokenEndpointURL = "" + +# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint. +# For example, if you want to specify the "audience" parameter, you can set as follow. +# frp will add "audience=" "var1=" to the additional parameters. +# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/" +# auth.oidc.additionalEndpointParams.var1 = "foobar" + +# OIDC TLS and proxy configuration +# Specify a custom CA certificate file for verifying the OIDC token endpoint's TLS certificate. +# This is useful when the OIDC provider uses a self-signed certificate or a custom CA. +# auth.oidc.trustedCaFile = "/path/to/ca.crt" + +# Skip TLS certificate verification for the OIDC token endpoint. +# INSECURE: Only use this for debugging purposes, not recommended for production. +# auth.oidc.insecureSkipVerify = false + +# Specify a proxy server for OIDC token endpoint connections. +# Supports http, https, socks5, and socks5h proxy protocols. +# If not specified, no proxy is used for OIDC connections. +# auth.oidc.proxyURL = "http://proxy.example.com:8080" + +# Set admin address for control frpc's action by http api such as reload +webServer.addr = "127.0.0.1" +webServer.port = 7400 +webServer.user = "admin" +webServer.password = "admin" +# Admin assets directory. By default, these assets are bundled with frpc. +# webServer.assetsDir = "./static" + +# Enable golang pprof handlers in admin listener. +webServer.pprofEnable = false + +# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds. +# transport.dialServerTimeout = 10 + +# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps. +# If negative, keep-alive probes are disabled. +# transport.dialServerKeepalive = 7200 + +# connections will be established in advance, default value is zero +transport.poolCount = 5 + +# If tcp stream multiplexing is used, default is true, it must be same with frps +# transport.tcpMux = true + +# Specify keep alive interval for tcp mux. +# only valid if tcpMux is enabled. +# transport.tcpMuxKeepaliveInterval = 30 + +# Communication protocol used to connect to server +# supports tcp, kcp, quic, websocket and wss now, default is tcp +transport.protocol = "tcp" + +# FRP wire protocol used inside the selected transport. +# supports v1 and v2, default is v1. v2 requires frps support and must be enabled explicitly. +# transport.wireProtocol = "v1" + +# set client binding ip when connect server, default is empty. +# only when protocol = tcp or websocket, the value will be used. +transport.connectServerLocalIP = "0.0.0.0" + +# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables +# it only works when protocol is tcp +# transport.proxyURL = "http://user:passwd@192.168.1.128:8080" +# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080" +# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080" + +# quic protocol options +# transport.quic.keepalivePeriod = 10 +# transport.quic.maxIdleTimeout = 30 +# transport.quic.maxIncomingStreams = 100000 + +# If tls.enable is true, frpc will connect frps by tls. +# Since v0.50.0, the default value has been changed to true, and tls is enabled by default. +transport.tls.enable = true + +# transport.tls.certFile = "client.crt" +# transport.tls.keyFile = "client.key" +# transport.tls.trustedCaFile = "ca.crt" +# transport.tls.serverName = "example.com" + +# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the +# first custom byte when tls is enabled. +# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default. +# transport.tls.disableCustomTLSFirstByte = true + +# Heartbeat configure, it's not recommended to modify the default value. +# The default value of heartbeatInterval is 10 and heartbeatTimeout is 90. Set negative value +# to disable it. +# transport.heartbeatInterval = 30 +# transport.heartbeatTimeout = 90 + +# Specify a dns server, so frpc will use this instead of default one +# dnsServer = "8.8.8.8" + +# Proxy names you want to start. +# Default is empty, means all proxies. +# This list is a global allowlist after config + store are merged, so entries +# created via Store API are also filtered by this list. +# If start is non-empty, any proxy/visitor not listed here will not be started. +# start = ["ssh", "dns"] + +# Alternative to 'start': You can control each proxy individually using the 'enabled' field. +# Set 'enabled = false' in a proxy configuration to disable it. +# If 'enabled' is not set or set to true, the proxy is enabled by default. +# The 'enabled' field provides more granular control and is recommended over 'start'. + +# Specify udp packet size, unit is byte. If not set, the default value is 1500. +# This parameter should be same between client and server. +# It affects the udp and sudp proxy. +udpPacketSize = 1500 + +# Feature gates allows you to enable or disable experimental features +# Format is a map of feature names to boolean values +# You can enable specific features: +#featureGates = { VirtualNet = true } + +# VirtualNet settings for experimental virtual network capabilities +# The virtual network feature requires enabling the VirtualNet feature gate above +# virtualNet.address = "100.86.1.1/24" + +# Additional metadatas for client. +metadatas.var1 = "abc" +metadatas.var2 = "123" + +# Include other config files for proxies. +# includes = ["./confd/*.ini"] + +[[proxies]] +# 'ssh' is the unique proxy name +# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh' +name = "ssh" +type = "tcp" +# Enable or disable this proxy. true or omit this field to enable, false to disable. +# enabled = true +localIP = "127.0.0.1" +localPort = 22 +# Limit bandwidth for this proxy, unit is KB and MB +transport.bandwidthLimit = "1MB" +# Where to limit bandwidth, can be 'client' or 'server', default is 'client' +transport.bandwidthLimitMode = "client" +# If true, traffic of this proxy will be encrypted, default is false +transport.useEncryption = false +# If true, traffic will be compressed +transport.useCompression = false +# Remote port listen by frps +remotePort = 6001 +# frps will load balancing connections for proxies in same group +loadBalancer.group = "test_group" +# group should have same group key +loadBalancer.groupKey = "123456" +# Enable health check for the backend service, it supports 'tcp' and 'http' now. +# frpc will connect local service's port to detect it's healthy status +healthCheck.type = "tcp" +# Health check connection timeout +healthCheck.timeoutSeconds = 3 +# If continuous failed in 3 times, the proxy will be removed from frps +healthCheck.maxFailed = 3 +# Every 10 seconds will do a health check +healthCheck.intervalSeconds = 10 +# Additional meta info for each proxy. It will be passed to the server-side plugin for use. +metadatas.var1 = "abc" +metadatas.var2 = "123" +# You can add some extra information to the proxy through annotations. +# These annotations will be displayed on the frps dashboard. +[proxies.annotations] +key1 = "value1" +"prefix/key2" = "value2" + +[[proxies]] +name = "ssh_random" +type = "tcp" +localIP = "192.168.31.100" +localPort = 22 +# If remotePort is 0, frps will assign a random port for you +remotePort = 0 + +[[proxies]] +name = "dns" +type = "udp" +localIP = "114.114.114.114" +localPort = 53 +remotePort = 6002 + +# Resolve your domain names to [serverAddr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02 +[[proxies]] +name = "web01" +type = "http" +localIP = "127.0.0.1" +localPort = 80 +# http username and password are safety certification for http protocol +# if not set, you can access this customDomains without certification +httpUser = "admin" +httpPassword = "admin" +# ipsAllowList restricts which client IPs/subnets may access this proxy (IPv4/IPv6). +# if not set, all IPs are allowed. +# ipsAllowList = ["192.168.0.0/16", "255.255.0.0"] +# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com +subdomain = "web01" +customDomains = ["web01.yourdomain.com"] +# locations is only available for http type +locations = ["/", "/pic"] +# route requests to this service if http basic auto user is abc +# routeByHTTPUser = abc +hostHeaderRewrite = "example.com" +requestHeaders.set.x-from-where = "frp" +responseHeaders.set.foo = "bar" +healthCheck.type = "http" +# frpc will send a GET http request '/status' to local http service +# http service is alive when it return 2xx http response code +healthCheck.path = "/status" +healthCheck.intervalSeconds = 10 +healthCheck.maxFailed = 3 +healthCheck.timeoutSeconds = 3 +# set health check headers +healthCheck.httpHeaders=[ + { name = "x-from-where", value = "frp" } +] + +[[proxies]] +name = "web02" +type = "https" +# Disable this proxy by setting enabled to false +# enabled = false +localIP = "127.0.0.1" +localPort = 8000 +subdomain = "web02" +customDomains = ["web02.yourdomain.com"] +# if not empty, frpc will use proxy protocol to transfer connection info to your local service +# v1 or v2 or empty +transport.proxyProtocolVersion = "v2" + +[[proxies]] +name = "tcpmuxhttpconnect" +type = "tcpmux" +multiplexer = "httpconnect" +localIP = "127.0.0.1" +localPort = 10701 +customDomains = ["tunnel1"] +# routeByHTTPUser = "user1" + +[[proxies]] +name = "plugin_unix_domain_socket" +type = "tcp" +remotePort = 6003 +# if plugin is defined, localIP and localPort is useless +# plugin will handle connections got from frps +[proxies.plugin] +type = "unix_domain_socket" +unixPath = "/var/run/docker.sock" + +[[proxies]] +name = "plugin_http_proxy" +type = "tcp" +remotePort = 6004 +[proxies.plugin] +type = "http_proxy" +httpUser = "abc" +httpPassword = "abc" + +[[proxies]] +name = "plugin_socks5" +type = "tcp" +remotePort = 6005 +[proxies.plugin] +type = "socks5" +username = "abc" +password = "abc" + +[[proxies]] +name = "plugin_static_file" +type = "tcp" +remotePort = 6006 +[proxies.plugin] +type = "static_file" +localPath = "/var/www/blog" +stripPrefix = "static" +httpUser = "abc" +httpPassword = "abc" + +[[proxies]] +name = "plugin_https2http" +type = "https" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "https2http" +localAddr = "127.0.0.1:80" +crtPath = "./server.crt" +keyPath = "./server.key" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "plugin_https2https" +type = "https" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "https2https" +localAddr = "127.0.0.1:443" +crtPath = "./server.crt" +keyPath = "./server.key" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "plugin_http2https" +type = "http" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "http2https" +localAddr = "127.0.0.1:443" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "plugin_http2http" +type = "tcp" +remotePort = 6007 +[proxies.plugin] +type = "http2http" +localAddr = "127.0.0.1:80" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "plugin_tls2raw" +type = "tcp" +remotePort = 6008 +[proxies.plugin] +type = "tls2raw" +localAddr = "127.0.0.1:80" +crtPath = "./server.crt" +keyPath = "./server.key" + +[[proxies]] +name = "secret_tcp" +# If the type is secret tcp, remotePort is useless +# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor +type = "stcp" +# secretKey is used for authentication for visitors +secretKey = "abcdefg" +localIP = "127.0.0.1" +localPort = 22 +# If not empty, only visitors from specified users can connect. +# Otherwise, visitors from same user can connect. '*' means allow all users. +allowUsers = ["*"] + +[[proxies]] +name = "p2p_tcp" +type = "xtcp" +secretKey = "abcdefg" +localIP = "127.0.0.1" +localPort = 22 +# If not empty, only visitors from specified users can connect. +# Otherwise, visitors from same user can connect. '*' means allow all users. +allowUsers = ["user1", "user2"] + +# NAT traversal configuration (optional) +[proxies.natTraversal] +# Disable the use of local network interfaces (assisted addresses) for NAT traversal. +# When enabled, only STUN-discovered public addresses will be used. +# This can improve performance when you have slow VPN connections. +# Default: false +disableAssistedAddrs = false + +[[proxies]] +name = "vnet-server" +type = "stcp" +secretKey = "your-secret-key" +[proxies.plugin] +type = "virtual_net" + +# frpc role visitor -> frps -> frpc role server +[[visitors]] +name = "secret_tcp_visitor" +type = "stcp" +# the server name you want to visitor +serverName = "secret_tcp" +secretKey = "abcdefg" +# connect this address to visitor stcp server +bindAddr = "127.0.0.1" +# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from +# other visitors. (This is not supported for SUDP now) +bindPort = 9000 + +[[visitors]] +name = "p2p_tcp_visitor" +type = "xtcp" +# if the server user is not set, it defaults to the current user +serverUser = "user1" +serverName = "p2p_tcp" +secretKey = "abcdefg" +bindAddr = "127.0.0.1" +# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from +# other visitors. (This is not supported for SUDP now) +bindPort = 9001 +# when automatic tunnel persistence is required, set it to true +keepTunnelOpen = false +# effective when keepTunnelOpen is set to true, the number of attempts to punch through per hour +maxRetriesAnHour = 8 +minRetryInterval = 90 +# fallbackTo = "stcp_visitor" +# fallbackTimeoutMs = 500 + +# NAT traversal configuration (optional) +[visitors.natTraversal] +# Disable the use of local network interfaces (assisted addresses) for NAT traversal. +# When enabled, only STUN-discovered public addresses will be used. +# Default: false +disableAssistedAddrs = false + +[[visitors]] +name = "vnet-visitor" +type = "stcp" +serverName = "vnet-server" +secretKey = "your-secret-key" +bindPort = -1 +[visitors.plugin] +type = "virtual_net" +destinationIP = "100.86.0.1" diff --git a/conf/frps.ini b/conf/frps.ini deleted file mode 100644 index 229567a9d6b..00000000000 --- a/conf/frps.ini +++ /dev/null @@ -1,2 +0,0 @@ -[common] -bind_port = 7000 diff --git a/conf/frps.toml b/conf/frps.toml new file mode 100644 index 00000000000..28e6d968b95 --- /dev/null +++ b/conf/frps.toml @@ -0,0 +1 @@ +bindPort = 7000 diff --git a/conf/frps_full_example.toml b/conf/frps_full_example.toml new file mode 100644 index 00000000000..aba37435ffc --- /dev/null +++ b/conf/frps_full_example.toml @@ -0,0 +1,169 @@ +# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues. + +# A literal address or host name for IPv6 must be enclosed +# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" +# For single "bindAddr" field, no need square brackets, like `bindAddr = "::"`. +bindAddr = "0.0.0.0" +bindPort = 7000 + +# udp port used for kcp protocol, it can be same with 'bindPort'. +# if not set, kcp is disabled in frps. +kcpBindPort = 7000 + +# udp port used for quic protocol. +# if not set, quic is disabled in frps. +# quicBindPort = 7002 + +# Specify which address proxy will listen for, default value is same with bindAddr +# proxyBindAddr = "127.0.0.1" + +# quic protocol options +# transport.quic.keepalivePeriod = 10 +# transport.quic.maxIdleTimeout = 30 +# transport.quic.maxIncomingStreams = 100000 + +# Heartbeat configure, it's not recommended to modify the default value +# The default value of heartbeatTimeout is 90. Set negative value to disable it. +# transport.heartbeatTimeout = 90 + +# Pool count in each proxy will keep no more than maxPoolCount. +transport.maxPoolCount = 5 + +# If tcp stream multiplexing is used, default is true +# transport.tcpMux = true + +# Specify keep alive interval for tcp mux. +# only valid if tcpMux is true. +# transport.tcpMuxKeepaliveInterval = 30 + +# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps. +# If negative, keep-alive probes are disabled. +# transport.tcpKeepalive = 7200 + +# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false. +transport.tls.force = false + +# transport.tls.certFile = "server.crt" +# transport.tls.keyFile = "server.key" +# transport.tls.trustedCaFile = "ca.crt" + +# If you want to support virtual host, you must set the http port for listening (optional) +# Note: http port and https port can be same with bindPort +vhostHTTPPort = 80 +vhostHTTPSPort = 443 + +# Response header timeout(seconds) for vhost http server, default is 60s +# vhostHTTPTimeout = 60 + +# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP +# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP +# requests on one single port. If it's not - it will listen on this value for +# HTTP CONNECT requests. By default, this value is 0. +# tcpmuxHTTPConnectPort = 1337 + +# If tcpmuxPassthrough is true, frps won't do any update on traffic. +# tcpmuxPassthrough = false + +# Configure the web server to enable the dashboard for frps. +# dashboard is available only if webServer.port is set. +webServer.addr = "127.0.0.1" +webServer.port = 7500 +webServer.user = "admin" +webServer.password = "admin" +# webServer.tls.certFile = "server.crt" +# webServer.tls.keyFile = "server.key" +# dashboard assets directory(only for debug mode) +# webServer.assetsDir = "./static" + +# Enable golang pprof handlers in dashboard listener. +# Dashboard port must be set first +webServer.pprofEnable = false + +# enablePrometheus will export prometheus metrics on webServer in /metrics api. +enablePrometheus = true + +# console or real logFile path like ./frps.log +log.to = "./frps.log" +# trace, debug, info, warn, error +log.level = "info" +log.maxDays = 3 +# disable log colors when log.to is console, default is false +log.disablePrintColor = false + +# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true. +detailedErrorsToClient = true + +# auth.method specifies what authentication method to use authenticate frpc with frps. +# If "token" is specified - token will be read into login message. +# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token". +auth.method = "token" + +# auth.additionalScopes specifies additional scopes to include authentication information. +# Optional values are HeartBeats, NewWorkConns. +# auth.additionalScopes = ["HeartBeats", "NewWorkConns"] + +# auth token +auth.token = "12345678" + +# alternatively, you can use tokenSource to load the token from a file +# this is mutually exclusive with auth.token +# auth.tokenSource.type = "file" +# auth.tokenSource.file.path = "/etc/frp/token" + +# oidc issuer specifies the issuer to verify OIDC tokens with. +auth.oidc.issuer = "" +# oidc audience specifies the audience OIDC tokens should contain when validated. +auth.oidc.audience = "" +# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired. +auth.oidc.skipExpiryCheck = false +# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer. +auth.oidc.skipIssuerCheck = false + +# userConnTimeout specifies the maximum time to wait for a work connection. +# userConnTimeout = 10 + +# Only allow frpc to bind ports you list. By default, there won't be any limit. +allowPorts = [ + { start = 2000, end = 3000 }, + { single = 3001 }, + { single = 3003 }, + { start = 4000, end = 50000 } +] + +# Max ports can be used for each client, default value is 0 means no limit +maxPortsPerClient = 0 + +# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file +# When subdomain is test, the host used by routing is test.frps.com +subDomainHost = "frps.com" + +# custom 404 page for HTTP requests +# custom404Page = "/path/to/404.html" + +# specify udp packet size, unit is byte. If not set, the default value is 1500. +# This parameter should be same between client and server. +# It affects the udp and sudp proxy. +udpPacketSize = 1500 + +# Retention time for NAT hole punching strategy data. +natholeAnalysisDataReserveHours = 168 + +# ssh tunnel gateway +# If you want to enable this feature, the bindPort parameter is required, while others are optional. +# By default, this feature is disabled. It will be enabled if bindPort is greater than 0. +# sshTunnelGateway.bindPort = 2200 +# sshTunnelGateway.privateKeyFile = "/home/frp-user/.ssh/id_rsa" +# sshTunnelGateway.autoGenPrivateKeyPath = "" +# sshTunnelGateway.authorizedKeysFile = "/home/frp-user/.ssh/authorized_keys" + +[[httpPlugins]] +name = "user-manager" +addr = "127.0.0.1:9000" +path = "/handler" +ops = ["Login"] + +[[httpPlugins]] +name = "port-manager" +addr = "127.0.0.1:9001" +path = "/handler" +ops = ["NewProxy"] diff --git a/conf/frpc_full.ini b/conf/legacy/frpc_legacy_full.ini similarity index 82% rename from conf/frpc_full.ini rename to conf/legacy/frpc_legacy_full.ini index 94c7ab4a2f1..1db375d0ad5 100644 --- a/conf/frpc_full.ini +++ b/conf/legacy/frpc_legacy_full.ini @@ -6,6 +6,9 @@ server_addr = 0.0.0.0 server_port = 7000 +# STUN server to help penetrate NAT hole. +# nat_hole_stun_server = stun.easyvoip.com:3478 + # The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds. # dial_server_timeout = 10 @@ -40,6 +43,8 @@ authenticate_new_work_conns = false # auth token token = 12345678 +authentication_method = + # oidc_client_id specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc". # By default, this value is "". oidc_client_id = @@ -51,6 +56,9 @@ oidc_client_secret = # oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". oidc_audience = +# oidc_scope specifies the permissions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". +oidc_scope = + # oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint. # It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "". oidc_token_endpoint_url = @@ -87,14 +95,20 @@ user = your_name login_fail_exit = true # communication protocol used to connect to server -# now it supports tcp, kcp, websocket and wss, default is tcp +# supports tcp, kcp, quic, websocket and wss now, default is tcp protocol = tcp # set client binding ip when connect server, default is empty. # only when protocol = tcp or websocket, the value will be used. connect_server_local_ip = 0.0.0.0 -# if tls_enable is true, frpc will connect frps by tls +# quic protocol options +# quic_keepalive_period = 10 +# quic_max_idle_timeout = 30 +# quic_max_incoming_streams = 100000 + +# If tls_enable is true, frpc will connect frps by tls. +# Since v0.50.0, the default value has been changed to true, and tls is enabled by default. tls_enable = true # tls_cert_file = client.crt @@ -127,9 +141,10 @@ udp_packet_size = 1500 # include other config files for proxies. # includes = ./confd/*.ini -# By default, frpc will connect frps with first custom byte if tls is enabled. -# If DisableCustomTLSFirstByte is true, frpc will not send that custom byte. -disable_custom_tls_first_byte = false +# If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the +# first custom byte when tls is enabled. +# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default. +disable_custom_tls_first_byte = true # Enable golang pprof handlers in admin listener. # Admin port must be set first. @@ -144,6 +159,8 @@ local_ip = 127.0.0.1 local_port = 22 # limit bandwidth for this proxy, unit is KB and MB bandwidth_limit = 1MB +# where to limit bandwidth, can be 'client' or 'server', default is 'client' +bandwidth_limit_mode = client # true or false, if true, messages between frps and frpc will be encrypted, default is false use_encryption = false # if true, message will be compressed @@ -211,6 +228,9 @@ use_compression = true # if not set, you can access this custom_domains without certification http_user = admin http_pwd = admin +# ips_allow_list restricts which client IPs/subnets may access this proxy (IPv4/IPv6). +# if not set, all IPs are allowed. +# ips_allow_list = 192.168.0.0/16,255.255.0.0 # if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com subdomain = web01 custom_domains = web01.yourdomain.com @@ -235,7 +255,7 @@ local_ip = 127.0.0.1 local_port = 8000 use_encryption = false use_compression = false -subdomain = web01 +subdomain = web02 custom_domains = web02.yourdomain.com # if not empty, frpc will use proxy protocol to transfer connection info to your local service # v1 or v2 or empty @@ -311,6 +331,9 @@ local_ip = 127.0.0.1 local_port = 22 use_encryption = false use_compression = false +# If not empty, only visitors from specified users can connect. +# Otherwise, visitors from same user can connect. '*' means allow all users. +allow_users = * # user of frpc should be same in both stcp server and stcp visitor [secret_tcp_visitor] @@ -322,6 +345,8 @@ server_name = secret_tcp sk = abcdefg # connect this address to visitor stcp server bind_addr = 127.0.0.1 +# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from +# other visitors. (This is not supported for SUDP now) bind_port = 9000 use_encryption = false use_compression = false @@ -333,16 +358,30 @@ local_ip = 127.0.0.1 local_port = 22 use_encryption = false use_compression = false +# If not empty, only visitors from specified users can connect. +# Otherwise, visitors from same user can connect. '*' means allow all users. +allow_users = user1, user2 [p2p_tcp_visitor] role = visitor type = xtcp +# if the server user is not set, it defaults to the current user +server_user = user1 server_name = p2p_tcp sk = abcdefg bind_addr = 127.0.0.1 +# bind_port can be less than 0, it means don't bind to the port and only receive connections redirected from +# other visitors. (This is not supported for SUDP now) bind_port = 9001 use_encryption = false use_compression = false +# when automatic tunnel persistence is required, set it to true +keep_tunnel_open = false +# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour +max_retries_an_hour = 8 +min_retry_interval = 90 +# fallback_to = stcp_visitor +# fallback_timeout_ms = 500 [tcpmuxhttpconnect] type = tcpmux diff --git a/conf/frps_full.ini b/conf/legacy/frps_legacy_full.ini similarity index 93% rename from conf/frps_full.ini rename to conf/legacy/frps_legacy_full.ini index 45b222f4d6e..525072dc87d 100644 --- a/conf/frps_full.ini +++ b/conf/legacy/frps_legacy_full.ini @@ -6,13 +6,18 @@ bind_addr = 0.0.0.0 bind_port = 7000 -# udp port to help make udp hole to penetrate nat -bind_udp_port = 7001 - -# udp port used for kcp protocol, it can be same with 'bind_port' -# if not set, kcp is disabled in frps +# udp port used for kcp protocol, it can be same with 'bind_port'. +# if not set, kcp is disabled in frps. kcp_bind_port = 7000 +# udp port used for quic protocol. +# if not set, quic is disabled in frps. +# quic_bind_port = 7002 +# quic protocol options +# quic_keepalive_period = 10 +# quic_max_idle_timeout = 30 +# quic_max_incoming_streams = 100000 + # specify which address proxy will listen for, default value is same with bind_addr # proxy_bind_addr = 127.0.0.1 @@ -149,6 +154,9 @@ udp_packet_size = 1500 # Dashboard port must be set first pprof_enable = false +# Retention time for NAT hole punching strategy data. +nat_hole_analysis_data_reserve_hours = 168 + [plugin.user-manager] addr = 127.0.0.1:9000 path = /handler diff --git a/doc/agents/release.md b/doc/agents/release.md new file mode 100644 index 00000000000..d353a088c48 --- /dev/null +++ b/doc/agents/release.md @@ -0,0 +1,124 @@ +# Release Process + +## 1. Update Release Notes + +Edit `Release.md` in the project root with the changes for this version: + +```markdown +## Features +* ... + +## Improvements +* ... + +## Fixes +* ... +``` + +This file is used by GoReleaser as the GitHub Release body. + +## 2. Bump Version + +Update the version string in `pkg/util/version/version.go`: + +```go +var version = "0.X.0" +``` + +Commit and push to `dev`: + +```bash +git add pkg/util/version/version.go Release.md +git commit -m "bump version to vX.Y.Z" +git push origin dev +``` + +## 3. Pre-release Validation + +Run the standard e2e suite locally: + +```bash +make e2e +``` + +For releases that touch compatibility-sensitive areas such as login, control +connections, work connections, visitors, transport, or wire protocol handling, +also run the manual compatibility e2e suite: + +```bash +make e2e-compatibility +make e2e-compatibility-floor +``` + +`make e2e-compatibility` builds the current `frps` and `frpc`, resolves the +recent stable release baselines from GitHub, downloads or reuses their binaries, +and tests current binaries against those historical releases. The default number +of recent baselines is controlled by `FRP_COMPAT_BASELINE_COUNT` in the +`Makefile`. + +Downloaded release binaries are cached under: + +```text +.cache/e2e-compat//_/ +``` + +For a release validation run that must be exactly reproducible, pass an explicit +baseline matrix instead of using the floating recent-release list: + +```bash +FRP_COMPAT_BASELINE_VERSIONS="0.X.0 0.Y.0" make e2e-compatibility +``` + +Use `make e2e-compatibility-smoke` for a quick single-baseline check while +iterating locally. If GitHub release metadata requests are rate-limited, set +`GITHUB_TOKEN` or use `FRP_COMPAT_BASELINE_VERSIONS`. + +The compatibility floor is a support-policy decision, not a value that should +change every release. Update `FRP_COMPAT_FLOOR_VERSION` only when the declared +compatibility window changes. + +## 4. Merge dev → master + +Create a PR from `dev` to `master`: + +```bash +gh pr create --base master --head dev --title "bump version" +``` + +Wait for CI to pass, then merge using **merge commit** (not squash). + +## 5. Tag the Release + +```bash +git checkout master +git pull origin master +git tag -a vX.Y.Z -m "bump version" +git push origin vX.Y.Z +``` + +## 6. Trigger GoReleaser + +Manually trigger the `goreleaser` workflow in GitHub Actions: + +```bash +gh workflow run goreleaser --ref master +``` + +GoReleaser will: +1. Run `package.sh` to cross-compile all platforms and create archives +2. Create a GitHub Release with all packages, using `Release.md` as release notes + +## Key Files + +| File | Purpose | +|------|---------| +| `pkg/util/version/version.go` | Version string | +| `Release.md` | Release notes (read by GoReleaser) | +| `.goreleaser.yml` | GoReleaser config | +| `package.sh` | Cross-compile and packaging script | +| `.github/workflows/goreleaser.yml` | GitHub Actions workflow (manual trigger) | + +## Versioning + +- Minor release: `v0.X.0` +- Patch release: `v0.X.Y` (e.g., `v0.62.1`) diff --git a/doc/deprecations.md b/doc/deprecations.md new file mode 100644 index 00000000000..a5d394c5695 --- /dev/null +++ b/doc/deprecations.md @@ -0,0 +1,38 @@ +# Deprecations + +This document tracks deprecated features and APIs that are still shipped but scheduled for removal. Maintainers should review this list before each release to decide whether any items are due for removal. + +For the version compatibility policy that bounds these support windows, see the latest `Release.md`. + +## Active + +### Wire protocol v1 + +- **Deprecated since:** v0.70.0 (planned, when v2 becomes the default). +- **Removal target:** v0.78.0 or later. v0.69.0 (the last release where v1 is the default) is supported until v0.78.0 is released, so v0.77.0 is the last release that must keep v1 support. +- **Replacement:** wire protocol v2 (`transport.wireProtocol = "v2"` in frpc). +- **Code references:** v1 message types and codec under `pkg/msg/` and the protocol negotiation path in `client/` and `server/`. +- **Notes:** Removing v1 will also drop compatibility with any frpc/frps that does not negotiate v2. + +### INI configuration format + +- **Deprecated since:** predates this document; startup warning has been in place for several releases. +- **Removal target:** TBD. +- **Replacement:** YAML / JSON / TOML. +- **Code references:** + - `cmd/frpc/sub/root.go` — frpc startup warning. + - `cmd/frps/root.go` — frps startup warning. + - `pkg/config/legacy/` — legacy INI parser; remove together with the warnings. + +### Visitor connections without `runID` + +- **Deprecated since:** v0.50.0 (when `runID` was introduced). +- **Removal target:** TBD. +- **Replacement:** require `runID` on every visitor connection. +- **Code references:** + - `server/service.go` — `RegisterVisitorConn` still accepts empty `runID` for backward compatibility. +- **Notes:** Removal will break frpc clients released before v0.50.0. Schedule for a release where dropping pre-v0.50.0 frpc is acceptable. + +## Removed + +_None yet._ diff --git a/doc/pic/architecture.jpg b/doc/pic/architecture.jpg new file mode 100644 index 00000000000..5d9b78b36a2 Binary files /dev/null and b/doc/pic/architecture.jpg differ diff --git a/doc/pic/architecture.png b/doc/pic/architecture.png deleted file mode 100644 index 2ee54fc23ec..00000000000 Binary files a/doc/pic/architecture.png and /dev/null differ diff --git a/doc/pic/donate-alipay.png b/doc/pic/donate-alipay.png deleted file mode 100644 index f717145ca67..00000000000 Binary files a/doc/pic/donate-alipay.png and /dev/null differ diff --git a/doc/pic/donate-wechatpay.png b/doc/pic/donate-wechatpay.png deleted file mode 100644 index d8fef5872f3..00000000000 Binary files a/doc/pic/donate-wechatpay.png and /dev/null differ diff --git a/doc/pic/sponsor_daytona.png b/doc/pic/sponsor_daytona.png new file mode 100644 index 00000000000..e36b6bba4af Binary files /dev/null and b/doc/pic/sponsor_daytona.png differ diff --git a/doc/pic/sponsor_doppler.png b/doc/pic/sponsor_doppler.png deleted file mode 100644 index 0d66038b717..00000000000 Binary files a/doc/pic/sponsor_doppler.png and /dev/null differ diff --git a/doc/pic/sponsor_jetbrains.jpg b/doc/pic/sponsor_jetbrains.jpg new file mode 100644 index 00000000000..be2113cbc49 Binary files /dev/null and b/doc/pic/sponsor_jetbrains.jpg differ diff --git a/doc/pic/sponsor_olares.jpeg b/doc/pic/sponsor_olares.jpeg new file mode 100644 index 00000000000..375c2a7ddb8 Binary files /dev/null and b/doc/pic/sponsor_olares.jpeg differ diff --git a/doc/pic/sponsor_workos.png b/doc/pic/sponsor_workos.png deleted file mode 100644 index 5bc5e627d33..00000000000 Binary files a/doc/pic/sponsor_workos.png and /dev/null differ diff --git a/doc/pic/zsxq.jpg b/doc/pic/zsxq.jpg deleted file mode 100644 index 0bb1f1d516d..00000000000 Binary files a/doc/pic/zsxq.jpg and /dev/null differ diff --git a/doc/server_plugin.md b/doc/server_plugin.md index d73d2439eb7..6ddef827470 100644 --- a/doc/server_plugin.md +++ b/doc/server_plugin.md @@ -110,6 +110,8 @@ Create new proxy "proxy_type": , "use_encryption": , "use_compression": , + "bandwidth_limit": , + "bandwidth_limit_mode": , "group": , "group_key": , @@ -119,7 +121,7 @@ Create new proxy // http and https only "custom_domains": [], "subdomain": , - "locations": , + "locations": [], "http_user": , "http_pwd": , "host_header_rewrite": , @@ -214,49 +216,50 @@ New user connection received from proxy (support `tcp`, `stcp`, `https` and `tcp ### Server Plugin Configuration -```ini -# frps.ini -[common] -bind_port = 7000 - -[plugin.user-manager] -addr = 127.0.0.1:9000 -path = /handler -ops = Login - -[plugin.port-manager] -addr = 127.0.0.1:9001 -path = /handler -ops = NewProxy +```toml +# frps.toml +bindPort = 7000 + +[[httpPlugins]] +name = "user-manager" +addr = "127.0.0.1:9000" +path = "/handler" +ops = ["Login"] + +[[httpPlugins]] +name = "port-manager" +addr = "127.0.0.1:9001" +path = "/handler" +ops = ["NewProxy"] ``` -- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = https://127.0.0.1:9001`. +- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = "https://127.0.0.1:9001"`. - path: http request url path for the POST request. - ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...). -- tls_verify: When the schema is https, we verify by default. Set this value to false if you want to skip verification. +- tlsVerify: When the schema is https, we verify by default. Set this value to false if you want to skip verification. ### Metadata Metadata will be sent to the server plugin in each RPC request. -There are 2 types of metadata entries - 1 under `[common]` and the other under each proxy configuration. -Metadata entries under `[common]` will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`. +There are 2 types of metadata entries - global one and the other under each proxy configuration. +Global metadata entries will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`. Metadata entries under each proxy configuration will be sent in `NewProxy` op only, under `metas`. -Metadata entries start with `meta_`. This is an example of metadata entries in `[common]` and under the proxy named `[ssh]`: - -``` -# frpc.ini -[common] -server_addr = 127.0.0.1 -server_port = 7000 -user = fake -meta_token = fake -meta_version = 1.0.0 - -[ssh] -type = tcp -local_port = 22 -remote_port = 6000 -meta_id = 123 +This is an example of metadata entries: + +```toml +# frpc.toml +serverAddr = "127.0.0.1" +serverPort = 7000 +user = "fake" +metadatas.token = "fake" +metadatas.version = "1.0.0" + +[[proxies]] +name = "ssh" +type = "tcp" +localPort = 22 +remotePort = 6000 +metadatas.id = "123" ``` diff --git a/doc/ssh_tunnel_gateway.md b/doc/ssh_tunnel_gateway.md new file mode 100644 index 00000000000..b3dd4c34376 --- /dev/null +++ b/doc/ssh_tunnel_gateway.md @@ -0,0 +1,160 @@ +### SSH Tunnel Gateway + +*Added in v0.53.0* + +### Concept + +SSH supports reverse proxy capabilities [rfc](https://www.rfc-editor.org/rfc/rfc4254#page-16). + +frp supports listening on an SSH port on the frps side to achieve TCP protocol proxying using the SSH -R protocol. This mode does not rely on frpc. + +SSH reverse tunneling proxying and proxying SSH ports through frp are two different concepts. SSH reverse tunneling proxying is essentially a basic reverse proxying accomplished by connecting to frps via an SSH client when you don't want to use frpc. + +```toml +# frps.toml +sshTunnelGateway.bindPort = 0 +sshTunnelGateway.privateKeyFile = "" +sshTunnelGateway.autoGenPrivateKeyPath = "" +sshTunnelGateway.authorizedKeysFile = "" +``` + +| Field | Type | Description | Required | +| :--- | :--- | :--- | :--- | +| bindPort| int | The ssh server port that frps listens on.| Yes | +| privateKeyFile | string | Default value is empty. The private key file used by the ssh server. If it is empty, frps will read the private key file under the autoGenPrivateKeyPath path. It can reuse the /home/user/.ssh/id_rsa file on the local machine, or a custom path can be specified.| No | +| autoGenPrivateKeyPath | string |Default value is ./.autogen_ssh_key. If the file does not exist or its content is empty, frps will automatically generate RSA private key file content and store it in this file.|No| +| authorizedKeysFile | string |Default value is empty. If it is empty, ssh client authentication is not authenticated. If it is not empty, it can implement ssh password-free login authentication. It can reuse the local /home/user/.ssh/authorized_keys file or a custom path can be specified.| No | + +### Basic Usage + +#### Server-side frps + +Minimal configuration: + +```toml +sshTunnelGateway.bindPort = 2200 +``` + +Place the above configuration in frps.toml and run `./frps -c frps.toml`. It will listen on port 2200 and accept SSH reverse proxy requests. + +Note: + +1. When using the minimal configuration, a `.autogen_ssh_key` private key file will be automatically created in the current working directory. The SSH server of frps will use this private key file for encryption and decryption. Alternatively, you can reuse an existing private key file on your local machine, such as `/home/user/.ssh/id_rsa`. + +2. When running frps in the minimal configuration mode, connecting to frps via SSH does not require authentication. It is strongly recommended to configure a token in frps and specify the token in the SSH command line. + +#### Client-side SSH + +The command format is: + +```bash +ssh -R :80:{local_ip:port} v0@{frps_address} -p {frps_ssh_listen_port} {tcp|http|https|stcp|tcpmux} --remote_port {real_remote_port} --proxy_name {proxy_name} --token {frp_token} +``` + +1. `--proxy_name` is optional, and if left empty, a random one will be generated. +2. The username for logging in to frps is always "v0" and currently has no significance, i.e., `v0@{frps_address}`. +3. The server-side proxy listens on the port determined by `--remote_port`. +4. `{tcp|http|https|stcp|tcpmux}` supports the complete command parameters, which can be obtained by using `--help`. For example: `ssh -R :80::8080 v0@127.0.0.1 -p 2200 http --help`. +5. The token is optional, but for security reasons, it is strongly recommended to configure the token in frps. + +#### TCP Proxy + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp_address} -p 2200 tcp --proxy_name "test-tcp" --remote_port 9090 +``` + +This sets up a proxy on frps that listens on port 9090 and proxies local service on port 8080. + +```bash +frp (via SSH) (Ctrl+C to quit) + +User: +ProxyName: test-tcp +Type: tcp +RemoteAddress: :9090 +``` + +Equivalent to: + +```bash +frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote_port 9090 +``` + +More parameters can be obtained by executing `--help`. + +#### HTTP Proxy + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 http --proxy_name "test-http" --custom_domain test-http.frps.com +``` + +Equivalent to: +```bash +frpc http --proxy_name "test-http" --custom_domain test-http.frps.com +``` + +You can access the HTTP service using the following command: + +curl 'http://test-http.frps.com' + +More parameters can be obtained by executing --help. + +#### HTTPS/STCP/TCPMUX Proxy + +To obtain the usage instructions, use the following command: + +```bash +ssh -R :80:127.0.0.1:8080 v0@{frp address} -p 2200 {https|stcp|tcpmux} --help +``` + +### Advanced Usage + +#### Reusing the id_rsa File on the Local Machine + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.privateKeyFile = "/home/user/.ssh/id_rsa" +``` + +During the SSH protocol handshake, public keys are exchanged for data encryption. Therefore, the SSH server on the frps side needs to specify a private key file, which can be reused from an existing file on the local machine. If the privateKeyFile field is empty, frps will automatically create an RSA private key file. + +#### Specifying the Auto-Generated Private Key File Path + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.autoGenPrivateKeyPath = "/var/frp/ssh-private-key-file" +``` + +frps will automatically create a private key file and store it at the specified path. + +Note: Changing the private key file in frps can cause SSH client login failures. If you need to log in successfully, you can delete the old records from the `/home/user/.ssh/known_hosts` file. + +#### Using an Existing authorized_keys File for SSH Public Key Authentication + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.authorizedKeysFile = "/home/user/.ssh/authorized_keys" +``` + +The authorizedKeysFile is the file used for SSH public key authentication, which contains the public key information for users, with one key per line. + +If authorizedKeysFile is empty, frps won't perform any authentication for SSH clients. Frps does not support SSH username and password authentication. + +You can reuse an existing `authorized_keys` file on your local machine for client authentication. + +Note: authorizedKeysFile is for user authentication during the SSH login phase, while the token is for frps authentication. These two authentication methods are independent. SSH authentication comes first, followed by frps token authentication. It is strongly recommended to enable at least one of them. If authorizedKeysFile is empty, it is highly recommended to enable token authentication in frps to avoid security risks. + +#### Using a Custom authorized_keys File for SSH Public Key Authentication + +```toml +# frps.toml +sshTunnelGateway.bindPort = 2200 +sshTunnelGateway.authorizedKeysFile = "/var/frps/custom_authorized_keys_file" +``` + +Specify the path to a custom `authorized_keys` file. + +Note that changes to the authorizedKeysFile file may result in SSH authentication failures. You may need to re-add the public key information to the authorizedKeysFile. diff --git a/doc/virtual_net.md b/doc/virtual_net.md new file mode 100644 index 00000000000..6f135dfc2fc --- /dev/null +++ b/doc/virtual_net.md @@ -0,0 +1,73 @@ +# Virtual Network (VirtualNet) + +*Alpha feature added in v0.62.0* + +The VirtualNet feature enables frp to create and manage virtual network connections between clients and visitors through a TUN interface. This allows for IP-level routing between machines, extending frp beyond simple port forwarding to support full network connectivity. + +> **Note**: VirtualNet is an Alpha stage feature and is currently unstable. Its configuration methods and functionality may be adjusted and changed at any time in subsequent versions. Do not use this feature in production environments; it is only recommended for testing and evaluation purposes. + +## Enabling VirtualNet + +Since VirtualNet is currently an alpha feature, you need to enable it with feature gates in your configuration: + +```toml +# frpc.toml +featureGates = { VirtualNet = true } +``` + +## Basic Configuration + +To use the virtual network capabilities: + +1. First, configure your frpc with a virtual network address: + +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 +featureGates = { VirtualNet = true } + +# Configure the virtual network interface +virtualNet.address = "100.86.0.1/24" +``` + +2. For client proxies, use the `virtual_net` plugin: + +```toml +# frpc.toml (server side) +[[proxies]] +name = "vnet-server" +type = "stcp" +secretKey = "your-secret-key" +[proxies.plugin] +type = "virtual_net" +``` + +3. For visitor connections, configure the `virtual_net` visitor plugin: + +```toml +# frpc.toml (client side) +serverAddr = "x.x.x.x" +serverPort = 7000 +featureGates = { VirtualNet = true } + +# Configure the virtual network interface +virtualNet.address = "100.86.0.2/24" + +[[visitors]] +name = "vnet-visitor" +type = "stcp" +serverName = "vnet-server" +secretKey = "your-secret-key" +bindPort = -1 +[visitors.plugin] +type = "virtual_net" +destinationIP = "100.86.0.1" +``` + +## Requirements and Limitations + +- **Permissions**: Creating a TUN interface requires elevated permissions (root/admin) +- **Platform Support**: Currently supported on Linux and macOS +- **Default Status**: As an alpha feature, VirtualNet is disabled by default +- **Configuration**: A valid IP/CIDR must be provided for each endpoint in the virtual network diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index 3464b5536ba..665ca800967 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -1,11 +1,24 @@ +FROM node:22 AS web-builder + +COPY web/package.json /web/package.json +COPY web/shared/ /web/shared/ +COPY web/frpc/ /web/frpc/ +WORKDIR /web +RUN npm install +WORKDIR /web/frpc +RUN npm run build + FROM golang:1.25 AS building COPY . /building +COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist WORKDIR /building -RUN make frpc +RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc + +FROM alpine:3 -FROM alpine:3.24.1 +RUN apk add --no-cache tzdata COPY --from=building /building/bin/frpc /usr/bin/frpc diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index bc396cdb70b..16319c8949f 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -1,11 +1,24 @@ +FROM node:22 AS web-builder + +COPY web/package.json /web/package.json +COPY web/shared/ /web/shared/ +COPY web/frps/ /web/frps/ +WORKDIR /web +RUN npm install +WORKDIR /web/frps +RUN npm run build + FROM golang:1.25 AS building COPY . /building +COPY --from=web-builder /web/frps/dist /building/web/frps/dist WORKDIR /building -RUN make frps +RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps + +FROM alpine:3 -FROM alpine:3.24.1 +RUN apk add --no-cache tzdata COPY --from=building /building/bin/frps /usr/bin/frps diff --git a/go.mod b/go.mod index 3f9c1d7594c..4645aa0c806 100644 --- a/go.mod +++ b/go.mod @@ -4,67 +4,76 @@ go 1.25.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 - github.com/coreos/go-oidc v2.2.1+incompatible - github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb - github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac - github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible - github.com/go-playground/validator/v10 v10.22.1 - github.com/google/uuid v1.2.0 - github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.4.2 - github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c - github.com/jpillora/ipfilter v1.2.7 - github.com/onsi/ginkgo v1.16.4 - github.com/onsi/gomega v1.13.0 - github.com/pires/go-proxyproto v0.6.2 - github.com/prometheus/client_golang v1.11.0 - github.com/rodaine/table v1.0.1 - github.com/spf13/cobra v1.1.3 - github.com/stretchr/testify v1.8.4 - golang.org/x/net v0.45.0 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba - gopkg.in/ini.v1 v1.62.0 - k8s.io/apimachinery v0.21.2 - k8s.io/client-go v0.21.2 + github.com/coreos/go-oidc/v3 v3.18.0 + 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 + github.com/hashicorp/yamux v0.1.1 + github.com/jpillora/ipfilter v1.5.0 + 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.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 + github.com/samber/lo v1.47.0 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.11.1 + github.com/tidwall/gjson v1.17.1 + github.com/vishvananda/netlink v1.3.0 + github.com/xtaci/kcp-go/v5 v5.6.13 + golang.org/x/crypto v0.54.0 + golang.org/x/net v0.56.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/time v0.10.0 + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 + gopkg.in/ini.v1 v1.67.0 + k8s.io/apimachinery v0.28.8 + k8s.io/client-go v0.28.8 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 ) require ( - github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect + github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.1 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/klauspost/cpuid/v2 v2.0.6 // indirect - github.com/klauspost/reedsolomon v1.9.15 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/phuslu/iploc v1.0.20220730 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/klauspost/reedsolomon v1.12.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.26.0 // indirect - github.com/prometheus/procfs v0.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect - github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/templexxx/cpu v0.1.1 // indirect + github.com/templexxx/xorsimd v0.4.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect - github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/appengine v1.6.5 // indirect - google.golang.org/protobuf v1.26.0 // indirect - gopkg.in/square/go-jose.v2 v2.4.1 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/text v0.40.0 // indirect + golang.org/x/tools v0.47.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) + +// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. +replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 diff --git a/go.sum b/go.sum index 11256605b2c..7fa9dbe199e 100644 --- a/go.sum +++ b/go.sum @@ -1,704 +1,246 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= -github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= +github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= -github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= +github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/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/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw= -github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk= -github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac h1:td1FJwN/oz8+9GldeEm3YdBX0Husc0FSPywLesZxi4w= -github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac/go.mod h1:fLV0TLwHqrnB/L3jbNl67Gn6PCLggDGHniX1wLrA2Qo= -github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74= -github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c h1:nqkErwUGfpZZMqj29WZ9U/wz2OpJVDuiokLhE/3Y7IQ= -github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jpillora/ipfilter v1.2.7 h1:fB+fIa/VtgjOrHjkR3Sw47dHYhZGCae/dIWc/Vur++U= -github.com/jpillora/ipfilter v1.2.7/go.mod h1:QS0miOgSqkxAsnTKLADlahASDOExe2K2pdoswGRt+FM= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= -github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo= -github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAKAP/qs14y4GCBOk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jpillora/ipfilter v1.5.0 h1:xlcTusy7YfFIMBBAhrpY0v3UgESjgXfbuII0tN+/CqQ= +github.com/jpillora/ipfilter v1.5.0/go.mod h1:cvZ9P1WAdvt36H6XVeIzqrpzmpTYfJtA7BGSKRIhkmM= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno= +github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phuslu/iploc v1.0.20220730 h1:Ly2Casvb9LVnaDg06RfkET6AwkMCUXrNANKJX40vsoE= -github.com/phuslu/iploc v1.0.20220730/go.mod h1:gsgExGWldwv1AEzZm+Ki9/vGfyjkL33pbSr9HGpt2Xg= -github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= -github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +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.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= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= -github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ= -github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= +github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= -github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= -github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI= -github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= +github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU= +github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk= +github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= -gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= -k8s.io/apimachinery v0.21.2 h1:vezUc/BHqWlQDnZ+XkrpXSmnANSLbpnlpwo0Lhk0gpc= -k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= -k8s.io/client-go v0.21.2 h1:Q1j4L/iMN4pTw6Y4DWppBoUxgKO8LbffEMVEV00MUp0= -k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +k8s.io/apimachinery v0.28.8 h1:hi/nrxHwk4QLV+W/SHve1bypTE59HCDorLY1stBIxKQ= +k8s.io/apimachinery v0.28.8/go.mod h1:cBnwIM3fXoRo28SqbV/Ihxf/iviw85KyXOrzxvZQ83U= +k8s.io/client-go v0.28.8 h1:TE59Tjd87WKvS2FPBTfIKLFX0nQJ4SSHsnDo5IHjgOw= +k8s.io/client-go v0.28.8/go.mod h1:uDVQ/rPzWpWIy40c6lZ4mUwaEvRWGnpoqSO4FM65P3o= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/hack/download.sh b/hack/download.sh new file mode 100755 index 00000000000..acdf033aff7 --- /dev/null +++ b/hack/download.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +OS="$(go env GOOS)" +ARCH="$(go env GOARCH)" + +if [ "${TARGET_OS}" ]; then + OS="${TARGET_OS}" +fi +if [ "${TARGET_ARCH}" ]; then + ARCH="${TARGET_ARCH}" +fi + +# Determine the latest version by version number ignoring alpha, beta, and rc versions. +if [ "${FRP_VERSION}" = "" ] ; then + FRP_VERSION="$(curl -sL https://github.com/fatedier/frp/releases | \ + grep -o 'releases/tag/v[0-9]*.[0-9]*.[0-9]*"' | sort -V | \ + tail -1 | awk -F'/' '{ print $3}')" + FRP_VERSION="${FRP_VERSION%?}" + FRP_VERSION="${FRP_VERSION#?}" +fi + +if [ "${FRP_VERSION}" = "" ] ; then + printf "Unable to get latest frp version. Set FRP_VERSION env var and re-run. For example: export FRP_VERSION=1.0.0" + exit 1; +fi + +SUFFIX=".tar.gz" +if [ "${OS}" = "windows" ] ; then + SUFFIX=".zip" +fi +NAME="frp_${FRP_VERSION}_${OS}_${ARCH}${SUFFIX}" +DIR_NAME="frp_${FRP_VERSION}_${OS}_${ARCH}" +URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${NAME}" + +download_and_extract() { + printf "Downloading %s from %s ...\n" "$NAME" "${URL}" + if ! curl -o /dev/null -sIf "${URL}"; then + printf "\n%s is not found, please specify a valid FRP_VERSION\n" "${URL}" + exit 1 + fi + curl -fsLO "${URL}" + filename=$NAME + + if [ "${OS}" = "windows" ]; then + unzip "${filename}" + else + tar -xzf "${filename}" + fi + rm "${filename}" + + if [ "${TARGET_DIRNAME}" ]; then + mv "${DIR_NAME}" "${TARGET_DIRNAME}" + DIR_NAME="${TARGET_DIRNAME}" + fi +} + +download_and_extract + +printf "" +printf "\nfrp %s Download Complete!\n" "$FRP_VERSION" +printf "\n" +printf "frp has been successfully downloaded into the %s folder on your system.\n" "$DIR_NAME" +printf "\n" diff --git a/hack/run-e2e-compatibility.sh b/hack/run-e2e-compatibility.sh new file mode 100755 index 00000000000..988d89b487e --- /dev/null +++ b/hack/run-e2e-compatibility.sh @@ -0,0 +1,162 @@ +#!/bin/sh + +set -eu + +SCRIPT=$(readlink -f "$0") +ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) + +if ! command -v ginkgo >/dev/null 2>&1; then + echo "ginkgo not found, try to install..." + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 +fi + +debug=false +if [ "x${DEBUG:-}" = "xtrue" ]; then + debug=true +fi +logLevel=debug +if [ "${LOG_LEVEL:-}" ]; then + logLevel="${LOG_LEVEL}" +fi + +currentFrpsPath=${CURRENT_FRPS_PATH:-${ROOT}/bin/frps} +currentFrpcPath=${CURRENT_FRPC_PATH:-${ROOT}/bin/frpc} +baselineCount=${FRP_COMPAT_BASELINE_COUNT:-8} +targetOS=${TARGET_OS:-$(go env GOOS)} +targetArch=${TARGET_ARCH:-$(go env GOARCH)} +targetPlatform="${targetOS}_${targetArch}" +cacheRoot=${FRP_COMPAT_CACHE_DIR:-${ROOT}/.cache/e2e-compat} + +check_file() { + if [ ! -f "$2" ]; then + echo "$1 not found: $2" + exit 1 + fi +} + +check_file "current frps" "${currentFrpsPath}" +check_file "current frpc" "${currentFrpcPath}" + +run_current_current=true + +run_compatibility() { + baselineVersion=$1 + baselineFrpsPath=$2 + baselineFrpcPath=$3 + + check_file "baseline frps" "${baselineFrpsPath}" + check_file "baseline frpc" "${baselineFrpcPath}" + + echo "Running compatibility e2e with baseline ${baselineVersion}" + ginkgo -nodes=1 --poll-progress-after=60s "${ROOT}/test/e2e/compatibility" -- \ + -current-frps-path="${currentFrpsPath}" \ + -current-frpc-path="${currentFrpcPath}" \ + -baseline-frps-path="${baselineFrpsPath}" \ + -baseline-frpc-path="${baselineFrpcPath}" \ + -baseline-version="${baselineVersion}" \ + -run-current-current="${run_current_current}" \ + -log-level="${logLevel}" \ + -debug="${debug}" + run_current_current=false +} + +github_api_curl() { + if [ "${GITHUB_TOKEN:-}" ]; then + curl -fsSL \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + "$1" + else + curl -fsSL "$1" + fi +} + +resolve_versions() { + if [ "${FRP_COMPAT_BASELINE_VERSIONS:-}" ]; then + printf "%s\n" "${FRP_COMPAT_BASELINE_VERSIONS}" + return + fi + + case "${baselineCount}" in + '' | *[!0-9]*) + echo "FRP_COMPAT_BASELINE_COUNT must be a positive integer: ${baselineCount}" >&2 + exit 1 + ;; + esac + if [ "${baselineCount}" -eq 0 ]; then + echo "FRP_COMPAT_BASELINE_COUNT must be greater than 0" >&2 + exit 1 + fi + if [ "${baselineCount}" -gt 100 ]; then + echo "FRP_COMPAT_BASELINE_COUNT must be less than or equal to 100" >&2 + exit 1 + fi + + releaseURL="https://api.github.com/repos/fatedier/frp/releases?per_page=100" + resolvedVersions="" + if releases=$(github_api_curl "${releaseURL}" 2>/dev/null); then + resolvedVersions=$(printf "%s\n" "${releases}" | + sed -n 's/.*"tag_name":[[:space:]]*"v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' | + awk '!seen[$0]++' | + head -n "${baselineCount}" | + tr '\n' ' ' | + sed 's/[[:space:]]*$//') + else + echo "Failed to fetch release metadata from GitHub API, falling back to GitHub releases page." >&2 + fi + + if [ -z "${resolvedVersions}" ]; then + releasesPageURL="https://github.com/fatedier/frp/releases" + if ! releases=$(curl -fsSL "${releasesPageURL}"); then + echo "Failed to fetch release metadata from GitHub: ${releasesPageURL}" >&2 + echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2 + exit 1 + fi + resolvedVersions=$(printf "%s\n" "${releases}" | + grep -o 'releases/tag/v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"' | + sed 's#.*/v##; s/"$//' | + awk '!seen[$0]++' | + head -n "${baselineCount}" | + tr '\n' ' ' | + sed 's/[[:space:]]*$//') + fi + + set -- ${resolvedVersions} + if [ "$#" -lt "${baselineCount}" ]; then + echo "Only resolved $# stable release versions from GitHub, expected ${baselineCount}." >&2 + echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2 + exit 1 + fi + + printf "%s\n" "${resolvedVersions}" +} + +if [ "${BASELINE_FRPS_PATH:-}" ] || [ "${BASELINE_FRPC_PATH:-}" ]; then + if [ -z "${BASELINE_FRPS_PATH:-}" ] || [ -z "${BASELINE_FRPC_PATH:-}" ]; then + echo "BASELINE_FRPS_PATH and BASELINE_FRPC_PATH must be set together" + exit 1 + fi + run_compatibility "${FRP_COMPAT_BASELINE_VERSION:-custom}" "${BASELINE_FRPS_PATH}" "${BASELINE_FRPC_PATH}" + exit 0 +fi + +versions=$(resolve_versions) +echo "Compatibility baseline versions: ${versions}" + +mkdir -p "${cacheRoot}" +for version in ${versions}; do + baselineDir="${cacheRoot}/${version}/${targetPlatform}" + if [ ! -f "${baselineDir}/frps" ] || [ ! -f "${baselineDir}/frpc" ]; then + tmpDir="${cacheRoot}/.download-${version}-${targetPlatform}" + rm -rf "${tmpDir}" + ( + cd "${cacheRoot}" + FRP_VERSION="${version}" TARGET_DIRNAME="$(basename "${tmpDir}")" "${ROOT}/hack/download.sh" + ) + mkdir -p "$(dirname "${baselineDir}")" + rm -rf "${baselineDir}" + mv "${tmpDir}" "${baselineDir}" + fi + + run_compatibility "${version}" "${baselineDir}/frps" "${baselineDir}/frpc" +done diff --git a/hack/run-e2e.sh b/hack/run-e2e.sh index ee396652215..d1b20b527f3 100755 --- a/hack/run-e2e.sh +++ b/hack/run-e2e.sh @@ -1,20 +1,34 @@ -#!/usr/bin/env bash +#!/bin/sh -ROOT=$(unset CDPATH && cd $(dirname "${BASH_SOURCE[0]}")/.. && pwd) +SCRIPT=$(readlink -f "$0") +ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) -which ginkgo &> /dev/null -if [ $? -ne 0 ]; then +# Check if ginkgo is available +if ! command -v ginkgo >/dev/null 2>&1; then echo "ginkgo not found, try to install..." - go install github.com/onsi/ginkgo/ginkgo@latest + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 fi debug=false -if [ x${DEBUG} == x"true" ]; then +if [ "x${DEBUG}" = "xtrue" ]; then debug=true fi logLevel=debug -if [ x${LOG_LEVEL} != x"" ]; then - logLevel=${LOG_LEVEL} +if [ "${LOG_LEVEL}" ]; then + logLevel="${LOG_LEVEL}" fi -ginkgo -nodes=8 -slowSpecThreshold=20 ${ROOT}/test/e2e -- -frpc-path=${ROOT}/bin/frpc -frps-path=${ROOT}/bin/frps -log-level=${logLevel} -debug=${debug} +frpcPath=${ROOT}/bin/frpc +if [ "${FRPC_PATH}" ]; then + frpcPath="${FRPC_PATH}" +fi +frpsPath=${ROOT}/bin/frps +if [ "${FRPS_PATH}" ]; then + frpsPath="${FRPS_PATH}" +fi +concurrency="16" +if [ "${CONCURRENCY}" ]; then + concurrency="${CONCURRENCY}" +fi + +ginkgo -nodes=${concurrency} --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug} diff --git a/package.sh b/package.sh index 2f9a0f205f4..16dfcee03ea 100755 --- a/package.sh +++ b/package.sh @@ -1,3 +1,6 @@ +#!/bin/sh +set -e + # compile for version make if [ $? -ne 0 ]; then @@ -14,49 +17,57 @@ make -f ./Makefile.cross-compiles rm -rf ./release/packages mkdir -p ./release/packages -os_all='linux windows darwin freebsd' -arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle' +os_all='linux windows darwin freebsd openbsd android' +arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64' +extra_all='_ hf' cd ./release for os in $os_all; do for arch in $arch_all; do - frp_dir_name="frp_${frp_version}_${os}_${arch}" - frp_path="./packages/frp_${frp_version}_${os}_${arch}" - - if [ "x${os}" = x"windows" ]; then - if [ ! -f "./frpc_${os}_${arch}.exe" ]; then - continue - fi - if [ ! -f "./frps_${os}_${arch}.exe" ]; then - continue - fi - mkdir ${frp_path} - mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe - mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe - else - if [ ! -f "./frpc_${os}_${arch}" ]; then - continue + for extra in $extra_all; do + suffix="${os}_${arch}" + if [ "x${extra}" != x"_" ]; then + suffix="${os}_${arch}_${extra}" fi - if [ ! -f "./frps_${os}_${arch}" ]; then - continue - fi - mkdir ${frp_path} - mv ./frpc_${os}_${arch} ${frp_path}/frpc - mv ./frps_${os}_${arch} ${frp_path}/frps - fi - cp ../LICENSE ${frp_path} - cp -rf ../conf/* ${frp_path} - - # packages - cd ./packages - if [ "x${os}" = x"windows" ]; then - zip -rq ${frp_dir_name}.zip ${frp_dir_name} - else - tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name} - fi - cd .. - rm -rf ${frp_path} + frp_dir_name="frp_${frp_version}_${suffix}" + frp_path="./packages/frp_${frp_version}_${suffix}" + + if [ "x${os}" = x"windows" ]; then + if [ ! -f "./frpc_${os}_${arch}.exe" ]; then + continue + fi + if [ ! -f "./frps_${os}_${arch}.exe" ]; then + continue + fi + mkdir ${frp_path} + mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe + mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe + else + if [ ! -f "./frpc_${suffix}" ]; then + continue + fi + if [ ! -f "./frps_${suffix}" ]; then + continue + fi + mkdir ${frp_path} + mv ./frpc_${suffix} ${frp_path}/frpc + mv ./frps_${suffix} ${frp_path}/frps + fi + cp ../LICENSE ${frp_path} + cp -f ../conf/frpc.toml ${frp_path} + cp -f ../conf/frps.toml ${frp_path} + + # packages + cd ./packages + if [ "x${os}" = x"windows" ]; then + zip -rq ${frp_dir_name}.zip ${frp_dir_name} + else + tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name} + fi + cd .. + rm -rf ${frp_path} + done done done diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 894da2471c7..366b62ef31e 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -15,79 +15,69 @@ package auth import ( + "context" "fmt" - "github.com/fatedier/frp/pkg/consts" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" ) -type BaseConfig struct { - // AuthenticationMethod specifies what authentication method to use to - // authenticate frpc with frps. If "token" is specified - token will be - // read into login message. If "oidc" is specified - OIDC (Open ID Connect) - // token will be issued using OIDC settings. By default, this value is "token". - AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"` - // AuthenticateHeartBeats specifies whether to include authentication token in - // heartbeats sent to frps. By default, this value is false. - AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"` - // AuthenticateNewWorkConns specifies whether to include authentication token in - // new work connections sent to frps. By default, this value is false. - AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"` +type Setter interface { + SetLogin(*msg.Login) error + SetPing(*msg.Ping) error + SetNewWorkConn(*msg.NewWorkConn) error } -func getDefaultBaseConf() BaseConfig { - return BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - } +type ClientAuth struct { + Setter Setter + key []byte } -type ClientConfig struct { - BaseConfig `ini:",extends"` - OidcClientConfig `ini:",extends"` - TokenConfig `ini:",extends"` +func (a *ClientAuth) EncryptionKey() []byte { + return a.key } -func GetDefaultClientConf() ClientConfig { - return ClientConfig{ - BaseConfig: getDefaultBaseConf(), - OidcClientConfig: getDefaultOidcClientConf(), - TokenConfig: getDefaultTokenConf(), +// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime. +// Caller must run validation before calling this function. +func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) { + if cfg == nil { + return nil, fmt.Errorf("auth config is nil") } -} - -type ServerConfig struct { - BaseConfig `ini:",extends"` - OidcServerConfig `ini:",extends"` - TokenConfig `ini:",extends"` -} - -func GetDefaultServerConf() ServerConfig { - return ServerConfig{ - BaseConfig: getDefaultBaseConf(), - OidcServerConfig: getDefaultOidcServerConf(), - TokenConfig: getDefaultTokenConf(), + resolved := *cfg + if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil { + token, err := resolved.TokenSource.Resolve(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err) + } + resolved.Token = token } + setter, err := NewAuthSetter(resolved) + if err != nil { + return nil, err + } + return &ClientAuth{ + Setter: setter, + key: []byte(resolved.Token), + }, nil } -type Setter interface { - SetLogin(*msg.Login) error - SetPing(*msg.Ping) error - SetNewWorkConn(*msg.NewWorkConn) error -} - -func NewAuthSetter(cfg ClientConfig) (authProvider Setter) { - switch cfg.AuthenticationMethod { - case consts.TokenAuthMethod: - authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig) - case consts.OidcAuthMethod: - authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig) +func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) { + switch cfg.Method { + case v1.AuthMethodToken: + authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + case v1.AuthMethodOIDC: + if cfg.OIDC.TokenSource != nil { + authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource) + } else { + authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) + if err != nil { + return nil, err + } + } default: - panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod)) + return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method) } - - return authProvider + return authProvider, nil } type Verifier interface { @@ -96,13 +86,42 @@ type Verifier interface { VerifyNewWorkConn(*msg.NewWorkConn) error } -func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) { - switch cfg.AuthenticationMethod { - case consts.TokenAuthMethod: - authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig) - case consts.OidcAuthMethod: - authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig) +type ServerAuth struct { + Verifier Verifier + key []byte +} + +func (a *ServerAuth) EncryptionKey() []byte { + return a.key +} + +// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime. +// Caller must run validation before calling this function. +func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) { + if cfg == nil { + return nil, fmt.Errorf("auth config is nil") } + resolved := *cfg + if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil { + token, err := resolved.TokenSource.Resolve(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err) + } + resolved.Token = token + } + return &ServerAuth{ + Verifier: NewAuthVerifier(resolved), + key: []byte(resolved.Token), + }, nil +} +func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { + switch cfg.Method { + case v1.AuthMethodToken: + authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + case v1.AuthMethodOIDC: + tokenVerifier := NewTokenVerifier(cfg.OIDC) + authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier) + } return authVerifier } diff --git a/pkg/auth/legacy/legacy.go b/pkg/auth/legacy/legacy.go new file mode 100644 index 00000000000..c16d38f2c91 --- /dev/null +++ b/pkg/auth/legacy/legacy.go @@ -0,0 +1,145 @@ +// Copyright 2023 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 legacy + +type BaseConfig struct { + // AuthenticationMethod specifies what authentication method to use to + // authenticate frpc with frps. If "token" is specified - token will be + // read into login message. If "oidc" is specified - OIDC (Open ID Connect) + // token will be issued using OIDC settings. By default, this value is "token". + AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"` + // AuthenticateHeartBeats specifies whether to include authentication token in + // heartbeats sent to frps. By default, this value is false. + AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"` + // AuthenticateNewWorkConns specifies whether to include authentication token in + // new work connections sent to frps. By default, this value is false. + AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"` +} + +func getDefaultBaseConf() BaseConfig { + return BaseConfig{ + AuthenticationMethod: "token", + AuthenticateHeartBeats: false, + AuthenticateNewWorkConns: false, + } +} + +type ClientConfig struct { + BaseConfig `ini:",extends"` + OidcClientConfig `ini:",extends"` + TokenConfig `ini:",extends"` +} + +func GetDefaultClientConf() ClientConfig { + return ClientConfig{ + BaseConfig: getDefaultBaseConf(), + OidcClientConfig: getDefaultOidcClientConf(), + TokenConfig: getDefaultTokenConf(), + } +} + +type ServerConfig struct { + BaseConfig `ini:",extends"` + OidcServerConfig `ini:",extends"` + TokenConfig `ini:",extends"` +} + +func GetDefaultServerConf() ServerConfig { + return ServerConfig{ + BaseConfig: getDefaultBaseConf(), + OidcServerConfig: getDefaultOidcServerConf(), + TokenConfig: getDefaultTokenConf(), + } +} + +type OidcClientConfig struct { + // OidcClientID specifies the client ID to use to get a token in OIDC + // authentication if AuthenticationMethod == "oidc". By default, this value + // is "". + OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"` + // OidcClientSecret specifies the client secret to use to get a token in OIDC + // authentication if AuthenticationMethod == "oidc". By default, this value + // is "". + OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"` + // OidcAudience specifies the audience of the token in OIDC authentication + // if AuthenticationMethod == "oidc". By default, this value is "". + OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` + // OidcScope specifies the scope of the token in OIDC authentication + // if AuthenticationMethod == "oidc". By default, this value is "". + OidcScope string `ini:"oidc_scope" json:"oidc_scope"` + // OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint. + // It will be used to get an OIDC token if AuthenticationMethod == "oidc". + // By default, this value is "". + OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"` + + // OidcAdditionalEndpointParams specifies additional parameters to be sent + // this field will be transfer to map[string][]string in OIDC token generator + // The field will be set by prefix "oidc_additional_" + OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"` +} + +func getDefaultOidcClientConf() OidcClientConfig { + return OidcClientConfig{ + OidcClientID: "", + OidcClientSecret: "", + OidcAudience: "", + OidcScope: "", + OidcTokenEndpointURL: "", + OidcAdditionalEndpointParams: make(map[string]string), + } +} + +type OidcServerConfig struct { + // OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer + // will be used to load public keys to verify signature and will be compared + // with the issuer claim in the OIDC token. It will be used if + // AuthenticationMethod == "oidc". By default, this value is "". + OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"` + // OidcAudience specifies the audience OIDC tokens should contain when validated. + // If this value is empty, audience ("client ID") verification will be skipped. + // It will be used when AuthenticationMethod == "oidc". By default, this + // value is "". + OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` + // OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is + // expired. It will be used when AuthenticationMethod == "oidc". By default, this + // value is false. + OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"` + // OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's + // issuer claim matches the issuer specified in OidcIssuer. It will be used when + // AuthenticationMethod == "oidc". By default, this value is false. + OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"` +} + +func getDefaultOidcServerConf() OidcServerConfig { + return OidcServerConfig{ + OidcIssuer: "", + OidcAudience: "", + OidcSkipExpiryCheck: false, + OidcSkipIssuerCheck: false, + } +} + +type TokenConfig struct { + // Token specifies the authorization token used to create keys to be sent + // to the server. The server must have a matching token for authorization + // to succeed. By default, this value is "". + Token string `ini:"token" json:"token"` +} + +func getDefaultTokenConf() TokenConfig { + return TokenConfig{ + Token: "", + } +} diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index 8a9f3404bf1..826a67152dd 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -16,105 +16,177 @@ package auth import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "net/http" + "net/url" + "os" + "slices" + "sync" - "github.com/fatedier/frp/pkg/msg" - - "github.com/coreos/go-oidc" + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/msg" ) -type OidcClientConfig struct { - // OidcClientID specifies the client ID to use to get a token in OIDC - // authentication if AuthenticationMethod == "oidc". By default, this value - // is "". - OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"` - // OidcClientSecret specifies the client secret to use to get a token in OIDC - // authentication if AuthenticationMethod == "oidc". By default, this value - // is "". - OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"` - // OidcAudience specifies the audience of the token in OIDC authentication - //if AuthenticationMethod == "oidc". By default, this value is "". - OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` - // OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint. - // It will be used to get an OIDC token if AuthenticationMethod == "oidc". - // By default, this value is "". - OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"` - - // OidcAdditionalEndpointParams specifies additional parameters to be sent - // this field will be transfer to map[string][]string in OIDC token generator - // The field will be set by prefix "oidc_additional_" - OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"` -} - -func getDefaultOidcClientConf() OidcClientConfig { - return OidcClientConfig{ - OidcClientID: "", - OidcClientSecret: "", - OidcAudience: "", - OidcTokenEndpointURL: "", - OidcAdditionalEndpointParams: make(map[string]string), - } -} - -type OidcServerConfig struct { - // OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer - // will be used to load public keys to verify signature and will be compared - // with the issuer claim in the OIDC token. It will be used if - // AuthenticationMethod == "oidc". By default, this value is "". - OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"` - // OidcAudience specifies the audience OIDC tokens should contain when validated. - // If this value is empty, audience ("client ID") verification will be skipped. - // It will be used when AuthenticationMethod == "oidc". By default, this - // value is "". - OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` - // OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is - // expired. It will be used when AuthenticationMethod == "oidc". By default, this - // value is false. - OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"` - // OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's - // issuer claim matches the issuer specified in OidcIssuer. It will be used when - // AuthenticationMethod == "oidc". By default, this value is false. - OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"` -} - -func getDefaultOidcServerConf() OidcServerConfig { - return OidcServerConfig{ - OidcIssuer: "", - OidcAudience: "", - OidcSkipExpiryCheck: false, - OidcSkipIssuerCheck: false, +// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests +func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyURL string) (*http.Client, error) { + // Clone the default transport to get all reasonable defaults + transport := http.DefaultTransport.(*http.Transport).Clone() + + // Configure TLS settings + if trustedCAFile != "" || insecureSkipVerify { + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + + if trustedCAFile != "" && !insecureSkipVerify { + caCert, err := os.ReadFile(trustedCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read OIDC CA certificate file %q: %w", trustedCAFile, err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse OIDC CA certificate from file %q", trustedCAFile) + } + + tlsConfig.RootCAs = caCertPool + } + transport.TLSClientConfig = tlsConfig + } + + // Configure proxy settings + if proxyURL != "" { + parsedURL, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("failed to parse OIDC proxy URL %q: %w", proxyURL, err) + } + transport.Proxy = http.ProxyURL(parsedURL) + } else { + // Explicitly disable proxy to override DefaultTransport's ProxyFromEnvironment + transport.Proxy = nil + } + + return &http.Client{Transport: transport}, nil +} + +// nonCachingTokenSource wraps a clientcredentials.Config to fetch a fresh +// token on every call. This is used as a fallback when the OIDC provider +// does not return expires_in, which would cause a caching TokenSource to +// hold onto a stale token forever. +type nonCachingTokenSource struct { + cfg *clientcredentials.Config + ctx context.Context +} + +func (s *nonCachingTokenSource) Token() (*oauth2.Token, error) { + return s.cfg.Token(s.ctx) +} + +// oidcTokenSource wraps a caching oauth2.TokenSource and, on the first +// successful Token() call, checks whether the provider returns an expiry. +// If not, it permanently switches to nonCachingTokenSource so that a fresh +// token is fetched every time. This avoids an eager network call at +// construction time, letting the login retry loop handle transient IdP +// outages. +type oidcTokenSource struct { + mu sync.Mutex + initialized bool + source oauth2.TokenSource + fallbackCfg *clientcredentials.Config + fallbackCtx context.Context +} + +func (s *oidcTokenSource) Token() (*oauth2.Token, error) { + s.mu.Lock() + if !s.initialized { + token, err := s.source.Token() + if err != nil { + s.mu.Unlock() + return nil, err + } + if token.Expiry.IsZero() { + s.source = &nonCachingTokenSource{cfg: s.fallbackCfg, ctx: s.fallbackCtx} + } + s.initialized = true + s.mu.Unlock() + return token, nil } + source := s.source + s.mu.Unlock() + return source.Token() } type OidcAuthProvider struct { - BaseConfig + additionalAuthScopes []v1.AuthScope - tokenGenerator *clientcredentials.Config + tokenSource oauth2.TokenSource } -func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider { +func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) { + if err := validation.ValidateOIDCClientCredentialsConfig(&cfg); err != nil { + return nil, err + } + eps := make(map[string][]string) - for k, v := range cfg.OidcAdditionalEndpointParams { + for k, v := range cfg.AdditionalEndpointParams { eps[k] = []string{v} } + if cfg.Audience != "" { + eps["audience"] = []string{cfg.Audience} + } + tokenGenerator := &clientcredentials.Config{ - ClientID: cfg.OidcClientID, - ClientSecret: cfg.OidcClientSecret, - Scopes: []string{cfg.OidcAudience}, - TokenURL: cfg.OidcTokenEndpointURL, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + Scopes: []string{cfg.Scope}, + TokenURL: cfg.TokenEndpointURL, EndpointParams: eps, } - return &OidcAuthProvider{ - BaseConfig: baseCfg, - tokenGenerator: tokenGenerator, + // Build the context that TokenSource will use for all future HTTP requests. + // context.Background() is appropriate here because the token source is + // long-lived and outlives any single request. + ctx := context.Background() + if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" { + httpClient, err := createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL) + if err != nil { + return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err) + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) } + + // Create a persistent TokenSource that caches the token and refreshes + // it before expiry. This avoids making a new HTTP request to the OIDC + // provider on every heartbeat/ping. + // + // We wrap it in an oidcTokenSource so that the first Token() call + // (deferred to SetLogin inside the login retry loop) probes whether the + // provider returns expires_in. If not, it switches to a non-caching + // source. This avoids an eager network call at construction time, which + // would prevent loopLoginUntilSuccess from retrying on transient IdP + // outages. + cachingSource := tokenGenerator.TokenSource(ctx) + + return &OidcAuthProvider{ + additionalAuthScopes: additionalAuthScopes, + tokenSource: &oidcTokenSource{ + source: cachingSource, + fallbackCfg: tokenGenerator, + fallbackCtx: ctx, + }, + }, nil } func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { - tokenObj, err := auth.tokenGenerator.Token(context.Background()) + tokenObj, err := auth.tokenSource.Token() if err != nil { return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) } @@ -127,7 +199,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) { } func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { - if !auth.AuthenticateHeartBeats { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -136,7 +208,52 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { } func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { - if !auth.AuthenticateNewWorkConns { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { + return nil + } + + newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +type OidcTokenSourceAuthProvider struct { + additionalAuthScopes []v1.AuthScope + + valueSource *v1.ValueSource +} + +func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider { + return &OidcTokenSourceAuthProvider{ + additionalAuthScopes: additionalAuthScopes, + valueSource: valueSource, + } +} + +func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) { + ctx := context.Background() + accessToken, err = auth.valueSource.Resolve(ctx) + if err != nil { + return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err) + } + return +} + +func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) { + loginMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { + return nil + } + + pingMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } @@ -144,27 +261,37 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e return err } +type TokenVerifier interface { + Verify(context.Context, string) (*oidc.IDToken, error) +} + type OidcAuthConsumer struct { - BaseConfig + additionalAuthScopes []v1.AuthScope - verifier *oidc.IDTokenVerifier - subjectFromLogin string + verifier TokenVerifier + mu sync.RWMutex + subjectsFromLogin map[string]struct{} } -func NewOidcAuthVerifier(baseCfg BaseConfig, cfg OidcServerConfig) *OidcAuthConsumer { - provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer) +func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier { + provider, err := oidc.NewProvider(context.Background(), cfg.Issuer) if err != nil { panic(err) } verifierConf := oidc.Config{ - ClientID: cfg.OidcAudience, - SkipClientIDCheck: cfg.OidcAudience == "", - SkipExpiryCheck: cfg.OidcSkipExpiryCheck, - SkipIssuerCheck: cfg.OidcSkipIssuerCheck, + ClientID: cfg.Audience, + SkipClientIDCheck: cfg.Audience == "", + SkipExpiryCheck: cfg.SkipExpiryCheck, + SkipIssuerCheck: cfg.SkipIssuerCheck, } + return provider.Verifier(&verifierConf) +} + +func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer { return &OidcAuthConsumer{ - BaseConfig: baseCfg, - verifier: provider.Verifier(&verifierConf), + additionalAuthScopes: additionalAuthScopes, + verifier: verifier, + subjectsFromLogin: make(map[string]struct{}), } } @@ -173,7 +300,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) { if err != nil { return fmt.Errorf("invalid OIDC token in login: %v", err) } - auth.subjectFromLogin = token.Subject + auth.mu.Lock() + auth.subjectsFromLogin[token.Subject] = struct{}{} + auth.mu.Unlock() return nil } @@ -182,17 +311,19 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err if err != nil { return fmt.Errorf("invalid OIDC token in ping: %v", err) } - if token.Subject != auth.subjectFromLogin { + auth.mu.RLock() + _, ok := auth.subjectsFromLogin[token.Subject] + auth.mu.RUnlock() + if !ok { return fmt.Errorf("received different OIDC subject in login and ping. "+ - "original subject: %s, "+ "new subject: %s", - auth.subjectFromLogin, token.Subject) + token.Subject) } return nil } func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) { - if !auth.AuthenticateHeartBeats { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -200,7 +331,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) { } func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { - if !auth.AuthenticateNewWorkConns { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } diff --git a/pkg/auth/oidc_test.go b/pkg/auth/oidc_test.go new file mode 100644 index 00000000000..70e5988383e --- /dev/null +++ b/pkg/auth/oidc_test.go @@ -0,0 +1,253 @@ +package auth_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/auth" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" +) + +type mockTokenVerifier struct{} + +func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.IDToken, error) { + return &oidc.IDToken{ + Subject: subject, + }, nil +} + +func TestPingWithEmptySubjectFromLoginFails(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-without-login", + Timestamp: time.Now().UnixMilli(), + }) + r.Error(err) + r.Contains(err.Error(), "received different OIDC subject in login and ping") +} + +func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyLogin(&msg.Login{ + PrivilegeKey: "ping-after-login", + }) + r.NoError(err) + + err = consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-after-login", + Timestamp: time.Now().UnixMilli(), + }) + r.NoError(err) +} + +func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyLogin(&msg.Login{ + PrivilegeKey: "login-with-first-subject", + }) + r.NoError(err) + + err = consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-with-different-subject", + Timestamp: time.Now().UnixMilli(), + }) + r.Error(err) + r.Contains(err.Error(), "received different OIDC subject in login and ping") +} + +func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) { + r := require.New(t) + + var requestCount atomic.Int32 + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + requestCount.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response + "access_token": "fresh-test-token", + "token_type": "Bearer", + }) + })) + defer tokenServer.Close() + + provider, err := auth.NewOidcAuthSetter( + []v1.AuthScope{v1.AuthScopeHeartBeats}, + v1.AuthOIDCClientConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenEndpointURL: tokenServer.URL, + }, + ) + r.NoError(err) + + // Constructor no longer fetches a token eagerly. + // The first SetLogin triggers the adaptive probe. + r.Equal(int32(0), requestCount.Load()) + + loginMsg := &msg.Login{} + err = provider.SetLogin(loginMsg) + r.NoError(err) + r.Equal("fresh-test-token", loginMsg.PrivilegeKey) + + for range 3 { + pingMsg := &msg.Ping{} + err = provider.SetPing(pingMsg) + r.NoError(err) + r.Equal("fresh-test-token", pingMsg.PrivilegeKey) + } + + // 1 probe (login) + 3 pings = 4 requests (probe doubles as the login token fetch) + r.Equal(int32(4), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing") +} + +func TestOidcAuthProviderCachesToken(t *testing.T) { + r := require.New(t) + + var requestCount atomic.Int32 + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + requestCount.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response + "access_token": "cached-test-token", + "token_type": "Bearer", + "expires_in": 3600, + }) + })) + defer tokenServer.Close() + + provider, err := auth.NewOidcAuthSetter( + []v1.AuthScope{v1.AuthScopeHeartBeats}, + v1.AuthOIDCClientConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenEndpointURL: tokenServer.URL, + }, + ) + r.NoError(err) + + // Constructor no longer fetches eagerly; first SetLogin triggers the probe. + r.Equal(int32(0), requestCount.Load()) + + // SetLogin triggers the adaptive probe and caches the token. + loginMsg := &msg.Login{} + err = provider.SetLogin(loginMsg) + r.NoError(err) + r.Equal("cached-test-token", loginMsg.PrivilegeKey) + r.Equal(int32(1), requestCount.Load()) + + // Subsequent calls should also reuse the cached token + for range 5 { + pingMsg := &msg.Ping{} + err = provider.SetPing(pingMsg) + r.NoError(err) + r.Equal("cached-test-token", pingMsg.PrivilegeKey) + } + r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused") +} + +func TestOidcAuthProviderRetriesOnInitialFailure(t *testing.T) { + r := require.New(t) + + var requestCount atomic.Int32 + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + n := requestCount.Add(1) + // The oauth2 library retries once internally, so we need two + // consecutive failures to surface an error to the caller. + if n <= 2 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "temporarily_unavailable", + "error_description": "service is starting up", + }) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response + "access_token": "retry-test-token", + "token_type": "Bearer", + "expires_in": 3600, + }) + })) + defer tokenServer.Close() + + // Constructor succeeds even though the IdP is "down". + provider, err := auth.NewOidcAuthSetter( + []v1.AuthScope{v1.AuthScopeHeartBeats}, + v1.AuthOIDCClientConfig{ + ClientID: "test-client", + ClientSecret: "test-secret", + TokenEndpointURL: tokenServer.URL, + }, + ) + r.NoError(err) + r.Equal(int32(0), requestCount.Load()) + + // First SetLogin hits the IdP, which returns an error (after internal retry). + loginMsg := &msg.Login{} + err = provider.SetLogin(loginMsg) + r.Error(err) + r.Equal(int32(2), requestCount.Load()) + + // Second SetLogin retries and succeeds. + err = provider.SetLogin(loginMsg) + r.NoError(err) + r.Equal("retry-test-token", loginMsg.PrivilegeKey) + r.Equal(int32(3), requestCount.Load()) + + // Subsequent calls use cached token. + pingMsg := &msg.Ping{} + err = provider.SetPing(pingMsg) + r.NoError(err) + r.Equal("retry-test-token", pingMsg.PrivilegeKey) + r.Equal(int32(3), requestCount.Load()) +} + +func TestNewOidcAuthSetterRejectsInvalidStaticConfig(t *testing.T) { + r := require.New(t) + tokenServer := httptest.NewServer(http.NotFoundHandler()) + defer tokenServer.Close() + + _, err := auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: "://bad", + }) + r.Error(err) + r.Contains(err.Error(), "auth.oidc.tokenEndpointURL") + + _, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + TokenEndpointURL: tokenServer.URL, + }) + r.Error(err) + r.Contains(err.Error(), "auth.oidc.clientID is required") + + _, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + AdditionalEndpointParams: map[string]string{ + "scope": "profile", + }, + }) + r.Error(err) + r.Contains(err.Error(), "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead") + + _, err = auth.NewOidcAuthSetter(nil, v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + Audience: "api", + AdditionalEndpointParams: map[string]string{"audience": "override"}, + }) + r.Error(err) + r.Contains(err.Error(), "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience") +} diff --git a/pkg/config/types_test.go b/pkg/auth/pass.go similarity index 52% rename from pkg/config/types_test.go rename to pkg/auth/pass.go index ab03dfd2a34..2eaf3f0bd70 100644 --- a/pkg/config/types_test.go +++ b/pkg/auth/pass.go @@ -1,4 +1,4 @@ -// Copyright 2019 fatedier, fatedier@gmail.com +// Copyright 2023 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. @@ -12,29 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package auth import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" + "github.com/fatedier/frp/pkg/msg" ) -type Wrap struct { - B BandwidthQuantity `json:"b"` - Int int `json:"int"` -} +var AlwaysPassVerifier = &alwaysPass{} + +var _ Verifier = &alwaysPass{} + +type alwaysPass struct{} -func TestBandwidthQuantity(t *testing.T) { - assert := assert.New(t) +func (*alwaysPass) VerifyLogin(*msg.Login) error { return nil } - var w Wrap - err := json.Unmarshal([]byte(`{"b":"1KB","int":5}`), &w) - assert.NoError(err) - assert.EqualValues(1*KB, w.B.Bytes()) +func (*alwaysPass) VerifyPing(*msg.Ping) error { return nil } - buf, err := json.Marshal(&w) - assert.NoError(err) - assert.Equal(`{"b":"1KB","int":5}`, string(buf)) -} +func (*alwaysPass) VerifyNewWorkConn(*msg.NewWorkConn) error { return nil } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 1049174dea9..9ee993f9d50 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -16,45 +16,33 @@ package auth import ( "fmt" + "slices" "time" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/util/util" ) -type TokenConfig struct { - // Token specifies the authorization token used to create keys to be sent - // to the server. The server must have a matching token for authorization - // to succeed. By default, this value is "". - Token string `ini:"token" json:"token"` -} - -func getDefaultTokenConf() TokenConfig { - return TokenConfig{ - Token: "", - } -} - type TokenAuthSetterVerifier struct { - BaseConfig - - token string + additionalAuthScopes []v1.AuthScope + token string } -func NewTokenAuth(baseCfg BaseConfig, cfg TokenConfig) *TokenAuthSetterVerifier { +func NewTokenAuth(additionalAuthScopes []v1.AuthScope, token string) *TokenAuthSetterVerifier { return &TokenAuthSetterVerifier{ - BaseConfig: baseCfg, - token: cfg.Token, + additionalAuthScopes: additionalAuthScopes, + token: token, } } -func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) (err error) { +func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error { loginMsg.PrivilegeKey = util.GetAuthKey(auth.token, loginMsg.Timestamp) return nil } func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { - if !auth.AuthenticateHeartBeats { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -64,7 +52,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { } func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { - if !auth.AuthenticateNewWorkConns { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } @@ -73,30 +61,30 @@ func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkC return nil } -func (auth *TokenAuthSetterVerifier) VerifyLogin(loginMsg *msg.Login) error { - if util.GetAuthKey(auth.token, loginMsg.Timestamp) != loginMsg.PrivilegeKey { +func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error { + if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) { return fmt.Errorf("token in login doesn't match token from configuration") } return nil } -func (auth *TokenAuthSetterVerifier) VerifyPing(pingMsg *msg.Ping) error { - if !auth.AuthenticateHeartBeats { +func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } - if util.GetAuthKey(auth.token, pingMsg.Timestamp) != pingMsg.PrivilegeKey { + if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) { return fmt.Errorf("token in heartbeat doesn't match token from configuration") } return nil } -func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { - if !auth.AuthenticateNewWorkConns { +func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } - if util.GetAuthKey(auth.token, newWorkConnMsg.Timestamp) != newWorkConnMsg.PrivilegeKey { + if !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) { return fmt.Errorf("token in NewWorkConn doesn't match token from configuration") } return nil diff --git a/pkg/config/client_test.go b/pkg/config/client_test.go deleted file mode 100644 index 12f1c9e1985..00000000000 --- a/pkg/config/client_test.go +++ /dev/null @@ -1,648 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/consts" - - "github.com/stretchr/testify/assert" -) - -const ( - testUser = "test" -) - -var ( - testClientBytesWithFull = []byte(` - # [common] is integral section - [common] - server_addr = 0.0.0.9 - server_port = 7009 - http_proxy = http://user:passwd@192.168.1.128:8080 - log_file = ./frpc.log9 - log_way = file - log_level = info9 - log_max_days = 39 - disable_log_color = false - authenticate_heartbeats = false - authenticate_new_work_conns = false - token = 12345678 - oidc_client_id = client-id - oidc_client_secret = client-secret - oidc_audience = audience - oidc_token_endpoint_url = endpoint_url - admin_addr = 127.0.0.9 - admin_port = 7409 - admin_user = admin9 - admin_pwd = admin9 - assets_dir = ./static9 - pool_count = 59 - tcp_mux - user = your_name - login_fail_exit - protocol = tcp - tls_enable = true - tls_cert_file = client.crt - tls_key_file = client.key - tls_trusted_ca_file = ca.crt - tls_server_name = example.com - dns_server = 8.8.8.9 - start = ssh,dns - heartbeat_interval = 39 - heartbeat_timeout = 99 - meta_var1 = 123 - meta_var2 = 234 - udp_packet_size = 1509 - - # all proxy - [ssh] - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - bandwidth_limit = 19MB - use_encryption - use_compression - remote_port = 6009 - group = test_group - group_key = 123456 - health_check_type = tcp - health_check_timeout_s = 3 - health_check_max_failed = 3 - health_check_interval_s = 19 - meta_var1 = 123 - meta_var2 = 234 - - [ssh_random] - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - remote_port = 9 - - [range:tcp_port] - type = tcp - local_ip = 127.0.0.9 - local_port = 6010-6011,6019 - remote_port = 6010-6011,6019 - use_encryption = false - use_compression = false - - [dns] - type = udp - local_ip = 114.114.114.114 - local_port = 59 - remote_port = 6009 - use_encryption - use_compression - - [range:udp_port] - type = udp - local_ip = 114.114.114.114 - local_port = 6000,6010-6011 - remote_port = 6000,6010-6011 - use_encryption - use_compression - - [web01] - type = http - local_ip = 127.0.0.9 - local_port = 89 - use_encryption - use_compression - http_user = admin - http_pwd = admin - subdomain = web01 - custom_domains = web02.yourdomain.com - locations = /,/pic - host_header_rewrite = example.com - header_X-From-Where = frp - health_check_type = http - health_check_url = /status - health_check_interval_s = 19 - health_check_max_failed = 3 - health_check_timeout_s = 3 - - [web02] - type = https - local_ip = 127.0.0.9 - local_port = 8009 - use_encryption - use_compression - subdomain = web01 - custom_domains = web02.yourdomain.com - proxy_protocol_version = v2 - - [secret_tcp] - type = stcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - - [p2p_tcp] - type = xtcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - - [tcpmuxhttpconnect] - type = tcpmux - multiplexer = httpconnect - local_ip = 127.0.0.1 - local_port = 10701 - custom_domains = tunnel1 - - [plugin_unix_domain_socket] - type = tcp - remote_port = 6003 - plugin = unix_domain_socket - plugin_unix_path = /var/run/docker.sock - - [plugin_http_proxy] - type = tcp - remote_port = 6004 - plugin = http_proxy - plugin_http_user = abc - plugin_http_passwd = abc - - [plugin_socks5] - type = tcp - remote_port = 6005 - plugin = socks5 - plugin_user = abc - plugin_passwd = abc - - [plugin_static_file] - type = tcp - remote_port = 6006 - plugin = static_file - plugin_local_path = /var/www/blog - plugin_strip_prefix = static - plugin_http_user = abc - plugin_http_passwd = abc - - [plugin_https2http] - type = https - custom_domains = test.yourdomain.com - plugin = https2http - plugin_local_addr = 127.0.0.1:80 - plugin_crt_path = ./server.crt - plugin_key_path = ./server.key - plugin_host_header_rewrite = 127.0.0.1 - plugin_header_X-From-Where = frp - - [plugin_http2https] - type = http - custom_domains = test.yourdomain.com - plugin = http2https - plugin_local_addr = 127.0.0.1:443 - plugin_host_header_rewrite = 127.0.0.1 - plugin_header_X-From-Where = frp - - # visitor - [secret_tcp_visitor] - role = visitor - type = stcp - server_name = secret_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9000 - use_encryption = false - use_compression = false - - [p2p_tcp_visitor] - role = visitor - type = xtcp - server_name = p2p_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9001 - use_encryption = false - use_compression = false - `) -) - -func Test_LoadClientCommonConf(t *testing.T) { - assert := assert.New(t) - - expected := ClientCommonConf{ - ClientConfig: auth.ClientConfig{ - BaseConfig: auth.BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - }, - TokenConfig: auth.TokenConfig{ - Token: "12345678", - }, - OidcClientConfig: auth.OidcClientConfig{ - OidcClientID: "client-id", - OidcClientSecret: "client-secret", - OidcAudience: "audience", - OidcTokenEndpointURL: "endpoint_url", - }, - }, - ServerAddr: "0.0.0.9", - ServerPort: 7009, - DialServerTimeout: 10, - DialServerKeepAlive: 7200, - HTTPProxy: "http://user:passwd@192.168.1.128:8080", - LogFile: "./frpc.log9", - LogWay: "file", - LogLevel: "info9", - LogMaxDays: 39, - DisableLogColor: false, - AdminAddr: "127.0.0.9", - AdminPort: 7409, - AdminUser: "admin9", - AdminPwd: "admin9", - AssetsDir: "./static9", - PoolCount: 59, - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - User: "your_name", - LoginFailExit: true, - Protocol: "tcp", - TLSEnable: true, - TLSCertFile: "client.crt", - TLSKeyFile: "client.key", - TLSTrustedCaFile: "ca.crt", - TLSServerName: "example.com", - DNSServer: "8.8.8.9", - Start: []string{"ssh", "dns"}, - HeartbeatInterval: 39, - HeartbeatTimeout: 99, - Metas: map[string]string{ - "var1": "123", - "var2": "234", - }, - UDPPacketSize: 1509, - IncludeConfigFiles: []string{}, - } - - common, err := UnmarshalClientConfFromIni(testClientBytesWithFull) - assert.NoError(err) - assert.EqualValues(expected, common) -} - -func Test_LoadClientBasicConf(t *testing.T) { - assert := assert.New(t) - - proxyExpected := map[string]ProxyConf{ - testUser + ".ssh": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".ssh", - ProxyType: consts.TCPProxy, - UseCompression: true, - UseEncryption: true, - Group: "test_group", - GroupKey: "123456", - BandwidthLimit: MustBandwidthQuantity("19MB"), - Metas: map[string]string{ - "var1": "123", - "var2": "234", - }, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.TCPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckAddr: "127.0.0.9:29", - }, - }, - RemotePort: 6009, - }, - testUser + ".ssh_random": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".ssh_random", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - }, - RemotePort: 9, - }, - testUser + ".tcp_port_0": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcp_port_0", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6010, - }, - }, - RemotePort: 6010, - }, - testUser + ".tcp_port_1": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcp_port_1", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6011, - }, - }, - RemotePort: 6011, - }, - testUser + ".tcp_port_2": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcp_port_2", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6019, - }, - }, - RemotePort: 6019, - }, - testUser + ".dns": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".dns", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 59, - }, - }, - RemotePort: 6009, - }, - testUser + ".udp_port_0": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".udp_port_0", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6000, - }, - }, - RemotePort: 6000, - }, - testUser + ".udp_port_1": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".udp_port_1", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6010, - }, - }, - RemotePort: 6010, - }, - testUser + ".udp_port_2": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".udp_port_2", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6011, - }, - }, - RemotePort: 6011, - }, - testUser + ".web01": &HTTPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".web01", - ProxyType: consts.HTTPProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 89, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.HTTPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckURL: "http://127.0.0.9:89/status", - }, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - Locations: []string{"/", "/pic"}, - HTTPUser: "admin", - HTTPPwd: "admin", - HostHeaderRewrite: "example.com", - Headers: map[string]string{ - "X-From-Where": "frp", - }, - }, - testUser + ".web02": &HTTPSProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".web02", - ProxyType: consts.HTTPSProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 8009, - }, - ProxyProtocolVersion: "v2", - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - }, - testUser + ".secret_tcp": &STCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".secret_tcp", - ProxyType: consts.STCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - }, - Role: "server", - Sk: "abcdefg", - }, - testUser + ".p2p_tcp": &XTCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".p2p_tcp", - ProxyType: consts.XTCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - }, - Role: "server", - Sk: "abcdefg", - }, - testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcpmuxhttpconnect", - ProxyType: consts.TCPMuxProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 10701, - }, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"tunnel1"}, - SubDomain: "", - }, - Multiplexer: "httpconnect", - }, - testUser + ".plugin_unix_domain_socket": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_unix_domain_socket", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "unix_domain_socket", - PluginParams: map[string]string{ - "plugin_unix_path": "/var/run/docker.sock", - }, - }, - }, - RemotePort: 6003, - }, - testUser + ".plugin_http_proxy": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_http_proxy", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "http_proxy", - PluginParams: map[string]string{ - "plugin_http_user": "abc", - "plugin_http_passwd": "abc", - }, - }, - }, - RemotePort: 6004, - }, - testUser + ".plugin_socks5": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_socks5", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "socks5", - PluginParams: map[string]string{ - "plugin_user": "abc", - "plugin_passwd": "abc", - }, - }, - }, - RemotePort: 6005, - }, - testUser + ".plugin_static_file": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_static_file", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "static_file", - PluginParams: map[string]string{ - "plugin_local_path": "/var/www/blog", - "plugin_strip_prefix": "static", - "plugin_http_user": "abc", - "plugin_http_passwd": "abc", - }, - }, - }, - RemotePort: 6006, - }, - testUser + ".plugin_https2http": &HTTPSProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_https2http", - ProxyType: consts.HTTPSProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "https2http", - PluginParams: map[string]string{ - "plugin_local_addr": "127.0.0.1:80", - "plugin_crt_path": "./server.crt", - "plugin_key_path": "./server.key", - "plugin_host_header_rewrite": "127.0.0.1", - "plugin_header_X-From-Where": "frp", - }, - }, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"test.yourdomain.com"}, - }, - }, - testUser + ".plugin_http2https": &HTTPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_http2https", - ProxyType: consts.HTTPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "http2https", - PluginParams: map[string]string{ - "plugin_local_addr": "127.0.0.1:443", - "plugin_host_header_rewrite": "127.0.0.1", - "plugin_header_X-From-Where": "frp", - }, - }, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"test.yourdomain.com"}, - }, - }, - } - - visitorExpected := map[string]VisitorConf{ - testUser + ".secret_tcp_visitor": &STCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testUser + ".secret_tcp_visitor", - ProxyType: consts.STCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testVisitorPrefix + "secret_tcp", - BindAddr: "127.0.0.1", - BindPort: 9000, - }, - }, - testUser + ".p2p_tcp_visitor": &XTCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testUser + ".p2p_tcp_visitor", - ProxyType: consts.XTCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testProxyPrefix + "p2p_tcp", - BindAddr: "127.0.0.1", - BindPort: 9001, - }, - }, - } - - proxyActual, visitorActual, err := LoadAllProxyConfsFromIni(testUser, testClientBytesWithFull, nil) - assert.NoError(err) - assert.Equal(proxyExpected, proxyActual) - assert.Equal(visitorExpected, visitorActual) -} diff --git a/pkg/config/flags.go b/pkg/config/flags.go new file mode 100644 index 00000000000..9a86dd6c5e6 --- /dev/null +++ b/pkg/config/flags.go @@ -0,0 +1,261 @@ +// Copyright 2023 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 config + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" +) + +// WordSepNormalizeFunc changes all flags that contain "_" separators +func WordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName { + if strings.Contains(name, "_") { + return pflag.NormalizedName(strings.ReplaceAll(name, "_", "-")) + } + return pflag.NormalizedName(name) +} + +type RegisterFlagOption func(*registerFlagOptions) + +type registerFlagOptions struct { + sshMode bool +} + +func WithSSHMode() RegisterFlagOption { + return func(o *registerFlagOptions) { + o.sshMode = true + } +} + +type BandwidthQuantityFlag struct { + V *types.BandwidthQuantity +} + +func (f *BandwidthQuantityFlag) Set(s string) error { + return f.V.UnmarshalString(s) +} + +func (f *BandwidthQuantityFlag) String() string { + return f.V.String() +} + +func (f *BandwidthQuantityFlag) Type() string { + return "string" +} + +func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer, opts ...RegisterFlagOption) { + registerProxyBaseConfigFlags(cmd, c.GetBaseConfig(), opts...) + + switch cc := c.(type) { + case *v1.TCPProxyConfig: + cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") + case *v1.UDPProxyConfig: + cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") + case *v1.HTTPProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations") + cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user") + cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password") + cmd.Flags().StringSliceVarP(&cc.IpsAllowList, "ips_allow_list", "", []string{}, + "lists the rules to configure which IP addresses and subnet masks can access your client (e.g. \"192.168.0.0/16,255.255.0.0\") - IPv4/IPv6 support") + cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") + case *v1.HTTPSProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + case *v1.TCPMuxProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer") + cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user") + cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password") + case *v1.STCPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users") + case *v1.SUDPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users") + case *v1.XTCPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + cmd.Flags().StringSliceVarP(&cc.AllowUsers, "allow_users", "", []string{}, "allow visitor users") + } +} + +func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig, opts ...RegisterFlagOption) { + if c == nil { + return + } + options := ®isterFlagOptions{} + for _, opt := range opts { + opt(options) + } + + cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name") + cmd.Flags().StringToStringVarP(&c.Metadatas, "metadatas", "", nil, "metadata key-value pairs (e.g., key1=value1,key2=value2)") + cmd.Flags().StringToStringVarP(&c.Annotations, "annotations", "", nil, "annotation key-value pairs (e.g., key1=value1,key2=value2)") + + if !options.sshMode { + cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip") + cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port") + cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") + cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") + cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode") + cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)") + } +} + +func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) { + if c == nil { + return + } + cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains") + cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain") +} + +func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer, opts ...RegisterFlagOption) { + registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig(), opts...) + + // add visitor flags if exist +} + +func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, _ ...RegisterFlagOption) { + if c == nil { + return + } + cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name") + cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") + cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") + cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key") + cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name") + cmd.Flags().StringVarP(&c.ServerUser, "server-user", "", "", "server user") + cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr") + cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port") +} + +func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig, opts ...RegisterFlagOption) { + options := ®isterFlagOptions{} + for _, opt := range opts { + opt(options) + } + + if !options.sshMode { + cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address") + cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port") + cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp", + fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols)) + cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path") + cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days") + cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") + cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate") + cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one") + c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls") + } + cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user") + cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance") + cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") +} + +type PortsRangeSliceFlag struct { + V *[]types.PortsRange +} + +func (f *PortsRangeSliceFlag) String() string { + if f.V == nil { + return "" + } + return types.PortsRangeSlice(*f.V).String() +} + +func (f *PortsRangeSliceFlag) Set(s string) error { + slice, err := types.NewPortsRangeSliceFromString(s) + if err != nil { + return err + } + *f.V = slice + return nil +} + +func (f *PortsRangeSliceFlag) Type() string { + return "string" +} + +type BoolFuncFlag struct { + TrueFunc func() + FalseFunc func() + + v bool +} + +func (f *BoolFuncFlag) String() string { + return strconv.FormatBool(f.v) +} + +func (f *BoolFuncFlag) Set(s string) error { + f.v = strconv.FormatBool(f.v) == "true" + + if !f.v { + if f.FalseFunc != nil { + f.FalseFunc() + } + return nil + } + + if f.TrueFunc != nil { + f.TrueFunc() + } + return nil +} + +func (f *BoolFuncFlag) Type() string { + return "bool" +} + +func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...RegisterFlagOption) { + cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address") + cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port") + cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port") + cmd.PersistentFlags().IntVarP(&c.QUICBindPort, "quic_bind_port", "", 0, "quic bind udp port") + cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") + cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") + cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") + cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") + cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address") + cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port") + cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user") + cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password") + cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file") + cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") + cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days") + cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") + cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") + cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host") + cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports") + cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") + cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only") + + webServerTLS := v1.TLSConfig{} + cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file") + cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file") + cmd.PersistentFlags().VarP(&BoolFuncFlag{ + TrueFunc: func() { c.WebServer.TLS = &webServerTLS }, + }, "dashboard_tls_mode", "", "if enable dashboard tls mode") +} diff --git a/pkg/config/README.md b/pkg/config/legacy/README.md similarity index 100% rename from pkg/config/README.md rename to pkg/config/legacy/README.md diff --git a/pkg/config/client.go b/pkg/config/legacy/client.go similarity index 83% rename from pkg/config/client.go rename to pkg/config/legacy/client.go index f3c2e41f88c..8cc02614798 100644 --- a/pkg/config/client.go +++ b/pkg/config/legacy/client.go @@ -1,4 +1,4 @@ -// Copyright 2020 The frp Authors +// Copyright 2023 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. @@ -12,25 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "fmt" "os" "path/filepath" + "slices" "strings" - "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/util/util" - "gopkg.in/ini.v1" + + legacyauth "github.com/fatedier/frp/pkg/auth/legacy" + "github.com/fatedier/frp/pkg/util/util" ) -// ClientCommonConf contains information for a client service. It is +// ClientCommonConf is the configuration parsed from ini. +// It contains information for a client service. It is // recommended to use GetDefaultClientConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ClientCommonConf struct { - auth.ClientConfig `ini:",extends"` + legacyauth.ClientConfig `ini:",extends"` // ServerAddr specifies the address of the server to connect to. By // default, this value is "0.0.0.0". @@ -38,6 +40,8 @@ type ClientCommonConf struct { // ServerPort specifies the port to connect to the server on. By default, // this value is 7000. ServerPort int `ini:"server_port" json:"server_port"` + // STUN server to help penetrate NAT hole. + NatHoleSTUNServer string `ini:"nat_hole_stun_server" json:"nat_hole_stun_server"` // The maximum amount of time a dial to server will wait for a connect to complete. DialServerTimeout int64 `ini:"dial_server_timeout" json:"dial_server_timeout"` // DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. @@ -95,7 +99,7 @@ type ClientCommonConf struct { // the server must have TCP multiplexing enabled as well. By default, this // value is true. TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` - // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"` // User specifies a prefix for proxy names to distinguish them from other @@ -113,14 +117,19 @@ type ClientCommonConf struct { // all supplied proxies are enabled. By default, this value is an empty // set. Start []string `ini:"start" json:"start"` - //Start map[string]struct{} `json:"start"` + // Start map[string]struct{} `json:"start"` // Protocol specifies the protocol to use when interacting with the server. - // Valid values are "tcp", "kcp" and "websocket". By default, this value + // Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value // is "tcp". Protocol string `ini:"protocol" json:"protocol"` + // QUIC protocol options + QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period"` + QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"` + QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"` // TLSEnable specifies whether or not TLS should be used when communicating // with the server. If "tls_cert_file" and "tls_key_file" are valid, // client will load the supplied tls configuration. + // Since v0.50.0, the default value has been changed to true, and tls is enabled by default. TLSEnable bool `ini:"tls_enable" json:"tls_enable"` // TLSCertPath specifies the path of the cert file that client will // load. It only works when "tls_enable" is true and "tls_key_file" is valid. @@ -136,8 +145,9 @@ type ClientCommonConf struct { // TLSServerName specifies the custom server name of tls certificate. By // default, server name if same to ServerAddr. TLSServerName string `ini:"tls_server_name" json:"tls_server_name"` - // By default, frpc will connect frps with first custom byte if tls is enabled. - // If DisableCustomTLSFirstByte is true, frpc will not send that custom byte. + // If the disable_custom_tls_first_byte is set to false, frpc will establish a connection with frps using the + // first custom byte when tls is enabled. + // Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default. DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"` // HeartBeatInterval specifies at what interval heartbeats are sent to the // server, in seconds. It is not recommended to change this value. By @@ -159,97 +169,8 @@ type ClientCommonConf struct { PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"` } -// GetDefaultClientConf returns a client configuration with default values. -func GetDefaultClientConf() ClientCommonConf { - return ClientCommonConf{ - ClientConfig: auth.GetDefaultClientConf(), - ServerAddr: "0.0.0.0", - ServerPort: 7000, - DialServerTimeout: 10, - DialServerKeepAlive: 7200, - HTTPProxy: os.Getenv("http_proxy"), - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - DisableLogColor: false, - AdminAddr: "127.0.0.1", - AdminPort: 0, - AdminUser: "", - AdminPwd: "", - AssetsDir: "", - PoolCount: 1, - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - User: "", - DNSServer: "", - LoginFailExit: true, - Start: make([]string, 0), - Protocol: "tcp", - TLSEnable: false, - TLSCertFile: "", - TLSKeyFile: "", - TLSTrustedCaFile: "", - HeartbeatInterval: 30, - HeartbeatTimeout: 90, - Metas: make(map[string]string), - UDPPacketSize: 1500, - IncludeConfigFiles: make([]string, 0), - PprofEnable: false, - } -} - -func (cfg *ClientCommonConf) Complete() { - if cfg.LogFile == "console" { - cfg.LogWay = "console" - } else { - cfg.LogWay = "file" - } -} - -func (cfg *ClientCommonConf) Validate() error { - if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 { - if cfg.HeartbeatTimeout < cfg.HeartbeatInterval { - return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") - } - } - - if cfg.TLSEnable == false { - if cfg.Protocol == "wss" { - return fmt.Errorf("tls_enable must be true for wss support") - } - - if cfg.TLSCertFile != "" { - fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false") - } - - if cfg.TLSKeyFile != "" { - fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false") - } - - if cfg.TLSTrustedCaFile != "" { - fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false") - } - } - - if cfg.Protocol != "tcp" && cfg.Protocol != "kcp" && cfg.Protocol != "websocket" && cfg.Protocol != "wss" { - return fmt.Errorf("invalid protocol") - } - - for _, f := range cfg.IncludeConfigFiles { - absDir, err := filepath.Abs(filepath.Dir(f)) - if err != nil { - return fmt.Errorf("include: parse directory of %s failed: %v", f, absDir) - } - if _, err := os.Stat(absDir); os.IsNotExist(err) { - return fmt.Errorf("include: directory of %s not exist", f) - } - } - return nil -} - // Supported sources including: string(file path), []byte, Reader interface. -func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { +func UnmarshalClientConfFromIni(source any) (ClientCommonConf, error) { f, err := ini.LoadSources(ini.LoadOptions{ Insensitive: false, InsensitiveSections: false, @@ -273,7 +194,7 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { } common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_") - common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_") + common.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_") return common, nil } @@ -282,10 +203,9 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { // otherwise just start proxies in startProxy map func LoadAllProxyConfsFromIni( prefix string, - source interface{}, + source any, start []string, ) (map[string]ProxyConf, map[string]VisitorConf, error) { - f, err := ini.LoadSources(ini.LoadOptions{ Insensitive: false, InsensitiveSections: false, @@ -309,10 +229,7 @@ func LoadAllProxyConfsFromIni( startProxy[s] = struct{}{} } - startAll := true - if len(startProxy) > 0 { - startAll = false - } + startAll := len(startProxy) == 0 // Build template sections from range section And append to ini.File. rangeSections := make([]*ini.Section, 0) @@ -359,7 +276,7 @@ func LoadAllProxyConfsFromIni( case "visitor": newConf, newErr := NewVisitorConfFromIni(prefix, name, section) if newErr != nil { - return nil, nil, newErr + return nil, nil, fmt.Errorf("failed to parse visitor %s, err: %v", name, newErr) } visitorConfs[prefix+name] = newConf default: @@ -370,7 +287,6 @@ func LoadAllProxyConfsFromIni( } func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error { - // Validation localPortStr := section.Key("local_port").String() remotePortStr := section.Key("remote_port").String() @@ -408,8 +324,12 @@ func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error { } copySection(section, tmpsection) - tmpsection.NewKey("local_port", fmt.Sprintf("%d", localPorts[i])) - tmpsection.NewKey("remote_port", fmt.Sprintf("%d", remotePorts[i])) + if _, err := tmpsection.NewKey("local_port", fmt.Sprintf("%d", localPorts[i])); err != nil { + return fmt.Errorf("local_port new key in section error: %v", err) + } + if _, err := tmpsection.NewKey("remote_port", fmt.Sprintf("%d", remotePorts[i])); err != nil { + return fmt.Errorf("remote_port new key in section error: %v", err) + } } return nil @@ -417,6 +337,61 @@ func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error { func copySection(source, target *ini.Section) { for key, value := range source.KeysHash() { - target.NewKey(key, value) + _, _ = target.NewKey(key, value) + } +} + +// GetDefaultClientConf returns a client configuration with default values. +// Note: Some default values here will be set to empty and will be converted to them +// new configuration through the 'Complete' function to set them as the default +// values of the new configuration. +func GetDefaultClientConf() ClientCommonConf { + return ClientCommonConf{ + ClientConfig: legacyauth.GetDefaultClientConf(), + TCPMux: true, + LoginFailExit: true, + Protocol: "tcp", + Start: make([]string, 0), + TLSEnable: true, + DisableCustomTLSFirstByte: true, + Metas: make(map[string]string), + IncludeConfigFiles: make([]string, 0), + } +} + +func (cfg *ClientCommonConf) Validate() error { + if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 { + if cfg.HeartbeatTimeout < cfg.HeartbeatInterval { + return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") + } } + + if !cfg.TLSEnable { + if cfg.TLSCertFile != "" { + fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false") + } + + if cfg.TLSKeyFile != "" { + fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false") + } + + if cfg.TLSTrustedCaFile != "" { + fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false") + } + } + + if !slices.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) { + return fmt.Errorf("invalid protocol") + } + + for _, f := range cfg.IncludeConfigFiles { + absDir, err := filepath.Abs(filepath.Dir(f)) + if err != nil { + return fmt.Errorf("include: parse directory of %s failed: %v", f, err) + } + if _, err := os.Stat(absDir); os.IsNotExist(err) { + return fmt.Errorf("include: directory of %s not exist", f) + } + } + return nil } diff --git a/pkg/config/legacy/conversion.go b/pkg/config/legacy/conversion.go new file mode 100644 index 00000000000..81b8f5f42d3 --- /dev/null +++ b/pkg/config/legacy/conversion.go @@ -0,0 +1,355 @@ +// Copyright 2023 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 legacy + +import ( + "strings" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig { + out := &v1.ClientCommonConfig{} + out.User = conf.User + out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod) + out.Auth.Token = conf.Token + if conf.AuthenticateHeartBeats { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) + } + if conf.AuthenticateNewWorkConns { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) + } + out.Auth.OIDC.ClientID = conf.OidcClientID + out.Auth.OIDC.ClientSecret = conf.OidcClientSecret + out.Auth.OIDC.Audience = conf.OidcAudience + out.Auth.OIDC.Scope = conf.OidcScope + out.Auth.OIDC.TokenEndpointURL = conf.OidcTokenEndpointURL + out.Auth.OIDC.AdditionalEndpointParams = conf.OidcAdditionalEndpointParams + + out.ServerAddr = conf.ServerAddr + out.ServerPort = conf.ServerPort + out.NatHoleSTUNServer = conf.NatHoleSTUNServer + out.Transport.DialServerTimeout = conf.DialServerTimeout + out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive + out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP + out.Transport.ProxyURL = conf.HTTPProxy + out.Transport.PoolCount = conf.PoolCount + out.Transport.TCPMux = lo.ToPtr(conf.TCPMux) + out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval + out.Transport.Protocol = conf.Protocol + out.Transport.HeartbeatInterval = conf.HeartbeatInterval + out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout + out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod + out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout + out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams + out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable) + out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte) + out.Transport.TLS.CertFile = conf.TLSCertFile + out.Transport.TLS.KeyFile = conf.TLSKeyFile + out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile + out.Transport.TLS.ServerName = conf.TLSServerName + + out.Log.To = conf.LogFile + out.Log.Level = conf.LogLevel + out.Log.MaxDays = conf.LogMaxDays + out.Log.DisablePrintColor = conf.DisableLogColor + + out.WebServer.Addr = conf.AdminAddr + out.WebServer.Port = conf.AdminPort + out.WebServer.User = conf.AdminUser + out.WebServer.Password = conf.AdminPwd + out.WebServer.AssetsDir = conf.AssetsDir + out.WebServer.PprofEnable = conf.PprofEnable + + out.DNSServer = conf.DNSServer + out.LoginFailExit = lo.ToPtr(conf.LoginFailExit) + out.Start = conf.Start + out.UDPPacketSize = conf.UDPPacketSize + out.Metadatas = conf.Metas + out.IncludeConfigFiles = conf.IncludeConfigFiles + return out +} + +func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig { + out := &v1.ServerConfig{} + out.Auth.Method = v1.AuthMethod(conf.AuthenticationMethod) + out.Auth.Token = conf.Token + if conf.AuthenticateHeartBeats { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) + } + if conf.AuthenticateNewWorkConns { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) + } + out.Auth.OIDC.Audience = conf.OidcAudience + out.Auth.OIDC.Issuer = conf.OidcIssuer + out.Auth.OIDC.SkipExpiryCheck = conf.OidcSkipExpiryCheck + out.Auth.OIDC.SkipIssuerCheck = conf.OidcSkipIssuerCheck + + out.BindAddr = conf.BindAddr + out.BindPort = conf.BindPort + out.KCPBindPort = conf.KCPBindPort + out.QUICBindPort = conf.QUICBindPort + out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod + out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout + out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams + + out.ProxyBindAddr = conf.ProxyBindAddr + out.VhostHTTPPort = conf.VhostHTTPPort + out.VhostHTTPSPort = conf.VhostHTTPSPort + out.TCPMuxHTTPConnectPort = conf.TCPMuxHTTPConnectPort + out.TCPMuxPassthrough = conf.TCPMuxPassthrough + out.VhostHTTPTimeout = conf.VhostHTTPTimeout + + out.WebServer.Addr = conf.DashboardAddr + out.WebServer.Port = conf.DashboardPort + out.WebServer.User = conf.DashboardUser + out.WebServer.Password = conf.DashboardPwd + out.WebServer.AssetsDir = conf.AssetsDir + if conf.DashboardTLSMode { + out.WebServer.TLS = &v1.TLSConfig{} + out.WebServer.TLS.CertFile = conf.DashboardTLSCertFile + out.WebServer.TLS.KeyFile = conf.DashboardTLSKeyFile + out.WebServer.PprofEnable = conf.PprofEnable + } + + out.EnablePrometheus = conf.EnablePrometheus + + out.Log.To = conf.LogFile + out.Log.Level = conf.LogLevel + out.Log.MaxDays = conf.LogMaxDays + out.Log.DisablePrintColor = conf.DisableLogColor + + out.DetailedErrorsToClient = lo.ToPtr(conf.DetailedErrorsToClient) + out.SubDomainHost = conf.SubDomainHost + out.Custom404Page = conf.Custom404Page + out.UserConnTimeout = conf.UserConnTimeout + out.UDPPacketSize = conf.UDPPacketSize + out.NatHoleAnalysisDataReserveHours = conf.NatHoleAnalysisDataReserveHours + + out.Transport.TCPMux = lo.ToPtr(conf.TCPMux) + out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval + out.Transport.TCPKeepAlive = conf.TCPKeepAlive + out.Transport.MaxPoolCount = conf.MaxPoolCount + out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout + + out.Transport.TLS.Force = conf.TLSOnly + out.Transport.TLS.CertFile = conf.TLSCertFile + out.Transport.TLS.KeyFile = conf.TLSKeyFile + out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile + + out.MaxPortsPerClient = conf.MaxPortsPerClient + + for _, v := range conf.HTTPPlugins { + out.HTTPPlugins = append(out.HTTPPlugins, v1.HTTPPluginOptions{ + Name: v.Name, + Addr: v.Addr, + Path: v.Path, + Ops: v.Ops, + TLSVerify: v.TLSVerify, + }) + } + + out.AllowPorts, _ = types.NewPortsRangeSliceFromString(conf.AllowPortsStr) + return out +} + +func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations { + out := v1.HeaderOperations{} + for k, v := range params { + k, ok := strings.CutPrefix(k, "plugin_header_") + if !ok || k == "" { + continue + } + if out.Set == nil { + out.Set = make(map[string]string) + } + out.Set[k] = v + } + return out +} + +func Convert_ProxyConf_To_v1_Base(conf ProxyConf) *v1.ProxyBaseConfig { + out := &v1.ProxyBaseConfig{} + base := conf.GetBaseConfig() + + out.Name = base.ProxyName + out.Type = base.ProxyType + out.Metadatas = base.Metas + + out.Transport.UseEncryption = base.UseEncryption + out.Transport.UseCompression = base.UseCompression + out.Transport.BandwidthLimit = base.BandwidthLimit + out.Transport.BandwidthLimitMode = base.BandwidthLimitMode + out.Transport.ProxyProtocolVersion = base.ProxyProtocolVersion + + out.LoadBalancer.Group = base.Group + out.LoadBalancer.GroupKey = base.GroupKey + + out.HealthCheck.Type = base.HealthCheckType + out.HealthCheck.TimeoutSeconds = base.HealthCheckTimeoutS + out.HealthCheck.MaxFailed = base.HealthCheckMaxFailed + out.HealthCheck.IntervalSeconds = base.HealthCheckIntervalS + out.HealthCheck.Path = base.HealthCheckURL + + out.LocalIP = base.LocalIP + out.LocalPort = base.LocalPort + + switch base.Plugin { + case "http2https": + out.Plugin.ClientPluginOptions = &v1.HTTP2HTTPSPluginOptions{ + LocalAddr: base.PluginParams["plugin_local_addr"], + HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"], + RequestHeaders: transformHeadersFromPluginParams(base.PluginParams), + } + case "http_proxy": + out.Plugin.ClientPluginOptions = &v1.HTTPProxyPluginOptions{ + HTTPUser: base.PluginParams["plugin_http_user"], + HTTPPassword: base.PluginParams["plugin_http_passwd"], + } + case "https2http": + out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPPluginOptions{ + LocalAddr: base.PluginParams["plugin_local_addr"], + HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"], + RequestHeaders: transformHeadersFromPluginParams(base.PluginParams), + CrtPath: base.PluginParams["plugin_crt_path"], + KeyPath: base.PluginParams["plugin_key_path"], + } + case "https2https": + out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPSPluginOptions{ + LocalAddr: base.PluginParams["plugin_local_addr"], + HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"], + RequestHeaders: transformHeadersFromPluginParams(base.PluginParams), + CrtPath: base.PluginParams["plugin_crt_path"], + KeyPath: base.PluginParams["plugin_key_path"], + } + case "socks5": + out.Plugin.ClientPluginOptions = &v1.Socks5PluginOptions{ + Username: base.PluginParams["plugin_user"], + Password: base.PluginParams["plugin_passwd"], + } + case "static_file": + out.Plugin.ClientPluginOptions = &v1.StaticFilePluginOptions{ + LocalPath: base.PluginParams["plugin_local_path"], + StripPrefix: base.PluginParams["plugin_strip_prefix"], + HTTPUser: base.PluginParams["plugin_http_user"], + HTTPPassword: base.PluginParams["plugin_http_passwd"], + } + case "unix_domain_socket": + out.Plugin.ClientPluginOptions = &v1.UnixDomainSocketPluginOptions{ + UnixPath: base.PluginParams["plugin_unix_path"], + } + } + out.Plugin.Type = base.Plugin + return out +} + +func Convert_ProxyConf_To_v1(conf ProxyConf) v1.ProxyConfigurer { + outBase := Convert_ProxyConf_To_v1_Base(conf) + var out v1.ProxyConfigurer + switch v := conf.(type) { + case *TCPProxyConf: + c := &v1.TCPProxyConfig{ProxyBaseConfig: *outBase} + c.RemotePort = v.RemotePort + out = c + case *UDPProxyConf: + c := &v1.UDPProxyConfig{ProxyBaseConfig: *outBase} + c.RemotePort = v.RemotePort + out = c + case *HTTPProxyConf: + c := &v1.HTTPProxyConfig{ProxyBaseConfig: *outBase} + c.CustomDomains = v.CustomDomains + c.SubDomain = v.SubDomain + c.Locations = v.Locations + c.HTTPUser = v.HTTPUser + c.HTTPPassword = v.HTTPPwd + c.IpsAllowList = v.IpsAllowList + c.HostHeaderRewrite = v.HostHeaderRewrite + c.RequestHeaders.Set = v.Headers + c.RouteByHTTPUser = v.RouteByHTTPUser + out = c + case *HTTPSProxyConf: + c := &v1.HTTPSProxyConfig{ProxyBaseConfig: *outBase} + c.CustomDomains = v.CustomDomains + c.SubDomain = v.SubDomain + out = c + case *TCPMuxProxyConf: + c := &v1.TCPMuxProxyConfig{ProxyBaseConfig: *outBase} + c.CustomDomains = v.CustomDomains + c.SubDomain = v.SubDomain + c.HTTPUser = v.HTTPUser + c.HTTPPassword = v.HTTPPwd + c.RouteByHTTPUser = v.RouteByHTTPUser + c.Multiplexer = v.Multiplexer + out = c + case *STCPProxyConf: + c := &v1.STCPProxyConfig{ProxyBaseConfig: *outBase} + c.Secretkey = v.Sk + c.AllowUsers = v.AllowUsers + out = c + case *SUDPProxyConf: + c := &v1.SUDPProxyConfig{ProxyBaseConfig: *outBase} + c.Secretkey = v.Sk + c.AllowUsers = v.AllowUsers + out = c + case *XTCPProxyConf: + c := &v1.XTCPProxyConfig{ProxyBaseConfig: *outBase} + c.Secretkey = v.Sk + c.AllowUsers = v.AllowUsers + out = c + } + return out +} + +func Convert_VisitorConf_To_v1_Base(conf VisitorConf) *v1.VisitorBaseConfig { + out := &v1.VisitorBaseConfig{} + base := conf.GetBaseConfig() + + out.Name = base.ProxyName + out.Type = base.ProxyType + out.Transport.UseEncryption = base.UseEncryption + out.Transport.UseCompression = base.UseCompression + out.SecretKey = base.Sk + out.ServerUser = base.ServerUser + out.ServerName = base.ServerName + out.BindAddr = base.BindAddr + out.BindPort = base.BindPort + return out +} + +func Convert_VisitorConf_To_v1(conf VisitorConf) v1.VisitorConfigurer { + outBase := Convert_VisitorConf_To_v1_Base(conf) + var out v1.VisitorConfigurer + switch v := conf.(type) { + case *STCPVisitorConf: + c := &v1.STCPVisitorConfig{VisitorBaseConfig: *outBase} + out = c + case *SUDPVisitorConf: + c := &v1.SUDPVisitorConfig{VisitorBaseConfig: *outBase} + out = c + case *XTCPVisitorConf: + c := &v1.XTCPVisitorConfig{VisitorBaseConfig: *outBase} + c.Protocol = v.Protocol + c.KeepTunnelOpen = v.KeepTunnelOpen + c.MaxRetriesAnHour = v.MaxRetriesAnHour + c.MinRetryInterval = v.MinRetryInterval + c.FallbackTo = v.FallbackTo + c.FallbackTimeoutMs = v.FallbackTimeoutMs + out = c + } + return out +} diff --git a/pkg/config/parse.go b/pkg/config/legacy/parse.go similarity index 91% rename from pkg/config/parse.go rename to pkg/config/legacy/parse.go index 7767981e32c..80850f6f957 100644 --- a/pkg/config/parse.go +++ b/pkg/config/legacy/parse.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "bytes" @@ -23,7 +23,7 @@ import ( func ParseClientConfig(filePath string) ( cfg ClientCommonConf, - pxyCfgs map[string]ProxyConf, + proxyCfgs map[string]ProxyConf, visitorCfgs map[string]VisitorConf, err error, ) { @@ -40,9 +40,8 @@ func ParseClientConfig(filePath string) ( if err != nil { return } - cfg.Complete() if err = cfg.Validate(); err != nil { - err = fmt.Errorf("Parse config error: %v", err) + err = fmt.Errorf("parse config error: %v", err) return } @@ -57,7 +56,7 @@ func ParseClientConfig(filePath string) ( configBuffer.Write(buf) // Parse all proxy and visitor configs. - pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start) + proxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start) if err != nil { return } diff --git a/pkg/config/legacy/proxy.go b/pkg/config/legacy/proxy.go new file mode 100644 index 00000000000..14b7710e8b7 --- /dev/null +++ b/pkg/config/legacy/proxy.go @@ -0,0 +1,388 @@ +// Copyright 2023 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 legacy + +import ( + "fmt" + "reflect" + + "gopkg.in/ini.v1" + + "github.com/fatedier/frp/pkg/config/types" +) + +type ProxyType string + +const ( + ProxyTypeTCP ProxyType = "tcp" + ProxyTypeUDP ProxyType = "udp" + ProxyTypeTCPMUX ProxyType = "tcpmux" + ProxyTypeHTTP ProxyType = "http" + ProxyTypeHTTPS ProxyType = "https" + ProxyTypeSTCP ProxyType = "stcp" + ProxyTypeXTCP ProxyType = "xtcp" + ProxyTypeSUDP ProxyType = "sudp" +) + +// Proxy +var ( + proxyConfTypeMap = map[ProxyType]reflect.Type{ + ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](), + ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](), + ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](), + ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](), + ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](), + ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](), + ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](), + ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](), + } +) + +type ProxyConf interface { + // GetBaseConfig returns the BaseProxyConf for this config. + GetBaseConfig() *BaseProxyConf + // UnmarshalFromIni unmarshals a ini.Section into this config. This function + // will be called on the frpc side. + UnmarshalFromIni(string, string, *ini.Section) error +} + +func NewConfByType(proxyType ProxyType) ProxyConf { + v, ok := proxyConfTypeMap[proxyType] + if !ok { + return nil + } + cfg := reflect.New(v).Interface().(ProxyConf) + return cfg +} + +// Proxy Conf Loader +// DefaultProxyConf creates a empty ProxyConf object by proxyType. +// If proxyType doesn't exist, return nil. +func DefaultProxyConf(proxyType ProxyType) ProxyConf { + return NewConfByType(proxyType) +} + +// Proxy loaded from ini +func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) { + // section.Key: if key not exists, section will set it with default value. + proxyType := ProxyType(section.Key("type").String()) + if proxyType == "" { + proxyType = ProxyTypeTCP + } + + conf := DefaultProxyConf(proxyType) + if conf == nil { + return nil, fmt.Errorf("invalid type [%s]", proxyType) + } + + if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { + return nil, err + } + return conf, nil +} + +// LocalSvrConf configures what location the client will to, or what +// plugin will be used. +type LocalSvrConf struct { + // LocalIP specifies the IP address or host name to to. + LocalIP string `ini:"local_ip" json:"local_ip"` + // LocalPort specifies the port to to. + LocalPort int `ini:"local_port" json:"local_port"` + + // Plugin specifies what plugin should be used for ng. If this value + // is set, the LocalIp and LocalPort values will be ignored. By default, + // this value is "". + Plugin string `ini:"plugin" json:"plugin"` + // PluginParams specify parameters to be passed to the plugin, if one is + // being used. By default, this value is an empty map. + PluginParams map[string]string `ini:"-"` +} + +// HealthCheckConf configures health checking. This can be useful for load +// balancing purposes to detect and remove proxies to failing services. +type HealthCheckConf struct { + // HealthCheckType specifies what protocol to use for health checking. + // Valid values include "tcp", "http", and "". If this value is "", health + // checking will not be performed. By default, this value is "". + // + // If the type is "tcp", a connection will be attempted to the target + // server. If a connection cannot be established, the health check fails. + // + // If the type is "http", a GET request will be made to the endpoint + // specified by HealthCheckURL. If the response is not a 200, the health + // check fails. + HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http + // HealthCheckTimeoutS specifies the number of seconds to wait for a health + // check attempt to connect. If the timeout is reached, this counts as a + // health check failure. By default, this value is 3. + HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"` + // HealthCheckMaxFailed specifies the number of allowed failures before the + // is stopped. By default, this value is 1. + HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"` + // HealthCheckIntervalS specifies the time in seconds between health + // checks. By default, this value is 10. + HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"` + // HealthCheckURL specifies the address to send health checks to if the + // health check type is "http". + HealthCheckURL string `ini:"health_check_url" json:"health_check_url"` + // HealthCheckAddr specifies the address to connect to if the health check + // type is "tcp". + HealthCheckAddr string `ini:"-"` +} + +// BaseProxyConf provides configuration info that is common to all types. +type BaseProxyConf struct { + // ProxyName is the name of this + ProxyName string `ini:"name" json:"name"` + // ProxyType specifies the type of this Valid values include "tcp", + // "udp", "http", "https", "stcp", and "xtcp". By default, this value is + // "tcp". + ProxyType string `ini:"type" json:"type"` + + // UseEncryption controls whether or not communication with the server will + // be encrypted. Encryption is done using the tokens supplied in the server + // and client configuration. By default, this value is false. + UseEncryption bool `ini:"use_encryption" json:"use_encryption"` + // UseCompression controls whether or not communication with the server + // will be compressed. By default, this value is false. + UseCompression bool `ini:"use_compression" json:"use_compression"` + // Group specifies which group the is a part of. The server will use + // this information to load balance proxies in the same group. If the value + // is "", this will not be in a group. By default, this value is "". + Group string `ini:"group" json:"group"` + // GroupKey specifies a group key, which should be the same among proxies + // of the same group. By default, this value is "". + GroupKey string `ini:"group_key" json:"group_key"` + + // ProxyProtocolVersion specifies which protocol version to use. Valid + // values include "v1", "v2", and "". If the value is "", a protocol + // version will be automatically selected. By default, this value is "". + ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"` + + // BandwidthLimit limit the bandwidth + // 0 means no limit + BandwidthLimit types.BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"` + // BandwidthLimitMode specifies whether to limit the bandwidth on the + // client or server side. Valid values include "client" and "server". + // By default, this value is "client". + BandwidthLimitMode string `ini:"bandwidth_limit_mode" json:"bandwidth_limit_mode"` + + // meta info for each proxy + Metas map[string]string `ini:"-" json:"metas"` + + LocalSvrConf `ini:",extends"` + HealthCheckConf `ini:",extends"` +} + +// Base +func (cfg *BaseProxyConf) GetBaseConfig() *BaseProxyConf { + return cfg +} + +// BaseProxyConf apply custom logic changes. +func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section) error { + cfg.ProxyName = name + // metas_xxx + cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_") + + // bandwidth_limit + if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil { + cfg.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidth.String()) + if err != nil { + return err + } + } + + // plugin_xxx + cfg.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") + return nil +} + +type DomainConf struct { + CustomDomains []string `ini:"custom_domains" json:"custom_domains"` + SubDomain string `ini:"subdomain" json:"subdomain"` +} + +type RoleServerCommonConf struct { + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` + AllowUsers []string `ini:"allow_users" json:"allow_users"` +} + +// HTTP +type HTTPProxyConf struct { + BaseProxyConf `ini:",extends"` + DomainConf `ini:",extends"` + + Locations []string `ini:"locations" json:"locations"` + HTTPUser string `ini:"http_user" json:"http_user"` + HTTPPwd string `ini:"http_pwd" json:"http_pwd"` + IpsAllowList []string `ini:"ips_allow_list" json:"ips_allow_list"` + HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"` + Headers map[string]string `ini:"-" json:"headers"` + RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` +} + +func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_") + return nil +} + +// HTTPS +type HTTPSProxyConf struct { + BaseProxyConf `ini:",extends"` + DomainConf `ini:",extends"` +} + +func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + return nil +} + +// TCP +type TCPProxyConf struct { + BaseProxyConf `ini:",extends"` + RemotePort int `ini:"remote_port" json:"remote_port"` +} + +func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + +// UDP +type UDPProxyConf struct { + BaseProxyConf `ini:",extends"` + + RemotePort int `ini:"remote_port" json:"remote_port"` +} + +func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + +// TCPMux +type TCPMuxProxyConf struct { + BaseProxyConf `ini:",extends"` + DomainConf `ini:",extends"` + HTTPUser string `ini:"http_user" json:"http_user,omitempty"` + HTTPPwd string `ini:"http_pwd" json:"http_pwd,omitempty"` + RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` + + Multiplexer string `ini:"multiplexer"` +} + +func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + +// STCP +type STCPProxyConf struct { + BaseProxyConf `ini:",extends"` + RoleServerCommonConf `ini:",extends"` +} + +func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + if cfg.Role == "" { + cfg.Role = "server" + } + return nil +} + +// XTCP +type XTCPProxyConf struct { + BaseProxyConf `ini:",extends"` + RoleServerCommonConf `ini:",extends"` +} + +func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + if cfg.Role == "" { + cfg.Role = "server" + } + return nil +} + +// SUDP +type SUDPProxyConf struct { + BaseProxyConf `ini:",extends"` + RoleServerCommonConf `ini:",extends"` +} + +func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + return nil +} + +func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error { + err := section.MapTo(cfg) + if err != nil { + return err + } + + err = cfg.GetBaseConfig().decorate(prefix, name, section) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/config/server.go b/pkg/config/legacy/server.go similarity index 74% rename from pkg/config/server.go rename to pkg/config/legacy/server.go index 50eb3054c3f..1cfa1bdca7f 100644 --- a/pkg/config/server.go +++ b/pkg/config/legacy/server.go @@ -1,4 +1,4 @@ -// Copyright 2020 The frp Authors +// Copyright 2023 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. @@ -12,56 +12,64 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( - "fmt" "strings" - "github.com/fatedier/frp/pkg/auth" - plugin "github.com/fatedier/frp/pkg/plugin/server" - "github.com/fatedier/frp/pkg/util/util" - - "github.com/go-playground/validator/v10" "gopkg.in/ini.v1" + + legacyauth "github.com/fatedier/frp/pkg/auth/legacy" ) +type HTTPPluginOptions struct { + Name string `ini:"name"` + Addr string `ini:"addr"` + Path string `ini:"path"` + Ops []string `ini:"ops"` + TLSVerify bool `ini:"tlsVerify"` +} + // ServerCommonConf contains information for a server service. It is // recommended to use GetDefaultServerConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ServerCommonConf struct { - auth.ServerConfig `ini:",extends"` + legacyauth.ServerConfig `ini:",extends"` // BindAddr specifies the address that the server binds to. By default, // this value is "0.0.0.0". BindAddr string `ini:"bind_addr" json:"bind_addr"` // BindPort specifies the port that the server listens on. By default, this // value is 7000. - BindPort int `ini:"bind_port" json:"bind_port" validate:"gte=0,lte=65535"` - // BindUDPPort specifies the UDP port that the server listens on. If this - // value is 0, the server will not listen for UDP connections. By default, - // this value is 0 - BindUDPPort int `ini:"bind_udp_port" json:"bind_udp_port" validate:"gte=0,lte=65535"` + BindPort int `ini:"bind_port" json:"bind_port"` // KCPBindPort specifies the KCP port that the server listens on. If this // value is 0, the server will not listen for KCP connections. By default, // this value is 0. - KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"` + KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port"` + // QUICBindPort specifies the QUIC port that the server listens on. + // Set this value to 0 will disable this feature. + // By default, the value is 0. + QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port"` + // QUIC protocol options + QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period"` + QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"` + QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"` // ProxyBindAddr specifies the address that the proxy binds to. This value // may be the same as BindAddr. ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"` // VhostHTTPPort specifies the port that the server listens for HTTP Vhost // requests. If this value is 0, the server will not listen for HTTP // requests. By default, this value is 0. - VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port" validate:"gte=0,lte=65535"` + VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port"` // VhostHTTPSPort specifies the port that the server listens for HTTPS // Vhost requests. If this value is 0, the server will not listen for HTTPS // requests. By default, this value is 0. - VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port" validate:"gte=0,lte=65535"` + VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port"` // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP // requests on one single port. If it's not - it will listen on this value for // HTTP CONNECT requests. By default, this value is 0. - TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"` + TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port"` // If TCPMuxPassthrough is true, frps won't do any update on traffic. TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"` // VhostHTTPTimeout specifies the response header timeout for the Vhost @@ -73,7 +81,7 @@ type ServerCommonConf struct { // DashboardPort specifies the port that the dashboard listens on. If this // value is 0, the dashboard will not be started. By default, this value is // 0. - DashboardPort int `ini:"dashboard_port" json:"dashboard_port" validate:"gte=0,lte=65535"` + DashboardPort int `ini:"dashboard_port" json:"dashboard_port"` // DashboardTLSCertFile specifies the path of the cert file that the server will // load. If "dashboard_tls_cert_file", "dashboard_tls_key_file" are valid, the server will use this // supplied tls configuration. @@ -131,7 +139,7 @@ type ServerCommonConf struct { // from a client to share a single TCP connection. By default, this value // is true. TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` - // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"` // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. @@ -146,6 +154,8 @@ type ServerCommonConf struct { // If the length of this value is 0, all ports are allowed. By default, // this value is an empty set. AllowPorts map[int]struct{} `ini:"-" json:"-"` + // Original string. + AllowPortsStr string `ini:"-" json:"-"` // MaxPoolCount specifies the maximum pool size for each proxy. By default, // this value is 5. MaxPoolCount int64 `ini:"max_pool_count" json:"max_pool_count"` @@ -179,64 +189,35 @@ type ServerCommonConf struct { // connection. By default, this value is 10. UserConnTimeout int64 `ini:"user_conn_timeout" json:"user_conn_timeout"` // HTTPPlugins specify the server plugins support HTTP protocol. - HTTPPlugins map[string]plugin.HTTPPluginOptions `ini:"-" json:"http_plugins"` + HTTPPlugins map[string]HTTPPluginOptions `ini:"-" json:"http_plugins"` // UDPPacketSize specifies the UDP packet size // By default, this value is 1500 UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"` // Enable golang pprof handlers in dashboard listener. // Dashboard port must be set first. PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"` + // NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data. + NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"` } -// GetDefaultServerConf returns a server configuration with reasonable -// defaults. +// GetDefaultServerConf returns a server configuration with reasonable defaults. +// Note: Some default values here will be set to empty and will be converted to them +// new configuration through the 'Complete' function to set them as the default +// values of the new configuration. func GetDefaultServerConf() ServerCommonConf { return ServerCommonConf{ - ServerConfig: auth.GetDefaultServerConf(), - BindAddr: "0.0.0.0", - BindPort: 7000, - BindUDPPort: 0, - KCPBindPort: 0, - ProxyBindAddr: "", - VhostHTTPPort: 0, - VhostHTTPSPort: 0, - TCPMuxHTTPConnectPort: 0, - TCPMuxPassthrough: false, - VhostHTTPTimeout: 60, - DashboardAddr: "0.0.0.0", - DashboardPort: 0, - DashboardUser: "", - DashboardPwd: "", - EnablePrometheus: false, - AssetsDir: "", - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - DisableLogColor: false, - DetailedErrorsToClient: true, - SubDomainHost: "", - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - TCPKeepAlive: 7200, - AllowPorts: make(map[int]struct{}), - MaxPoolCount: 5, - MaxPortsPerClient: 0, - TLSOnly: false, - TLSCertFile: "", - TLSKeyFile: "", - TLSTrustedCaFile: "", - HeartbeatTimeout: 90, - UserConnTimeout: 10, - Custom404Page: "", - HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), - UDPPacketSize: 1500, - PprofEnable: false, + ServerConfig: legacyauth.GetDefaultServerConf(), + DashboardAddr: "0.0.0.0", + LogFile: "console", + LogWay: "console", + DetailedErrorsToClient: true, + TCPMux: true, + AllowPorts: make(map[int]struct{}), + HTTPPlugins: make(map[string]HTTPPluginOptions), } } -func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { - +func UnmarshalServerConfFromIni(source any) (ServerCommonConf, error) { f, err := ini.LoadSources(ini.LoadOptions{ Insensitive: false, InsensitiveSections: false, @@ -262,17 +243,11 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { // allow_ports allowPortStr := s.Key("allow_ports").String() if allowPortStr != "" { - allowPorts, err := util.ParseRangeNumbers(allowPortStr) - if err != nil { - return ServerCommonConf{}, fmt.Errorf("invalid allow_ports: %v", err) - } - for _, port := range allowPorts { - common.AllowPorts[int(port)] = struct{}{} - } + common.AllowPortsStr = allowPortStr } // plugin.xxx - pluginOpts := make(map[string]plugin.HTTPPluginOptions) + pluginOpts := make(map[string]HTTPPluginOptions) for _, section := range f.Sections() { name := section.Name() if !strings.HasPrefix(name, "plugin.") { @@ -291,47 +266,10 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { return common, nil } -func (cfg *ServerCommonConf) Complete() { - if cfg.LogFile == "console" { - cfg.LogWay = "console" - } else { - cfg.LogWay = "file" - } - - if cfg.ProxyBindAddr == "" { - cfg.ProxyBindAddr = cfg.BindAddr - } - - if cfg.TLSTrustedCaFile != "" { - cfg.TLSOnly = true - } -} - -func (cfg *ServerCommonConf) Validate() error { - if cfg.DashboardTLSMode == false { - if cfg.DashboardTLSCertFile != "" { - fmt.Println("WARNING! dashboard_tls_cert_file is invalid when dashboard_tls_mode is false") - } - - if cfg.DashboardTLSKeyFile != "" { - fmt.Println("WARNING! dashboard_tls_key_file is invalid when dashboard_tls_mode is false") - } - } else { - if cfg.DashboardTLSCertFile == "" { - return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true") - } - - if cfg.DashboardTLSKeyFile == "" { - return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true") - } - } - return validator.New().Struct(cfg) -} - -func loadHTTPPluginOpt(section *ini.Section) (*plugin.HTTPPluginOptions, error) { +func loadHTTPPluginOpt(section *ini.Section) (*HTTPPluginOptions, error) { name := strings.TrimSpace(strings.TrimPrefix(section.Name(), "plugin.")) - opt := new(plugin.HTTPPluginOptions) + opt := &HTTPPluginOptions{} err := section.MapTo(opt) if err != nil { return nil, err diff --git a/pkg/config/utils.go b/pkg/config/legacy/utils.go similarity index 91% rename from pkg/config/utils.go rename to pkg/config/legacy/utils.go index aef674d4308..ad53f1c590e 100644 --- a/pkg/config/utils.go +++ b/pkg/config/legacy/utils.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "strings" @@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string m := make(map[string]string) for key, value := range set { - if strings.HasPrefix(key, prefix) { - m[strings.TrimPrefix(key, prefix)] = value + if trimmed, ok := strings.CutPrefix(key, prefix); ok { + m[trimmed] = value } } diff --git a/pkg/config/value.go b/pkg/config/legacy/value.go similarity index 96% rename from pkg/config/value.go rename to pkg/config/legacy/value.go index e44fbd693e1..ecf805c9912 100644 --- a/pkg/config/value.go +++ b/pkg/config/legacy/value.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "bytes" @@ -21,9 +21,7 @@ import ( "text/template" ) -var ( - glbEnvs map[string]string -) +var glbEnvs map[string]string func init() { glbEnvs = make(map[string]string) diff --git a/pkg/config/legacy/visitor.go b/pkg/config/legacy/visitor.go new file mode 100644 index 00000000000..110a214d183 --- /dev/null +++ b/pkg/config/legacy/visitor.go @@ -0,0 +1,185 @@ +// Copyright 2023 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 legacy + +import ( + "fmt" + "reflect" + + "gopkg.in/ini.v1" +) + +type VisitorType string + +const ( + VisitorTypeSTCP VisitorType = "stcp" + VisitorTypeXTCP VisitorType = "xtcp" + VisitorTypeSUDP VisitorType = "sudp" +) + +// Visitor +var ( + visitorConfTypeMap = map[VisitorType]reflect.Type{ + VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](), + VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](), + VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](), + } +) + +type VisitorConf interface { + // GetBaseConfig returns the base config of visitor. + GetBaseConfig() *BaseVisitorConf + // UnmarshalFromIni unmarshals config from ini. + UnmarshalFromIni(prefix string, name string, section *ini.Section) error +} + +// DefaultVisitorConf creates a empty VisitorConf object by visitorType. +// If visitorType doesn't exist, return nil. +func DefaultVisitorConf(visitorType VisitorType) VisitorConf { + v, ok := visitorConfTypeMap[visitorType] + if !ok { + return nil + } + return reflect.New(v).Interface().(VisitorConf) +} + +type BaseVisitorConf struct { + ProxyName string `ini:"name" json:"name"` + ProxyType string `ini:"type" json:"type"` + UseEncryption bool `ini:"use_encryption" json:"use_encryption"` + UseCompression bool `ini:"use_compression" json:"use_compression"` + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` + // if the server user is not set, it defaults to the current user + ServerUser string `ini:"server_user" json:"server_user"` + ServerName string `ini:"server_name" json:"server_name"` + BindAddr string `ini:"bind_addr" json:"bind_addr"` + // BindPort is the port that visitor listens on. + // It can be less than 0, it means don't bind to the port and only receive connections redirected from + // other visitors. (This is not supported for SUDP now) + BindPort int `ini:"bind_port" json:"bind_port"` +} + +// Base +func (cfg *BaseVisitorConf) GetBaseConfig() *BaseVisitorConf { + return cfg +} + +func (cfg *BaseVisitorConf) unmarshalFromIni(_ string, name string, _ *ini.Section) error { + // Custom decoration after basic unmarshal: + cfg.ProxyName = name + + // bind_addr + if cfg.BindAddr == "" { + cfg.BindAddr = "127.0.0.1" + } + return nil +} + +func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, section *ini.Section) error { + err := section.MapTo(cfg) + if err != nil { + return err + } + + err = cfg.GetBaseConfig().unmarshalFromIni(prefix, name, section) + if err != nil { + return err + } + return nil +} + +type SUDPVisitorConf struct { + BaseVisitorConf `ini:",extends"` +} + +func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { + err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return + } + + // Add custom logic unmarshal, if exists + + return +} + +type STCPVisitorConf struct { + BaseVisitorConf `ini:",extends"` +} + +func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { + err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return + } + + // Add custom logic unmarshal, if exists + + return +} + +type XTCPVisitorConf struct { + BaseVisitorConf `ini:",extends"` + + Protocol string `ini:"protocol" json:"protocol,omitempty"` + KeepTunnelOpen bool `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"` + MaxRetriesAnHour int `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"` + MinRetryInterval int `ini:"min_retry_interval" json:"min_retry_interval,omitempty"` + FallbackTo string `ini:"fallback_to" json:"fallback_to,omitempty"` + FallbackTimeoutMs int `ini:"fallback_timeout_ms" json:"fallback_timeout_ms,omitempty"` +} + +func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { + err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return + } + + // Add custom logic unmarshal, if exists + if cfg.Protocol == "" { + cfg.Protocol = "quic" + } + if cfg.MaxRetriesAnHour <= 0 { + cfg.MaxRetriesAnHour = 8 + } + if cfg.MinRetryInterval <= 0 { + cfg.MinRetryInterval = 90 + } + if cfg.FallbackTimeoutMs <= 0 { + cfg.FallbackTimeoutMs = 1000 + } + return +} + +// Visitor loaded from ini +func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) { + // section.Key: if key not exists, section will set it with default value. + visitorType := VisitorType(section.Key("type").String()) + + if visitorType == "" { + return nil, fmt.Errorf("type shouldn't be empty") + } + + conf := DefaultVisitorConf(visitorType) + if conf == nil { + return nil, fmt.Errorf("type [%s] error", visitorType) + } + + if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { + return nil, fmt.Errorf("type [%s] error", visitorType) + } + return conf, nil +} diff --git a/pkg/config/load.go b/pkg/config/load.go new file mode 100644 index 00000000000..2aaf1457870 --- /dev/null +++ b/pkg/config/load.go @@ -0,0 +1,538 @@ +// Copyright 2023 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 config + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + toml "github.com/pelletier/go-toml/v2" + "github.com/samber/lo" + "gopkg.in/ini.v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/fatedier/frp/pkg/config/legacy" + 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/util/jsonx" + "github.com/fatedier/frp/pkg/util/util" +) + +var glbEnvs map[string]string + +func init() { + glbEnvs = make(map[string]string) + envs := os.Environ() + for _, env := range envs { + pair := strings.SplitN(env, "=", 2) + if len(pair) != 2 { + continue + } + glbEnvs[pair[0]] = pair[1] + } +} + +type Values struct { + Envs map[string]string // environment vars +} + +func GetValues() *Values { + return &Values{ + Envs: glbEnvs, + } +} + +func DetectLegacyINIFormat(content []byte) bool { + f, err := ini.Load(content) + if err != nil { + return false + } + if _, err := f.GetSection("common"); err == nil { + return true + } + return false +} + +func DetectLegacyINIFormatFromFile(path string) bool { + b, err := os.ReadFile(path) + if err != nil { + return false + } + return DetectLegacyINIFormat(b) +} + +func RenderWithTemplate(in []byte, values *Values) ([]byte, error) { + tmpl, err := template.New("frp").Funcs(template.FuncMap{ + "parseNumberRange": parseNumberRange, + "parseNumberRangePair": parseNumberRangePair, + }).Parse(string(in)) + if err != nil { + return nil, err + } + + buffer := bytes.NewBufferString("") + if err := tmpl.Execute(buffer, values); err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return RenderWithTemplate(b, values) +} + +func LoadConfigureFromFile(path string, c any, strict bool) error { + content, err := LoadFileContentWithTemplate(path, GetValues()) + if err != nil { + return err + } + return LoadConfigure(content, c, strict, detectFormatFromPath(path)) +} + +// detectFormatFromPath returns a format hint based on the file extension. +func detectFormatFromPath(path string) string { + switch strings.ToLower(filepath.Ext(path)) { + case ".toml": + return "toml" + case ".yaml", ".yml": + return "yaml" + case ".json": + return "json" + default: + return "" + } +} + +// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling +// This function handles both cases efficiently: with or without dot fields +func parseYAMLWithDotFieldsHandling(content []byte, target any) error { + var temp any + if err := yaml.Unmarshal(content, &temp); err != nil { + return err + } + + // Remove dot fields if it's a map + if tempMap, ok := temp.(map[string]any); ok { + for key := range tempMap { + if strings.HasPrefix(key, ".") { + delete(tempMap, key) + } + } + } + + // Convert to JSON and decode with strict validation + jsonBytes, err := jsonx.Marshal(temp) + if err != nil { + return err + } + return decodeJSONContent(jsonBytes, target, true) +} + +func decodeJSONContent(content []byte, target any, strict bool) error { + if clientCfg, ok := target.(*v1.ClientConfig); ok { + decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{ + DisallowUnknownFields: strict, + }) + if err != nil { + return err + } + *clientCfg = decoded + return nil + } + + return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{ + RejectUnknownMembers: strict, + }) +} + +// LoadConfigure loads configuration from bytes and unmarshal into c. +// Now it supports json, yaml and toml format. +// An optional format hint (e.g. "toml", "yaml", "json") can be provided +// to enable better error messages with line number information. +func LoadConfigure(b []byte, c any, strict bool, formats ...string) error { + format := "" + if len(formats) > 0 { + format = formats[0] + } + + originalBytes := b + parsedFromTOML := false + + var tomlObj any + tomlErr := toml.Unmarshal(b, &tomlObj) + if tomlErr == nil { + parsedFromTOML = true + var err error + b, err = jsonx.Marshal(&tomlObj) + if err != nil { + return err + } + } else if format == "toml" { + // File is known to be TOML but has syntax errors. + return formatTOMLError(tomlErr) + } + + // If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly. + if yaml.IsJSONBuffer(b) { + if err := decodeJSONContent(b, c, strict); err != nil { + return enhanceDecodeError(err, originalBytes, !parsedFromTOML) + } + return nil + } + + // Handle YAML content + if strict { + // In strict mode, always use our custom handler to support YAML merge + if err := parseYAMLWithDotFieldsHandling(b, c); err != nil { + return enhanceDecodeError(err, originalBytes, !parsedFromTOML) + } + return nil + } + // Non-strict mode, parse normally + return yaml.Unmarshal(b, c) +} + +// formatTOMLError extracts line/column information from TOML decode errors. +func formatTOMLError(err error) error { + var decErr *toml.DecodeError + if errors.As(err, &decErr) { + row, col := decErr.Position() + return fmt.Errorf("toml: line %d, column %d: %s", row, col, decErr.Error()) + } + var strictErr *toml.StrictMissingError + if errors.As(err, &strictErr) { + return strictErr + } + return err +} + +// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors. +func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error { + var typeErr *json.UnmarshalTypeError + if errors.As(err, &typeErr) && typeErr.Field != "" { + if includeLine { + line := findFieldLineInContent(originalContent, typeErr.Field) + if line > 0 { + return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type) + } + } + return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type) + } + return err +} + +// findFieldLineInContent searches the original config content for a field name +// and returns the 1-indexed line number where it appears, or 0 if not found. +func findFieldLineInContent(content []byte, fieldPath string) int { + if fieldPath == "" { + return 0 + } + + // Use the last component of the field path (e.g. "proxies" from "proxies" or + // "protocol" from "transport.protocol"). + parts := strings.Split(fieldPath, ".") + searchKey := parts[len(parts)-1] + + lines := bytes.Split(content, []byte("\n")) + for i, line := range lines { + trimmed := bytes.TrimSpace(line) + // Match TOML key assignments like: key = ... + if bytes.HasPrefix(trimmed, []byte(searchKey)) { + rest := bytes.TrimSpace(trimmed[len(searchKey):]) + if len(rest) > 0 && rest[0] == '=' { + return i + 1 + } + } + // Match TOML table array headers like: [[proxies]] + if bytes.Contains(trimmed, []byte("[["+searchKey+"]]")) { + return i + 1 + } + } + return 0 +} + +func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { + m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP)) + + configurer := v1.NewProxyConfigurerByType(v1.ProxyType(m.ProxyType)) + if configurer == nil { + return nil, fmt.Errorf("unknown proxy type: %s", m.ProxyType) + } + + configurer.UnmarshalFromMsg(m) + configurer.Complete() + + if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil { + return nil, err + } + return configurer, nil +} + +func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error) { + var ( + svrCfg *v1.ServerConfig + isLegacyFormat bool + ) + // detect legacy ini format + if DetectLegacyINIFormatFromFile(path) { + content, err := legacy.GetRenderedConfFromFile(path) + if err != nil { + return nil, true, err + } + legacyCfg, err := legacy.UnmarshalServerConfFromIni(content) + if err != nil { + return nil, true, err + } + svrCfg = legacy.Convert_ServerCommonConf_To_v1(&legacyCfg) + isLegacyFormat = true + } else { + svrCfg = &v1.ServerConfig{} + if err := LoadConfigureFromFile(path, svrCfg, strict); err != nil { + return nil, false, err + } + } + if svrCfg != nil { + if err := svrCfg.Complete(); err != nil { + return nil, isLegacyFormat, err + } + } + return svrCfg, isLegacyFormat, nil +} + +// ClientConfigLoadResult contains the result of loading a client configuration file. +type ClientConfigLoadResult struct { + // Common contains the common client configuration. + Common *v1.ClientCommonConfig + + // Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles. + // These are NOT completed (user prefix not added). + Proxies []v1.ProxyConfigurer + + // Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles. + // These are NOT completed. + Visitors []v1.VisitorConfigurer + + // IsLegacyFormat indicates whether the config file is in legacy INI format. + IsLegacyFormat bool +} + +// LoadClientConfigResult loads and parses a client configuration file. +// It returns the raw configuration without completing proxies/visitors. +// The caller should call Complete on the configs manually for legacy behavior. +func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) { + result := &ClientConfigLoadResult{ + Proxies: make([]v1.ProxyConfigurer, 0), + Visitors: make([]v1.VisitorConfigurer, 0), + } + + if DetectLegacyINIFormatFromFile(path) { + legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) + if err != nil { + return nil, err + } + result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) + for _, c := range legacyProxyCfgs { + result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c)) + } + for _, c := range legacyVisitorCfgs { + result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c)) + } + result.IsLegacyFormat = true + } else { + allCfg := v1.ClientConfig{} + if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil { + return nil, err + } + result.Common = &allCfg.ClientCommonConfig + for _, c := range allCfg.Proxies { + result.Proxies = append(result.Proxies, c.ProxyConfigurer) + } + for _, c := range allCfg.Visitors { + result.Visitors = append(result.Visitors, c.VisitorConfigurer) + } + } + + // Load additional config from includes. + // legacy ini format already handle this in ParseClientConfig. + if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat { + extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict) + if err != nil { + return nil, err + } + result.Proxies = append(result.Proxies, extProxyCfgs...) + result.Visitors = append(result.Visitors, extVisitorCfgs...) + } + + // Complete the common config + if result.Common != nil { + if err := result.Common.Complete(); err != nil { + return nil, err + } + } + + if err := validateNoDuplicateNames(result.Proxies, result.Visitors); err != nil { + return nil, err + } + + return result, nil +} + +func LoadClientConfig(path string, strict bool) ( + *v1.ClientCommonConfig, + []v1.ProxyConfigurer, + []v1.VisitorConfigurer, + bool, error, +) { + result, err := LoadClientConfigResult(path, strict) + if err != nil { + return nil, nil, nil, result != nil && result.IsLegacyFormat, err + } + + proxyCfgs := result.Proxies + visitorCfgs := result.Visitors + + proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs) + proxyCfgs = CompleteProxyConfigurers(proxyCfgs) + visitorCfgs = CompleteVisitorConfigurers(visitorCfgs) + return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil +} + +// validateNoDuplicateNames rejects proxies or visitors that share a name. They are +// keyed by name in the config sources, so a duplicate would otherwise be silently +// overwritten and never started, with no error or log. +func validateNoDuplicateNames(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error { + proxyNames := make(map[string]struct{}, len(proxies)) + for _, p := range proxies { + name := p.GetBaseConfig().Name + if _, ok := proxyNames[name]; ok { + return fmt.Errorf("proxy name [%s] is duplicated", name) + } + proxyNames[name] = struct{}{} + } + + visitorNames := make(map[string]struct{}, len(visitors)) + for _, v := range visitors { + name := v.GetBaseConfig().Name + if _, ok := visitorNames[name]; ok { + return fmt.Errorf("visitor name [%s] is duplicated", name) + } + visitorNames[name] = struct{}{} + } + + return nil +} + +func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer { + proxyCfgs := proxies + for _, c := range proxyCfgs { + c.Complete() + } + return proxyCfgs +} + +func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer { + visitorCfgs := visitors + for _, c := range visitorCfgs { + c.Complete() + } + return visitorCfgs +} + +func FilterClientConfigurers( + common *v1.ClientCommonConfig, + proxies []v1.ProxyConfigurer, + visitors []v1.VisitorConfigurer, +) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) { + if common == nil { + common = &v1.ClientCommonConfig{} + } + + proxyCfgs := proxies + visitorCfgs := visitors + + // Filter by start across merged configurers from all sources. + // For example, store entries are also filtered by this set. + if len(common.Start) > 0 { + startSet := sets.New(common.Start...) + proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { + return startSet.Has(c.GetBaseConfig().Name) + }) + visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { + return startSet.Has(c.GetBaseConfig().Name) + }) + } + + // Filter by enabled field in each proxy + // nil or true means enabled, false means disabled + proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { + enabled := c.GetBaseConfig().Enabled + return enabled == nil || *enabled + }) + visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { + enabled := c.GetBaseConfig().Enabled + return enabled == nil || *enabled + }) + return proxyCfgs, visitorCfgs +} + +func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + proxyCfgs := make([]v1.ProxyConfigurer, 0) + visitorCfgs := make([]v1.VisitorConfigurer, 0) + for _, path := range paths { + absDir, err := filepath.Abs(filepath.Dir(path)) + if err != nil { + return nil, nil, err + } + if _, err := os.Stat(absDir); os.IsNotExist(err) { + return nil, nil, err + } + files, err := os.ReadDir(absDir) + if err != nil { + return nil, nil, err + } + for _, fi := range files { + if fi.IsDir() { + continue + } + absFile := filepath.Join(absDir, fi.Name()) + if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched { + // support yaml/json/toml + cfg := v1.ClientConfig{} + if err := LoadConfigureFromFile(absFile, &cfg, strict); err != nil { + return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err) + } + for _, c := range cfg.Proxies { + proxyCfgs = append(proxyCfgs, c.ProxyConfigurer) + } + for _, c := range cfg.Visitors { + visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) + } + } + } + } + return proxyCfgs, visitorCfgs, nil +} diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go new file mode 100644 index 00000000000..a1e29247874 --- /dev/null +++ b/pkg/config/load_test.go @@ -0,0 +1,712 @@ +// Copyright 2023 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 config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +const tomlServerContent = ` +bindAddr = "127.0.0.1" +kcpBindPort = 7000 +quicBindPort = 7001 +tcpmuxHTTPConnectPort = 7005 +custom404Page = "/abc.html" +transport.tcpKeepalive = 10 +` + +const yamlServerContent = ` +bindAddr: 127.0.0.1 +kcpBindPort: 7000 +quicBindPort: 7001 +tcpmuxHTTPConnectPort: 7005 +custom404Page: /abc.html +transport: + tcpKeepalive: 10 +` + +const jsonServerContent = ` +{ + "bindAddr": "127.0.0.1", + "kcpBindPort": 7000, + "quicBindPort": 7001, + "tcpmuxHTTPConnectPort": 7005, + "custom404Page": "/abc.html", + "transport": { + "tcpKeepalive": 10 + } +} +` + +func TestLoadServerConfig(t *testing.T) { + tests := []struct { + name string + content string + }{ + {"toml", tomlServerContent}, + {"yaml", yamlServerContent}, + {"json", jsonServerContent}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + svrCfg := v1.ServerConfig{} + err := LoadConfigure([]byte(test.content), &svrCfg, true) + require.NoError(err) + require.EqualValues("127.0.0.1", svrCfg.BindAddr) + require.EqualValues(7000, svrCfg.KCPBindPort) + require.EqualValues(7001, svrCfg.QUICBindPort) + require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort) + require.EqualValues("/abc.html", svrCfg.Custom404Page) + require.EqualValues(10, svrCfg.Transport.TCPKeepAlive) + }) + } +} + +// Test that loading in strict mode fails when the config is invalid. +func TestLoadServerConfigStrictMode(t *testing.T) { + tests := []struct { + name string + content string + }{ + {"toml", tomlServerContent}, + {"yaml", yamlServerContent}, + {"json", jsonServerContent}, + } + + for _, strict := range []bool{false, true} { + for _, test := range tests { + t.Run(fmt.Sprintf("%s-strict-%t", test.name, strict), func(t *testing.T) { + require := require.New(t) + // Break the content with an innocent typo + brokenContent := strings.Replace(test.content, "bindAddr", "bindAdur", 1) + svrCfg := v1.ServerConfig{} + err := LoadConfigure([]byte(brokenContent), &svrCfg, strict) + if strict { + require.ErrorContains(err, "bindAdur") + } else { + require.NoError(err) + // BindAddr didn't get parsed because of the typo. + require.EqualValues("", svrCfg.BindAddr) + } + }) + } + } +} + +func TestRenderWithTemplate(t *testing.T) { + tests := []struct { + name string + content string + want string + }{ + {"toml", tomlServerContent, tomlServerContent}, + {"yaml", yamlServerContent, yamlServerContent}, + {"json", jsonServerContent, jsonServerContent}, + {"template numeric", `key = {{ 123 }}`, "key = 123"}, + {"template string", `key = {{ "xyz" }}`, "key = xyz"}, + {"template quote", `key = {{ printf "%q" "with space" }}`, `key = "with space"`}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + got, err := RenderWithTemplate([]byte(test.content), nil) + require.NoError(err) + require.EqualValues(test.want, string(got)) + }) + } +} + +func TestCustomStructStrictMode(t *testing.T) { + require := require.New(t) + + proxyStr := ` +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +` + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(proxyStr), &clientCfg, true) + require.NoError(err) + + proxyStr += `unknown = "unknown"` + err = LoadConfigure([]byte(proxyStr), &clientCfg, true) + require.Error(err) + + visitorStr := ` +serverPort = 7000 + +[[visitors]] +name = "test" +type = "stcp" +bindPort = 6000 +serverName = "server" +` + err = LoadConfigure([]byte(visitorStr), &clientCfg, true) + require.NoError(err) + + visitorStr += `unknown = "unknown"` + err = LoadConfigure([]byte(visitorStr), &clientCfg, true) + require.Error(err) + + pluginStr := ` +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +[proxies.plugin] +type = "unix_domain_socket" +unixPath = "/tmp/uds.sock" +` + err = LoadConfigure([]byte(pluginStr), &clientCfg, true) + require.NoError(err) + pluginStr += `unknown = "unknown"` + err = LoadConfigure([]byte(pluginStr), &clientCfg, true) + require.Error(err) +} + +func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) { + require := require.New(t) + + content := ` +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +localPort = 6000 +[proxies.plugin] +type = "http2https" +localAddr = "127.0.0.1:8080" +unknownInPlugin = "value" +` + + clientCfg := v1.ClientConfig{} + + err := LoadConfigure([]byte(content), &clientCfg, false) + require.NoError(err) + + err = LoadConfigure([]byte(content), &clientCfg, true) + require.ErrorContains(err, "unknownInPlugin") +} + +// TestYAMLMergeInStrictMode tests that YAML merge functionality works +// even in strict mode by properly handling dot-prefixed fields +func TestYAMLMergeInStrictMode(t *testing.T) { + require := require.New(t) + + yamlContent := ` +serverAddr: "127.0.0.1" +serverPort: 7000 + +.common: &common + type: stcp + secretKey: "test-secret" + localIP: 127.0.0.1 + transport: + useEncryption: true + useCompression: true + +proxies: +- name: ssh + localPort: 22 + <<: *common +- name: web + localPort: 80 + <<: *common +` + + clientCfg := v1.ClientConfig{} + // This should work in strict mode + err := LoadConfigure([]byte(yamlContent), &clientCfg, true) + require.NoError(err) + + // Verify the merge worked correctly + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) + require.Len(clientCfg.Proxies, 2) + + // Check first proxy + sshProxy := clientCfg.Proxies[0].ProxyConfigurer + require.Equal("ssh", sshProxy.GetBaseConfig().Name) + require.Equal("stcp", sshProxy.GetBaseConfig().Type) + + // Check second proxy + webProxy := clientCfg.Proxies[1].ProxyConfigurer + require.Equal("web", webProxy.GetBaseConfig().Name) + require.Equal("stcp", webProxy.GetBaseConfig().Type) +} + +// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing +func TestOptimizedYAMLProcessing(t *testing.T) { + require := require.New(t) + + yamlWithDotFields := []byte(` +serverAddr: "127.0.0.1" +.common: &common + type: stcp +proxies: +- name: test + <<: *common +`) + + yamlWithoutDotFields := []byte(` +serverAddr: "127.0.0.1" +proxies: +- name: test + type: tcp + localPort: 22 +`) + + // Test that YAML without dot fields works in strict mode + clientCfg := v1.ClientConfig{} + err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Len(clientCfg.Proxies, 1) + require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name) + + // Test that YAML with dot fields still works in strict mode + err = LoadConfigure(yamlWithDotFields, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Len(clientCfg.Proxies, 1) + require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name) + require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type) +} + +func TestFilterClientConfigurers_PreserveRawNamesAndNoMutation(t *testing.T) { + require := require.New(t) + + enabled := true + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy-raw" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + proxyCfg.Enabled = &enabled + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor-raw" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server-raw" + visitorCfg.FallbackTo = "fallback-raw" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + visitorCfg.Enabled = &enabled + + common := &v1.ClientCommonConfig{ + User: "alice", + } + + proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg}) + require.Len(proxies, 1) + require.Len(visitors, 1) + + p := proxies[0].GetBaseConfig() + require.Equal("proxy-raw", p.Name) + require.Empty(p.LocalIP) + + v := visitors[0].GetBaseConfig() + require.Equal("visitor-raw", v.Name) + require.Equal("server-raw", v.ServerName) + require.Empty(v.BindAddr) + + xtcp := visitors[0].(*v1.XTCPVisitorConfig) + require.Equal("fallback-raw", xtcp.FallbackTo) + require.Empty(xtcp.Protocol) +} + +func TestCompleteProxyConfigurers_PreserveRawNames(t *testing.T) { + require := require.New(t) + + enabled := true + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy-raw" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + proxyCfg.Enabled = &enabled + + proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg}) + require.Len(proxies, 1) + + p := proxies[0].GetBaseConfig() + require.Equal("proxy-raw", p.Name) + require.Equal("127.0.0.1", p.LocalIP) +} + +func TestCompleteVisitorConfigurers_PreserveRawNames(t *testing.T) { + require := require.New(t) + + enabled := true + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor-raw" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server-raw" + visitorCfg.FallbackTo = "fallback-raw" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + visitorCfg.Enabled = &enabled + + visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg}) + require.Len(visitors, 1) + + v := visitors[0].GetBaseConfig() + require.Equal("visitor-raw", v.Name) + require.Equal("server-raw", v.ServerName) + require.Equal("127.0.0.1", v.BindAddr) + + xtcp := visitors[0].(*v1.XTCPVisitorConfig) + require.Equal("fallback-raw", xtcp.FallbackTo) + require.Equal("quic", xtcp.Protocol) +} + +func TestCompleteProxyConfigurers_Idempotent(t *testing.T) { + require := require.New(t) + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg}) + firstProxyJSON, err := json.Marshal(proxies[0]) + require.NoError(err) + + proxies = CompleteProxyConfigurers(proxies) + secondProxyJSON, err := json.Marshal(proxies[0]) + require.NoError(err) + + require.Equal(string(firstProxyJSON), string(secondProxyJSON)) +} + +func TestCompleteVisitorConfigurers_Idempotent(t *testing.T) { + require := require.New(t) + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg}) + firstVisitorJSON, err := json.Marshal(visitors[0]) + require.NoError(err) + + visitors = CompleteVisitorConfigurers(visitors) + secondVisitorJSON, err := json.Marshal(visitors[0]) + require.NoError(err) + + require.Equal(string(firstVisitorJSON), string(secondVisitorJSON)) +} + +func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) { + require := require.New(t) + + enabled := true + disabled := false + + proxyKeep := &v1.TCPProxyConfig{} + proxyKeep.Name = "keep" + proxyKeep.Type = "tcp" + proxyKeep.LocalPort = 10080 + proxyKeep.Enabled = &enabled + + proxyDropByStart := &v1.TCPProxyConfig{} + proxyDropByStart.Name = "drop-by-start" + proxyDropByStart.Type = "tcp" + proxyDropByStart.LocalPort = 10081 + proxyDropByStart.Enabled = &enabled + + proxyDropByEnabled := &v1.TCPProxyConfig{} + proxyDropByEnabled.Name = "drop-by-enabled" + proxyDropByEnabled.Type = "tcp" + proxyDropByEnabled.LocalPort = 10082 + proxyDropByEnabled.Enabled = &disabled + + common := &v1.ClientCommonConfig{ + Start: []string{"keep"}, + } + + proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{ + proxyKeep, + proxyDropByStart, + proxyDropByEnabled, + }, nil) + require.Len(visitors, 0) + require.Len(proxies, 1) + require.Equal("keep", proxies[0].GetBaseConfig().Name) +} + +func TestLoadClientConfigResult_DuplicateNames(t *testing.T) { + tests := []struct { + name string + content string + errSubstr string + }{ + { + name: "duplicate proxy names", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "dup" +type = "tcp" +localPort = 22 +remotePort = 6000 + +[[proxies]] +name = "dup" +type = "tcp" +localPort = 3306 +remotePort = 6001 +`, + errSubstr: "proxy name [dup] is duplicated", + }, + { + name: "duplicate visitor names", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[visitors]] +name = "dup" +type = "stcp" +serverName = "a" +secretKey = "secret" +bindPort = 9001 + +[[visitors]] +name = "dup" +type = "stcp" +serverName = "b" +secretKey = "secret" +bindPort = 9002 +`, + errSubstr: "visitor name [dup] is duplicated", + }, + { + name: "unique names", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "p1" +type = "tcp" +localPort = 22 +remotePort = 6000 + +[[proxies]] +name = "p2" +type = "tcp" +localPort = 3306 +remotePort = 6001 +`, + }, + { + name: "same name across proxy and visitor", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "same" +type = "tcp" +localPort = 22 +remotePort = 6000 + +[[visitors]] +name = "same" +type = "stcp" +serverName = "a" +secretKey = "secret" +bindPort = 9001 +`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + path := filepath.Join(t.TempDir(), "frpc.toml") + require.NoError(os.WriteFile(path, []byte(tc.content), 0o600)) + + _, err := LoadClientConfigResult(path, false) + if tc.errSubstr == "" { + require.NoError(err) + } else { + require.ErrorContains(err, tc.errSubstr) + } + }) + } +} + +// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types +func TestYAMLEdgeCases(t *testing.T) { + require := require.New(t) + + // Test array at root (should fail for frp config) + arrayYAML := []byte(` +- item1 +- item2 +`) + clientCfg := v1.ClientConfig{} + err := LoadConfigure(arrayYAML, &clientCfg, true) + require.Error(err) // Should fail because ClientConfig expects an object + + // Test scalar at root (should fail for frp config) + scalarYAML := []byte(`"just a string"`) + err = LoadConfigure(scalarYAML, &clientCfg, true) + require.Error(err) // Should fail because ClientConfig expects an object + + // Test empty object (should work) + emptyYAML := []byte(`{}`) + err = LoadConfigure(emptyYAML, &clientCfg, true) + require.NoError(err) + + // Test nested structure without dots (should work) + nestedYAML := []byte(` +serverAddr: "127.0.0.1" +serverPort: 7000 +`) + err = LoadConfigure(nestedYAML, &clientCfg, true) + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) +} + +func TestTOMLSyntaxErrorWithPosition(t *testing.T) { + require := require.New(t) + + // TOML with syntax error (unclosed table array header) + content := `serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies] +name = "test" +` + + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(content), &clientCfg, false, "toml") + require.Error(err) + require.Contains(err.Error(), "toml") + require.Contains(err.Error(), "line") + require.Contains(err.Error(), "column") +} + +func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) { + require := require.New(t) + + // TOML with wrong type: proxies should be a table array, not a string + content := `serverAddr = "127.0.0.1" +serverPort = 7000 +proxies = "this should be a table array" +` + + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(content), &clientCfg, false, "toml") + require.Error(err) + // The error should contain field info + errMsg := err.Error() + require.Contains(errMsg, "proxies") + require.NotContains(errMsg, "line") +} + +func TestFindFieldLineInContent(t *testing.T) { + content := []byte(`serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +`) + + tests := []struct { + fieldPath string + wantLine int + }{ + {"serverAddr", 1}, + {"serverPort", 2}, + {"name", 5}, + {"type", 6}, + {"remotePort", 7}, + {"nonexistent", 0}, + } + + for _, tt := range tests { + t.Run(tt.fieldPath, func(t *testing.T) { + got := findFieldLineInContent(content, tt.fieldPath) + require.Equal(t, tt.wantLine, got) + }) + } +} + +func TestFormatDetection(t *testing.T) { + tests := []struct { + path string + format string + }{ + {"config.toml", "toml"}, + {"config.TOML", "toml"}, + {"config.yaml", "yaml"}, + {"config.yml", "yaml"}, + {"config.json", "json"}, + {"config.ini", ""}, + {"config", ""}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + require.Equal(t, tt.format, detectFormatFromPath(tt.path)) + }) + } +} + +func TestValidTOMLStillWorks(t *testing.T) { + require := require.New(t) + + // Valid TOML with format hint should work fine + content := `serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "test" +type = "tcp" +remotePort = 6000 +` + clientCfg := v1.ClientConfig{} + err := LoadConfigure([]byte(content), &clientCfg, false, "toml") + require.NoError(err) + require.Equal("127.0.0.1", clientCfg.ServerAddr) + require.Equal(7000, clientCfg.ServerPort) + require.Len(clientCfg.Proxies, 1) +} diff --git a/pkg/config/proxy.go b/pkg/config/proxy.go deleted file mode 100644 index 9275e649238..00000000000 --- a/pkg/config/proxy.go +++ /dev/null @@ -1,1075 +0,0 @@ -// Copyright 2016 fatedier, fatedier@gmail.com -// -// 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 config - -import ( - "fmt" - "net" - "reflect" - "strconv" - "strings" - - "github.com/fatedier/frp/pkg/consts" - "github.com/fatedier/frp/pkg/msg" - - "gopkg.in/ini.v1" -) - -// Proxy -var ( - proxyConfTypeMap = map[string]reflect.Type{ - consts.TCPProxy: reflect.TypeOf(TCPProxyConf{}), - consts.TCPMuxProxy: reflect.TypeOf(TCPMuxProxyConf{}), - consts.UDPProxy: reflect.TypeOf(UDPProxyConf{}), - consts.HTTPProxy: reflect.TypeOf(HTTPProxyConf{}), - consts.HTTPSProxy: reflect.TypeOf(HTTPSProxyConf{}), - consts.STCPProxy: reflect.TypeOf(STCPProxyConf{}), - consts.XTCPProxy: reflect.TypeOf(XTCPProxyConf{}), - consts.SUDPProxy: reflect.TypeOf(SUDPProxyConf{}), - } -) - -func NewConfByType(proxyType string) ProxyConf { - v, ok := proxyConfTypeMap[proxyType] - if !ok { - return nil - } - cfg := reflect.New(v).Interface().(ProxyConf) - return cfg -} - -type ProxyConf interface { - GetBaseInfo() *BaseProxyConf - UnmarshalFromMsg(*msg.NewProxy) - UnmarshalFromIni(string, string, *ini.Section) error - MarshalToMsg(*msg.NewProxy) - CheckForCli() error - CheckForSvr(ServerCommonConf) error - Compare(ProxyConf) bool -} - -// LocalSvrConf configures what location the client will to, or what -// plugin will be used. -type LocalSvrConf struct { - // LocalIP specifies the IP address or host name to to. - LocalIP string `ini:"local_ip" json:"local_ip"` - // LocalPort specifies the port to to. - LocalPort int `ini:"local_port" json:"local_port"` - - // Plugin specifies what plugin should be used for ng. If this value - // is set, the LocalIp and LocalPort values will be ignored. By default, - // this value is "". - Plugin string `ini:"plugin" json:"plugin"` - // PluginParams specify parameters to be passed to the plugin, if one is - // being used. By default, this value is an empty map. - PluginParams map[string]string `ini:"-"` -} - -// HealthCheckConf configures health checking. This can be useful for load -// balancing purposes to detect and remove proxies to failing services. -type HealthCheckConf struct { - // HealthCheckType specifies what protocol to use for health checking. - // Valid values include "tcp", "http", and "". If this value is "", health - // checking will not be performed. By default, this value is "". - // - // If the type is "tcp", a connection will be attempted to the target - // server. If a connection cannot be established, the health check fails. - // - // If the type is "http", a GET request will be made to the endpoint - // specified by HealthCheckURL. If the response is not a 200, the health - // check fails. - HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http - // HealthCheckTimeoutS specifies the number of seconds to wait for a health - // check attempt to connect. If the timeout is reached, this counts as a - // health check failure. By default, this value is 3. - HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"` - // HealthCheckMaxFailed specifies the number of allowed failures before the - // is stopped. By default, this value is 1. - HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"` - // HealthCheckIntervalS specifies the time in seconds between health - // checks. By default, this value is 10. - HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"` - // HealthCheckURL specifies the address to send health checks to if the - // health check type is "http". - HealthCheckURL string `ini:"health_check_url" json:"health_check_url"` - // HealthCheckAddr specifies the address to connect to if the health check - // type is "tcp". - HealthCheckAddr string `ini:"-"` -} - -// BaseProxyConf provides configuration info that is common to all types. -type BaseProxyConf struct { - // ProxyName is the name of this - ProxyName string `ini:"name" json:"name"` - // ProxyType specifies the type of this Valid values include "tcp", - // "udp", "http", "https", "stcp", and "xtcp". By default, this value is - // "tcp". - ProxyType string `ini:"type" json:"type"` - - // UseEncryption controls whether or not communication with the server will - // be encrypted. Encryption is done using the tokens supplied in the server - // and client configuration. By default, this value is false. - UseEncryption bool `ini:"use_encryption" json:"use_encryption"` - // UseCompression controls whether or not communication with the server - // will be compressed. By default, this value is false. - UseCompression bool `ini:"use_compression" json:"use_compression"` - // Group specifies which group the is a part of. The server will use - // this information to load balance proxies in the same group. If the value - // is "", this will not be in a group. By default, this value is "". - Group string `ini:"group" json:"group"` - // GroupKey specifies a group key, which should be the same among proxies - // of the same group. By default, this value is "". - GroupKey string `ini:"group_key" json:"group_key"` - - // ProxyProtocolVersion specifies which protocol version to use. Valid - // values include "v1", "v2", and "". If the value is "", a protocol - // version will be automatically selected. By default, this value is "". - ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"` - - // BandwidthLimit limit the bandwidth - // 0 means no limit - BandwidthLimit BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"` - - // meta info for each proxy - Metas map[string]string `ini:"-" json:"metas"` - - LocalSvrConf `ini:",extends"` - HealthCheckConf `ini:",extends"` -} - -type DomainConf struct { - CustomDomains []string `ini:"custom_domains" json:"custom_domains"` - SubDomain string `ini:"subdomain" json:"subdomain"` -} - -// HTTP -type HTTPProxyConf struct { - BaseProxyConf `ini:",extends"` - DomainConf `ini:",extends"` - - Locations []string `ini:"locations" json:"locations"` - HTTPUser string `ini:"http_user" json:"http_user"` - HTTPPwd string `ini:"http_pwd" json:"http_pwd"` - IpsAllowList []string `ini:"ips_allow_list" json:"ips_allow_list"` - HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"` - Headers map[string]string `ini:"-" json:"headers"` - RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` -} - -// HTTPS -type HTTPSProxyConf struct { - BaseProxyConf `ini:",extends"` - DomainConf `ini:",extends"` -} - -// TCP -type TCPProxyConf struct { - BaseProxyConf `ini:",extends"` - RemotePort int `ini:"remote_port" json:"remote_port"` -} - -// TCPMux -type TCPMuxProxyConf struct { - BaseProxyConf `ini:",extends"` - DomainConf `ini:",extends"` - RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` - - Multiplexer string `ini:"multiplexer"` -} - -// STCP -type STCPProxyConf struct { - BaseProxyConf `ini:",extends"` - - Role string `ini:"role" json:"role"` - Sk string `ini:"sk" json:"sk"` -} - -// XTCP -type XTCPProxyConf struct { - BaseProxyConf `ini:",extends"` - - Role string `ini:"role" json:"role"` - Sk string `ini:"sk" json:"sk"` -} - -// UDP -type UDPProxyConf struct { - BaseProxyConf `ini:",extends"` - - RemotePort int `ini:"remote_port" json:"remote_port"` -} - -// SUDP -type SUDPProxyConf struct { - BaseProxyConf `ini:",extends"` - - Role string `ini:"role" json:"role"` - Sk string `ini:"sk" json:"sk"` -} - -// Proxy Conf Loader -// DefaultProxyConf creates a empty ProxyConf object by proxyType. -// If proxyType doesn't exist, return nil. -func DefaultProxyConf(proxyType string) ProxyConf { - var conf ProxyConf - switch proxyType { - case consts.TCPProxy: - conf = &TCPProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - } - case consts.TCPMuxProxy: - conf = &TCPMuxProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - } - case consts.UDPProxy: - conf = &UDPProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - } - case consts.HTTPProxy: - conf = &HTTPProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - } - case consts.HTTPSProxy: - conf = &HTTPSProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - } - case consts.STCPProxy: - conf = &STCPProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - Role: "server", - } - case consts.XTCPProxy: - conf = &XTCPProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - Role: "server", - } - case consts.SUDPProxy: - conf = &SUDPProxyConf{ - BaseProxyConf: defaultBaseProxyConf(proxyType), - Role: "server", - } - default: - return nil - } - - return conf -} - -// Proxy loaded from ini -func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) { - // section.Key: if key not exists, section will set it with default value. - proxyType := section.Key("type").String() - if proxyType == "" { - proxyType = consts.TCPProxy - } - - conf := DefaultProxyConf(proxyType) - if conf == nil { - return nil, fmt.Errorf("proxy %s has invalid type [%s]", name, proxyType) - } - - if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { - return nil, err - } - - if err := conf.CheckForCli(); err != nil { - return nil, err - } - - return conf, nil -} - -// Proxy loaded from msg -func NewProxyConfFromMsg(pMsg *msg.NewProxy, serverCfg ServerCommonConf) (ProxyConf, error) { - if pMsg.ProxyType == "" { - pMsg.ProxyType = consts.TCPProxy - } - - conf := DefaultProxyConf(pMsg.ProxyType) - if conf == nil { - return nil, fmt.Errorf("proxy [%s] type [%s] error", pMsg.ProxyName, pMsg.ProxyType) - } - - conf.UnmarshalFromMsg(pMsg) - - err := conf.CheckForSvr(serverCfg) - if err != nil { - return nil, err - } - - return conf, nil -} - -// Base -func defaultBaseProxyConf(proxyType string) BaseProxyConf { - return BaseProxyConf{ - ProxyType: proxyType, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - }, - } -} - -func (cfg *BaseProxyConf) GetBaseInfo() *BaseProxyConf { - return cfg -} - -func (cfg *BaseProxyConf) compare(cmp *BaseProxyConf) bool { - if cfg.ProxyName != cmp.ProxyName || - cfg.ProxyType != cmp.ProxyType || - cfg.UseEncryption != cmp.UseEncryption || - cfg.UseCompression != cmp.UseCompression || - cfg.Group != cmp.Group || - cfg.GroupKey != cmp.GroupKey || - cfg.ProxyProtocolVersion != cmp.ProxyProtocolVersion || - !cfg.BandwidthLimit.Equal(&cmp.BandwidthLimit) || - !reflect.DeepEqual(cfg.Metas, cmp.Metas) { - return false - } - - if !reflect.DeepEqual(cfg.LocalSvrConf, cmp.LocalSvrConf) { - return false - } - if !reflect.DeepEqual(cfg.HealthCheckConf, cmp.HealthCheckConf) { - return false - } - - return true -} - -// BaseProxyConf apply custom logic changes. -func (cfg *BaseProxyConf) decorate(prefix string, name string, section *ini.Section) error { - // proxy_name - cfg.ProxyName = prefix + name - - // metas_xxx - cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_") - - // bandwidth_limit - if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil { - cfg.BandwidthLimit, err = NewBandwidthQuantity(bandwidth.String()) - if err != nil { - return err - } - } - - // plugin_xxx - cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") - - // custom logic code - if cfg.HealthCheckType == "tcp" && cfg.Plugin == "" { - cfg.HealthCheckAddr = cfg.LocalIP + fmt.Sprintf(":%d", cfg.LocalPort) - } - - if cfg.HealthCheckType == "http" && cfg.Plugin == "" && cfg.HealthCheckURL != "" { - s := "http://" + net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - if !strings.HasPrefix(cfg.HealthCheckURL, "/") { - s += "/" - } - cfg.HealthCheckURL = s + cfg.HealthCheckURL - } - - return nil -} - -func (cfg *BaseProxyConf) marshalToMsg(pMsg *msg.NewProxy) { - pMsg.ProxyName = cfg.ProxyName - pMsg.ProxyType = cfg.ProxyType - pMsg.UseEncryption = cfg.UseEncryption - pMsg.UseCompression = cfg.UseCompression - pMsg.Group = cfg.Group - pMsg.GroupKey = cfg.GroupKey - pMsg.Metas = cfg.Metas -} - -func (cfg *BaseProxyConf) unmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.ProxyName = pMsg.ProxyName - cfg.ProxyType = pMsg.ProxyType - cfg.UseEncryption = pMsg.UseEncryption - cfg.UseCompression = pMsg.UseCompression - cfg.Group = pMsg.Group - cfg.GroupKey = pMsg.GroupKey - cfg.Metas = pMsg.Metas -} - -func (cfg *BaseProxyConf) checkForCli() (err error) { - if cfg.ProxyProtocolVersion != "" { - if cfg.ProxyProtocolVersion != "v1" && cfg.ProxyProtocolVersion != "v2" { - return fmt.Errorf("no support proxy protocol version: %s", cfg.ProxyProtocolVersion) - } - } - - if err = cfg.LocalSvrConf.checkForCli(); err != nil { - return - } - if err = cfg.HealthCheckConf.checkForCli(); err != nil { - return - } - return nil -} - -func (cfg *BaseProxyConf) checkForSvr(conf ServerCommonConf) error { - return nil -} - -// DomainConf -func (cfg *DomainConf) check() (err error) { - if len(cfg.CustomDomains) == 0 && cfg.SubDomain == "" { - err = fmt.Errorf("custom_domains and subdomain should set at least one of them") - return - } - return -} - -func (cfg *DomainConf) checkForCli() (err error) { - if err = cfg.check(); err != nil { - return - } - return -} - -func (cfg *DomainConf) checkForSvr(serverCfg ServerCommonConf) (err error) { - if err = cfg.check(); err != nil { - return - } - - for _, domain := range cfg.CustomDomains { - if serverCfg.SubDomainHost != "" && len(strings.Split(serverCfg.SubDomainHost, ".")) < len(strings.Split(domain, ".")) { - if strings.Contains(domain, serverCfg.SubDomainHost) { - return fmt.Errorf("custom domain [%s] should not belong to subdomain_host [%s]", domain, serverCfg.SubDomainHost) - } - } - } - - if cfg.SubDomain != "" { - if serverCfg.SubDomainHost == "" { - return fmt.Errorf("subdomain is not supported because this feature is not enabled in remote frps") - } - if strings.Contains(cfg.SubDomain, ".") || strings.Contains(cfg.SubDomain, "*") { - return fmt.Errorf("'.' and '*' is not supported in subdomain") - } - } - return nil -} - -// LocalSvrConf -func (cfg *LocalSvrConf) checkForCli() (err error) { - if cfg.Plugin == "" { - if cfg.LocalIP == "" { - err = fmt.Errorf("local ip or plugin is required") - return - } - if cfg.LocalPort <= 0 { - err = fmt.Errorf("error local_port") - return - } - } - return -} - -// HealthCheckConf -func (cfg *HealthCheckConf) checkForCli() error { - if cfg.HealthCheckType != "" && cfg.HealthCheckType != "tcp" && cfg.HealthCheckType != "http" { - return fmt.Errorf("unsupport health check type") - } - if cfg.HealthCheckType != "" { - if cfg.HealthCheckType == "http" && cfg.HealthCheckURL == "" { - return fmt.Errorf("health_check_url is required for health check type 'http'") - } - } - return nil -} - -func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error { - err := section.MapTo(cfg) - if err != nil { - return err - } - - err = cfg.GetBaseInfo().decorate(prefix, name, section) - if err != nil { - return err - } - - return nil -} - -// TCP -func (cfg *TCPProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*TCPProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if cfg.RemotePort != cmpConf.RemotePort { - return false - } - - return true -} - -func (cfg *TCPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.RemotePort = pMsg.RemotePort -} - -func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *TCPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.RemotePort = cfg.RemotePort -} - -func (cfg *TCPProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - - return -} - -func (cfg *TCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { - return nil -} - -// TCPMux -func (cfg *TCPMuxProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*TCPMuxProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if !reflect.DeepEqual(cfg.DomainConf, cmpConf.DomainConf) { - return false - } - - if cfg.Multiplexer != cmpConf.Multiplexer || cfg.RouteByHTTPUser != cmpConf.RouteByHTTPUser { - return false - } - - return true -} - -func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *TCPMuxProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.CustomDomains = pMsg.CustomDomains - cfg.SubDomain = pMsg.SubDomain - cfg.Multiplexer = pMsg.Multiplexer - cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser -} - -func (cfg *TCPMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.CustomDomains = cfg.CustomDomains - pMsg.SubDomain = cfg.SubDomain - pMsg.Multiplexer = cfg.Multiplexer - pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser -} - -func (cfg *TCPMuxProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - if err = cfg.DomainConf.checkForCli(); err != nil { - return - } - - if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer { - return fmt.Errorf("parse conf error: incorrect multiplexer [%s]", cfg.Multiplexer) - } - - return -} - -func (cfg *TCPMuxProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { - if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer { - return fmt.Errorf("proxy [%s] incorrect multiplexer [%s]", cfg.ProxyName, cfg.Multiplexer) - } - - if cfg.Multiplexer == consts.HTTPConnectTCPMultiplexer && serverCfg.TCPMuxHTTPConnectPort == 0 { - return fmt.Errorf("proxy [%s] type [tcpmux] with multiplexer [httpconnect] requires tcpmux_httpconnect_port configuration", cfg.ProxyName) - } - - if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { - err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) - return - } - - return -} - -// UDP -func (cfg *UDPProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*UDPProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if cfg.RemotePort != cmpConf.RemotePort { - return false - } - - return true -} - -func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *UDPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.RemotePort = pMsg.RemotePort -} - -func (cfg *UDPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.RemotePort = cfg.RemotePort -} - -func (cfg *UDPProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - - return -} - -func (cfg *UDPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { - return nil -} - -// HTTP -func (cfg *HTTPProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*HTTPProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if !reflect.DeepEqual(cfg.DomainConf, cmpConf.DomainConf) { - return false - } - - if !reflect.DeepEqual(cfg.Locations, cmpConf.Locations) || - cfg.HTTPUser != cmpConf.HTTPUser || - cfg.HTTPPwd != cmpConf.HTTPPwd || - cfg.HostHeaderRewrite != cmpConf.HostHeaderRewrite || - cfg.RouteByHTTPUser != cmpConf.RouteByHTTPUser || - !reflect.DeepEqual(cfg.Headers, cmpConf.Headers) { - return false - } - - return true -} - -func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_") - - return nil -} - -func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.CustomDomains = pMsg.CustomDomains - cfg.SubDomain = pMsg.SubDomain - cfg.Locations = pMsg.Locations - cfg.HostHeaderRewrite = pMsg.HostHeaderRewrite - cfg.HTTPUser = pMsg.HTTPUser - cfg.HTTPPwd = pMsg.HTTPPwd - cfg.IpsAllowList = pMsg.IpsAllowList - cfg.Headers = pMsg.Headers - cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser -} - -func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.CustomDomains = cfg.CustomDomains - pMsg.SubDomain = cfg.SubDomain - pMsg.Locations = cfg.Locations - pMsg.HostHeaderRewrite = cfg.HostHeaderRewrite - pMsg.HTTPUser = cfg.HTTPUser - pMsg.HTTPPwd = cfg.HTTPPwd - pMsg.IpsAllowList = cfg.IpsAllowList - pMsg.Headers = cfg.Headers - pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser -} - -func (cfg *HTTPProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - if err = cfg.DomainConf.checkForCli(); err != nil { - return - } - - return -} - -func (cfg *HTTPProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { - if serverCfg.VhostHTTPPort == 0 { - return fmt.Errorf("type [http] not support when vhost_http_port is not set") - } - - if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { - err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) - return - } - - return -} - -// HTTPS -func (cfg *HTTPSProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*HTTPSProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if !reflect.DeepEqual(cfg.DomainConf, cmpConf.DomainConf) { - return false - } - - return true -} - -func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *HTTPSProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.CustomDomains = pMsg.CustomDomains - cfg.SubDomain = pMsg.SubDomain -} - -func (cfg *HTTPSProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.CustomDomains = cfg.CustomDomains - pMsg.SubDomain = cfg.SubDomain -} - -func (cfg *HTTPSProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - if err = cfg.DomainConf.checkForCli(); err != nil { - return - } - - return -} - -func (cfg *HTTPSProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { - if serverCfg.VhostHTTPSPort == 0 { - return fmt.Errorf("type [https] not support when vhost_https_port is not set") - } - - if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { - err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) - return - } - - return -} - -// SUDP -func (cfg *SUDPProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*SUDPProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if cfg.Role != cmpConf.Role || - cfg.Sk != cmpConf.Sk { - return false - } - - return true -} - -func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -// Only for role server. -func (cfg *SUDPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.Sk = pMsg.Sk -} - -func (cfg *SUDPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.Sk = cfg.Sk -} - -func (cfg *SUDPProxyConf) CheckForCli() (err error) { - if err := cfg.BaseProxyConf.checkForCli(); err != nil { - return err - } - - // Add custom logic check if exists - if cfg.Role != "server" { - return fmt.Errorf("role should be 'server'") - } - - return nil -} - -func (cfg *SUDPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { - return nil -} - -// STCP -func (cfg *STCPProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*STCPProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if cfg.Role != cmpConf.Role || - cfg.Sk != cmpConf.Sk { - return false - } - - return true -} - -func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - if cfg.Role == "" { - cfg.Role = "server" - } - - return nil -} - -// Only for role server. -func (cfg *STCPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.Sk = pMsg.Sk -} - -func (cfg *STCPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.Sk = cfg.Sk -} - -func (cfg *STCPProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - if cfg.Role != "server" { - return fmt.Errorf("role should be 'server'") - } - - return -} - -func (cfg *STCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { - return nil -} - -// XTCP -func (cfg *XTCPProxyConf) Compare(cmp ProxyConf) bool { - cmpConf, ok := cmp.(*XTCPProxyConf) - if !ok { - return false - } - - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { - return false - } - - // Add custom logic equal if exists. - if cfg.Role != cmpConf.Role || - cfg.Sk != cmpConf.Sk { - return false - } - - return true -} - -func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - if cfg.Role == "" { - cfg.Role = "server" - } - - return nil -} - -// Only for role server. -func (cfg *XTCPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(pMsg) - - // Add custom logic unmarshal if exists - cfg.Sk = pMsg.Sk -} - -func (cfg *XTCPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(pMsg) - - // Add custom logic marshal if exists - pMsg.Sk = cfg.Sk -} - -func (cfg *XTCPProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - - // Add custom logic check if exists - if cfg.Role != "server" { - return fmt.Errorf("role should be 'server'") - } - - return -} - -func (cfg *XTCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { - return nil -} diff --git a/pkg/config/proxy_test.go b/pkg/config/proxy_test.go deleted file mode 100644 index 68c87b95e78..00000000000 --- a/pkg/config/proxy_test.go +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/fatedier/frp/pkg/consts" - "github.com/stretchr/testify/assert" - - "gopkg.in/ini.v1" -) - -var ( - testLoadOptions = ini.LoadOptions{ - Insensitive: false, - InsensitiveSections: false, - InsensitiveKeys: false, - IgnoreInlineComment: true, - AllowBooleanKeys: true, - } - - testProxyPrefix = "test." -) - -func Test_Proxy_Interface(t *testing.T) { - for name := range proxyConfTypeMap { - NewConfByType(name) - } -} - -func Test_Proxy_UnmarshalFromIni(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - sname string - source []byte - expected ProxyConf - }{ - - { - sname: "ssh", - source: []byte(` - [ssh] - # tcp | udp | http | https | stcp | xtcp, default is tcp - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - bandwidth_limit = 19MB - use_encryption - use_compression - remote_port = 6009 - group = test_group - group_key = 123456 - health_check_type = tcp - health_check_timeout_s = 3 - health_check_max_failed = 3 - health_check_interval_s = 19 - meta_var1 = 123 - meta_var2 = 234`), - expected: &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "ssh", - ProxyType: consts.TCPProxy, - UseCompression: true, - UseEncryption: true, - Group: "test_group", - GroupKey: "123456", - BandwidthLimit: MustBandwidthQuantity("19MB"), - Metas: map[string]string{ - "var1": "123", - "var2": "234", - }, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.TCPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckAddr: "127.0.0.9:29", - }, - }, - RemotePort: 6009, - }, - }, - { - sname: "ssh_random", - source: []byte(` - [ssh_random] - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - remote_port = 9 - `), - expected: &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "ssh_random", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - }, - RemotePort: 9, - }, - }, - { - sname: "dns", - source: []byte(` - [dns] - type = udp - local_ip = 114.114.114.114 - local_port = 59 - remote_port = 6009 - use_encryption - use_compression - `), - expected: &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "dns", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 59, - }, - }, - RemotePort: 6009, - }, - }, - { - sname: "web01", - source: []byte(` - [web01] - type = http - local_ip = 127.0.0.9 - local_port = 89 - use_encryption - use_compression - http_user = admin - http_pwd = admin - subdomain = web01 - custom_domains = web02.yourdomain.com - locations = /,/pic - host_header_rewrite = example.com - header_X-From-Where = frp - health_check_type = http - health_check_url = /status - health_check_interval_s = 19 - health_check_max_failed = 3 - health_check_timeout_s = 3 - `), - expected: &HTTPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "web01", - ProxyType: consts.HTTPProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 89, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.HTTPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckURL: "http://127.0.0.9:89/status", - }, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - Locations: []string{"/", "/pic"}, - HTTPUser: "admin", - HTTPPwd: "admin", - HostHeaderRewrite: "example.com", - Headers: map[string]string{ - "X-From-Where": "frp", - }, - }, - }, - { - sname: "web02", - source: []byte(` - [web02] - type = https - local_ip = 127.0.0.9 - local_port = 8009 - use_encryption - use_compression - subdomain = web01 - custom_domains = web02.yourdomain.com - proxy_protocol_version = v2 - `), - expected: &HTTPSProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "web02", - ProxyType: consts.HTTPSProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 8009, - }, - ProxyProtocolVersion: "v2", - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - }, - }, - { - sname: "secret_tcp", - source: []byte(` - [secret_tcp] - type = stcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - `), - expected: &STCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "secret_tcp", - ProxyType: consts.STCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - }, - Role: "server", - Sk: "abcdefg", - }, - }, - { - sname: "p2p_tcp", - source: []byte(` - [p2p_tcp] - type = xtcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - `), - expected: &XTCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "p2p_tcp", - ProxyType: consts.XTCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - }, - Role: "server", - Sk: "abcdefg", - }, - }, - { - sname: "tcpmuxhttpconnect", - source: []byte(` - [tcpmuxhttpconnect] - type = tcpmux - multiplexer = httpconnect - local_ip = 127.0.0.1 - local_port = 10701 - custom_domains = tunnel1 - `), - expected: &TCPMuxProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcpmuxhttpconnect", - ProxyType: consts.TCPMuxProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 10701, - }, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"tunnel1"}, - SubDomain: "", - }, - Multiplexer: "httpconnect", - }, - }, - } - - for _, c := range testcases { - f, err := ini.LoadSources(testLoadOptions, c.source) - assert.NoError(err) - - proxyType := f.Section(c.sname).Key("type").String() - assert.NotEmpty(proxyType) - - actual := DefaultProxyConf(proxyType) - assert.NotNil(actual) - - err = actual.UnmarshalFromIni(testProxyPrefix, c.sname, f.Section(c.sname)) - assert.NoError(err) - assert.Equal(c.expected, actual) - } -} - -func Test_RangeProxy_UnmarshalFromIni(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - sname string - source []byte - expected map[string]ProxyConf - }{ - { - sname: "range:tcp_port", - source: []byte(` - [range:tcp_port] - type = tcp - local_ip = 127.0.0.9 - local_port = 6010-6011,6019 - remote_port = 6010-6011,6019 - use_encryption = false - use_compression = false - `), - expected: map[string]ProxyConf{ - "tcp_port_0": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcp_port_0", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6010, - }, - }, - RemotePort: 6010, - }, - "tcp_port_1": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcp_port_1", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6011, - }, - }, - RemotePort: 6011, - }, - "tcp_port_2": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcp_port_2", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6019, - }, - }, - RemotePort: 6019, - }, - }, - }, - { - sname: "range:udp_port", - source: []byte(` - [range:udp_port] - type = udp - local_ip = 114.114.114.114 - local_port = 6000,6010-6011 - remote_port = 6000,6010-6011 - use_encryption - use_compression - `), - expected: map[string]ProxyConf{ - "udp_port_0": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "udp_port_0", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6000, - }, - }, - RemotePort: 6000, - }, - "udp_port_1": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "udp_port_1", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6010, - }, - }, - RemotePort: 6010, - }, - "udp_port_2": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "udp_port_2", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6011, - }, - }, - RemotePort: 6011, - }, - }, - }, - } - - for _, c := range testcases { - - f, err := ini.LoadSources(testLoadOptions, c.source) - assert.NoError(err) - - actual := make(map[string]ProxyConf) - s := f.Section(c.sname) - - err = renderRangeProxyTemplates(f, s) - assert.NoError(err) - - f.DeleteSection(ini.DefaultSection) - f.DeleteSection(c.sname) - - for _, section := range f.Sections() { - proxyType := section.Key("type").String() - newsname := section.Name() - - tmp := DefaultProxyConf(proxyType) - err = tmp.UnmarshalFromIni(testProxyPrefix, newsname, section) - assert.NoError(err) - - actual[newsname] = tmp - } - - assert.Equal(c.expected, actual) - } - -} diff --git a/pkg/config/server_test.go b/pkg/config/server_test.go deleted file mode 100644 index 8646ca62038..00000000000 --- a/pkg/config/server_test.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/fatedier/frp/pkg/auth" - plugin "github.com/fatedier/frp/pkg/plugin/server" - - "github.com/stretchr/testify/assert" -) - -func Test_LoadServerCommonConf(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - source []byte - expected ServerCommonConf - }{ - { - source: []byte(` - # [common] is integral section - [common] - bind_addr = 0.0.0.9 - bind_port = 7009 - bind_udp_port = 7008 - kcp_bind_port = 7007 - proxy_bind_addr = 127.0.0.9 - vhost_http_port = 89 - vhost_https_port = 449 - vhost_http_timeout = 69 - tcpmux_httpconnect_port = 1339 - dashboard_addr = 0.0.0.9 - dashboard_port = 7509 - dashboard_user = admin9 - dashboard_pwd = admin9 - enable_prometheus - assets_dir = ./static9 - log_file = ./frps.log9 - log_way = file - log_level = info9 - log_max_days = 39 - disable_log_color = false - detailed_errors_to_client - authentication_method = token - authenticate_heartbeats = false - authenticate_new_work_conns = false - token = 123456789 - oidc_issuer = test9 - oidc_audience = test9 - oidc_skip_expiry_check - oidc_skip_issuer_check - heartbeat_timeout = 99 - user_conn_timeout = 9 - allow_ports = 10-12,99 - max_pool_count = 59 - max_ports_per_client = 9 - tls_only = false - tls_cert_file = server.crt - tls_key_file = server.key - tls_trusted_ca_file = ca.crt - subdomain_host = frps.com - tcp_mux - udp_packet_size = 1509 - [plugin.user-manager] - addr = 127.0.0.1:9009 - path = /handler - ops = Login - [plugin.port-manager] - addr = 127.0.0.1:9009 - path = /handler - ops = NewProxy - tls_verify - `), - expected: ServerCommonConf{ - ServerConfig: auth.ServerConfig{ - BaseConfig: auth.BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - }, - TokenConfig: auth.TokenConfig{ - Token: "123456789", - }, - OidcServerConfig: auth.OidcServerConfig{ - OidcIssuer: "test9", - OidcAudience: "test9", - OidcSkipExpiryCheck: true, - OidcSkipIssuerCheck: true, - }, - }, - BindAddr: "0.0.0.9", - BindPort: 7009, - BindUDPPort: 7008, - KCPBindPort: 7007, - ProxyBindAddr: "127.0.0.9", - VhostHTTPPort: 89, - VhostHTTPSPort: 449, - VhostHTTPTimeout: 69, - TCPMuxHTTPConnectPort: 1339, - DashboardAddr: "0.0.0.9", - DashboardPort: 7509, - DashboardUser: "admin9", - DashboardPwd: "admin9", - EnablePrometheus: true, - AssetsDir: "./static9", - LogFile: "./frps.log9", - LogWay: "file", - LogLevel: "info9", - LogMaxDays: 39, - DisableLogColor: false, - DetailedErrorsToClient: true, - HeartbeatTimeout: 99, - UserConnTimeout: 9, - AllowPorts: map[int]struct{}{ - 10: struct{}{}, - 11: struct{}{}, - 12: struct{}{}, - 99: struct{}{}, - }, - MaxPoolCount: 59, - MaxPortsPerClient: 9, - TLSOnly: true, - TLSCertFile: "server.crt", - TLSKeyFile: "server.key", - TLSTrustedCaFile: "ca.crt", - SubDomainHost: "frps.com", - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - TCPKeepAlive: 7200, - UDPPacketSize: 1509, - - HTTPPlugins: map[string]plugin.HTTPPluginOptions{ - "user-manager": { - Name: "user-manager", - Addr: "127.0.0.1:9009", - Path: "/handler", - Ops: []string{"Login"}, - }, - "port-manager": { - Name: "port-manager", - Addr: "127.0.0.1:9009", - Path: "/handler", - Ops: []string{"NewProxy"}, - TLSVerify: true, - }, - }, - }, - }, - { - source: []byte(` - # [common] is integral section - [common] - bind_addr = 0.0.0.9 - bind_port = 7009 - bind_udp_port = 7008 - `), - expected: ServerCommonConf{ - ServerConfig: auth.ServerConfig{ - BaseConfig: auth.BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - }, - }, - BindAddr: "0.0.0.9", - BindPort: 7009, - BindUDPPort: 7008, - ProxyBindAddr: "0.0.0.9", - VhostHTTPTimeout: 60, - DashboardAddr: "0.0.0.0", - DashboardUser: "", - DashboardPwd: "", - EnablePrometheus: false, - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - DetailedErrorsToClient: true, - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - TCPKeepAlive: 7200, - AllowPorts: make(map[int]struct{}), - MaxPoolCount: 5, - HeartbeatTimeout: 90, - UserConnTimeout: 10, - HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), - UDPPacketSize: 1500, - }, - }, - } - - for _, c := range testcases { - actual, err := UnmarshalServerConfFromIni(c.source) - assert.NoError(err) - actual.Complete() - assert.Equal(c.expected, actual) - } -} diff --git a/pkg/config/source/aggregator.go b/pkg/config/source/aggregator.go new file mode 100644 index 00000000000..58496932f70 --- /dev/null +++ b/pkg/config/source/aggregator.go @@ -0,0 +1,109 @@ +// 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 source + +import ( + "cmp" + "errors" + "fmt" + "maps" + "slices" + "sync" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type Aggregator struct { + mu sync.RWMutex + + configSource *ConfigSource + storeSource *StoreSource +} + +func NewAggregator(configSource *ConfigSource) *Aggregator { + if configSource == nil { + configSource = NewConfigSource() + } + return &Aggregator{ + configSource: configSource, + } +} + +func (a *Aggregator) SetStoreSource(storeSource *StoreSource) { + a.mu.Lock() + defer a.mu.Unlock() + + a.storeSource = storeSource +} + +func (a *Aggregator) ConfigSource() *ConfigSource { + return a.configSource +} + +func (a *Aggregator) StoreSource() *StoreSource { + return a.storeSource +} + +func (a *Aggregator) getSourcesLocked() []Source { + sources := make([]Source, 0, 2) + if a.configSource != nil { + sources = append(sources, a.configSource) + } + if a.storeSource != nil { + sources = append(sources, a.storeSource) + } + return sources +} + +func (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + a.mu.RLock() + entries := a.getSourcesLocked() + a.mu.RUnlock() + + if len(entries) == 0 { + return nil, nil, errors.New("no sources configured") + } + + proxyMap := make(map[string]v1.ProxyConfigurer) + visitorMap := make(map[string]v1.VisitorConfigurer) + + for _, src := range entries { + proxies, visitors, err := src.Load() + if err != nil { + return nil, nil, fmt.Errorf("load source: %w", err) + } + for _, p := range proxies { + proxyMap[p.GetBaseConfig().Name] = p + } + for _, v := range visitors { + visitorMap[v.GetBaseConfig().Name] = v + } + } + proxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap) + return proxies, visitors, nil +} + +func (a *Aggregator) mapsToSortedSlices( + proxyMap map[string]v1.ProxyConfigurer, + visitorMap map[string]v1.VisitorConfigurer, +) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) { + proxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int { + return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name) + }) + visitors := slices.SortedFunc(maps.Values(visitorMap), func(x, y v1.VisitorConfigurer) int { + return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name) + }) + return proxies, visitors +} diff --git a/pkg/config/source/aggregator_test.go b/pkg/config/source/aggregator_test.go new file mode 100644 index 00000000000..380c05cfb4a --- /dev/null +++ b/pkg/config/source/aggregator_test.go @@ -0,0 +1,238 @@ +// 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 source + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// mockProxy creates a TCP proxy config for testing +func mockProxy(name string) v1.ProxyConfigurer { + cfg := &v1.TCPProxyConfig{} + cfg.Name = name + cfg.Type = "tcp" + cfg.LocalPort = 8080 + cfg.RemotePort = 9090 + return cfg +} + +// mockVisitor creates a STCP visitor config for testing +func mockVisitor(name string) v1.VisitorConfigurer { + cfg := &v1.STCPVisitorConfig{} + cfg.Name = name + cfg.Type = "stcp" + cfg.ServerName = "test-server" + return cfg +} + +func newTestStoreSource(t *testing.T) *StoreSource { + t.Helper() + + path := filepath.Join(t.TempDir(), "store.json") + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(t, err) + return storeSource +} + +func newTestAggregator(t *testing.T, storeSource *StoreSource) *Aggregator { + t.Helper() + + configSource := NewConfigSource() + agg := NewAggregator(configSource) + if storeSource != nil { + agg.SetStoreSource(storeSource) + } + return agg +} + +func TestNewAggregator_CreatesConfigSourceWhenNil(t *testing.T) { + require := require.New(t) + + agg := NewAggregator(nil) + require.NotNil(agg) + require.NotNil(agg.ConfigSource()) + require.Nil(agg.StoreSource()) +} + +func TestNewAggregator_WithoutStore(t *testing.T) { + require := require.New(t) + + configSource := NewConfigSource() + agg := NewAggregator(configSource) + require.NotNil(agg) + require.Same(configSource, agg.ConfigSource()) + require.Nil(agg.StoreSource()) +} + +func TestNewAggregator_WithStore(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + configSource := NewConfigSource() + agg := NewAggregator(configSource) + agg.SetStoreSource(storeSource) + + require.Same(configSource, agg.ConfigSource()) + require.Same(storeSource, agg.StoreSource()) +} + +func TestAggregator_SetStoreSource_Overwrite(t *testing.T) { + require := require.New(t) + + agg := newTestAggregator(t, nil) + first := newTestStoreSource(t) + second := newTestStoreSource(t) + + agg.SetStoreSource(first) + require.Same(first, agg.StoreSource()) + + agg.SetStoreSource(second) + require.Same(second, agg.StoreSource()) + + agg.SetStoreSource(nil) + require.Nil(agg.StoreSource()) +} + +func TestAggregator_MergeBySourceOrder(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + agg := newTestAggregator(t, storeSource) + + configSource := agg.ConfigSource() + + configShared := mockProxy("shared").(*v1.TCPProxyConfig) + configShared.LocalPort = 1111 + configOnly := mockProxy("only-in-config").(*v1.TCPProxyConfig) + configOnly.LocalPort = 1112 + + err := configSource.ReplaceAll([]v1.ProxyConfigurer{configShared, configOnly}, nil) + require.NoError(err) + + storeShared := mockProxy("shared").(*v1.TCPProxyConfig) + storeShared.LocalPort = 2222 + storeOnly := mockProxy("only-in-store").(*v1.TCPProxyConfig) + storeOnly.LocalPort = 2223 + err = storeSource.AddProxy(storeShared) + require.NoError(err) + err = storeSource.AddProxy(storeOnly) + require.NoError(err) + + proxies, visitors, err := agg.Load() + require.NoError(err) + require.Len(visitors, 0) + require.Len(proxies, 3) + + var sharedProxy *v1.TCPProxyConfig + for _, p := range proxies { + if p.GetBaseConfig().Name == "shared" { + sharedProxy = p.(*v1.TCPProxyConfig) + break + } + } + require.NotNil(sharedProxy) + require.Equal(2222, sharedProxy.LocalPort) +} + +func TestAggregator_DisabledEntryIsSourceLocalFilter(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + agg := newTestAggregator(t, storeSource) + configSource := agg.ConfigSource() + + lowProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig) + lowProxy.LocalPort = 1111 + err := configSource.ReplaceAll([]v1.ProxyConfigurer{lowProxy}, nil) + require.NoError(err) + + disabled := false + highProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig) + highProxy.LocalPort = 2222 + highProxy.Enabled = &disabled + err = storeSource.AddProxy(highProxy) + require.NoError(err) + + proxies, visitors, err := agg.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Len(visitors, 0) + + proxy := proxies[0].(*v1.TCPProxyConfig) + require.Equal("shared-proxy", proxy.Name) + require.Equal(1111, proxy.LocalPort) +} + +func TestAggregator_VisitorMerge(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + agg := newTestAggregator(t, storeSource) + + err := agg.ConfigSource().ReplaceAll(nil, []v1.VisitorConfigurer{mockVisitor("visitor1")}) + require.NoError(err) + err = storeSource.AddVisitor(mockVisitor("visitor2")) + require.NoError(err) + + _, visitors, err := agg.Load() + require.NoError(err) + require.Len(visitors, 2) +} + +func TestAggregator_Load_ReturnsSortedByName(t *testing.T) { + require := require.New(t) + + agg := newTestAggregator(t, nil) + err := agg.ConfigSource().ReplaceAll( + []v1.ProxyConfigurer{mockProxy("charlie"), mockProxy("alice"), mockProxy("bob")}, + []v1.VisitorConfigurer{mockVisitor("zulu"), mockVisitor("alpha")}, + ) + require.NoError(err) + + proxies, visitors, err := agg.Load() + require.NoError(err) + require.Len(proxies, 3) + require.Equal("alice", proxies[0].GetBaseConfig().Name) + require.Equal("bob", proxies[1].GetBaseConfig().Name) + require.Equal("charlie", proxies[2].GetBaseConfig().Name) + require.Len(visitors, 2) + require.Equal("alpha", visitors[0].GetBaseConfig().Name) + require.Equal("zulu", visitors[1].GetBaseConfig().Name) +} + +func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) { + require := require.New(t) + + agg := newTestAggregator(t, nil) + err := agg.ConfigSource().ReplaceAll([]v1.ProxyConfigurer{mockProxy("ssh")}, nil) + require.NoError(err) + + proxies, _, err := agg.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Equal("ssh", proxies[0].GetBaseConfig().Name) + + proxies[0].GetBaseConfig().Name = "alice.ssh" + + proxies2, _, err := agg.Load() + require.NoError(err) + require.Len(proxies2, 1) + require.Equal("ssh", proxies2[0].GetBaseConfig().Name) +} diff --git a/pkg/config/source/base_source.go b/pkg/config/source/base_source.go new file mode 100644 index 00000000000..ab1f59fe0da --- /dev/null +++ b/pkg/config/source/base_source.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 source + +import ( + "sync" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// baseSource provides shared state and behavior for Source implementations. +// It manages proxy/visitor storage. +// Concrete types (ConfigSource, StoreSource) embed this struct. +type baseSource struct { + mu sync.RWMutex + + proxies map[string]v1.ProxyConfigurer + visitors map[string]v1.VisitorConfigurer +} + +func newBaseSource() baseSource { + return baseSource{ + proxies: make(map[string]v1.ProxyConfigurer), + visitors: make(map[string]v1.VisitorConfigurer), + } +} + +// Load returns all enabled proxy and visitor configurations. +// Configurations with Enabled explicitly set to false are filtered out. +func (s *baseSource) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + proxies := make([]v1.ProxyConfigurer, 0, len(s.proxies)) + for _, p := range s.proxies { + // Filter out disabled proxies (nil or true means enabled) + if enabled := p.GetBaseConfig().Enabled; enabled != nil && !*enabled { + continue + } + proxies = append(proxies, p) + } + + visitors := make([]v1.VisitorConfigurer, 0, len(s.visitors)) + for _, v := range s.visitors { + // Filter out disabled visitors (nil or true means enabled) + if enabled := v.GetBaseConfig().Enabled; enabled != nil && !*enabled { + continue + } + visitors = append(visitors, v) + } + + return cloneConfigurers(proxies, visitors) +} diff --git a/pkg/config/source/base_source_test.go b/pkg/config/source/base_source_test.go new file mode 100644 index 00000000000..34ea9d9d6ae --- /dev/null +++ b/pkg/config/source/base_source_test.go @@ -0,0 +1,48 @@ +package source + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + proxyCfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "proxy1", + Type: "tcp", + }, + } + visitorCfg := &v1.STCPVisitorConfig{ + VisitorBaseConfig: v1.VisitorBaseConfig{ + Name: "visitor1", + Type: "stcp", + }, + } + + err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg}) + require.NoError(err) + + firstProxies, firstVisitors, err := src.Load() + require.NoError(err) + require.Len(firstProxies, 1) + require.Len(firstVisitors, 1) + + // Mutate loaded objects as runtime completion would do. + firstProxies[0].Complete() + firstVisitors[0].Complete() + + secondProxies, secondVisitors, err := src.Load() + require.NoError(err) + require.Len(secondProxies, 1) + require.Len(secondVisitors, 1) + + require.Empty(secondProxies[0].GetBaseConfig().LocalIP) + require.Empty(secondVisitors[0].GetBaseConfig().BindAddr) +} diff --git a/pkg/config/source/clone.go b/pkg/config/source/clone.go new file mode 100644 index 00000000000..cacd5dbb8cb --- /dev/null +++ b/pkg/config/source/clone.go @@ -0,0 +1,43 @@ +// 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 source + +import ( + "fmt" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func cloneConfigurers( + proxies []v1.ProxyConfigurer, + visitors []v1.VisitorConfigurer, +) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + clonedProxies := make([]v1.ProxyConfigurer, 0, len(proxies)) + clonedVisitors := make([]v1.VisitorConfigurer, 0, len(visitors)) + + for _, cfg := range proxies { + if cfg == nil { + return nil, nil, fmt.Errorf("proxy cannot be nil") + } + clonedProxies = append(clonedProxies, cfg.Clone()) + } + for _, cfg := range visitors { + if cfg == nil { + return nil, nil, fmt.Errorf("visitor cannot be nil") + } + clonedVisitors = append(clonedVisitors, cfg.Clone()) + } + return clonedProxies, clonedVisitors, nil +} diff --git a/pkg/config/source/config_source.go b/pkg/config/source/config_source.go new file mode 100644 index 00000000000..95c8cfa1ffc --- /dev/null +++ b/pkg/config/source/config_source.go @@ -0,0 +1,55 @@ +// 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 source + +import v1 "github.com/fatedier/frp/pkg/config/v1" + +// ConfigSource implements Source for in-memory configuration. +// All operations are thread-safe. +type ConfigSource struct { + baseSource +} + +func NewConfigSource() *ConfigSource { + return &ConfigSource{ + baseSource: newBaseSource(), + } +} + +// ReplaceAll replaces all proxy and visitor configurations atomically. +func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error { + s.mu.Lock() + defer s.mu.Unlock() + + nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies)) + for _, p := range proxies { + name, err := validateProxyName(p) + if err != nil { + return err + } + nextProxies[name] = p + } + nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors)) + for _, v := range visitors { + name, err := validateVisitorName(v) + if err != nil { + return err + } + nextVisitors[name] = v + } + s.proxies = nextProxies + s.visitors = nextVisitors + return nil +} diff --git a/pkg/config/source/config_source_test.go b/pkg/config/source/config_source_test.go new file mode 100644 index 00000000000..793284e19d9 --- /dev/null +++ b/pkg/config/source/config_source_test.go @@ -0,0 +1,173 @@ +// 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 source + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestNewConfigSource(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + require.NotNil(src) +} + +func TestConfigSource_ReplaceAll(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + err := src.ReplaceAll( + []v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")}, + []v1.VisitorConfigurer{mockVisitor("visitor1")}, + ) + require.NoError(err) + + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 2) + require.Len(visitors, 1) + + // ReplaceAll again should replace everything + err = src.ReplaceAll( + []v1.ProxyConfigurer{mockProxy("proxy3")}, + nil, + ) + require.NoError(err) + + proxies, visitors, err = src.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Len(visitors, 0) + require.Equal("proxy3", proxies[0].GetBaseConfig().Name) + + // ReplaceAll with nil proxy should fail + err = src.ReplaceAll([]v1.ProxyConfigurer{nil}, nil) + require.Error(err) + + // ReplaceAll with empty name proxy should fail + err = src.ReplaceAll([]v1.ProxyConfigurer{&v1.TCPProxyConfig{}}, nil) + require.Error(err) +} + +func TestConfigSource_Load(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + err := src.ReplaceAll( + []v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")}, + []v1.VisitorConfigurer{mockVisitor("visitor1")}, + ) + require.NoError(err) + + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 2) + require.Len(visitors, 1) +} + +// TestConfigSource_Load_FiltersDisabled verifies that Load() filters out +// proxies and visitors with Enabled explicitly set to false. +func TestConfigSource_Load_FiltersDisabled(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + disabled := false + enabled := true + + // Create enabled proxy (nil Enabled = enabled by default) + enabledProxy := mockProxy("enabled-proxy") + + // Create disabled proxy + disabledProxy := &v1.TCPProxyConfig{} + disabledProxy.Name = "disabled-proxy" + disabledProxy.Type = "tcp" + disabledProxy.Enabled = &disabled + + // Create explicitly enabled proxy + explicitEnabledProxy := &v1.TCPProxyConfig{} + explicitEnabledProxy.Name = "explicit-enabled-proxy" + explicitEnabledProxy.Type = "tcp" + explicitEnabledProxy.Enabled = &enabled + + // Create enabled visitor (nil Enabled = enabled by default) + enabledVisitor := mockVisitor("enabled-visitor") + + // Create disabled visitor + disabledVisitor := &v1.STCPVisitorConfig{} + disabledVisitor.Name = "disabled-visitor" + disabledVisitor.Type = "stcp" + disabledVisitor.Enabled = &disabled + + err := src.ReplaceAll( + []v1.ProxyConfigurer{enabledProxy, disabledProxy, explicitEnabledProxy}, + []v1.VisitorConfigurer{enabledVisitor, disabledVisitor}, + ) + require.NoError(err) + + // Load should filter out disabled configs + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 2, "Should have 2 enabled proxies") + require.Len(visitors, 1, "Should have 1 enabled visitor") + + // Verify the correct proxies are returned + proxyNames := make([]string, 0, len(proxies)) + for _, p := range proxies { + proxyNames = append(proxyNames, p.GetBaseConfig().Name) + } + require.Contains(proxyNames, "enabled-proxy") + require.Contains(proxyNames, "explicit-enabled-proxy") + require.NotContains(proxyNames, "disabled-proxy") + + // Verify the correct visitor is returned + require.Equal("enabled-visitor", visitors[0].GetBaseConfig().Name) +} + +func TestConfigSource_ReplaceAll_DoesNotApplyRuntimeDefaults(t *testing.T) { + require := require.New(t) + + src := NewConfigSource() + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy1" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor1" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server1" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg}) + require.NoError(err) + + proxies, visitors, err := src.Load() + require.NoError(err) + require.Len(proxies, 1) + require.Len(visitors, 1) + require.Empty(proxies[0].GetBaseConfig().LocalIP) + require.Empty(visitors[0].GetBaseConfig().BindAddr) + require.Empty(visitors[0].(*v1.XTCPVisitorConfig).Protocol) +} diff --git a/pkg/config/source/source.go b/pkg/config/source/source.go new file mode 100644 index 00000000000..a6cff226122 --- /dev/null +++ b/pkg/config/source/source.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 source + +import ( + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +// Source is the interface for configuration sources. +// A Source provides proxy and visitor configurations from various backends. +// Aggregator currently uses the built-in config source as base and an optional +// store source as higher-priority overlay. +type Source interface { + // Load loads the proxy and visitor configurations from this source. + // Returns the loaded configurations and any error encountered. + // A disabled entry in one source is source-local filtering, not a cross-source + // tombstone for entries from lower-priority sources. + // + // Error handling contract with Aggregator: + // - When err is nil, returned slices are consumed. + // - When err is non-nil, Aggregator aborts the merge and returns the error. + // - To publish best-effort or partial results, return those results with + // err set to nil. + Load() (proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, err error) +} diff --git a/pkg/config/source/store.go b/pkg/config/source/store.go new file mode 100644 index 00000000000..f568f8853c0 --- /dev/null +++ b/pkg/config/source/store.go @@ -0,0 +1,349 @@ +// 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 source + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/jsonx" +) + +type StoreSourceConfig struct { + Path string `json:"path"` +} + +type storeData struct { + Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"` + Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"` +} + +type StoreSource struct { + baseSource + config StoreSourceConfig +} + +var ( + ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") +) + +const ( + storeKindProxy = "proxy" + storeKindVisitor = "visitor" +) + +func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) { + if cfg.Path == "" { + return nil, fmt.Errorf("path is required") + } + + s := &StoreSource{ + baseSource: newBaseSource(), + config: cfg, + } + + if err := s.loadFromFile(); err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to load existing data: %w", err) + } + } + + return s, nil +} + +func (s *StoreSource) loadFromFile() error { + s.mu.Lock() + defer s.mu.Unlock() + return s.loadFromFileUnlocked() +} + +func (s *StoreSource) loadFromFileUnlocked() error { + data, err := os.ReadFile(s.config.Path) + if err != nil { + return err + } + + type rawStoreData struct { + Proxies []jsonx.RawMessage `json:"proxies,omitempty"` + Visitors []jsonx.RawMessage `json:"visitors,omitempty"` + } + stored := rawStoreData{} + if err := jsonx.Unmarshal(data, &stored); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + + s.proxies = make(map[string]v1.ProxyConfigurer) + s.visitors = make(map[string]v1.VisitorConfigurer) + + for i, proxyData := range stored.Proxies { + proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{ + DisallowUnknownFields: false, + }) + if err != nil { + return fmt.Errorf("failed to decode proxy at index %d: %w", i, err) + } + name := proxyCfg.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("proxy name cannot be empty") + } + s.proxies[name] = proxyCfg + } + + for i, visitorData := range stored.Visitors { + visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{ + DisallowUnknownFields: false, + }) + if err != nil { + return fmt.Errorf("failed to decode visitor at index %d: %w", i, err) + } + name := visitorCfg.GetBaseConfig().Name + if name == "" { + return fmt.Errorf("visitor name cannot be empty") + } + s.visitors[name] = visitorCfg + } + + return nil +} + +func (s *StoreSource) saveToFileUnlocked() error { + stored := storeData{ + Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)), + Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)), + } + + for _, p := range s.proxies { + stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p}) + } + for _, v := range s.visitors { + stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v}) + } + + data, err := jsonx.MarshalIndent(stored, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + + dir := filepath.Dir(s.config.Path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + tmpPath := s.config.Path + ".tmp" + + f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(tmpPath) + return fmt.Errorf("failed to write temp file: %w", err) + } + + if err := f.Sync(); err != nil { + f.Close() + os.Remove(tmpPath) + return fmt.Errorf("failed to sync temp file: %w", err) + } + + if err := f.Close(); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to close temp file: %w", err) + } + + if err := os.Rename(tmpPath, s.config.Path); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to rename temp file: %w", err) + } + + return nil +} + +func (s *StoreSource) persistOrRollbackUnlocked(rollback func()) error { + if err := s.saveToFileUnlocked(); err != nil { + rollback() + return fmt.Errorf("failed to persist: %w", err) + } + return nil +} + +// Store map selectors return the target map for generic helpers. +func proxyStoreEntries(s *StoreSource) map[string]v1.ProxyConfigurer { + return s.proxies +} + +func visitorStoreEntries(s *StoreSource) map[string]v1.VisitorConfigurer { + return s.visitors +} + +// Store entry helpers share mutation, persistence, and rollback for proxy and visitor maps. +// T is intentionally limited by callers to v1.ProxyConfigurer or v1.VisitorConfigurer. +func addStoreEntry[T any]( + s *StoreSource, + entriesFn func(*StoreSource) map[string]T, + kind string, + name string, + value T, +) error { + s.mu.Lock() + defer s.mu.Unlock() + + entries := entriesFn(s) + if _, exists := entries[name]; exists { + return fmt.Errorf("%w: %s %q", ErrAlreadyExists, kind, name) + } + + entries[name] = value + return s.persistOrRollbackUnlocked(func() { + delete(entries, name) + }) +} + +func updateStoreEntry[T any]( + s *StoreSource, + entriesFn func(*StoreSource) map[string]T, + kind string, + name string, + value T, +) error { + s.mu.Lock() + defer s.mu.Unlock() + + entries := entriesFn(s) + old, exists := entries[name] + if !exists { + return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name) + } + + entries[name] = value + return s.persistOrRollbackUnlocked(func() { + entries[name] = old + }) +} + +func removeStoreEntry[T any]( + s *StoreSource, + entriesFn func(*StoreSource) map[string]T, + kind string, + name string, +) error { + if name == "" { + return fmt.Errorf("%s name cannot be empty", kind) + } + + s.mu.Lock() + defer s.mu.Unlock() + + entries := entriesFn(s) + old, exists := entries[name] + if !exists { + return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name) + } + + delete(entries, name) + return s.persistOrRollbackUnlocked(func() { + entries[name] = old + }) +} + +func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error { + name, err := validateProxyName(proxy) + if err != nil { + return err + } + return addStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy) +} + +func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error { + name, err := validateProxyName(proxy) + if err != nil { + return err + } + return updateStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy) +} + +func (s *StoreSource) RemoveProxy(name string) error { + return removeStoreEntry(s, proxyStoreEntries, storeKindProxy, name) +} + +func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer { + s.mu.RLock() + defer s.mu.RUnlock() + + p, exists := s.proxies[name] + if !exists { + return nil + } + return p +} + +func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error { + name, err := validateVisitorName(visitor) + if err != nil { + return err + } + return addStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor) +} + +func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error { + name, err := validateVisitorName(visitor) + if err != nil { + return err + } + return updateStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor) +} + +func (s *StoreSource) RemoveVisitor(name string) error { + return removeStoreEntry(s, visitorStoreEntries, storeKindVisitor, name) +} + +func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer { + s.mu.RLock() + defer s.mu.RUnlock() + + v, exists := s.visitors[name] + if !exists { + return nil + } + return v +} + +func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + result := make([]v1.ProxyConfigurer, 0, len(s.proxies)) + for _, p := range s.proxies { + result = append(result, p) + } + return result, nil +} + +func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + result := make([]v1.VisitorConfigurer, 0, len(s.visitors)) + for _, v := range s.visitors { + result = append(result, v) + } + return result, nil +} diff --git a/pkg/config/source/store_test.go b/pkg/config/source/store_test.go new file mode 100644 index 00000000000..8bb107c531f --- /dev/null +++ b/pkg/config/source/store_test.go @@ -0,0 +1,217 @@ +// 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 source + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/jsonx" +) + +func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) { + require := require.New(t) + + path := filepath.Join(t.TempDir(), "store.json") + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy1" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor1" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server1" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + err = storeSource.AddProxy(proxyCfg) + require.NoError(err) + err = storeSource.AddVisitor(visitorCfg) + require.NoError(err) + + gotProxy := storeSource.GetProxy("proxy1") + require.NotNil(gotProxy) + require.Empty(gotProxy.GetBaseConfig().LocalIP) + + gotVisitor := storeSource.GetVisitor("visitor1") + require.NotNil(gotVisitor) + require.Empty(gotVisitor.GetBaseConfig().BindAddr) + require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol) +} + +func TestStoreSource_UpdateAndRemoveProxyAndVisitor(t *testing.T) { + require := require.New(t) + + storeSource := newTestStoreSource(t) + + proxyCfg := mockProxy("proxy1") + visitorCfg := mockVisitor("visitor1") + + require.NoError(storeSource.AddProxy(proxyCfg)) + require.NoError(storeSource.AddVisitor(visitorCfg)) + require.ErrorIs(storeSource.AddProxy(proxyCfg), ErrAlreadyExists) + require.ErrorIs(storeSource.AddVisitor(visitorCfg), ErrAlreadyExists) + require.ErrorContains(storeSource.RemoveProxy(""), "proxy name cannot be empty") + require.ErrorContains(storeSource.RemoveVisitor(""), "visitor name cannot be empty") + + updatedProxy := mockProxy("proxy1").(*v1.TCPProxyConfig) + updatedProxy.RemotePort = 19090 + require.NoError(storeSource.UpdateProxy(updatedProxy)) + require.Equal(19090, storeSource.GetProxy("proxy1").(*v1.TCPProxyConfig).RemotePort) + + updatedVisitor := mockVisitor("visitor1").(*v1.STCPVisitorConfig) + updatedVisitor.ServerName = "updated-server" + require.NoError(storeSource.UpdateVisitor(updatedVisitor)) + require.Equal("updated-server", storeSource.GetVisitor("visitor1").(*v1.STCPVisitorConfig).ServerName) + + require.NoError(storeSource.RemoveProxy("proxy1")) + require.Nil(storeSource.GetProxy("proxy1")) + require.ErrorIs(storeSource.RemoveProxy("proxy1"), ErrNotFound) + + require.NoError(storeSource.RemoveVisitor("visitor1")) + require.Nil(storeSource.GetVisitor("visitor1")) + require.ErrorIs(storeSource.RemoveVisitor("visitor1"), ErrNotFound) + + require.ErrorIs(storeSource.UpdateProxy(updatedProxy), ErrNotFound) + require.ErrorIs(storeSource.UpdateVisitor(updatedVisitor), ErrNotFound) +} + +func TestStoreSource_MutationRollsBackOnPersistFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("chmod does not make directories unwritable on Windows") + } + if os.Getuid() == 0 { + t.Skip("chmod does not block writes for uid 0") + } + + require := require.New(t) + + dir := t.TempDir() + path := filepath.Join(dir, "store.json") + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + proxyCfg := mockProxy("proxy1") + visitorCfg := mockVisitor("visitor1") + originalRemotePort := proxyCfg.(*v1.TCPProxyConfig).RemotePort + originalServerName := visitorCfg.(*v1.STCPVisitorConfig).ServerName + require.NoError(storeSource.AddProxy(proxyCfg)) + require.NoError(storeSource.AddVisitor(visitorCfg)) + + require.NoError(os.Chmod(dir, 0o500)) + t.Cleanup(func() { + _ = os.Chmod(dir, 0o700) + }) + + requirePersistError := func(err error) { + t.Helper() + require.Error(err) + require.ErrorContains(err, "failed to persist") + require.NotErrorIs(err, ErrAlreadyExists) + require.NotErrorIs(err, ErrNotFound) + } + + requirePersistError(storeSource.AddProxy(mockProxy("proxy2"))) + require.Nil(storeSource.GetProxy("proxy2")) + + updatedProxy := mockProxy("proxy1").(*v1.TCPProxyConfig) + updatedProxy.RemotePort = 19090 + requirePersistError(storeSource.UpdateProxy(updatedProxy)) + require.Equal(originalRemotePort, storeSource.GetProxy("proxy1").(*v1.TCPProxyConfig).RemotePort) + + requirePersistError(storeSource.RemoveProxy("proxy1")) + require.NotNil(storeSource.GetProxy("proxy1")) + + requirePersistError(storeSource.AddVisitor(mockVisitor("visitor2"))) + require.Nil(storeSource.GetVisitor("visitor2")) + + updatedVisitor := mockVisitor("visitor1").(*v1.STCPVisitorConfig) + updatedVisitor.ServerName = "updated-server" + requirePersistError(storeSource.UpdateVisitor(updatedVisitor)) + require.Equal(originalServerName, storeSource.GetVisitor("visitor1").(*v1.STCPVisitorConfig).ServerName) + + requirePersistError(storeSource.RemoveVisitor("visitor1")) + require.NotNil(storeSource.GetVisitor("visitor1")) +} + +func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) { + require := require.New(t) + + path := filepath.Join(t.TempDir(), "store.json") + + proxyCfg := &v1.TCPProxyConfig{} + proxyCfg.Name = "proxy1" + proxyCfg.Type = "tcp" + proxyCfg.LocalPort = 10080 + + visitorCfg := &v1.XTCPVisitorConfig{} + visitorCfg.Name = "visitor1" + visitorCfg.Type = "xtcp" + visitorCfg.ServerName = "server1" + visitorCfg.SecretKey = "secret" + visitorCfg.BindPort = 10081 + + stored := storeData{ + Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}}, + Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}}, + } + data, err := jsonx.Marshal(stored) + require.NoError(err) + err = os.WriteFile(path, data, 0o600) + require.NoError(err) + + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + gotProxy := storeSource.GetProxy("proxy1") + require.NotNil(gotProxy) + require.Empty(gotProxy.GetBaseConfig().LocalIP) + + gotVisitor := storeSource.GetVisitor("visitor1") + require.NotNil(gotVisitor) + require.Empty(gotVisitor.GetBaseConfig().BindAddr) + require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol) +} + +func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) { + require := require.New(t) + + path := filepath.Join(t.TempDir(), "store.json") + raw := []byte(`{ + "proxies": [ + {"name":"proxy1","type":"tcp","localPort":10080,"unexpected":"value"} + ], + "visitors": [ + {"name":"visitor1","type":"xtcp","serverName":"server1","secretKey":"secret","bindPort":10081,"unexpected":"value"} + ] + }`) + err := os.WriteFile(path, raw, 0o600) + require.NoError(err) + + storeSource, err := NewStoreSource(StoreSourceConfig{Path: path}) + require.NoError(err) + + require.NotNil(storeSource.GetProxy("proxy1")) + require.NotNil(storeSource.GetVisitor("visitor1")) +} diff --git a/pkg/config/source/validation.go b/pkg/config/source/validation.go new file mode 100644 index 00000000000..55bc42204d8 --- /dev/null +++ b/pkg/config/source/validation.go @@ -0,0 +1,43 @@ +// 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 source + +import ( + "fmt" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func validateProxyName(proxy v1.ProxyConfigurer) (string, error) { + if proxy == nil { + return "", fmt.Errorf("proxy cannot be nil") + } + name := proxy.GetBaseConfig().Name + if name == "" { + return "", fmt.Errorf("proxy name cannot be empty") + } + return name, nil +} + +func validateVisitorName(visitor v1.VisitorConfigurer) (string, error) { + if visitor == nil { + return "", fmt.Errorf("visitor cannot be nil") + } + name := visitor.GetBaseConfig().Name + if name == "" { + return "", fmt.Errorf("visitor name cannot be empty") + } + return name, nil +} diff --git a/pkg/config/template.go b/pkg/config/template.go new file mode 100644 index 00000000000..16fe069f2ff --- /dev/null +++ b/pkg/config/template.go @@ -0,0 +1,52 @@ +// Copyright 2024 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 config + +import ( + "fmt" + + "github.com/fatedier/frp/pkg/util/util" +) + +type NumberPair struct { + First int64 + Second int64 +} + +func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, error) { + firstRangeNumbers, err := util.ParseRangeNumbers(firstRangeStr) + if err != nil { + return nil, err + } + secondRangeNumbers, err := util.ParseRangeNumbers(secondRangeStr) + if err != nil { + return nil, err + } + if len(firstRangeNumbers) != len(secondRangeNumbers) { + return nil, fmt.Errorf("first and second range numbers are not in pairs") + } + pairs := make([]NumberPair, 0, len(firstRangeNumbers)) + for i := range firstRangeNumbers { + pairs = append(pairs, NumberPair{ + First: firstRangeNumbers[i], + Second: secondRangeNumbers[i], + }) + } + return pairs, nil +} + +func parseNumberRange(firstRangeStr string) ([]int64, error) { + return util.ParseRangeNumbers(firstRangeStr) +} diff --git a/pkg/config/types.go b/pkg/config/types/types.go similarity index 50% rename from pkg/config/types.go rename to pkg/config/types/types.go index 062cc7462cf..972908ed3bb 100644 --- a/pkg/config/types.go +++ b/pkg/config/types/types.go @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package types import ( "encoding/json" "errors" + "fmt" "strconv" "strings" ) @@ -24,6 +25,9 @@ import ( const ( MB = 1024 * 1024 KB = 1024 + + BandwidthLimitModeClient = "client" + BandwidthLimitModeServer = "server" ) type BandwidthQuantity struct { @@ -41,15 +45,6 @@ func NewBandwidthQuantity(s string) (BandwidthQuantity, error) { return q, nil } -func MustBandwidthQuantity(s string) BandwidthQuantity { - q := BandwidthQuantity{} - err := q.UnmarshalString(s) - if err != nil { - panic(err) - } - return q -} - func (q *BandwidthQuantity) Equal(u *BandwidthQuantity) bool { if q == nil && u == nil { return true @@ -75,23 +70,18 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error { f float64 err error ) - if strings.HasSuffix(s, "MB") { + if fstr, ok := strings.CutSuffix(s, "MB"); ok { base = MB - fstr := strings.TrimSuffix(s, "MB") f, err = strconv.ParseFloat(fstr, 64) - if err != nil { - return err - } - } else if strings.HasSuffix(s, "KB") { + } else if fstr, ok := strings.CutSuffix(s, "KB"); ok { base = KB - fstr := strings.TrimSuffix(s, "KB") f, err = strconv.ParseFloat(fstr, 64) - if err != nil { - return err - } } else { return errors.New("unit not support") } + if err != nil { + return err + } q.s = s q.i = int64(f * float64(base)) @@ -119,3 +109,65 @@ func (q *BandwidthQuantity) MarshalJSON() ([]byte, error) { func (q *BandwidthQuantity) Bytes() int64 { return q.i } + +type PortsRange struct { + Start int `json:"start,omitempty"` + End int `json:"end,omitempty"` + Single int `json:"single,omitempty"` +} + +type PortsRangeSlice []PortsRange + +func (p PortsRangeSlice) String() string { + if len(p) == 0 { + return "" + } + strs := []string{} + for _, v := range p { + if v.Single > 0 { + strs = append(strs, strconv.Itoa(v.Single)) + } else { + strs = append(strs, strconv.Itoa(v.Start)+"-"+strconv.Itoa(v.End)) + } + } + return strings.Join(strs, ",") +} + +// the format of str is like "1000-2000,3000,4000-5000" +func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) { + str = strings.TrimSpace(str) + out := []PortsRange{} + numRanges := strings.SplitSeq(str, ",") + for numRangeStr := range numRanges { + // 1000-2000 or 2001 + numArray := strings.Split(numRangeStr, "-") + // length: only 1 or 2 is correct + rangeType := len(numArray) + switch rangeType { + case 1: + // single number + singleNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + if err != nil { + return nil, fmt.Errorf("range number is invalid, %v", err) + } + out = append(out, PortsRange{Single: int(singleNum)}) + case 2: + // range numbers + minNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + if err != nil { + return nil, fmt.Errorf("range number is invalid, %v", err) + } + maxNum, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) + if err != nil { + return nil, fmt.Errorf("range number is invalid, %v", err) + } + if maxNum < minNum { + return nil, fmt.Errorf("range number is invalid") + } + out = append(out, PortsRange{Start: int(minNum), End: int(maxNum)}) + default: + return nil, fmt.Errorf("range number is invalid") + } + } + return out, nil +} diff --git a/pkg/config/types/types_test.go b/pkg/config/types/types_test.go new file mode 100644 index 00000000000..c05ac9eef66 --- /dev/null +++ b/pkg/config/types/types_test.go @@ -0,0 +1,97 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// 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 types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +type Wrap struct { + B BandwidthQuantity `json:"b"` + Int int `json:"int"` +} + +func TestBandwidthQuantity(t *testing.T) { + require := require.New(t) + + var w Wrap + err := json.Unmarshal([]byte(`{"b":"1KB","int":5}`), &w) + require.NoError(err) + require.EqualValues(1*KB, w.B.Bytes()) + + buf, err := json.Marshal(&w) + require.NoError(err) + require.Equal(`{"b":"1KB","int":5}`, string(buf)) +} + +func TestBandwidthQuantity_MB(t *testing.T) { + require := require.New(t) + + var w Wrap + err := json.Unmarshal([]byte(`{"b":"2MB","int":1}`), &w) + require.NoError(err) + require.EqualValues(2*MB, w.B.Bytes()) + + buf, err := json.Marshal(&w) + require.NoError(err) + require.Equal(`{"b":"2MB","int":1}`, string(buf)) +} + +func TestBandwidthQuantity_InvalidUnit(t *testing.T) { + var w Wrap + err := json.Unmarshal([]byte(`{"b":"1GB","int":1}`), &w) + require.Error(t, err) +} + +func TestBandwidthQuantity_InvalidNumber(t *testing.T) { + var w Wrap + err := json.Unmarshal([]byte(`{"b":"abcKB","int":1}`), &w) + require.Error(t, err) +} + +func TestPortsRangeSlice2String(t *testing.T) { + require := require.New(t) + + ports := []PortsRange{ + { + Start: 1000, + End: 2000, + }, + { + Single: 3000, + }, + } + str := PortsRangeSlice(ports).String() + require.Equal("1000-2000,3000", str) +} + +func TestNewPortsRangeSliceFromString(t *testing.T) { + require := require.New(t) + + ports, err := NewPortsRangeSliceFromString("1000-2000,3000") + require.NoError(err) + require.Equal([]PortsRange{ + { + Start: 1000, + End: 2000, + }, + { + Single: 3000, + }, + }, ports) +} diff --git a/pkg/config/v1/api.go b/pkg/config/v1/api.go new file mode 100644 index 00000000000..0991fc93513 --- /dev/null +++ b/pkg/config/v1/api.go @@ -0,0 +1,19 @@ +// Copyright 2023 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 v1 + +type APIMetadata struct { + Version string `json:"version"` +} diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go new file mode 100644 index 00000000000..05d02c944af --- /dev/null +++ b/pkg/config/v1/client.go @@ -0,0 +1,246 @@ +// Copyright 2023 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 v1 + +import ( + "os" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/util/util" +) + +type ClientConfig struct { + ClientCommonConfig + + Proxies []TypedProxyConfig `json:"proxies,omitempty"` + Visitors []TypedVisitorConfig `json:"visitors,omitempty"` +} + +type ClientCommonConfig struct { + APIMetadata + + Auth AuthClientConfig `json:"auth,omitempty"` + // User specifies a prefix for proxy names to distinguish them from other + // clients. If this value is not "", proxy names will automatically be + // changed to "{user}.{proxy_name}". + User string `json:"user,omitempty"` + // ClientID uniquely identifies this frpc instance. + ClientID string `json:"clientID,omitempty"` + + // ServerAddr specifies the address of the server to connect to. By + // default, this value is "0.0.0.0". + ServerAddr string `json:"serverAddr,omitempty"` + // ServerPort specifies the port to connect to the server on. By default, + // this value is 7000. + ServerPort int `json:"serverPort,omitempty"` + // STUN server to help penetrate NAT hole. + NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"` + // DNSServer specifies a DNS server address for FRPC to use. If this value + // is "", the default DNS will be used. + DNSServer string `json:"dnsServer,omitempty"` + // LoginFailExit controls whether or not the client should exit after a + // failed login attempt. If false, the client will retry until a login + // attempt succeeds. By default, this value is true. + LoginFailExit *bool `json:"loginFailExit,omitempty"` + // Start specifies a set of enabled proxies by name. If this set is empty, + // all supplied proxies are enabled. By default, this value is an empty + // set. + Start []string `json:"start,omitempty"` + + Log LogConfig `json:"log,omitempty"` + WebServer WebServerConfig `json:"webServer,omitempty"` + Transport ClientTransportConfig `json:"transport,omitempty"` + VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"` + + // FeatureGates specifies a set of feature gates to enable or disable. + // This can be used to enable alpha/beta features or disable default features. + FeatureGates map[string]bool `json:"featureGates,omitempty"` + + // UDPPacketSize specifies the udp packet size + // By default, this value is 1500 + UDPPacketSize int64 `json:"udpPacketSize,omitempty"` + // Client metadata info + Metadatas map[string]string `json:"metadatas,omitempty"` + + // Include other config files for proxies. + IncludeConfigFiles []string `json:"includes,omitempty"` + + // Store config enables the built-in store source (not configurable via sources list). + Store StoreConfig `json:"store,omitempty"` +} + +func (c *ClientCommonConfig) Complete() error { + c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0") + c.ServerPort = util.EmptyOr(c.ServerPort, 7000) + c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true)) + c.NatHoleSTUNServer = util.EmptyOr(c.NatHoleSTUNServer, "stun.easyvoip.com:3478") + + if err := c.Auth.Complete(); err != nil { + return err + } + c.Log.Complete() + c.Transport.Complete() + c.WebServer.Complete() + + c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) + return nil +} + +type ClientTransportConfig struct { + // Protocol specifies the protocol to use when interacting with the server. + // Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value + // is "tcp". + Protocol string `json:"protocol,omitempty"` + // WireProtocol specifies the frpc/frps internal wire protocol version. + // Valid values are "v1" and "v2". By default, this value is "v1". + WireProtocol string `json:"wireProtocol,omitempty"` + // The maximum amount of time a dial to server will wait for a connect to complete. + DialServerTimeout int64 `json:"dialServerTimeout,omitempty"` + // DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. + // If negative, keep-alive probes are disabled. + DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"` + // ConnectServerLocalIP specifies the address of the client bind when it connect to server. + // Note: This value only use in TCP/Websocket protocol. Not support in KCP protocol. + ConnectServerLocalIP string `json:"connectServerLocalIP,omitempty"` + // ProxyURL specifies a proxy address to connect to the server through. If + // this value is "", the server will be connected to directly. By default, + // this value is read from the "http_proxy" environment variable. + ProxyURL string `json:"proxyURL,omitempty"` + // PoolCount specifies the number of connections the client will make to + // the server in advance. + PoolCount int `json:"poolCount,omitempty"` + // TCPMux toggles TCP stream multiplexing. This allows multiple requests + // from a client to share a single TCP connection. If this value is true, + // the server must have TCP multiplexing enabled as well. By default, this + // value is true. + TCPMux *bool `json:"tcpMux,omitempty"` + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. + // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. + TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` + // QUIC protocol options. + QUIC QUICOptions `json:"quic,omitempty"` + // HeartBeatInterval specifies at what interval heartbeats are sent to the + // server, in seconds. It is not recommended to change this value. By + // default, this value is 30. Set negative value to disable it. + HeartbeatInterval int64 `json:"heartbeatInterval,omitempty"` + // HeartBeatTimeout specifies the maximum allowed heartbeat response delay + // before the connection is terminated, in seconds. It is not recommended + // to change this value. By default, this value is 90. Set negative value to disable it. + HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"` + // TLS specifies TLS settings for the connection to the server. + TLS TLSClientConfig `json:"tls,omitempty"` +} + +func (c *ClientTransportConfig) Complete() { + c.Protocol = util.EmptyOr(c.Protocol, "tcp") + c.WireProtocol = util.EmptyOr(c.WireProtocol, "v1") + c.DialServerTimeout = util.EmptyOr(c.DialServerTimeout, 10) + c.DialServerKeepAlive = util.EmptyOr(c.DialServerKeepAlive, 7200) + c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy")) + c.PoolCount = util.EmptyOr(c.PoolCount, 1) + c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true)) + c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30) + if lo.FromPtr(c.TCPMux) { + // If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux. + c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1) + c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1) + } else { + c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30) + c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90) + } + c.QUIC.Complete() + c.TLS.Complete() +} + +type TLSClientConfig struct { + // TLSEnable specifies whether or not TLS should be used when communicating + // with the server. If "tls.certFile" and "tls.keyFile" are valid, + // client will load the supplied tls configuration. + // Since v0.50.0, the default value has been changed to true, and tls is enabled by default. + Enable *bool `json:"enable,omitempty"` + // If DisableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the + // first custom byte when tls is enabled. + // Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default. + DisableCustomTLSFirstByte *bool `json:"disableCustomTLSFirstByte,omitempty"` + + TLSConfig +} + +func (c *TLSClientConfig) Complete() { + c.Enable = util.EmptyOr(c.Enable, lo.ToPtr(true)) + c.DisableCustomTLSFirstByte = util.EmptyOr(c.DisableCustomTLSFirstByte, lo.ToPtr(true)) +} + +type AuthClientConfig struct { + // Method specifies what authentication method to use to + // authenticate frpc with frps. If "token" is specified - token will be + // read into login message. If "oidc" is specified - OIDC (Open ID Connect) + // token will be issued using OIDC settings. By default, this value is "token". + Method AuthMethod `json:"method,omitempty"` + // Specify whether to include auth info in additional scope. + // Current supported scopes are: "HeartBeats", "NewWorkConns". + AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` + // Token specifies the authorization token used to create keys to be sent + // to the server. The server must have a matching token for authorization + // to succeed. By default, this value is "". + Token string `json:"token,omitempty"` + // TokenSource specifies a dynamic source for the authorization token. + // This is mutually exclusive with Token field. + TokenSource *ValueSource `json:"tokenSource,omitempty"` + OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` +} + +func (c *AuthClientConfig) Complete() error { + c.Method = util.EmptyOr(c.Method, "token") + return nil +} + +type AuthOIDCClientConfig struct { + // ClientID specifies the client ID to use to get a token in OIDC authentication. + ClientID string `json:"clientID,omitempty"` + // ClientSecret specifies the client secret to use to get a token in OIDC + // authentication. + ClientSecret string `json:"clientSecret,omitempty"` + // Audience specifies the audience of the token in OIDC authentication. + Audience string `json:"audience,omitempty"` + // Scope specifies the scope of the token in OIDC authentication. + Scope string `json:"scope,omitempty"` + // TokenEndpointURL specifies the URL which implements OIDC Token Endpoint. + // It will be used to get an OIDC token. + TokenEndpointURL string `json:"tokenEndpointURL,omitempty"` + // AdditionalEndpointParams specifies additional parameters to be sent + // this field will be transfer to map[string][]string in OIDC token generator. + AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` + + // TrustedCaFile specifies the path to a custom CA certificate file + // for verifying the OIDC token endpoint's TLS certificate. + TrustedCaFile string `json:"trustedCaFile,omitempty"` + // InsecureSkipVerify disables TLS certificate verification for the + // OIDC token endpoint. Only use this for debugging, not recommended for production. + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + // ProxyURL specifies a proxy to use when connecting to the OIDC token endpoint. + // Supports http, https, socks5, and socks5h proxy protocols. + // If empty, no proxy is used for OIDC connections. + ProxyURL string `json:"proxyURL,omitempty"` + + // TokenSource specifies a custom dynamic source for the authorization token. + // This is mutually exclusive with every other field of this structure. + TokenSource *ValueSource `json:"tokenSource,omitempty"` +} + +type VirtualNetConfig struct { + Address string `json:"address,omitempty"` +} diff --git a/pkg/config/v1/client_test.go b/pkg/config/v1/client_test.go new file mode 100644 index 00000000000..67214a5df5f --- /dev/null +++ b/pkg/config/v1/client_test.go @@ -0,0 +1,45 @@ +// Copyright 2023 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 v1 + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" +) + +func TestClientConfigComplete(t *testing.T) { + require := require.New(t) + c := &ClientConfig{} + err := c.Complete() + require.NoError(err) + + require.EqualValues("token", c.Auth.Method) + require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) + require.Equal("v1", c.Transport.WireProtocol) + require.Equal(true, lo.FromPtr(c.LoginFailExit)) + require.Equal(true, lo.FromPtr(c.Transport.TLS.Enable)) + require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte)) + require.NotEmpty(c.NatHoleSTUNServer) +} + +func TestAuthClientConfig_Complete(t *testing.T) { + require := require.New(t) + cfg := &AuthClientConfig{} + err := cfg.Complete() + require.NoError(err) + require.EqualValues("token", cfg.Method) +} diff --git a/pkg/config/v1/clone_test.go b/pkg/config/v1/clone_test.go new file mode 100644 index 00000000000..9b108528ada --- /dev/null +++ b/pkg/config/v1/clone_test.go @@ -0,0 +1,109 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProxyCloneDeepCopy(t *testing.T) { + require := require.New(t) + + enabled := true + pluginHTTP2 := true + cfg := &HTTPProxyConfig{ + ProxyBaseConfig: ProxyBaseConfig{ + Name: "p1", + Type: "http", + Enabled: &enabled, + Annotations: map[string]string{"a": "1"}, + Metadatas: map[string]string{"m": "1"}, + HealthCheck: HealthCheckConfig{ + Type: "http", + HTTPHeaders: []HTTPHeader{ + {Name: "X-Test", Value: "v1"}, + }, + }, + ProxyBackend: ProxyBackend{ + Plugin: TypedClientPluginOptions{ + Type: PluginHTTPS2HTTP, + ClientPluginOptions: &HTTPS2HTTPPluginOptions{ + Type: PluginHTTPS2HTTP, + EnableHTTP2: &pluginHTTP2, + RequestHeaders: HeaderOperations{Set: map[string]string{"k": "v"}}, + }, + }, + }, + }, + DomainConfig: DomainConfig{ + CustomDomains: []string{"a.example.com"}, + SubDomain: "a", + }, + Locations: []string{"/api"}, + RequestHeaders: HeaderOperations{Set: map[string]string{"h1": "v1"}}, + ResponseHeaders: HeaderOperations{Set: map[string]string{"h2": "v2"}}, + } + + cloned := cfg.Clone().(*HTTPProxyConfig) + + *cloned.Enabled = false + cloned.Annotations["a"] = "changed" + cloned.Metadatas["m"] = "changed" + cloned.HealthCheck.HTTPHeaders[0].Value = "changed" + cloned.CustomDomains[0] = "b.example.com" + cloned.Locations[0] = "/new" + cloned.RequestHeaders.Set["h1"] = "changed" + cloned.ResponseHeaders.Set["h2"] = "changed" + clientPlugin := cloned.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions) + *clientPlugin.EnableHTTP2 = false + clientPlugin.RequestHeaders.Set["k"] = "changed" + + require.True(*cfg.Enabled) + require.Equal("1", cfg.Annotations["a"]) + require.Equal("1", cfg.Metadatas["m"]) + require.Equal("v1", cfg.HealthCheck.HTTPHeaders[0].Value) + require.Equal("a.example.com", cfg.CustomDomains[0]) + require.Equal("/api", cfg.Locations[0]) + require.Equal("v1", cfg.RequestHeaders.Set["h1"]) + require.Equal("v2", cfg.ResponseHeaders.Set["h2"]) + + origPlugin := cfg.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions) + require.True(*origPlugin.EnableHTTP2) + require.Equal("v", origPlugin.RequestHeaders.Set["k"]) +} + +func TestVisitorCloneDeepCopy(t *testing.T) { + require := require.New(t) + + enabled := true + cfg := &XTCPVisitorConfig{ + VisitorBaseConfig: VisitorBaseConfig{ + Name: "v1", + Type: "xtcp", + Enabled: &enabled, + ServerName: "server", + BindPort: 7000, + Plugin: TypedVisitorPluginOptions{ + Type: VisitorPluginVirtualNet, + VisitorPluginOptions: &VirtualNetVisitorPluginOptions{ + Type: VisitorPluginVirtualNet, + DestinationIP: "10.0.0.1", + }, + }, + }, + NatTraversal: &NatTraversalConfig{ + DisableAssistedAddrs: true, + }, + } + + cloned := cfg.Clone().(*XTCPVisitorConfig) + *cloned.Enabled = false + cloned.NatTraversal.DisableAssistedAddrs = false + visitorPlugin := cloned.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions) + visitorPlugin.DestinationIP = "10.0.0.2" + + require.True(*cfg.Enabled) + require.True(cfg.NatTraversal.DisableAssistedAddrs) + origPlugin := cfg.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions) + require.Equal("10.0.0.1", origPlugin.DestinationIP) +} diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go new file mode 100644 index 00000000000..41bd9a4caa1 --- /dev/null +++ b/pkg/config/v1/common.go @@ -0,0 +1,146 @@ +// Copyright 2023 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 v1 + +import ( + "maps" + + "github.com/fatedier/frp/pkg/util/util" +) + +type AuthScope string + +const ( + AuthScopeHeartBeats AuthScope = "HeartBeats" + AuthScopeNewWorkConns AuthScope = "NewWorkConns" +) + +type AuthMethod string + +const ( + AuthMethodToken AuthMethod = "token" + AuthMethodOIDC AuthMethod = "oidc" +) + +// QUIC protocol options +type QUICOptions struct { + KeepalivePeriod int `json:"keepalivePeriod,omitempty"` + MaxIdleTimeout int `json:"maxIdleTimeout,omitempty"` + MaxIncomingStreams int `json:"maxIncomingStreams,omitempty"` +} + +func (c *QUICOptions) Complete() { + c.KeepalivePeriod = util.EmptyOr(c.KeepalivePeriod, 10) + c.MaxIdleTimeout = util.EmptyOr(c.MaxIdleTimeout, 30) + c.MaxIncomingStreams = util.EmptyOr(c.MaxIncomingStreams, 100000) +} + +type WebServerConfig struct { + // This is the network address to bind on for serving the web interface and API. + // By default, this value is "127.0.0.1". + Addr string `json:"addr,omitempty"` + // Port specifies the port for the web server to listen on. If this + // value is 0, the admin server will not be started. + Port int `json:"port,omitempty"` + // User specifies the username that the web server will use for login. + User string `json:"user,omitempty"` + // Password specifies the password that the admin server will use for login. + Password string `json:"password,omitempty"` + // AssetsDir specifies the local directory that the admin server will load + // resources from. If this value is "", assets will be loaded from the + // bundled executable using embed package. + AssetsDir string `json:"assetsDir,omitempty"` + // Enable golang pprof handlers. + PprofEnable bool `json:"pprofEnable,omitempty"` + // Enable TLS if TLSConfig is not nil. + TLS *TLSConfig `json:"tls,omitempty"` +} + +func (c *WebServerConfig) Complete() { + c.Addr = util.EmptyOr(c.Addr, "127.0.0.1") +} + +type TLSConfig struct { + // CertFile specifies the path of the cert file that client will load. + CertFile string `json:"certFile,omitempty"` + // KeyFile specifies the path of the secret key file that client will load. + KeyFile string `json:"keyFile,omitempty"` + // TrustedCaFile specifies the path of the trusted ca file that will load. + TrustedCaFile string `json:"trustedCaFile,omitempty"` + // ServerName specifies the custom server name of tls certificate. By + // default, server name if same to ServerAddr. + ServerName string `json:"serverName,omitempty"` +} + +// NatTraversalConfig defines configuration options for NAT traversal +type NatTraversalConfig struct { + // DisableAssistedAddrs disables the use of local network interfaces + // for assisted connections during NAT traversal. When enabled, + // only STUN-discovered public addresses will be used. + DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"` +} + +func (c *NatTraversalConfig) Clone() *NatTraversalConfig { + if c == nil { + return nil + } + out := *c + return &out +} + +type LogConfig struct { + // This is destination where frp should write the logs. + // If "console" is used, logs will be printed to stdout, otherwise, + // logs will be written to the specified file. + // By default, this value is "console". + To string `json:"to,omitempty"` + // Level specifies the minimum log level. Valid values are "trace", + // "debug", "info", "warn", and "error". By default, this value is "info". + Level string `json:"level,omitempty"` + // MaxDays specifies the maximum number of days to store log information + // before deletion. + MaxDays int64 `json:"maxDays"` + // DisablePrintColor disables log colors when log.to is "console". + DisablePrintColor bool `json:"disablePrintColor,omitempty"` +} + +func (c *LogConfig) Complete() { + c.To = util.EmptyOr(c.To, "console") + c.Level = util.EmptyOr(c.Level, "info") + c.MaxDays = util.EmptyOr(c.MaxDays, 3) +} + +type HTTPPluginOptions struct { + Name string `json:"name"` + Addr string `json:"addr"` + Path string `json:"path"` + Ops []string `json:"ops"` + TLSVerify bool `json:"tlsVerify,omitempty"` +} + +type HeaderOperations struct { + Set map[string]string `json:"set,omitempty"` +} + +func (o HeaderOperations) Clone() HeaderOperations { + return HeaderOperations{ + Set: maps.Clone(o.Set), + } +} + +type HTTPHeader struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/pkg/config/v1/decode.go b/pkg/config/v1/decode.go new file mode 100644 index 00000000000..427cea7fff1 --- /dev/null +++ b/pkg/config/v1/decode.go @@ -0,0 +1,195 @@ +// 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 v1 + +import ( + "errors" + "fmt" + "reflect" + + "github.com/fatedier/frp/pkg/util/jsonx" +) + +type DecodeOptions struct { + DisallowUnknownFields bool +} + +func decodeJSONWithOptions(b []byte, out any, options DecodeOptions) error { + return jsonx.UnmarshalWithOptions(b, out, jsonx.DecodeOptions{ + RejectUnknownMembers: options.DisallowUnknownFields, + }) +} + +func isJSONNull(b []byte) bool { + return len(b) == 0 || string(b) == "null" +} + +type typedEnvelope struct { + Type string `json:"type"` + Plugin jsonx.RawMessage `json:"plugin,omitempty"` +} + +func DecodeProxyConfigurerJSON(b []byte, options DecodeOptions) (ProxyConfigurer, error) { + if isJSONNull(b) { + return nil, errors.New("type is required") + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return nil, err + } + + configurer := NewProxyConfigurerByType(ProxyType(env.Type)) + if configurer == nil { + return nil, fmt.Errorf("unknown proxy type: %s", env.Type) + } + if err := decodeJSONWithOptions(b, configurer, options); err != nil { + return nil, fmt.Errorf("unmarshal ProxyConfig error: %v", err) + } + + if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) { + plugin, err := DecodeClientPluginOptionsJSON(env.Plugin, options) + if err != nil { + return nil, fmt.Errorf("unmarshal proxy plugin error: %v", err) + } + configurer.GetBaseConfig().Plugin = plugin + } + return configurer, nil +} + +func DecodeVisitorConfigurerJSON(b []byte, options DecodeOptions) (VisitorConfigurer, error) { + if isJSONNull(b) { + return nil, errors.New("type is required") + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return nil, err + } + + configurer := NewVisitorConfigurerByType(VisitorType(env.Type)) + if configurer == nil { + return nil, fmt.Errorf("unknown visitor type: %s", env.Type) + } + if err := decodeJSONWithOptions(b, configurer, options); err != nil { + return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err) + } + + if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) { + plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options) + if err != nil { + return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err) + } + configurer.GetBaseConfig().Plugin = plugin + } + return configurer, nil +} + +func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) { + if isJSONNull(b) { + return TypedClientPluginOptions{}, nil + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return TypedClientPluginOptions{}, err + } + if env.Type == "" { + return TypedClientPluginOptions{}, errors.New("plugin type is empty") + } + + v, ok := clientPluginOptionsTypeMap[env.Type] + if !ok { + return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type) + } + optionsStruct := reflect.New(v).Interface().(ClientPluginOptions) + if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil { + return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err) + } + return TypedClientPluginOptions{ + Type: env.Type, + ClientPluginOptions: optionsStruct, + }, nil +} + +func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) { + if isJSONNull(b) { + return TypedVisitorPluginOptions{}, nil + } + + var env typedEnvelope + if err := jsonx.Unmarshal(b, &env); err != nil { + return TypedVisitorPluginOptions{}, err + } + if env.Type == "" { + return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty") + } + + v, ok := visitorPluginOptionsTypeMap[env.Type] + if !ok { + return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type) + } + optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions) + if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil { + return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err) + } + return TypedVisitorPluginOptions{ + Type: env.Type, + VisitorPluginOptions: optionsStruct, + }, nil +} + +func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) { + type rawClientConfig struct { + ClientCommonConfig + Proxies []jsonx.RawMessage `json:"proxies,omitempty"` + Visitors []jsonx.RawMessage `json:"visitors,omitempty"` + } + + raw := rawClientConfig{} + if err := decodeJSONWithOptions(b, &raw, options); err != nil { + return ClientConfig{}, err + } + + cfg := ClientConfig{ + ClientCommonConfig: raw.ClientCommonConfig, + Proxies: make([]TypedProxyConfig, 0, len(raw.Proxies)), + Visitors: make([]TypedVisitorConfig, 0, len(raw.Visitors)), + } + + for i, proxyData := range raw.Proxies { + proxyCfg, err := DecodeProxyConfigurerJSON(proxyData, options) + if err != nil { + return ClientConfig{}, fmt.Errorf("decode proxy at index %d: %w", i, err) + } + cfg.Proxies = append(cfg.Proxies, TypedProxyConfig{ + Type: proxyCfg.GetBaseConfig().Type, + ProxyConfigurer: proxyCfg, + }) + } + + for i, visitorData := range raw.Visitors { + visitorCfg, err := DecodeVisitorConfigurerJSON(visitorData, options) + if err != nil { + return ClientConfig{}, fmt.Errorf("decode visitor at index %d: %w", i, err) + } + cfg.Visitors = append(cfg.Visitors, TypedVisitorConfig{ + Type: visitorCfg.GetBaseConfig().Type, + VisitorConfigurer: visitorCfg, + }) + } + + return cfg, nil +} diff --git a/pkg/config/v1/decode_test.go b/pkg/config/v1/decode_test.go new file mode 100644 index 00000000000..cba433adb03 --- /dev/null +++ b/pkg/config/v1/decode_test.go @@ -0,0 +1,86 @@ +// 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 v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDecodeProxyConfigurerJSON_StrictPluginUnknownFields(t *testing.T) { + require := require.New(t) + + data := []byte(`{ + "name":"p1", + "type":"tcp", + "localPort":10080, + "plugin":{ + "type":"http2https", + "localAddr":"127.0.0.1:8080", + "unknownInPlugin":"value" + } + }`) + + _, err := DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false}) + require.NoError(err) + + _, err = DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true}) + require.ErrorContains(err, "unknownInPlugin") +} + +func TestDecodeVisitorConfigurerJSON_StrictPluginUnknownFields(t *testing.T) { + require := require.New(t) + + data := []byte(`{ + "name":"v1", + "type":"stcp", + "serverName":"server", + "bindPort":10081, + "plugin":{ + "type":"virtual_net", + "destinationIP":"10.0.0.1", + "unknownInPlugin":"value" + } + }`) + + _, err := DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false}) + require.NoError(err) + + _, err = DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true}) + require.ErrorContains(err, "unknownInPlugin") +} + +func TestDecodeClientConfigJSON_StrictUnknownProxyField(t *testing.T) { + require := require.New(t) + + data := []byte(`{ + "serverPort":7000, + "proxies":[ + { + "name":"p1", + "type":"tcp", + "localPort":10080, + "unknownField":"value" + } + ] + }`) + + _, err := DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: false}) + require.NoError(err) + + _, err = DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: true}) + require.ErrorContains(err, "unknownField") +} diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go new file mode 100644 index 00000000000..5b637b70dfc --- /dev/null +++ b/pkg/config/v1/proxy.go @@ -0,0 +1,538 @@ +// Copyright 2023 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 v1 + +import ( + "maps" + "reflect" + "slices" + + "github.com/fatedier/frp/pkg/config/types" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/jsonx" + "github.com/fatedier/frp/pkg/util/util" +) + +type ProxyTransport struct { + // UseEncryption controls whether or not communication with the server will + // be encrypted. Encryption is done using the tokens supplied in the server + // and client configuration. + UseEncryption bool `json:"useEncryption,omitempty"` + // UseCompression controls whether or not communication with the server + // will be compressed. + UseCompression bool `json:"useCompression,omitempty"` + // BandwidthLimit limit the bandwidth + // 0 means no limit + BandwidthLimit types.BandwidthQuantity `json:"bandwidthLimit,omitempty"` + // BandwidthLimitMode specifies whether to limit the bandwidth on the + // client or server side. Valid values include "client" and "server". + // By default, this value is "client". + BandwidthLimitMode string `json:"bandwidthLimitMode,omitempty"` + // ProxyProtocolVersion specifies which protocol version to use. Valid + // values include "v1", "v2", and "". If the value is "", a protocol + // version will be automatically selected. By default, this value is "". + ProxyProtocolVersion string `json:"proxyProtocolVersion,omitempty"` +} + +type LoadBalancerConfig struct { + // Group specifies which group the is a part of. The server will use + // this information to load balance proxies in the same group. If the value + // is "", this will not be in a group. + Group string `json:"group"` + // GroupKey specifies a group key, which should be the same among proxies + // of the same group. + GroupKey string `json:"groupKey,omitempty"` +} + +type ProxyBackend struct { + // LocalIP specifies the IP address or host name of the backend. + LocalIP string `json:"localIP,omitempty"` + // LocalPort specifies the port of the backend. + LocalPort int `json:"localPort,omitempty"` + + // Plugin specifies what plugin should be used for handling connections. If this value + // is set, the LocalIP and LocalPort values will be ignored. + Plugin TypedClientPluginOptions `json:"plugin,omitempty"` +} + +// HealthCheckConfig configures health checking. This can be useful for load +// balancing purposes to detect and remove proxies to failing services. +type HealthCheckConfig struct { + // Type specifies what protocol to use for health checking. + // Valid values include "tcp", "http", and "". If this value is "", health + // checking will not be performed. + // + // If the type is "tcp", a connection will be attempted to the target + // server. If a connection cannot be established, the health check fails. + // + // If the type is "http", a GET request will be made to the endpoint + // specified by HealthCheckURL. If the response is not a 200, the health + // check fails. + Type string `json:"type"` // tcp | http + // TimeoutSeconds specifies the number of seconds to wait for a health + // check attempt to connect. If the timeout is reached, this counts as a + // health check failure. By default, this value is 3. + TimeoutSeconds int `json:"timeoutSeconds,omitempty"` + // MaxFailed specifies the number of allowed failures before the + // is stopped. By default, this value is 1. + MaxFailed int `json:"maxFailed,omitempty"` + // IntervalSeconds specifies the time in seconds between health + // checks. By default, this value is 10. + IntervalSeconds int `json:"intervalSeconds"` + // Path specifies the path to send health checks to if the + // health check type is "http". + Path string `json:"path,omitempty"` + // HTTPHeaders specifies the headers to send with the health request, if + // the health check type is "http". + HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"` +} + +func (c HealthCheckConfig) Clone() HealthCheckConfig { + out := c + out.HTTPHeaders = slices.Clone(c.HTTPHeaders) + return out +} + +type DomainConfig struct { + CustomDomains []string `json:"customDomains,omitempty"` + SubDomain string `json:"subdomain,omitempty"` +} + +func (c DomainConfig) Clone() DomainConfig { + out := c + out.CustomDomains = slices.Clone(c.CustomDomains) + return out +} + +type ProxyBaseConfig struct { + Name string `json:"name"` + Type string `json:"type"` + // Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled. + // This allows individual control over each proxy, complementing the global "start" field. + Enabled *bool `json:"enabled,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + Transport ProxyTransport `json:"transport,omitempty"` + // metadata info for each proxy + Metadatas map[string]string `json:"metadatas,omitempty"` + LoadBalancer LoadBalancerConfig `json:"loadBalancer,omitempty"` + HealthCheck HealthCheckConfig `json:"healthCheck,omitempty"` + ProxyBackend +} + +func (c ProxyBaseConfig) Clone() ProxyBaseConfig { + out := c + out.Enabled = util.ClonePtr(c.Enabled) + out.Annotations = maps.Clone(c.Annotations) + out.Metadatas = maps.Clone(c.Metadatas) + out.HealthCheck = c.HealthCheck.Clone() + out.ProxyBackend = c.ProxyBackend.Clone() + return out +} + +func (c ProxyBackend) Clone() ProxyBackend { + out := c + out.Plugin = c.Plugin.Clone() + return out +} + +func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig { + return c +} + +func (c *ProxyBaseConfig) Complete() { + c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1") + c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) + + if c.Plugin.ClientPluginOptions != nil { + c.Plugin.Complete() + } +} + +func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) { + m.ProxyName = c.Name + m.ProxyType = c.Type + m.UseEncryption = c.Transport.UseEncryption + m.UseCompression = c.Transport.UseCompression + m.BandwidthLimit = c.Transport.BandwidthLimit.String() + // leave it empty for default value to reduce traffic + if c.Transport.BandwidthLimitMode != "client" { + m.BandwidthLimitMode = c.Transport.BandwidthLimitMode + } + m.Group = c.LoadBalancer.Group + m.GroupKey = c.LoadBalancer.GroupKey + m.Metas = c.Metadatas + m.Annotations = c.Annotations +} + +func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.Name = m.ProxyName + c.Type = m.ProxyType + c.Transport.UseEncryption = m.UseEncryption + c.Transport.UseCompression = m.UseCompression + if m.BandwidthLimit != "" { + c.Transport.BandwidthLimit, _ = types.NewBandwidthQuantity(m.BandwidthLimit) + } + if m.BandwidthLimitMode != "" { + c.Transport.BandwidthLimitMode = m.BandwidthLimitMode + } + c.LoadBalancer.Group = m.Group + c.LoadBalancer.GroupKey = m.GroupKey + c.Metadatas = m.Metas + c.Annotations = m.Annotations +} + +type TypedProxyConfig struct { + Type string `json:"type"` + ProxyConfigurer +} + +func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error { + configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{}) + if err != nil { + return err + } + + c.Type = configurer.GetBaseConfig().Type + c.ProxyConfigurer = configurer + return nil +} + +func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) { + return jsonx.Marshal(c.ProxyConfigurer) +} + +type ProxyConfigurer interface { + Complete() + GetBaseConfig() *ProxyBaseConfig + Clone() ProxyConfigurer + // MarshalToMsg marshals this config into a msg.NewProxy message. This + // function will be called on the frpc side. + MarshalToMsg(*msg.NewProxy) + // UnmarshalFromMsg unmarshal a msg.NewProxy message into this config. + // This function will be called on the frps side. + UnmarshalFromMsg(*msg.NewProxy) +} + +type ProxyType string + +const ( + ProxyTypeTCP ProxyType = "tcp" + ProxyTypeUDP ProxyType = "udp" + ProxyTypeTCPMUX ProxyType = "tcpmux" + ProxyTypeHTTP ProxyType = "http" + ProxyTypeHTTPS ProxyType = "https" + ProxyTypeSTCP ProxyType = "stcp" + ProxyTypeXTCP ProxyType = "xtcp" + ProxyTypeSUDP ProxyType = "sudp" +) + +var proxyConfigTypeMap = map[ProxyType]reflect.Type{ + ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](), + ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](), + ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](), + ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](), + ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](), + ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](), + ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](), + ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](), +} + +func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer { + v, ok := proxyConfigTypeMap[proxyType] + if !ok { + return nil + } + pc := reflect.New(v).Interface().(ProxyConfigurer) + pc.GetBaseConfig().Type = string(proxyType) + return pc +} + +var _ ProxyConfigurer = &TCPProxyConfig{} + +type TCPProxyConfig struct { + ProxyBaseConfig + + RemotePort int `json:"remotePort,omitempty"` +} + +func (c *TCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.RemotePort = c.RemotePort +} + +func (c *TCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.RemotePort = m.RemotePort +} + +func (c *TCPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + return &out +} + +var _ ProxyConfigurer = &UDPProxyConfig{} + +type UDPProxyConfig struct { + ProxyBaseConfig + + RemotePort int `json:"remotePort,omitempty"` +} + +func (c *UDPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.RemotePort = c.RemotePort +} + +func (c *UDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.RemotePort = m.RemotePort +} + +func (c *UDPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + return &out +} + +var _ ProxyConfigurer = &HTTPProxyConfig{} + +type HTTPProxyConfig struct { + ProxyBaseConfig + DomainConfig + + Locations []string `json:"locations,omitempty"` + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` + IpsAllowList []string `json:"ipsAllowList,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + ResponseHeaders HeaderOperations `json:"responseHeaders,omitempty"` + RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` +} + +func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.CustomDomains = c.CustomDomains + m.SubDomain = c.SubDomain + m.Locations = c.Locations + m.HostHeaderRewrite = c.HostHeaderRewrite + m.HTTPUser = c.HTTPUser + m.HTTPPwd = c.HTTPPassword + m.IpsAllowList = c.IpsAllowList + m.Headers = c.RequestHeaders.Set + m.ResponseHeaders = c.ResponseHeaders.Set + m.RouteByHTTPUser = c.RouteByHTTPUser +} + +func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.CustomDomains = m.CustomDomains + c.SubDomain = m.SubDomain + c.Locations = m.Locations + c.HostHeaderRewrite = m.HostHeaderRewrite + c.HTTPUser = m.HTTPUser + c.HTTPPassword = m.HTTPPwd + c.IpsAllowList = m.IpsAllowList + c.RequestHeaders.Set = m.Headers + c.ResponseHeaders.Set = m.ResponseHeaders + c.RouteByHTTPUser = m.RouteByHTTPUser +} + +func (c *HTTPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.DomainConfig = c.DomainConfig.Clone() + out.Locations = slices.Clone(c.Locations) + out.IpsAllowList = slices.Clone(c.IpsAllowList) + out.RequestHeaders = c.RequestHeaders.Clone() + out.ResponseHeaders = c.ResponseHeaders.Clone() + return &out +} + +var _ ProxyConfigurer = &HTTPSProxyConfig{} + +type HTTPSProxyConfig struct { + ProxyBaseConfig + DomainConfig +} + +func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.CustomDomains = c.CustomDomains + m.SubDomain = c.SubDomain +} + +func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.CustomDomains = m.CustomDomains + c.SubDomain = m.SubDomain +} + +func (c *HTTPSProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.DomainConfig = c.DomainConfig.Clone() + return &out +} + +type TCPMultiplexerType string + +const ( + TCPMultiplexerHTTPConnect TCPMultiplexerType = "httpconnect" +) + +var _ ProxyConfigurer = &TCPMuxProxyConfig{} + +type TCPMuxProxyConfig struct { + ProxyBaseConfig + DomainConfig + + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` + RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` + Multiplexer string `json:"multiplexer,omitempty"` +} + +func (c *TCPMuxProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.CustomDomains = c.CustomDomains + m.SubDomain = c.SubDomain + m.Multiplexer = c.Multiplexer + m.HTTPUser = c.HTTPUser + m.HTTPPwd = c.HTTPPassword + m.RouteByHTTPUser = c.RouteByHTTPUser +} + +func (c *TCPMuxProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.CustomDomains = m.CustomDomains + c.SubDomain = m.SubDomain + c.Multiplexer = m.Multiplexer + c.HTTPUser = m.HTTPUser + c.HTTPPassword = m.HTTPPwd + c.RouteByHTTPUser = m.RouteByHTTPUser +} + +func (c *TCPMuxProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.DomainConfig = c.DomainConfig.Clone() + return &out +} + +var _ ProxyConfigurer = &STCPProxyConfig{} + +type STCPProxyConfig struct { + ProxyBaseConfig + + Secretkey string `json:"secretKey,omitempty"` + AllowUsers []string `json:"allowUsers,omitempty"` +} + +func (c *STCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.Sk = c.Secretkey + m.AllowUsers = c.AllowUsers +} + +func (c *STCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.Secretkey = m.Sk + c.AllowUsers = m.AllowUsers +} + +func (c *STCPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.AllowUsers = slices.Clone(c.AllowUsers) + return &out +} + +var _ ProxyConfigurer = &XTCPProxyConfig{} + +type XTCPProxyConfig struct { + ProxyBaseConfig + + Secretkey string `json:"secretKey,omitempty"` + AllowUsers []string `json:"allowUsers,omitempty"` + + // NatTraversal configuration for NAT traversal + NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` +} + +func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.Sk = c.Secretkey + m.AllowUsers = c.AllowUsers +} + +func (c *XTCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.Secretkey = m.Sk + c.AllowUsers = m.AllowUsers +} + +func (c *XTCPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.AllowUsers = slices.Clone(c.AllowUsers) + out.NatTraversal = c.NatTraversal.Clone() + return &out +} + +var _ ProxyConfigurer = &SUDPProxyConfig{} + +type SUDPProxyConfig struct { + ProxyBaseConfig + + Secretkey string `json:"secretKey,omitempty"` + AllowUsers []string `json:"allowUsers,omitempty"` +} + +func (c *SUDPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.Sk = c.Secretkey + m.AllowUsers = c.AllowUsers +} + +func (c *SUDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.Secretkey = m.Sk + c.AllowUsers = m.AllowUsers +} + +func (c *SUDPProxyConfig) Clone() ProxyConfigurer { + out := *c + out.ProxyBaseConfig = c.ProxyBaseConfig.Clone() + out.AllowUsers = slices.Clone(c.AllowUsers) + return &out +} diff --git a/pkg/config/v1/proxy_plugin.go b/pkg/config/v1/proxy_plugin.go new file mode 100644 index 00000000000..0d6ca3d0868 --- /dev/null +++ b/pkg/config/v1/proxy_plugin.go @@ -0,0 +1,261 @@ +// Copyright 2023 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 v1 + +import ( + "reflect" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/util/jsonx" + "github.com/fatedier/frp/pkg/util/util" +) + +const ( + PluginHTTP2HTTPS = "http2https" + PluginHTTPProxy = "http_proxy" + PluginHTTPS2HTTP = "https2http" + PluginHTTPS2HTTPS = "https2https" + PluginHTTP2HTTP = "http2http" + PluginSocks5 = "socks5" + PluginStaticFile = "static_file" + PluginUnixDomainSocket = "unix_domain_socket" + PluginTLS2Raw = "tls2raw" + PluginVirtualNet = "virtual_net" +) + +var clientPluginOptionsTypeMap = map[string]reflect.Type{ + PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](), + PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](), + PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](), + PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](), + PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](), + PluginSocks5: reflect.TypeFor[Socks5PluginOptions](), + PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](), + PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](), + PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](), + PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](), +} + +type ClientPluginOptions interface { + Complete() + Clone() ClientPluginOptions +} + +type TypedClientPluginOptions struct { + Type string `json:"type"` + ClientPluginOptions +} + +func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions { + out := c + if c.ClientPluginOptions != nil { + out.ClientPluginOptions = c.ClientPluginOptions.Clone() + } + return out +} + +func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { + decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{}) + if err != nil { + return err + } + *c = decoded + return nil +} + +func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) { + return jsonx.Marshal(c.ClientPluginOptions) +} + +type HTTP2HTTPSPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` +} + +func (o *HTTP2HTTPSPluginOptions) Complete() {} + +func (o *HTTP2HTTPSPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + return &out +} + +type HTTPProxyPluginOptions struct { + Type string `json:"type,omitempty"` + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` +} + +func (o *HTTPProxyPluginOptions) Complete() {} + +func (o *HTTPProxyPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + +type HTTPS2HTTPPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + EnableHTTP2 *bool `json:"enableHTTP2,omitempty"` + CrtPath string `json:"crtPath,omitempty"` + KeyPath string `json:"keyPath,omitempty"` +} + +func (o *HTTPS2HTTPPluginOptions) Complete() { + o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true)) +} + +func (o *HTTPS2HTTPPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2) + return &out +} + +type HTTPS2HTTPSPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + EnableHTTP2 *bool `json:"enableHTTP2,omitempty"` + CrtPath string `json:"crtPath,omitempty"` + KeyPath string `json:"keyPath,omitempty"` +} + +func (o *HTTPS2HTTPSPluginOptions) Complete() { + o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true)) +} + +func (o *HTTPS2HTTPSPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2) + return &out +} + +type HTTP2HTTPPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` +} + +func (o *HTTP2HTTPPluginOptions) Complete() {} + +func (o *HTTP2HTTPPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + out.RequestHeaders = o.RequestHeaders.Clone() + return &out +} + +type Socks5PluginOptions struct { + Type string `json:"type,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +func (o *Socks5PluginOptions) Complete() {} + +func (o *Socks5PluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + +type StaticFilePluginOptions struct { + Type string `json:"type,omitempty"` + LocalPath string `json:"localPath,omitempty"` + StripPrefix string `json:"stripPrefix,omitempty"` + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` +} + +func (o *StaticFilePluginOptions) Complete() {} + +func (o *StaticFilePluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + +type UnixDomainSocketPluginOptions struct { + Type string `json:"type,omitempty"` + UnixPath string `json:"unixPath,omitempty"` +} + +func (o *UnixDomainSocketPluginOptions) Complete() {} + +func (o *UnixDomainSocketPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + +type TLS2RawPluginOptions struct { + Type string `json:"type,omitempty"` + LocalAddr string `json:"localAddr,omitempty"` + CrtPath string `json:"crtPath,omitempty"` + KeyPath string `json:"keyPath,omitempty"` +} + +func (o *TLS2RawPluginOptions) Complete() {} + +func (o *TLS2RawPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} + +type VirtualNetPluginOptions struct { + Type string `json:"type,omitempty"` +} + +func (o *VirtualNetPluginOptions) Complete() {} + +func (o *VirtualNetPluginOptions) Clone() ClientPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} diff --git a/pkg/config/v1/proxy_test.go b/pkg/config/v1/proxy_test.go new file mode 100644 index 00000000000..64753250818 --- /dev/null +++ b/pkg/config/v1/proxy_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 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 v1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUnmarshalTypedProxyConfig(t *testing.T) { + require := require.New(t) + proxyConfigs := struct { + Proxies []TypedProxyConfig `json:"proxies,omitempty"` + }{} + + strs := `{ + "proxies": [ + { + "type": "tcp", + "localPort": 22, + "remotePort": 6000 + }, + { + "type": "http", + "localPort": 80, + "customDomains": ["www.example.com"] + } + ] + }` + err := json.Unmarshal([]byte(strs), &proxyConfigs) + require.NoError(err) + + require.IsType(&TCPProxyConfig{}, proxyConfigs.Proxies[0].ProxyConfigurer) + require.IsType(&HTTPProxyConfig{}, proxyConfigs.Proxies[1].ProxyConfigurer) +} diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go new file mode 100644 index 00000000000..67c1d84b6ab --- /dev/null +++ b/pkg/config/v1/server.go @@ -0,0 +1,215 @@ +// Copyright 2023 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 v1 + +import ( + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/config/types" + "github.com/fatedier/frp/pkg/util/util" +) + +type ServerConfig struct { + APIMetadata + + Auth AuthServerConfig `json:"auth,omitempty"` + // BindAddr specifies the address that the server binds to. By default, + // this value is "0.0.0.0". + BindAddr string `json:"bindAddr,omitempty"` + // BindPort specifies the port that the server listens on. By default, this + // value is 7000. + BindPort int `json:"bindPort,omitempty"` + // KCPBindPort specifies the KCP port that the server listens on. If this + // value is 0, the server will not listen for KCP connections. + KCPBindPort int `json:"kcpBindPort,omitempty"` + // QUICBindPort specifies the QUIC port that the server listens on. + // Set this value to 0 will disable this feature. + QUICBindPort int `json:"quicBindPort,omitempty"` + // ProxyBindAddr specifies the address that the proxy binds to. This value + // may be the same as BindAddr. + ProxyBindAddr string `json:"proxyBindAddr,omitempty"` + // VhostHTTPPort specifies the port that the server listens for HTTP Vhost + // requests. If this value is 0, the server will not listen for HTTP + // requests. + VhostHTTPPort int `json:"vhostHTTPPort,omitempty"` + // VhostHTTPTimeout specifies the response header timeout for the Vhost + // HTTP server, in seconds. By default, this value is 60. + VhostHTTPTimeout int64 `json:"vhostHTTPTimeout,omitempty"` + // VhostHTTPSPort specifies the port that the server listens for HTTPS + // Vhost requests. If this value is 0, the server will not listen for HTTPS + // requests. + VhostHTTPSPort int `json:"vhostHTTPSPort,omitempty"` + // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP + // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP + // requests on one single port. If it's not - it will listen on this value for + // HTTP CONNECT requests. + TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort,omitempty"` + // If TCPMuxPassthrough is true, frps won't do any update on traffic. + TCPMuxPassthrough bool `json:"tcpmuxPassthrough,omitempty"` + // SubDomainHost specifies the domain that will be attached to sub-domains + // requested by the client when using Vhost proxying. For example, if this + // value is set to "frps.com" and the client requested the subdomain + // "test", the resulting URL would be "test.frps.com". + SubDomainHost string `json:"subDomainHost,omitempty"` + // Custom404Page specifies a path to a custom 404 page to display. If this + // value is "", a default page will be displayed. + Custom404Page string `json:"custom404Page,omitempty"` + + SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"` + + WebServer WebServerConfig `json:"webServer,omitempty"` + // EnablePrometheus will export prometheus metrics on webserver address + // in /metrics api. + EnablePrometheus bool `json:"enablePrometheus,omitempty"` + + Log LogConfig `json:"log,omitempty"` + + Transport ServerTransportConfig `json:"transport,omitempty"` + + // DetailedErrorsToClient defines whether to send the specific error (with + // debug info) to frpc. By default, this value is true. + DetailedErrorsToClient *bool `json:"detailedErrorsToClient,omitempty"` + // MaxPortsPerClient specifies the maximum number of ports a single client + // may proxy to. If this value is 0, no limit will be applied. + MaxPortsPerClient int64 `json:"maxPortsPerClient,omitempty"` + // UserConnTimeout specifies the maximum time to wait for a work + // connection. By default, this value is 10. + UserConnTimeout int64 `json:"userConnTimeout,omitempty"` + // UDPPacketSize specifies the UDP packet size + // By default, this value is 1500 + UDPPacketSize int64 `json:"udpPacketSize,omitempty"` + // NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data. + NatHoleAnalysisDataReserveHours int64 `json:"natholeAnalysisDataReserveHours,omitempty"` + + AllowPorts []types.PortsRange `json:"allowPorts,omitempty"` + + HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"` +} + +func (c *ServerConfig) Complete() error { + if err := c.Auth.Complete(); err != nil { + return err + } + c.Log.Complete() + c.Transport.Complete() + c.WebServer.Complete() + c.SSHTunnelGateway.Complete() + + c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0") + c.BindPort = util.EmptyOr(c.BindPort, 7000) + if c.ProxyBindAddr == "" { + c.ProxyBindAddr = c.BindAddr + } + + if c.WebServer.Port > 0 { + c.WebServer.Addr = util.EmptyOr(c.WebServer.Addr, "0.0.0.0") + } + + c.VhostHTTPTimeout = util.EmptyOr(c.VhostHTTPTimeout, 60) + c.DetailedErrorsToClient = util.EmptyOr(c.DetailedErrorsToClient, lo.ToPtr(true)) + c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10) + c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) + c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24) + return nil +} + +type AuthServerConfig struct { + Method AuthMethod `json:"method,omitempty"` + AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` + Token string `json:"token,omitempty"` + TokenSource *ValueSource `json:"tokenSource,omitempty"` + OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` +} + +func (c *AuthServerConfig) Complete() error { + c.Method = util.EmptyOr(c.Method, "token") + return nil +} + +type AuthOIDCServerConfig struct { + // Issuer specifies the issuer to verify OIDC tokens with. This issuer + // will be used to load public keys to verify signature and will be compared + // with the issuer claim in the OIDC token. + Issuer string `json:"issuer,omitempty"` + // Audience specifies the audience OIDC tokens should contain when validated. + // If this value is empty, audience ("client ID") verification will be skipped. + Audience string `json:"audience,omitempty"` + // SkipExpiryCheck specifies whether to skip checking if the OIDC token is + // expired. + SkipExpiryCheck bool `json:"skipExpiryCheck,omitempty"` + // SkipIssuerCheck specifies whether to skip checking if the OIDC token's + // issuer claim matches the issuer specified in OidcIssuer. + SkipIssuerCheck bool `json:"skipIssuerCheck,omitempty"` +} + +type ServerTransportConfig struct { + // TCPMux toggles TCP stream multiplexing. This allows multiple requests + // from a client to share a single TCP connection. By default, this value + // is true. + // $HideFromDoc + TCPMux *bool `json:"tcpMux,omitempty"` + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multiplier. + // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. + TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` + // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. + // 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. 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 + // value. By default, this value is 90. Set negative value to disable it. + HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"` + // QUIC options. + QUIC QUICOptions `json:"quic,omitempty"` + // TLS specifies TLS settings for the connection from the client. + TLS TLSServerConfig `json:"tls,omitempty"` +} + +func (c *ServerTransportConfig) Complete() { + c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true)) + c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30) + c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200) + c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5) + if lo.FromPtr(c.TCPMux) { + // If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux. + c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1) + } else { + c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90) + } + c.QUIC.Complete() + if c.TLS.TrustedCaFile != "" { + c.TLS.Force = true + } +} + +type TLSServerConfig struct { + // Force specifies whether to only accept TLS-encrypted connections. + Force bool `json:"force,omitempty"` + + TLSConfig +} + +type SSHTunnelGateway struct { + BindPort int `json:"bindPort,omitempty"` + PrivateKeyFile string `json:"privateKeyFile,omitempty"` + AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"` + AuthorizedKeysFile string `json:"authorizedKeysFile,omitempty"` +} + +func (c *SSHTunnelGateway) Complete() { + c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key") +} diff --git a/pkg/config/v1/server_test.go b/pkg/config/v1/server_test.go new file mode 100644 index 00000000000..cd9381c509d --- /dev/null +++ b/pkg/config/v1/server_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 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 v1 + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" +) + +func TestServerConfigComplete(t *testing.T) { + require := require.New(t) + c := &ServerConfig{} + err := c.Complete() + require.NoError(err) + + require.EqualValues("token", c.Auth.Method) + require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) + require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient)) +} + +func TestAuthServerConfig_Complete(t *testing.T) { + require := require.New(t) + cfg := &AuthServerConfig{} + err := cfg.Complete() + require.NoError(err) + require.EqualValues("token", cfg.Method) +} diff --git a/pkg/config/v1/store.go b/pkg/config/v1/store.go new file mode 100644 index 00000000000..7f992c3bb13 --- /dev/null +++ b/pkg/config/v1/store.go @@ -0,0 +1,26 @@ +// 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 v1 + +// StoreConfig configures the built-in store source. +type StoreConfig struct { + // Path is the store file path. + Path string `json:"path,omitempty"` +} + +// IsEnabled returns true if the store is configured with a valid path. +func (c *StoreConfig) IsEnabled() bool { + return c.Path != "" +} diff --git a/pkg/config/v1/validation/auth.go b/pkg/config/v1/validation/auth.go new file mode 100644 index 00000000000..c70235dc37b --- /dev/null +++ b/pkg/config/v1/validation/auth.go @@ -0,0 +1,43 @@ +// 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" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/policy/security" +) + +func (v *ConfigValidator) validateAuthTokenSource(token string, tokenSource *v1.ValueSource) error { + var errs error + // Preserve the previous client/server validation order for joined errors. + if token != "" && tokenSource != nil { + errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource")) + } + if tokenSource == nil { + return errs + } + + if tokenSource.Type == "exec" { + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) + } + } + if err := tokenSource.Validate(); err != nil { + errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err)) + } + return errs +} diff --git a/pkg/config/v1/validation/auth_test.go b/pkg/config/v1/validation/auth_test.go new file mode 100644 index 00000000000..4e90b44c194 --- /dev/null +++ b/pkg/config/v1/validation/auth_test.go @@ -0,0 +1,228 @@ +// 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/security" +) + +const ( + tokenSourceConflictErr = "cannot specify both auth.token and auth.tokenSource" + tokenSourceExecErr = "unsafe feature \"TokenSourceExec\" is not enabled. To enable it, ensure it is allowed in the configuration or command line flags" + invalidFileSourceErr = "invalid auth.tokenSource: file configuration is required when type is 'file'" + unsupportedSourceErr = "invalid auth.tokenSource: unsupported value source type: env (only 'file' and 'exec' are supported)" +) + +func TestValidateAuthTokenSource(t *testing.T) { + for _, tc := range authTokenSourceTestCases() { + t.Run(tc.name, func(t *testing.T) { + validator := newAuthTokenSourceValidator(tc.unsafeAllowed) + err := validator.validateAuthTokenSource(tc.token, tc.tokenSource()) + requireValidationErrors(t, err, tc.wantErrs) + }) + } +} + +func TestValidateClientAuthTokenSource(t *testing.T) { + for _, tc := range authTokenSourceTestCases() { + t.Run(tc.name, func(t *testing.T) { + auth := v1.AuthClientConfig{ + Method: v1.AuthMethodToken, + Token: tc.token, + TokenSource: tc.tokenSource(), + } + validator := newAuthTokenSourceValidator(tc.unsafeAllowed) + _, err := validator.ValidateClientCommonConfig(validClientConfigWithAuth(auth)) + requireValidationErrors(t, err, tc.wantErrs) + }) + } +} + +func TestValidateServerAuthTokenSource(t *testing.T) { + for _, tc := range authTokenSourceTestCases() { + t.Run(tc.name, func(t *testing.T) { + auth := v1.AuthServerConfig{ + Method: v1.AuthMethodToken, + Token: tc.token, + TokenSource: tc.tokenSource(), + } + validator := newAuthTokenSourceValidator(tc.unsafeAllowed) + _, err := validator.ValidateServerConfig(validServerConfigWithAuth(auth)) + requireValidationErrors(t, err, tc.wantErrs) + }) + } +} + +type authTokenSourceTestCase struct { + name string + token string + tokenSource func() *v1.ValueSource + unsafeAllowed bool + wantErrs []string +} + +func authTokenSourceTestCases() []authTokenSourceTestCase { + return []authTokenSourceTestCase{ + { + name: "empty token config", + tokenSource: nilTokenSource, + }, + { + name: "valid file tokenSource", + tokenSource: validFileTokenSource, + }, + { + name: "literal token without tokenSource", + token: "token", + tokenSource: nilTokenSource, + }, + { + name: "literal token conflicts with file tokenSource", + token: "token", + tokenSource: validFileTokenSource, + wantErrs: []string{tokenSourceConflictErr}, + }, + { + name: "exec tokenSource requires unsafe feature", + tokenSource: validExecTokenSource, + wantErrs: []string{tokenSourceExecErr}, + }, + { + name: "exec tokenSource with unsafe feature allowed", + tokenSource: validExecTokenSource, + unsafeAllowed: true, + }, + { + name: "literal token conflicts with exec tokenSource and unsafe feature disabled", + token: "token", + tokenSource: validExecTokenSource, + wantErrs: []string{ + tokenSourceConflictErr, + tokenSourceExecErr, + }, + }, + { + name: "literal token conflicts with exec tokenSource and unsafe feature allowed", + token: "token", + tokenSource: validExecTokenSource, + unsafeAllowed: true, + wantErrs: []string{tokenSourceConflictErr}, + }, + { + name: "invalid file tokenSource is wrapped", + tokenSource: invalidFileTokenSource, + wantErrs: []string{invalidFileSourceErr}, + }, + { + name: "unsupported tokenSource type is wrapped", + tokenSource: unsupportedTokenSource, + wantErrs: []string{unsupportedSourceErr}, + }, + } +} + +func newAuthTokenSourceValidator(unsafeAllowed bool) *ConfigValidator { + if !unsafeAllowed { + return NewConfigValidator(nil) + } + return NewConfigValidator(security.NewUnsafeFeatures([]string{security.TokenSourceExec})) +} + +func requireValidationErrors(t *testing.T, err error, wantErrs []string) { + t.Helper() + if len(wantErrs) == 0 { + require.NoError(t, err) + return + } + require.Error(t, err) + // Client/server validators may wrap joined errors in another join layer; compare leaf errors. + gotErrs := unwrapValidationErrors(err) + require.Len(t, gotErrs, len(wantErrs)) + for i, wantErr := range wantErrs { + require.EqualError(t, gotErrs[i], wantErr) + } +} + +func unwrapValidationErrors(err error) []error { + type joinedError interface { + Unwrap() []error + } + joined, ok := err.(joinedError) + if !ok { + return []error{err} + } + + var errs []error + for _, err := range joined.Unwrap() { + errs = append(errs, unwrapValidationErrors(err)...) + } + return errs +} + +// nilTokenSource keeps the shared table shape uniform for cases without a tokenSource. +func nilTokenSource() *v1.ValueSource { + return nil +} + +func validFileTokenSource() *v1.ValueSource { + return &v1.ValueSource{ + Type: "file", + File: &v1.FileSource{Path: "token.txt"}, + } +} + +func validExecTokenSource() *v1.ValueSource { + return &v1.ValueSource{ + Type: "exec", + Exec: &v1.ExecSource{Command: "print-token"}, + } +} + +func invalidFileTokenSource() *v1.ValueSource { + return &v1.ValueSource{ + Type: "file", + } +} + +func unsupportedTokenSource() *v1.ValueSource { + return &v1.ValueSource{Type: "env"} +} + +func validClientConfigWithAuth(auth v1.AuthClientConfig) *v1.ClientCommonConfig { + return &v1.ClientCommonConfig{ + Auth: auth, + Log: v1.LogConfig{ + Level: "info", + }, + Transport: v1.ClientTransportConfig{ + Protocol: "tcp", + WireProtocol: "v1", + }, + } +} + +func validServerConfigWithAuth(auth v1.AuthServerConfig) *v1.ServerConfig { + return &v1.ServerConfig{ + Auth: auth, + Log: v1.LogConfig{ + Level: "info", + }, + } +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go new file mode 100644 index 00000000000..28409c4bf07 --- /dev/null +++ b/pkg/config/v1/validation/client.go @@ -0,0 +1,220 @@ +// Copyright 2023 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" + "os" + "path/filepath" + "slices" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/policy/featuregate" + "github.com/fatedier/frp/pkg/policy/security" +) + +func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) + + validators := []func() (Warning, error){ + func() (Warning, error) { return validateFeatureGates(c) }, + func() (Warning, error) { return v.validateAuthConfig(&c.Auth) }, + func() (Warning, error) { return nil, validateLogConfig(&c.Log) }, + func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) }, + func() (Warning, error) { return validateTransportConfig(&c.Transport) }, + func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) }, + } + + for _, validator := range validators { + w, err := validator() + warnings = AppendError(warnings, w) + errs = AppendError(errs, err) + } + return warnings, errs +} + +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 !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) { + errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) + } + if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) { + errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) + } + + errs = AppendError(errs, v.validateAuthTokenSource(c.Token, c.TokenSource)) + + if err := v.validateOIDCConfig(&c.OIDC); err != nil { + errs = AppendError(errs, err) + } + if c.Method == v1.AuthMethodOIDC && c.OIDC.TokenSource == nil { + if err := ValidateOIDCClientCredentialsConfig(&c.OIDC); err != nil { + errs = AppendError(errs, err) + } + } + return nil, errs +} + +func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error { + if c.TokenSource == nil { + return nil + } + var errs error + // Validate oidc.tokenSource mutual exclusivity with other fields of oidc + if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" || + c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 || + c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" { + errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc")) + } + if c.TokenSource.Type == "exec" { + if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil { + errs = AppendError(errs, err) + } + } + if err := c.TokenSource.Validate(); err != nil { + errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err)) + } + return errs +} + +func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) + + if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 { + if c.HeartbeatTimeout < c.HeartbeatInterval { + errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval")) + } + } + + if !lo.FromPtr(c.TLS.Enable) { + checkTLSConfig := func(name string, value string) Warning { + if value != "" { + return fmt.Errorf("%s is invalid when transport.tls.enable is false", name) + } + return nil + } + + warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile)) + } + + if !slices.Contains(SupportedTransportProtocols, c.Protocol) { + errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols)) + } + if !slices.Contains(SupportedWireProtocols, c.WireProtocol) { + errs = AppendError(errs, fmt.Errorf("invalid transport.wireProtocol, optional values are %v", SupportedWireProtocols)) + } + return warnings, errs +} + +func validateIncludeFiles(files []string) (Warning, error) { + var errs error + for _, f := range files { + absDir, err := filepath.Abs(filepath.Dir(f)) + if err != nil { + errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err)) + continue + } + if _, err := os.Stat(absDir); os.IsNotExist(err) { + errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f)) + } + } + return nil, errs +} + +func ValidateAllClientConfig( + c *v1.ClientCommonConfig, + proxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, + unsafeFeatures *security.UnsafeFeatures, +) (Warning, error) { + validator := NewConfigValidator(unsafeFeatures) + var warnings Warning + if c != nil { + warning, err := validator.ValidateClientCommonConfig(c) + warnings = AppendError(warnings, warning) + if err != nil { + return warnings, err + } + } + + for _, c := range proxyCfgs { + if err := ValidateProxyConfigurerForClient(c); err != nil { + return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err) + } + } + + for _, c := range visitorCfgs { + if err := ValidateVisitorConfigurer(c); err != nil { + return warnings, fmt.Errorf("visitor %s: %v", c.GetBaseConfig().Name, err) + } + } + return warnings, nil +} diff --git a/pkg/config/v1/validation/client_test.go b/pkg/config/v1/validation/client_test.go new file mode 100644 index 00000000000..1370e8909a2 --- /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()) +} diff --git a/pkg/config/v1/validation/common.go b/pkg/config/v1/validation/common.go new file mode 100644 index 00000000000..c7b93a32243 --- /dev/null +++ b/pkg/config/v1/validation/common.go @@ -0,0 +1,50 @@ +// Copyright 2023 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" + "slices" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func validateWebServerConfig(c *v1.WebServerConfig) error { + if c.TLS != nil { + if c.TLS.CertFile == "" { + return fmt.Errorf("tls.certFile must be specified when tls is enabled") + } + if c.TLS.KeyFile == "" { + return fmt.Errorf("tls.keyFile must be specified when tls is enabled") + } + } + + return ValidatePort(c.Port, "webServer.port") +} + +// ValidatePort checks that the network port is in range +func ValidatePort(port int, fieldPath string) error { + if 0 <= port && port <= 65535 { + return nil + } + return fmt.Errorf("%s: port number %d must be in the range 0..65535", fieldPath, port) +} + +func validateLogConfig(c *v1.LogConfig) error { + if !slices.Contains(SupportedLogLevels, c.Level) { + return fmt.Errorf("invalid log level, optional values are %v", SupportedLogLevels) + } + return nil +} diff --git a/pkg/config/v1/validation/name.go b/pkg/config/v1/validation/name.go new file mode 100644 index 00000000000..ce03de88aba --- /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 00000000000..1095e78f5d5 --- /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/config/v1/validation/oidc.go b/pkg/config/v1/validation/oidc.go new file mode 100644 index 00000000000..c905e8e5a61 --- /dev/null +++ b/pkg/config/v1/validation/oidc.go @@ -0,0 +1,57 @@ +// 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 ( + "errors" + "net/url" + "strings" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateOIDCClientCredentialsConfig(c *v1.AuthOIDCClientConfig) error { + var errs []string + + if c.ClientID == "" { + errs = append(errs, "auth.oidc.clientID is required") + } + + if c.TokenEndpointURL == "" { + errs = append(errs, "auth.oidc.tokenEndpointURL is required") + } else { + tokenURL, err := url.Parse(c.TokenEndpointURL) + if err != nil || !tokenURL.IsAbs() || tokenURL.Host == "" { + errs = append(errs, "auth.oidc.tokenEndpointURL must be an absolute http or https URL") + } else if tokenURL.Scheme != "http" && tokenURL.Scheme != "https" { + errs = append(errs, "auth.oidc.tokenEndpointURL must use http or https") + } + } + + if _, ok := c.AdditionalEndpointParams["scope"]; ok { + errs = append(errs, "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead") + } + + if c.Audience != "" { + if _, ok := c.AdditionalEndpointParams["audience"]; ok { + errs = append(errs, "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience") + } + } + + if len(errs) == 0 { + return nil + } + return errors.New(strings.Join(errs, "; ")) +} diff --git a/pkg/config/v1/validation/oidc_test.go b/pkg/config/v1/validation/oidc_test.go new file mode 100644 index 00000000000..bc21da6e552 --- /dev/null +++ b/pkg/config/v1/validation/oidc_test.go @@ -0,0 +1,78 @@ +// 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 ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestValidateOIDCClientCredentialsConfig(t *testing.T) { + tokenServer := httptest.NewServer(http.NotFoundHandler()) + defer tokenServer.Close() + + t.Run("valid", func(t *testing.T) { + require.NoError(t, ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + AdditionalEndpointParams: map[string]string{ + "resource": "api", + }, + })) + }) + + t.Run("invalid token endpoint url", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: "://bad", + }) + require.ErrorContains(t, err, "auth.oidc.tokenEndpointURL") + }) + + t.Run("missing client id", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + TokenEndpointURL: tokenServer.URL, + }) + require.ErrorContains(t, err, "auth.oidc.clientID is required") + }) + + t.Run("scope endpoint param is not allowed", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + AdditionalEndpointParams: map[string]string{ + "scope": "email", + }, + }) + require.ErrorContains(t, err, "auth.oidc.additionalEndpointParams.scope is not allowed; use auth.oidc.scope instead") + }) + + t.Run("audience conflict", func(t *testing.T) { + err := ValidateOIDCClientCredentialsConfig(&v1.AuthOIDCClientConfig{ + ClientID: "test-client", + TokenEndpointURL: tokenServer.URL, + Audience: "api", + AdditionalEndpointParams: map[string]string{ + "audience": "override", + }, + }) + require.ErrorContains(t, err, "cannot specify both auth.oidc.audience and auth.oidc.additionalEndpointParams.audience") + }) +} diff --git a/pkg/config/v1/validation/plugin.go b/pkg/config/v1/validation/plugin.go new file mode 100644 index 00000000000..30a66d83728 --- /dev/null +++ b/pkg/config/v1/validation/plugin.go @@ -0,0 +1,81 @@ +// Copyright 2023 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 ( + "errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateClientPluginOptions(c v1.ClientPluginOptions) error { + switch v := c.(type) { + case *v1.HTTP2HTTPSPluginOptions: + return validateHTTP2HTTPSPluginOptions(v) + case *v1.HTTPS2HTTPPluginOptions: + return validateHTTPS2HTTPPluginOptions(v) + case *v1.HTTPS2HTTPSPluginOptions: + return validateHTTPS2HTTPSPluginOptions(v) + case *v1.StaticFilePluginOptions: + return validateStaticFilePluginOptions(v) + case *v1.UnixDomainSocketPluginOptions: + return validateUnixDomainSocketPluginOptions(v) + case *v1.TLS2RawPluginOptions: + return validateTLS2RawPluginOptions(v) + } + return nil +} + +func validateHTTP2HTTPSPluginOptions(c *v1.HTTP2HTTPSPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} + +func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} + +func validateHTTPS2HTTPSPluginOptions(c *v1.HTTPS2HTTPSPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} + +func validateStaticFilePluginOptions(c *v1.StaticFilePluginOptions) error { + if c.LocalPath == "" { + return errors.New("localPath is required") + } + return nil +} + +func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions) error { + if c.UnixPath == "" { + return errors.New("unixPath is required") + } + return nil +} + +func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} diff --git a/pkg/config/v1/validation/proxy.go b/pkg/config/v1/validation/proxy.go new file mode 100644 index 00000000000..5d430e6bfe6 --- /dev/null +++ b/pkg/config/v1/validation/proxy.go @@ -0,0 +1,272 @@ +// Copyright 2023 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 ( + "errors" + "fmt" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/util/validation" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error { + if c.Name == "" { + return errors.New("name should not be empty") + } + + if err := ValidateAnnotations(c.Annotations); err != nil { + return err + } + if !slices.Contains([]string{"", "v1", "v2"}, c.Transport.ProxyProtocolVersion) { + return fmt.Errorf("not support proxy protocol version: %s", c.Transport.ProxyProtocolVersion) + } + if !slices.Contains([]string{"client", "server"}, c.Transport.BandwidthLimitMode) { + return fmt.Errorf("bandwidth limit mode should be client or server") + } + + if c.Plugin.Type == "" { + if err := ValidatePort(c.LocalPort, "localPort"); err != nil { + return fmt.Errorf("localPort: %v", err) + } + } + + if !slices.Contains([]string{"", "tcp", "http"}, c.HealthCheck.Type) { + return fmt.Errorf("not support health check type: %s", c.HealthCheck.Type) + } + if c.HealthCheck.Type != "" { + if c.HealthCheck.Type == "http" && + c.HealthCheck.Path == "" { + return fmt.Errorf("health check path should not be empty") + } + } + + if c.Plugin.Type != "" { + if err := ValidateClientPluginOptions(c.Plugin.ClientPluginOptions); err != nil { + return fmt.Errorf("plugin %s: %v", c.Plugin.Type, err) + } + } + return nil +} + +func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig) error { + if err := ValidateAnnotations(c.Annotations); err != nil { + return err + } + return nil +} + +func validateDomainConfigForClient(c *v1.DomainConfig) error { + if c.SubDomain == "" && len(c.CustomDomains) == 0 { + return errors.New("subdomain and custom domains should not be both empty") + } + return nil +} + +func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error { + subDomainHost := strings.ToLower(s.SubDomainHost) + for _, domain := range c.CustomDomains { + 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) + } + } + } + + if c.SubDomain != "" { + if s.SubDomainHost == "" { + return errors.New("subdomain is not supported because this feature is not enabled in server") + } + + if strings.Contains(c.SubDomain, ".") || strings.Contains(c.SubDomain, "*") { + return errors.New("'.' and '*' are not supported in subdomain") + } + } + return nil +} + +func ValidateProxyConfigurerForClient(c v1.ProxyConfigurer) error { + base := c.GetBaseConfig() + if err := validateProxyBaseConfigForClient(base); err != nil { + return err + } + + switch v := c.(type) { + case *v1.TCPProxyConfig: + return validateTCPProxyConfigForClient(v) + case *v1.UDPProxyConfig: + return validateUDPProxyConfigForClient(v) + case *v1.TCPMuxProxyConfig: + return validateTCPMuxProxyConfigForClient(v) + case *v1.HTTPProxyConfig: + return validateHTTPProxyConfigForClient(v) + case *v1.HTTPSProxyConfig: + return validateHTTPSProxyConfigForClient(v) + case *v1.STCPProxyConfig: + return validateSTCPProxyConfigForClient(v) + case *v1.XTCPProxyConfig: + return validateXTCPProxyConfigForClient(v) + case *v1.SUDPProxyConfig: + return validateSUDPProxyConfigForClient(v) + } + return errors.New("unknown proxy config type") +} + +func validateTCPProxyConfigForClient(c *v1.TCPProxyConfig) error { + return nil +} + +func validateUDPProxyConfigForClient(c *v1.UDPProxyConfig) error { + return nil +} + +func validateTCPMuxProxyConfigForClient(c *v1.TCPMuxProxyConfig) error { + if err := validateDomainConfigForClient(&c.DomainConfig); err != nil { + return err + } + + if !slices.Contains([]string{string(v1.TCPMultiplexerHTTPConnect)}, c.Multiplexer) { + return fmt.Errorf("not support multiplexer: %s", c.Multiplexer) + } + return nil +} + +func validateHTTPProxyConfigForClient(c *v1.HTTPProxyConfig) error { + return validateDomainConfigForClient(&c.DomainConfig) +} + +func validateHTTPSProxyConfigForClient(c *v1.HTTPSProxyConfig) error { + return validateDomainConfigForClient(&c.DomainConfig) +} + +func validateSTCPProxyConfigForClient(c *v1.STCPProxyConfig) error { + return nil +} + +func validateXTCPProxyConfigForClient(c *v1.XTCPProxyConfig) error { + return nil +} + +func validateSUDPProxyConfigForClient(c *v1.SUDPProxyConfig) error { + return nil +} + +func ValidateProxyConfigurerForServer(c v1.ProxyConfigurer, s *v1.ServerConfig) error { + base := c.GetBaseConfig() + if err := validateProxyBaseConfigForServer(base); err != nil { + return err + } + + switch v := c.(type) { + case *v1.TCPProxyConfig: + return validateTCPProxyConfigForServer(v, s) + case *v1.UDPProxyConfig: + return validateUDPProxyConfigForServer(v, s) + case *v1.TCPMuxProxyConfig: + return validateTCPMuxProxyConfigForServer(v, s) + case *v1.HTTPProxyConfig: + return validateHTTPProxyConfigForServer(v, s) + case *v1.HTTPSProxyConfig: + return validateHTTPSProxyConfigForServer(v, s) + case *v1.STCPProxyConfig: + return validateSTCPProxyConfigForServer(v, s) + case *v1.XTCPProxyConfig: + return validateXTCPProxyConfigForServer(v, s) + case *v1.SUDPProxyConfig: + return validateSUDPProxyConfigForServer(v, s) + default: + return errors.New("unknown proxy config type") + } +} + +func validateTCPProxyConfigForServer(c *v1.TCPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateUDPProxyConfigForServer(c *v1.UDPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateTCPMuxProxyConfigForServer(c *v1.TCPMuxProxyConfig, s *v1.ServerConfig) error { + if c.Multiplexer == string(v1.TCPMultiplexerHTTPConnect) && + s.TCPMuxHTTPConnectPort == 0 { + return fmt.Errorf("tcpmux with multiplexer httpconnect not supported because this feature is not enabled in server") + } + + return validateDomainConfigForServer(&c.DomainConfig, s) +} + +func validateHTTPProxyConfigForServer(c *v1.HTTPProxyConfig, s *v1.ServerConfig) error { + if s.VhostHTTPPort == 0 { + return fmt.Errorf("type [http] not supported when vhost http port is not set") + } + + return validateDomainConfigForServer(&c.DomainConfig, s) +} + +func validateHTTPSProxyConfigForServer(c *v1.HTTPSProxyConfig, s *v1.ServerConfig) error { + if s.VhostHTTPSPort == 0 { + return fmt.Errorf("type [https] not supported when vhost https port is not set") + } + + return validateDomainConfigForServer(&c.DomainConfig, s) +} + +func validateSTCPProxyConfigForServer(c *v1.STCPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateXTCPProxyConfigForServer(c *v1.XTCPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateSUDPProxyConfigForServer(c *v1.SUDPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +// ValidateAnnotations validates that a set of annotations are correctly defined. +func ValidateAnnotations(annotations map[string]string) error { + if len(annotations) == 0 { + return nil + } + + var errs error + for k := range annotations { + for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) { + errs = AppendError(errs, fmt.Errorf("annotation key %s is invalid: %s", k, msg)) + } + } + if err := ValidateAnnotationsSize(annotations); err != nil { + errs = AppendError(errs, err) + } + return errs +} + +const TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB + +func ValidateAnnotationsSize(annotations map[string]string) error { + var totalSize int64 + for k, v := range annotations { + totalSize += (int64)(len(k)) + (int64)(len(v)) + } + if totalSize > (int64)(TotalAnnotationSizeLimitB) { + return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, TotalAnnotationSizeLimitB) + } + return nil +} diff --git a/pkg/config/v1/validation/proxy_test.go b/pkg/config/v1/validation/proxy_test.go new file mode 100644 index 00000000000..b9411dc84ab --- /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) + }) + } +} diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go new file mode 100644 index 00000000000..54bd8348dfe --- /dev/null +++ b/pkg/config/v1/validation/server.go @@ -0,0 +1,64 @@ +// Copyright 2023 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" + "slices" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) + if !slices.Contains(SupportedAuthMethods, c.Auth.Method) { + errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) + } + if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { + errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) + } + + errs = AppendError(errs, v.validateAuthTokenSource(c.Auth.Token, c.Auth.TokenSource)) + + if err := validateLogConfig(&c.Log); err != nil { + errs = AppendError(errs, err) + } + + if err := validateWebServerConfig(&c.WebServer); err != nil { + errs = AppendError(errs, err) + } + + errs = AppendError(errs, ValidatePort(c.BindPort, "bindPort")) + errs = AppendError(errs, ValidatePort(c.KCPBindPort, "kcpBindPort")) + errs = AppendError(errs, ValidatePort(c.QUICBindPort, "quicBindPort")) + 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) { + errs = AppendError(errs, fmt.Errorf("invalid http plugin ops, optional values are %v", SupportedHTTPPluginOps)) + } + } + return warnings, errs +} diff --git a/pkg/config/v1/validation/server_test.go b/pkg/config/v1/validation/server_test.go new file mode 100644 index 00000000000..7c0c57ad591 --- /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/pkg/config/v1/validation/validation.go b/pkg/config/v1/validation/validation.go new file mode 100644 index 00000000000..417cec0f29a --- /dev/null +++ b/pkg/config/v1/validation/validation.go @@ -0,0 +1,72 @@ +// Copyright 2023 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 ( + "errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" + splugin "github.com/fatedier/frp/pkg/plugin/server" +) + +var ( + SupportedTransportProtocols = []string{ + "tcp", + "kcp", + "quic", + "websocket", + "wss", + } + SupportedWireProtocols = []string{ + "v1", + "v2", + } + + SupportedAuthMethods = []v1.AuthMethod{ + "token", + "oidc", + } + + SupportedAuthAdditionalScopes = []v1.AuthScope{ + "HeartBeats", + "NewWorkConns", + } + + SupportedLogLevels = []string{ + "trace", + "debug", + "info", + "warn", + "error", + } + + SupportedHTTPPluginOps = []string{ + splugin.OpLogin, + splugin.OpNewProxy, + splugin.OpCloseProxy, + splugin.OpPing, + splugin.OpNewWorkConn, + splugin.OpNewUserConn, + } +) + +type Warning error + +func AppendError(err error, errs ...error) error { + if len(errs) == 0 { + return err + } + return errors.Join(append([]error{err}, errs...)...) +} diff --git a/pkg/config/v1/validation/validator.go b/pkg/config/v1/validation/validator.go new file mode 100644 index 00000000000..1cfe3b21299 --- /dev/null +++ b/pkg/config/v1/validation/validator.go @@ -0,0 +1,28 @@ +package validation + +import ( + "fmt" + + "github.com/fatedier/frp/pkg/policy/security" +) + +// ConfigValidator holds the context dependencies for configuration validation. +type ConfigValidator struct { + unsafeFeatures *security.UnsafeFeatures +} + +// NewConfigValidator creates a new ConfigValidator instance. +func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator { + return &ConfigValidator{ + unsafeFeatures: unsafeFeatures, + } +} + +// ValidateUnsafeFeature checks if a specific unsafe feature is enabled. +func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error { + if !v.unsafeFeatures.IsEnabled(feature) { + return fmt.Errorf("unsafe feature %q is not enabled. "+ + "To enable it, ensure it is allowed in the configuration or command line flags", feature) + } + return nil +} diff --git a/pkg/config/v1/validation/visitor.go b/pkg/config/v1/validation/visitor.go new file mode 100644 index 00000000000..50efc47ddb6 --- /dev/null +++ b/pkg/config/v1/validation/visitor.go @@ -0,0 +1,62 @@ +// Copyright 2023 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 ( + "errors" + "fmt" + "slices" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateVisitorConfigurer(c v1.VisitorConfigurer) error { + base := c.GetBaseConfig() + if err := validateVisitorBaseConfig(base); err != nil { + return err + } + + switch v := c.(type) { + case *v1.STCPVisitorConfig: + case *v1.SUDPVisitorConfig: + case *v1.XTCPVisitorConfig: + return validateXTCPVisitorConfig(v) + default: + return errors.New("unknown visitor config type") + } + return nil +} + +func validateVisitorBaseConfig(c *v1.VisitorBaseConfig) error { + if c.Name == "" { + return errors.New("name is required") + } + + if c.ServerName == "" { + return errors.New("server name is required") + } + + if c.BindPort == 0 { + return errors.New("bind port is required") + } + return nil +} + +func validateXTCPVisitorConfig(c *v1.XTCPVisitorConfig) error { + if !slices.Contains([]string{"kcp", "quic"}, c.Protocol) { + return fmt.Errorf("protocol should be kcp or quic") + } + return nil +} diff --git a/pkg/config/v1/value_source.go b/pkg/config/v1/value_source.go new file mode 100644 index 00000000000..88dbaff3641 --- /dev/null +++ b/pkg/config/v1/value_source.go @@ -0,0 +1,158 @@ +// Copyright 2025 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 v1 + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "strings" +) + +// ValueSource provides a way to dynamically resolve configuration values +// from various sources like files, environment variables, or external services. +type ValueSource struct { + Type string `json:"type"` + File *FileSource `json:"file,omitempty"` + Exec *ExecSource `json:"exec,omitempty"` +} + +// FileSource specifies how to load a value from a file. +type FileSource struct { + Path string `json:"path"` +} + +// ExecSource specifies how to get a value from another program launched as subprocess. +type ExecSource struct { + Command string `json:"command"` + Args []string `json:"args,omitempty"` + Env []ExecEnvVar `json:"env,omitempty"` +} + +type ExecEnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Validate validates the ValueSource configuration. +func (v *ValueSource) Validate() error { + if v == nil { + return errors.New("valueSource cannot be nil") + } + + switch v.Type { + case "file": + if v.File == nil { + return errors.New("file configuration is required when type is 'file'") + } + return v.File.Validate() + case "exec": + if v.Exec == nil { + return errors.New("exec configuration is required when type is 'exec'") + } + return v.Exec.Validate() + default: + return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type) + } +} + +// Resolve resolves the value from the configured source. +func (v *ValueSource) Resolve(ctx context.Context) (string, error) { + if err := v.Validate(); err != nil { + return "", err + } + + switch v.Type { + case "file": + return v.File.Resolve(ctx) + case "exec": + return v.Exec.Resolve(ctx) + default: + return "", fmt.Errorf("unsupported value source type: %s", v.Type) + } +} + +// Validate validates the FileSource configuration. +func (f *FileSource) Validate() error { + if f == nil { + return errors.New("fileSource cannot be nil") + } + + if f.Path == "" { + return errors.New("file path cannot be empty") + } + return nil +} + +// Resolve reads and returns the content from the specified file. +func (f *FileSource) Resolve(_ context.Context) (string, error) { + if err := f.Validate(); err != nil { + return "", err + } + + content, err := os.ReadFile(f.Path) + if err != nil { + return "", fmt.Errorf("failed to read file %s: %v", f.Path, err) + } + + // Trim whitespace, which is important for file-based tokens + return strings.TrimSpace(string(content)), nil +} + +// Validate validates the ExecSource configuration. +func (e *ExecSource) Validate() error { + if e == nil { + return errors.New("execSource cannot be nil") + } + + if e.Command == "" { + return errors.New("exec command cannot be empty") + } + + for _, env := range e.Env { + if env.Name == "" { + return errors.New("exec env name cannot be empty") + } + if strings.Contains(env.Name, "=") { + return errors.New("exec env name cannot contain '='") + } + } + return nil +} + +// Resolve reads and returns the content captured from stdout of launched subprocess. +func (e *ExecSource) Resolve(ctx context.Context) (string, error) { + if err := e.Validate(); err != nil { + return "", err + } + + cmd := exec.CommandContext(ctx, e.Command, e.Args...) + if len(e.Env) != 0 { + cmd.Env = os.Environ() + for _, env := range e.Env { + cmd.Env = append(cmd.Env, env.Name+"="+env.Value) + } + } + + content, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err) + } + + // Trim whitespace, which is important for exec-based tokens + return strings.TrimSpace(string(content)), nil +} diff --git a/pkg/config/v1/value_source_test.go b/pkg/config/v1/value_source_test.go new file mode 100644 index 00000000000..685151f44ad --- /dev/null +++ b/pkg/config/v1/value_source_test.go @@ -0,0 +1,246 @@ +// Copyright 2025 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 v1 + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +func TestValueSource_Validate(t *testing.T) { + tests := []struct { + name string + vs *ValueSource + wantErr bool + }{ + { + name: "nil valueSource", + vs: nil, + wantErr: true, + }, + { + name: "unsupported type", + vs: &ValueSource{ + Type: "unsupported", + }, + wantErr: true, + }, + { + name: "file type without file config", + vs: &ValueSource{ + Type: "file", + File: nil, + }, + wantErr: true, + }, + { + name: "valid file type with absolute path", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: "/tmp/test", + }, + }, + wantErr: false, + }, + { + name: "valid file type with relative path", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: "configs/token", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.vs.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("ValueSource.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFileSource_Validate(t *testing.T) { + tests := []struct { + name string + fs *FileSource + wantErr bool + }{ + { + name: "nil fileSource", + fs: nil, + wantErr: true, + }, + { + name: "empty path", + fs: &FileSource{ + Path: "", + }, + wantErr: true, + }, + { + name: "relative path (allowed)", + fs: &FileSource{ + Path: "relative/path", + }, + wantErr: false, + }, + { + name: "absolute path", + fs: &FileSource{ + Path: "/absolute/path", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.fs.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("FileSource.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFileSource_Resolve(t *testing.T) { + // Create a temporary file for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test_token") + testContent := "test-token-value\n\t " + expectedContent := "test-token-value" + + err := os.WriteFile(testFile, []byte(testContent), 0o600) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + tests := []struct { + name string + fs *FileSource + want string + wantErr bool + }{ + { + name: "valid file path", + fs: &FileSource{ + Path: testFile, + }, + want: expectedContent, + wantErr: false, + }, + { + name: "non-existent file", + fs: &FileSource{ + Path: "/non/existent/file", + }, + want: "", + wantErr: true, + }, + { + name: "path traversal attempt (should fail validation)", + fs: &FileSource{ + Path: "../../../etc/passwd", + }, + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fs.Resolve(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("FileSource.Resolve() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("FileSource.Resolve() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValueSource_Resolve(t *testing.T) { + // Create a temporary file for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test_token") + testContent := "test-token-value" + + err := os.WriteFile(testFile, []byte(testContent), 0o600) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + tests := []struct { + name string + vs *ValueSource + want string + wantErr bool + }{ + { + name: "valid file type", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: testFile, + }, + }, + want: testContent, + wantErr: false, + }, + { + name: "unsupported type", + vs: &ValueSource{ + Type: "unsupported", + }, + want: "", + wantErr: true, + }, + { + name: "file type with path traversal", + vs: &ValueSource{ + Type: "file", + File: &FileSource{ + Path: "../../../etc/passwd", + }, + }, + want: "", + wantErr: true, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.vs.Resolve(ctx) + if (err != nil) != tt.wantErr { + t.Errorf("ValueSource.Resolve() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ValueSource.Resolve() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go new file mode 100644 index 00000000000..d1035d4f473 --- /dev/null +++ b/pkg/config/v1/visitor.go @@ -0,0 +1,171 @@ +// Copyright 2023 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 v1 + +import ( + "reflect" + + "github.com/fatedier/frp/pkg/util/jsonx" + "github.com/fatedier/frp/pkg/util/util" +) + +type VisitorTransport struct { + UseEncryption bool `json:"useEncryption,omitempty"` + UseCompression bool `json:"useCompression,omitempty"` +} + +type VisitorBaseConfig struct { + Name string `json:"name"` + Type string `json:"type"` + // Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled. + // This allows individual control over each visitor, complementing the global "start" field. + Enabled *bool `json:"enabled,omitempty"` + Transport VisitorTransport `json:"transport,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + // if the server user is not set, it defaults to the current user + ServerUser string `json:"serverUser,omitempty"` + ServerName string `json:"serverName,omitempty"` + BindAddr string `json:"bindAddr,omitempty"` + // BindPort is the port that visitor listens on. + // It can be less than 0, it means don't bind to the port and only receive connections redirected from + // other visitors. (This is not supported for SUDP now) + BindPort int `json:"bindPort,omitempty"` + + // Plugin specifies what plugin should be used. + Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"` +} + +func (c VisitorBaseConfig) Clone() VisitorBaseConfig { + out := c + out.Enabled = util.ClonePtr(c.Enabled) + out.Plugin = c.Plugin.Clone() + return out +} + +func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig { + return c +} + +func (c *VisitorBaseConfig) Complete() { + if c.BindAddr == "" { + c.BindAddr = "127.0.0.1" + } +} + +type VisitorConfigurer interface { + Complete() + GetBaseConfig() *VisitorBaseConfig + Clone() VisitorConfigurer +} + +type VisitorType string + +const ( + VisitorTypeSTCP VisitorType = "stcp" + VisitorTypeXTCP VisitorType = "xtcp" + VisitorTypeSUDP VisitorType = "sudp" +) + +var visitorConfigTypeMap = map[VisitorType]reflect.Type{ + VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](), + VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](), + VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](), +} + +type TypedVisitorConfig struct { + Type string `json:"type"` + VisitorConfigurer +} + +func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error { + configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{}) + if err != nil { + return err + } + + c.Type = configurer.GetBaseConfig().Type + c.VisitorConfigurer = configurer + return nil +} + +func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) { + return jsonx.Marshal(c.VisitorConfigurer) +} + +func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer { + v, ok := visitorConfigTypeMap[t] + if !ok { + return nil + } + vc := reflect.New(v).Interface().(VisitorConfigurer) + vc.GetBaseConfig().Type = string(t) + return vc +} + +var _ VisitorConfigurer = &STCPVisitorConfig{} + +type STCPVisitorConfig struct { + VisitorBaseConfig +} + +func (c *STCPVisitorConfig) Clone() VisitorConfigurer { + out := *c + out.VisitorBaseConfig = c.VisitorBaseConfig.Clone() + return &out +} + +var _ VisitorConfigurer = &SUDPVisitorConfig{} + +type SUDPVisitorConfig struct { + VisitorBaseConfig +} + +func (c *SUDPVisitorConfig) Clone() VisitorConfigurer { + out := *c + out.VisitorBaseConfig = c.VisitorBaseConfig.Clone() + return &out +} + +var _ VisitorConfigurer = &XTCPVisitorConfig{} + +type XTCPVisitorConfig struct { + VisitorBaseConfig + + Protocol string `json:"protocol,omitempty"` + KeepTunnelOpen bool `json:"keepTunnelOpen,omitempty"` + MaxRetriesAnHour int `json:"maxRetriesAnHour,omitempty"` + MinRetryInterval int `json:"minRetryInterval,omitempty"` + FallbackTo string `json:"fallbackTo,omitempty"` + FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` + + // NatTraversal configuration for NAT traversal + NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"` +} + +func (c *XTCPVisitorConfig) Complete() { + c.VisitorBaseConfig.Complete() + + c.Protocol = util.EmptyOr(c.Protocol, "quic") + c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8) + c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90) + c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000) +} + +func (c *XTCPVisitorConfig) Clone() VisitorConfigurer { + out := *c + out.VisitorBaseConfig = c.VisitorBaseConfig.Clone() + out.NatTraversal = c.NatTraversal.Clone() + return &out +} diff --git a/pkg/config/v1/visitor_plugin.go b/pkg/config/v1/visitor_plugin.go new file mode 100644 index 00000000000..6541e3de10a --- /dev/null +++ b/pkg/config/v1/visitor_plugin.go @@ -0,0 +1,75 @@ +// Copyright 2025 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 v1 + +import ( + "reflect" + + "github.com/fatedier/frp/pkg/util/jsonx" +) + +const ( + VisitorPluginVirtualNet = "virtual_net" +) + +var visitorPluginOptionsTypeMap = map[string]reflect.Type{ + VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](), +} + +type VisitorPluginOptions interface { + Complete() + Clone() VisitorPluginOptions +} + +type TypedVisitorPluginOptions struct { + Type string `json:"type"` + VisitorPluginOptions +} + +func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions { + out := c + if c.VisitorPluginOptions != nil { + out.VisitorPluginOptions = c.VisitorPluginOptions.Clone() + } + return out +} + +func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error { + decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{}) + if err != nil { + return err + } + *c = decoded + return nil +} + +func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) { + return jsonx.Marshal(c.VisitorPluginOptions) +} + +type VirtualNetVisitorPluginOptions struct { + Type string `json:"type"` + DestinationIP string `json:"destinationIP"` +} + +func (o *VirtualNetVisitorPluginOptions) Complete() {} + +func (o *VirtualNetVisitorPluginOptions) Clone() VisitorPluginOptions { + if o == nil { + return nil + } + out := *o + return &out +} diff --git a/pkg/config/visitor.go b/pkg/config/visitor.go deleted file mode 100644 index aede14d8b04..00000000000 --- a/pkg/config/visitor.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 config - -import ( - "fmt" - "reflect" - - "github.com/fatedier/frp/pkg/consts" - - "gopkg.in/ini.v1" -) - -// Visitor -var ( - visitorConfTypeMap = map[string]reflect.Type{ - consts.STCPProxy: reflect.TypeOf(STCPVisitorConf{}), - consts.XTCPProxy: reflect.TypeOf(XTCPVisitorConf{}), - consts.SUDPProxy: reflect.TypeOf(SUDPVisitorConf{}), - } -) - -type VisitorConf interface { - GetBaseInfo() *BaseVisitorConf - Compare(cmp VisitorConf) bool - UnmarshalFromIni(prefix string, name string, section *ini.Section) error - Check() error -} - -type BaseVisitorConf struct { - ProxyName string `ini:"name" json:"name"` - ProxyType string `ini:"type" json:"type"` - UseEncryption bool `ini:"use_encryption" json:"use_encryption"` - UseCompression bool `ini:"use_compression" json:"use_compression"` - Role string `ini:"role" json:"role"` - Sk string `ini:"sk" json:"sk"` - ServerName string `ini:"server_name" json:"server_name"` - BindAddr string `ini:"bind_addr" json:"bind_addr"` - BindPort int `ini:"bind_port" json:"bind_port"` -} - -type SUDPVisitorConf struct { - BaseVisitorConf `ini:",extends"` -} - -type STCPVisitorConf struct { - BaseVisitorConf `ini:",extends"` -} - -type XTCPVisitorConf struct { - BaseVisitorConf `ini:",extends"` -} - -// DefaultVisitorConf creates a empty VisitorConf object by visitorType. -// If visitorType doesn't exist, return nil. -func DefaultVisitorConf(visitorType string) VisitorConf { - v, ok := visitorConfTypeMap[visitorType] - if !ok { - return nil - } - - return reflect.New(v).Interface().(VisitorConf) -} - -// Visitor loaded from ini -func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) { - // section.Key: if key not exists, section will set it with default value. - visitorType := section.Key("type").String() - - if visitorType == "" { - return nil, fmt.Errorf("visitor [%s] type shouldn't be empty", name) - } - - conf := DefaultVisitorConf(visitorType) - if conf == nil { - return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType) - } - - if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { - return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType) - } - - if err := conf.Check(); err != nil { - return nil, err - } - - return conf, nil -} - -// Base -func (cfg *BaseVisitorConf) GetBaseInfo() *BaseVisitorConf { - return cfg -} - -func (cfg *BaseVisitorConf) compare(cmp *BaseVisitorConf) bool { - if cfg.ProxyName != cmp.ProxyName || - cfg.ProxyType != cmp.ProxyType || - cfg.UseEncryption != cmp.UseEncryption || - cfg.UseCompression != cmp.UseCompression || - cfg.Role != cmp.Role || - cfg.Sk != cmp.Sk || - cfg.ServerName != cmp.ServerName || - cfg.BindAddr != cmp.BindAddr || - cfg.BindPort != cmp.BindPort { - return false - } - return true -} - -func (cfg *BaseVisitorConf) check() (err error) { - if cfg.Role != "visitor" { - err = fmt.Errorf("invalid role") - return - } - if cfg.BindAddr == "" { - err = fmt.Errorf("bind_addr shouldn't be empty") - return - } - if cfg.BindPort <= 0 { - err = fmt.Errorf("bind_port is required") - return - } - return -} - -func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section *ini.Section) error { - - // Custom decoration after basic unmarshal: - // proxy name - cfg.ProxyName = prefix + name - - // server_name - cfg.ServerName = prefix + cfg.ServerName - - // bind_addr - if cfg.BindAddr == "" { - cfg.BindAddr = "127.0.0.1" - } - - return nil -} - -func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, section *ini.Section) error { - err := section.MapTo(cfg) - if err != nil { - return err - } - - err = cfg.GetBaseInfo().unmarshalFromIni(prefix, name, section) - if err != nil { - return err - } - - return nil -} - -// SUDP -var _ VisitorConf = &SUDPVisitorConf{} - -func (cfg *SUDPVisitorConf) Compare(cmp VisitorConf) bool { - cmpConf, ok := cmp.(*SUDPVisitorConf) - if !ok { - return false - } - - if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) { - return false - } - - // Add custom login equal, if exists - - return true -} - -func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { - err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return - } - - // Add custom logic unmarshal, if exists - - return -} - -func (cfg *SUDPVisitorConf) Check() (err error) { - if err = cfg.BaseVisitorConf.check(); err != nil { - return - } - - // Add custom logic validate, if exists - - return -} - -// STCP -var _ VisitorConf = &STCPVisitorConf{} - -func (cfg *STCPVisitorConf) Compare(cmp VisitorConf) bool { - cmpConf, ok := cmp.(*STCPVisitorConf) - if !ok { - return false - } - - if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) { - return false - } - - // Add custom login equal, if exists - - return true -} - -func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { - err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return - } - - // Add custom logic unmarshal, if exists - - return -} - -func (cfg *STCPVisitorConf) Check() (err error) { - if err = cfg.BaseVisitorConf.check(); err != nil { - return - } - - // Add custom logic validate, if exists - - return -} - -// XTCP -var _ VisitorConf = &XTCPVisitorConf{} - -func (cfg *XTCPVisitorConf) Compare(cmp VisitorConf) bool { - cmpConf, ok := cmp.(*XTCPVisitorConf) - if !ok { - return false - } - - if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) { - return false - } - - // Add custom login equal, if exists - - return true -} - -func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { - err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return - } - - // Add custom logic unmarshal, if exists - - return -} - -func (cfg *XTCPVisitorConf) Check() (err error) { - if err = cfg.BaseVisitorConf.check(); err != nil { - return - } - - // Add custom logic validate, if exists - - return -} diff --git a/pkg/config/visitor_test.go b/pkg/config/visitor_test.go deleted file mode 100644 index ce200ed049b..00000000000 --- a/pkg/config/visitor_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/fatedier/frp/pkg/consts" - - "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" -) - -const testVisitorPrefix = "test." - -func Test_Visitor_Interface(t *testing.T) { - for name := range visitorConfTypeMap { - DefaultVisitorConf(name) - } -} - -func Test_Visitor_UnmarshalFromIni(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - sname string - source []byte - expected VisitorConf - }{ - { - sname: "secret_tcp_visitor", - source: []byte(` - [secret_tcp_visitor] - role = visitor - type = stcp - server_name = secret_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9000 - use_encryption = false - use_compression = false - `), - expected: &STCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testVisitorPrefix + "secret_tcp_visitor", - ProxyType: consts.STCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testVisitorPrefix + "secret_tcp", - BindAddr: "127.0.0.1", - BindPort: 9000, - }, - }, - }, - { - sname: "p2p_tcp_visitor", - source: []byte(` - [p2p_tcp_visitor] - role = visitor - type = xtcp - server_name = p2p_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9001 - use_encryption = false - use_compression = false - `), - expected: &XTCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testVisitorPrefix + "p2p_tcp_visitor", - ProxyType: consts.XTCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testProxyPrefix + "p2p_tcp", - BindAddr: "127.0.0.1", - BindPort: 9001, - }, - }, - }, - } - - for _, c := range testcases { - f, err := ini.LoadSources(testLoadOptions, c.source) - assert.NoError(err) - - visitorType := f.Section(c.sname).Key("type").String() - assert.NotEmpty(visitorType) - - actual := DefaultVisitorConf(visitorType) - assert.NotNil(actual) - - err = actual.UnmarshalFromIni(testVisitorPrefix, c.sname, f.Section(c.sname)) - assert.NoError(err) - assert.Equal(c.expected, actual) - } -} diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go deleted file mode 100644 index 907bc73e763..00000000000 --- a/pkg/consts/consts.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2016 fatedier, fatedier@gmail.com -// -// 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 consts - -var ( - // proxy status - Idle string = "idle" - Working string = "working" - Closed string = "closed" - Online string = "online" - Offline string = "offline" - - // proxy type - TCPProxy string = "tcp" - UDPProxy string = "udp" - TCPMuxProxy string = "tcpmux" - HTTPProxy string = "http" - HTTPSProxy string = "https" - STCPProxy string = "stcp" - XTCPProxy string = "xtcp" - SUDPProxy string = "sudp" - - // authentication method - TokenAuthMethod string = "token" - OidcAuthMethod string = "oidc" - - // TCP multiplexer - HTTPConnectTCPMultiplexer string = "httpconnect" -) diff --git a/pkg/metrics/aggregate/server.go b/pkg/metrics/aggregate/server.go index 1ff15e42d0e..9ef70ebd39c 100644 --- a/pkg/metrics/aggregate/server.go +++ b/pkg/metrics/aggregate/server.go @@ -30,7 +30,7 @@ func EnablePrometheus() { sm.Add(prometheus.ServerMetrics) } -var sm *serverMetrics = &serverMetrics{} +var sm = &serverMetrics{} func init() { metrics.Register(sm) @@ -56,9 +56,9 @@ func (m *serverMetrics) CloseClient() { } } -func (m *serverMetrics) NewProxy(name string, proxyType string) { +func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) { for _, v := range m.ms { - v.NewProxy(name, proxyType) + v.NewProxy(name, proxyType, user, clientID) } } diff --git a/pkg/metrics/mem/server.go b/pkg/metrics/mem/server.go index 55d8daf120a..fad8baef6d8 100644 --- a/pkg/metrics/mem/server.go +++ b/pkg/metrics/mem/server.go @@ -18,14 +18,19 @@ import ( "sync" "time" + "k8s.io/utils/clock" + "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/metric" server "github.com/fatedier/frp/server/metrics" ) -var sm *serverMetrics = newServerMetrics() -var ServerMetrics server.ServerMetrics -var StatsCollector Collector +var ( + sm = newServerMetrics() + + ServerMetrics server.ServerMetrics + StatsCollector Collector +) func init() { ServerMetrics = sm @@ -34,12 +39,21 @@ func init() { } type serverMetrics struct { - info *ServerStatistics - mu sync.Mutex + info *ServerStatistics + clock clock.WithTicker + mu sync.Mutex } func newServerMetrics() *serverMetrics { + return newServerMetricsWithClock(clock.RealClock{}) +} + +func newServerMetricsWithClock(clk clock.WithTicker) *serverMetrics { + if clk == nil { + clk = clock.RealClock{} + } return &serverMetrics{ + clock: clk, info: &ServerStatistics{ TotalTrafficIn: metric.NewDateCounter(ReserveDays), TotalTrafficOut: metric.NewDateCounter(ReserveDays), @@ -54,28 +68,54 @@ func newServerMetrics() *serverMetrics { } func (m *serverMetrics) run() { - go func() { - for { - time.Sleep(12 * time.Hour) - log.Debug("start to clear useless proxy statistics data...") - m.clearUselessInfo() - log.Debug("finish to clear useless proxy statistics data") + go m.runUntil(nil) +} + +func (m *serverMetrics) runUntil(stopCh <-chan struct{}) { + ticker := m.clock.NewTicker(12 * time.Hour) + defer ticker.Stop() + + for { + select { + case <-ticker.C(): + start := m.clock.Now() + count, total := m.clearUselessInfo(time.Duration(7*24) * time.Hour) + log.Debugf("clear useless proxy statistics data count %d/%d, cost %v", count, total, m.clock.Since(start)) + case <-stopCh: + return } - }() + } } -func (m *serverMetrics) clearUselessInfo() { - // To check if there are proxies that closed than 7 days and drop them. +func (m *serverMetrics) clearUselessInfo(continuousOfflineDuration time.Duration) (int, int) { + count := 0 + total := 0 + // To check if there are any proxies that have been closed for more than continuousOfflineDuration and remove them. m.mu.Lock() defer m.mu.Unlock() + total = len(m.info.ProxyStatistics) for name, data := range m.info.ProxyStatistics { - if !data.LastCloseTime.IsZero() && - data.LastStartTime.Before(data.LastCloseTime) && - time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour { + if m.shouldClearProxyStats(data, continuousOfflineDuration) { delete(m.info.ProxyStatistics, name) - log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String()) + count++ + log.Tracef("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String()) } } + return count, total +} + +func (m *serverMetrics) shouldClearProxyStats(data *ProxyStatistics, continuousOfflineDuration time.Duration) bool { + return !data.LastCloseTime.IsZero() && + data.LastStartTime.Before(data.LastCloseTime) && + m.clock.Since(data.LastCloseTime) > continuousOfflineDuration +} + +func (m *serverMetrics) ClearOfflineProxies() (int, int) { + return m.clearUselessInfo(0) +} + +func (m *serverMetrics) PruneOfflineProxies() (int, int) { + return m.clearUselessInfo(0) } func (m *serverMetrics) NewClient() { @@ -86,7 +126,7 @@ func (m *serverMetrics) CloseClient() { m.info.ClientCounts.Dec(1) } -func (m *serverMetrics) NewProxy(name string, proxyType string) { +func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) { m.mu.Lock() defer m.mu.Unlock() counter, ok := m.info.ProxyTypeCounts[proxyType] @@ -97,7 +137,7 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) { m.info.ProxyTypeCounts[proxyType] = counter proxyStats, ok := m.info.ProxyStatistics[name] - if !(ok && proxyStats.ProxyType == proxyType) { + if !ok || proxyStats.ProxyType != proxyType { proxyStats = &ProxyStatistics{ Name: name, ProxyType: proxyType, @@ -107,7 +147,9 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) { } m.info.ProxyStatistics[name] = proxyStats } - proxyStats.LastStartTime = time.Now() + proxyStats.User = user + proxyStats.ClientID = clientID + proxyStats.LastStartTime = m.clock.Now() } func (m *serverMetrics) CloseProxy(name string, proxyType string) { @@ -117,11 +159,11 @@ func (m *serverMetrics) CloseProxy(name string, proxyType string) { counter.Dec(1) } if proxyStats, ok := m.info.ProxyStatistics[name]; ok { - proxyStats.LastCloseTime = time.Now() + proxyStats.LastCloseTime = m.clock.Now() } } -func (m *serverMetrics) OpenConnection(name string, proxyType string) { +func (m *serverMetrics) OpenConnection(name string, _ string) { m.info.CurConns.Inc(1) m.mu.Lock() @@ -129,11 +171,10 @@ func (m *serverMetrics) OpenConnection(name string, proxyType string) { proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.CurConns.Inc(1) - m.info.ProxyStatistics[name] = proxyStats } } -func (m *serverMetrics) CloseConnection(name string, proxyType string) { +func (m *serverMetrics) CloseConnection(name string, _ string) { m.info.CurConns.Dec(1) m.mu.Lock() @@ -141,11 +182,10 @@ func (m *serverMetrics) CloseConnection(name string, proxyType string) { proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.CurConns.Dec(1) - m.info.ProxyStatistics[name] = proxyStats } } -func (m *serverMetrics) AddTrafficIn(name string, proxyType string, trafficBytes int64) { +func (m *serverMetrics) AddTrafficIn(name string, _ string, trafficBytes int64) { m.info.TotalTrafficIn.Inc(trafficBytes) m.mu.Lock() @@ -154,11 +194,10 @@ func (m *serverMetrics) AddTrafficIn(name string, proxyType string, trafficBytes proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.TrafficIn.Inc(trafficBytes) - m.info.ProxyStatistics[name] = proxyStats } } -func (m *serverMetrics) AddTrafficOut(name string, proxyType string, trafficBytes int64) { +func (m *serverMetrics) AddTrafficOut(name string, _ string, trafficBytes int64) { m.info.TotalTrafficOut.Inc(trafficBytes) m.mu.Lock() @@ -167,7 +206,6 @@ func (m *serverMetrics) AddTrafficOut(name string, proxyType string, trafficByte proxyStats, ok := m.info.ProxyStatistics[name] if ok { proxyStats.TrafficOut.Inc(trafficBytes) - m.info.ProxyStatistics[name] = proxyStats } } @@ -189,6 +227,27 @@ func (m *serverMetrics) GetServer() *ServerStats { return s } +func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats { + ps := &ProxyStats{ + Name: name, + Type: proxyStats.ProxyType, + User: proxyStats.User, + ClientID: proxyStats.ClientID, + TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), + TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), + CurConns: int64(proxyStats.CurConns.Count()), + } + if !proxyStats.LastStartTime.IsZero() { + ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") + ps.LastStartAt = proxyStats.LastStartTime.Unix() + } + if !proxyStats.LastCloseTime.IsZero() { + ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") + ps.LastCloseAt = proxyStats.LastCloseTime.Unix() + } + return ps +} + func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats { res := make([]*ProxyStats, 0) m.mu.Lock() @@ -198,21 +257,7 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats { if proxyStats.ProxyType != proxyType { continue } - - ps := &ProxyStats{ - Name: name, - Type: proxyStats.ProxyType, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: int64(proxyStats.CurConns.Count()), - } - if !proxyStats.LastStartTime.IsZero() { - ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } - res = append(res, ps) + res = append(res, toProxyStats(name, proxyStats)) } return res } @@ -221,29 +266,20 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri m.mu.Lock() defer m.mu.Unlock() - for name, proxyStats := range m.info.ProxyStatistics { - if proxyStats.ProxyType != proxyType { - continue - } + proxyStats, ok := m.info.ProxyStatistics[proxyName] + if ok && proxyStats.ProxyType == proxyType { + res = toProxyStats(proxyName, proxyStats) + } + return +} - if name != proxyName { - continue - } +func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) { + m.mu.Lock() + defer m.mu.Unlock() - res = &ProxyStats{ - Name: name, - Type: proxyStats.ProxyType, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: int64(proxyStats.CurConns.Count()), - } - if !proxyStats.LastStartTime.IsZero() { - res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } - break + proxyStats, ok := m.info.ProxyStatistics[proxyName] + if ok { + res = toProxyStats(proxyName, proxyStats) } return } diff --git a/pkg/metrics/mem/server_test.go b/pkg/metrics/mem/server_test.go new file mode 100644 index 00000000000..12040c9fbfe --- /dev/null +++ b/pkg/metrics/mem/server_test.go @@ -0,0 +1,153 @@ +package mem + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + clocktesting "k8s.io/utils/clock/testing" +) + +func TestServerMetricsUsesClockForProxyTimestamps(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + metrics := newServerMetricsWithClock(clk) + + metrics.NewProxy("proxy", "tcp", "user", "client-id") + require.Equal(start, metrics.info.ProxyStatistics["proxy"].LastStartTime) + + closedAt := start.Add(time.Minute) + clk.SetTime(closedAt) + metrics.CloseProxy("proxy", "tcp") + require.Equal(closedAt, metrics.info.ProxyStatistics["proxy"].LastCloseTime) + + stats := metrics.GetProxyByName("proxy") + require.Equal(start.Format("01-02 15:04:05"), stats.LastStartTime) + require.Equal(closedAt.Format("01-02 15:04:05"), stats.LastCloseTime) + require.Equal(start.Unix(), stats.LastStartAt) + require.Equal(closedAt.Unix(), stats.LastCloseAt) +} + +func TestServerMetricsClearUselessInfoUsesClock(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start.Add(25 * time.Hour)) + metrics := newServerMetricsWithClock(clk) + metrics.info.ProxyStatistics["proxy"] = &ProxyStatistics{ + Name: "proxy", + LastStartTime: start.Add(-time.Hour), + LastCloseTime: start, + } + + count, total := metrics.clearUselessInfo(24 * time.Hour) + + require.Equal(1, count) + require.Equal(1, total) + require.Empty(metrics.info.ProxyStatistics) +} + +func TestServerMetricsClearOfflineProxiesPreservesLegacyTotal(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start.Add(time.Minute)) + metrics := newServerMetricsWithClock(clk) + metrics.info.ProxyStatistics["offline"] = &ProxyStatistics{ + Name: "offline", + LastStartTime: start.Add(-time.Hour), + LastCloseTime: start, + } + metrics.info.ProxyStatistics["online"] = &ProxyStatistics{ + Name: "online", + LastStartTime: start, + } + + cleared, total := metrics.ClearOfflineProxies() + + require.Equal(1, cleared) + require.Equal(2, total) + require.False(metrics.hasProxyStatistics("offline")) + require.True(metrics.hasProxyStatistics("online")) +} + +func TestServerMetricsPruneOfflineProxiesReportsTotalStats(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start.Add(time.Minute)) + metrics := newServerMetricsWithClock(clk) + metrics.info.ProxyStatistics["offline"] = &ProxyStatistics{ + Name: "offline", + LastStartTime: start.Add(-time.Hour), + LastCloseTime: start, + } + metrics.info.ProxyStatistics["online"] = &ProxyStatistics{ + Name: "online", + LastStartTime: start, + } + metrics.info.ProxyStatistics["restarted"] = &ProxyStatistics{ + Name: "restarted", + LastStartTime: start.Add(30 * time.Second), + LastCloseTime: start, + } + metrics.info.ProxyStatistics["same-time"] = &ProxyStatistics{ + Name: "same-time", + LastStartTime: start, + LastCloseTime: start, + } + + cleared, total := metrics.PruneOfflineProxies() + + require.Equal(1, cleared) + require.Equal(4, total) + require.False(metrics.hasProxyStatistics("offline")) + require.True(metrics.hasProxyStatistics("online")) + require.True(metrics.hasProxyStatistics("restarted")) + require.True(metrics.hasProxyStatistics("same-time")) + + cleared, total = metrics.PruneOfflineProxies() + require.Equal(0, cleared) + require.Equal(3, total) +} + +func TestServerMetricsRunUsesClockTicker(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + metrics := newServerMetricsWithClock(clk) + metrics.info.ProxyStatistics["proxy"] = &ProxyStatistics{ + Name: "proxy", + LastStartTime: start.Add(-time.Hour), + LastCloseTime: start, + } + + stopCh := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer close(done) + metrics.runUntil(stopCh) + }() + t.Cleanup(func() { + close(stopCh) + <-done + }) + + require.Eventually(clk.HasWaiters, time.Second, time.Millisecond) + clk.Step(8 * 24 * time.Hour) + + require.Eventually(func() bool { + return !metrics.hasProxyStatistics("proxy") + }, time.Second, time.Millisecond) +} + +func (m *serverMetrics) hasProxyStatistics(name string) bool { + m.mu.Lock() + defer m.mu.Unlock() + + _, ok := m.info.ProxyStatistics[name] + return ok +} diff --git a/pkg/metrics/mem/types.go b/pkg/metrics/mem/types.go index 2650b54b4a1..6361693b997 100644 --- a/pkg/metrics/mem/types.go +++ b/pkg/metrics/mem/types.go @@ -35,10 +35,14 @@ type ServerStats struct { type ProxyStats struct { Name string Type string + User string + ClientID string TodayTrafficIn int64 TodayTrafficOut int64 LastStartTime string LastCloseTime string + LastStartAt int64 + LastCloseAt int64 CurConns int64 } @@ -51,6 +55,8 @@ type ProxyTrafficInfo struct { type ProxyStatistics struct { Name string ProxyType string + User string + ClientID string TrafficIn metric.DateCounter TrafficOut metric.DateCounter CurConns metric.Counter @@ -78,5 +84,8 @@ type Collector interface { GetServer() *ServerStats GetProxiesByType(proxyType string) []*ProxyStats GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats + GetProxyByName(proxyName string) *ProxyStats GetProxyTraffic(name string) *ProxyTrafficInfo + ClearOfflineProxies() (int, int) + PruneOfflineProxies() (int, int) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 6520317d8fd..12c388a5229 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -1,8 +1,24 @@ +// Copyright 2023 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 metrics import ( "github.com/fatedier/frp/pkg/metrics/aggregate" ) -var EnableMem = aggregate.EnableMem -var EnablePrometheus = aggregate.EnablePrometheus +var ( + EnableMem = aggregate.EnableMem + EnablePrometheus = aggregate.EnablePrometheus +) diff --git a/pkg/metrics/prometheus/server.go b/pkg/metrics/prometheus/server.go index 9cfdfda65aa..6a300ffe0f4 100644 --- a/pkg/metrics/prometheus/server.go +++ b/pkg/metrics/prometheus/server.go @@ -1,9 +1,9 @@ package prometheus import ( - "github.com/fatedier/frp/server/metrics" - "github.com/prometheus/client_golang/prometheus" + + "github.com/fatedier/frp/server/metrics" ) const ( @@ -14,11 +14,12 @@ const ( var ServerMetrics metrics.ServerMetrics = newServerMetrics() type serverMetrics struct { - clientCount prometheus.Gauge - proxyCount *prometheus.GaugeVec - connectionCount *prometheus.GaugeVec - trafficIn *prometheus.CounterVec - trafficOut *prometheus.CounterVec + clientCount prometheus.Gauge + proxyCount *prometheus.GaugeVec + proxyCountDetailed *prometheus.GaugeVec + connectionCount *prometheus.GaugeVec + trafficIn *prometheus.CounterVec + trafficOut *prometheus.CounterVec } func (m *serverMetrics) NewClient() { @@ -29,12 +30,14 @@ func (m *serverMetrics) CloseClient() { m.clientCount.Dec() } -func (m *serverMetrics) NewProxy(name string, proxyType string) { +func (m *serverMetrics) NewProxy(name string, proxyType string, _ string, _ string) { m.proxyCount.WithLabelValues(proxyType).Inc() + m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc() } func (m *serverMetrics) CloseProxy(name string, proxyType string) { m.proxyCount.WithLabelValues(proxyType).Dec() + m.proxyCountDetailed.WithLabelValues(proxyType, name).Dec() } func (m *serverMetrics) OpenConnection(name string, proxyType string) { @@ -67,6 +70,12 @@ func newServerMetrics() *serverMetrics { Name: "proxy_counts", Help: "The current proxy counts", }, []string{"type"}), + proxyCountDetailed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "proxy_counts_detailed", + Help: "The current number of proxies grouped by type and name", + }, []string{"type", "name"}), connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, Subsystem: serverSubsystem, @@ -88,6 +97,7 @@ func newServerMetrics() *serverMetrics { } prometheus.MustRegister(m.clientCount) prometheus.MustRegister(m.proxyCount) + prometheus.MustRegister(m.proxyCountDetailed) prometheus.MustRegister(m.connectionCount) prometheus.MustRegister(m.trafficIn) prometheus.MustRegister(m.trafficOut) diff --git a/pkg/msg/conn_test.go b/pkg/msg/conn_test.go new file mode 100644 index 00000000000..f3498f031ba --- /dev/null +++ b/pkg/msg/conn_test.go @@ -0,0 +1,56 @@ +// 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 ( + "net" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestConnReadWriteMsg(t *testing.T) { + tests := []struct { + name string + protocol string + }{ + {name: "v1", protocol: wire.ProtocolV1}, + {name: "v2", protocol: wire.ProtocolV2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + clientConn := NewConn(client, NewReadWriter(client, tt.protocol)) + serverConn := NewConn(server, NewReadWriter(server, tt.protocol)) + + in := &Ping{PrivilegeKey: "key", Timestamp: 123} + errCh := make(chan error, 1) + go func() { + errCh <- clientConn.WriteMsg(in) + }() + + out, err := serverConn.ReadMsg() + require.NoError(t, err) + require.Equal(t, in, out) + require.NoError(t, <-errCh) + }) + } +} diff --git a/pkg/msg/ctl.go b/pkg/msg/ctl.go index 0eafdbcc5be..57681b12fd1 100644 --- a/pkg/msg/ctl.go +++ b/pkg/msg/ctl.go @@ -22,9 +22,7 @@ import ( type Message = jsonMsg.Message -var ( - msgCtl *jsonMsg.MsgCtl -) +var msgCtl *jsonMsg.MsgCtl func init() { msgCtl = jsonMsg.NewMsgCtl() @@ -41,6 +39,6 @@ func ReadMsgInto(c io.Reader, msg Message) (err error) { return msgCtl.ReadMsgInto(c, msg) } -func WriteMsg(c io.Writer, msg interface{}) (err error) { +func WriteMsg(c io.Writer, msg any) (err error) { return msgCtl.WriteMsg(c, msg) } diff --git a/pkg/msg/handler.go b/pkg/msg/handler.go new file mode 100644 index 00000000000..1f2775c947e --- /dev/null +++ b/pkg/msg/handler.go @@ -0,0 +1,172 @@ +// Copyright 2023 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 ( + "context" + "io" + "net" + "reflect" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +type ReadWriter interface { + ReadMsg() (Message, error) + ReadMsgInto(Message) error + WriteMsg(Message) error +} + +type Conn struct { + net.Conn + rw ReadWriter +} + +func NewConn(conn net.Conn, rw ReadWriter) *Conn { + return &Conn{ + Conn: conn, + rw: rw, + } +} + +func (c *Conn) ReadMsg() (Message, error) { + return c.rw.ReadMsg() +} + +func (c *Conn) ReadMsgInto(m Message) error { + return c.rw.ReadMsgInto(m) +} + +func (c *Conn) WriteMsg(m Message) error { + return c.rw.WriteMsg(m) +} + +func (c *Conn) Context() context.Context { + if getter, ok := c.Conn.(interface{ Context() context.Context }); ok { + return getter.Context() + } + return context.Background() +} + +func (c *Conn) WithContext(ctx context.Context) { + if setter, ok := c.Conn.(interface{ WithContext(context.Context) }); ok { + setter.WithContext(ctx) + } +} + +type V1ReadWriter struct { + rw io.ReadWriter +} + +func NewV1ReadWriter(rw io.ReadWriter) ReadWriter { + return &V1ReadWriter{rw: rw} +} + +// NewReadWriter wraps rw with the message codec for the selected wire protocol. +// An empty protocol keeps the historical v1 behavior for tests and older call sites. +func NewReadWriter(rw io.ReadWriter, wireProtocol string) ReadWriter { + switch wireProtocol { + case wire.ProtocolV2: + return NewV2ReadWriter(rw) + case "", wire.ProtocolV1: + return NewV1ReadWriter(rw) + default: + return NewV1ReadWriter(rw) + } +} + +func (rw *V1ReadWriter) ReadMsg() (Message, error) { + return ReadMsg(rw.rw) +} + +func (rw *V1ReadWriter) ReadMsgInto(m Message) error { + return ReadMsgInto(rw.rw, m) +} + +func (rw *V1ReadWriter) WriteMsg(m Message) error { + return WriteMsg(rw.rw, m) +} + +func AsyncHandler(f func(Message)) func(Message) { + return func(m Message) { + go f(m) + } +} + +// Dispatcher is used to send messages to net.Conn or register handlers for messages read from net.Conn. +type Dispatcher struct { + rw ReadWriter + + sendCh chan Message + doneCh chan struct{} + msgHandlers map[reflect.Type]func(Message) +} + +func NewDispatcher(rw ReadWriter) *Dispatcher { + return &Dispatcher{ + rw: rw, + sendCh: make(chan Message, 100), + doneCh: make(chan struct{}), + msgHandlers: make(map[reflect.Type]func(Message)), + } +} + +// Run will block until io.EOF or some error occurs. +func (d *Dispatcher) Run() { + go d.sendLoop() + go d.readLoop() +} + +func (d *Dispatcher) sendLoop() { + for { + select { + case <-d.doneCh: + return + case m := <-d.sendCh: + _ = d.rw.WriteMsg(m) + } + } +} + +func (d *Dispatcher) readLoop() { + for { + m, err := d.rw.ReadMsg() + if err != nil { + close(d.doneCh) + return + } + + if handler, ok := d.msgHandlers[reflect.TypeOf(m)]; ok { + handler(m) + } + } +} + +func (d *Dispatcher) Send(m Message) error { + select { + case <-d.doneCh: + return io.EOF + case d.sendCh <- m: + return nil + } +} + +func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) { + d.msgHandlers[reflect.TypeOf(msg)] = handler +} + +func (d *Dispatcher) Done() chan struct{} { + return d.doneCh +} diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index 4f1c8798adb..74c752f91b5 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -14,51 +14,63 @@ package msg -import "net" +import ( + "net" + "reflect" +) const ( - TypeLogin = 'o' - TypeLoginResp = '1' - TypeNewProxy = 'p' - TypeNewProxyResp = '2' - TypeCloseProxy = 'c' - TypeNewWorkConn = 'w' - TypeReqWorkConn = 'r' - TypeStartWorkConn = 's' - TypeNewVisitorConn = 'v' - TypeNewVisitorConnResp = '3' - TypePing = 'h' - TypePong = '4' - TypeUDPPacket = 'u' - TypeNatHoleVisitor = 'i' - TypeNatHoleClient = 'n' - TypeNatHoleResp = 'm' - TypeNatHoleClientDetectOK = 'd' - TypeNatHoleSid = '5' + TypeLogin byte = 'o' + TypeLoginResp byte = '1' + TypeNewProxy byte = 'p' + TypeNewProxyResp byte = '2' + TypeCloseProxy byte = 'c' + TypeNewWorkConn byte = 'w' + TypeReqWorkConn byte = 'r' + TypeStartWorkConn byte = 's' + TypeNewVisitorConn byte = 'v' + TypeNewVisitorConnResp byte = '3' + TypePing byte = 'h' + TypePong byte = '4' + TypeUDPPacket byte = 'u' + TypeNatHoleVisitor byte = 'i' + TypeNatHoleClient byte = 'n' + TypeNatHoleResp byte = 'm' + TypeNatHoleSid byte = '5' + TypeNatHoleReport byte = '6' ) -var ( - msgTypeMap = map[byte]interface{}{ - TypeLogin: Login{}, - TypeLoginResp: LoginResp{}, - TypeNewProxy: NewProxy{}, - TypeNewProxyResp: NewProxyResp{}, - TypeCloseProxy: CloseProxy{}, - TypeNewWorkConn: NewWorkConn{}, - TypeReqWorkConn: ReqWorkConn{}, - TypeStartWorkConn: StartWorkConn{}, - TypeNewVisitorConn: NewVisitorConn{}, - TypeNewVisitorConnResp: NewVisitorConnResp{}, - TypePing: Ping{}, - TypePong: Pong{}, - TypeUDPPacket: UDPPacket{}, - TypeNatHoleVisitor: NatHoleVisitor{}, - TypeNatHoleClient: NatHoleClient{}, - TypeNatHoleResp: NatHoleResp{}, - TypeNatHoleClientDetectOK: NatHoleClientDetectOK{}, - TypeNatHoleSid: NatHoleSid{}, - } -) +var msgTypeMap = map[byte]any{ + TypeLogin: Login{}, + TypeLoginResp: LoginResp{}, + TypeNewProxy: NewProxy{}, + TypeNewProxyResp: NewProxyResp{}, + TypeCloseProxy: CloseProxy{}, + TypeNewWorkConn: NewWorkConn{}, + TypeReqWorkConn: ReqWorkConn{}, + TypeStartWorkConn: StartWorkConn{}, + TypeNewVisitorConn: NewVisitorConn{}, + TypeNewVisitorConnResp: NewVisitorConnResp{}, + TypePing: Ping{}, + TypePong: Pong{}, + TypeUDPPacket: UDPPacket{}, + TypeNatHoleVisitor: NatHoleVisitor{}, + TypeNatHoleClient: NatHoleClient{}, + TypeNatHoleResp: NatHoleResp{}, + TypeNatHoleSid: NatHoleSid{}, + TypeNatHoleReport: NatHoleReport{}, +} + +var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name() + +type ClientSpec struct { + // Due to the support of VirtualClient, frps needs to know the client type in order to + // differentiate the processing logic. + // Optional values: ssh-tunnel + Type string `json:"type,omitempty"` + // If the value is true, the client will not require authentication. + AlwaysAuthPass bool `json:"always_auth_pass,omitempty"` +} // When frpc start, client send this message to login to server. type Login struct { @@ -70,28 +82,34 @@ type Login struct { PrivilegeKey string `json:"privilege_key,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` RunID string `json:"run_id,omitempty"` + ClientID string `json:"client_id,omitempty"` Metas map[string]string `json:"metas,omitempty"` + // Currently only effective for VirtualClient. + ClientSpec ClientSpec `json:"client_spec,omitempty"` + // Some global configures. PoolCount int `json:"pool_count,omitempty"` } type LoginResp struct { - Version string `json:"version,omitempty"` - RunID string `json:"run_id,omitempty"` - ServerUDPPort int `json:"server_udp_port,omitempty"` - Error string `json:"error,omitempty"` + Version string `json:"version,omitempty"` + RunID string `json:"run_id,omitempty"` + Error string `json:"error,omitempty"` } // When frpc login success, send this message to frps for running a new proxy. type NewProxy struct { - ProxyName string `json:"proxy_name,omitempty"` - ProxyType string `json:"proxy_type,omitempty"` - UseEncryption bool `json:"use_encryption,omitempty"` - UseCompression bool `json:"use_compression,omitempty"` - Group string `json:"group,omitempty"` - GroupKey string `json:"group_key,omitempty"` - Metas map[string]string `json:"metas,omitempty"` + ProxyName string `json:"proxy_name,omitempty"` + ProxyType string `json:"proxy_type,omitempty"` + UseEncryption bool `json:"use_encryption,omitempty"` + UseCompression bool `json:"use_compression,omitempty"` + BandwidthLimit string `json:"bandwidth_limit,omitempty"` + BandwidthLimitMode string `json:"bandwidth_limit_mode,omitempty"` + Group string `json:"group,omitempty"` + GroupKey string `json:"group_key,omitempty"` + Metas map[string]string `json:"metas,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` // tcp and udp only RemotePort int `json:"remote_port,omitempty"` @@ -105,10 +123,12 @@ type NewProxy struct { IpsAllowList []string `json:"ips_allow_list,omitempty"` HostHeaderRewrite string `json:"host_header_rewrite,omitempty"` Headers map[string]string `json:"headers,omitempty"` + ResponseHeaders map[string]string `json:"response_headers,omitempty"` RouteByHTTPUser string `json:"route_by_http_user,omitempty"` - // stcp - Sk string `json:"sk,omitempty"` + // stcp, sudp, xtcp + Sk string `json:"sk,omitempty"` + AllowUsers []string `json:"allow_users,omitempty"` // tcpmux Multiplexer string `json:"multiplexer,omitempty"` @@ -130,8 +150,7 @@ type NewWorkConn struct { Timestamp int64 `json:"timestamp,omitempty"` } -type ReqWorkConn struct { -} +type ReqWorkConn struct{} type StartWorkConn struct { ProxyName string `json:"proxy_name,omitempty"` @@ -143,6 +162,7 @@ type StartWorkConn struct { } type NewVisitorConn struct { + RunID string `json:"run_id,omitempty"` ProxyName string `json:"proxy_name,omitempty"` SignKey string `json:"sign_key,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` @@ -165,32 +185,64 @@ type Pong struct { } type UDPPacket struct { - Content string `json:"c,omitempty"` + Content []byte `json:"c,omitempty"` LocalAddr *net.UDPAddr `json:"l,omitempty"` RemoteAddr *net.UDPAddr `json:"r,omitempty"` } type NatHoleVisitor struct { - ProxyName string `json:"proxy_name,omitempty"` - SignKey string `json:"sign_key,omitempty"` - Timestamp int64 `json:"timestamp,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + ProxyName string `json:"proxy_name,omitempty"` + PreCheck bool `json:"pre_check,omitempty"` + Protocol string `json:"protocol,omitempty"` + SignKey string `json:"sign_key,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + MappedAddrs []string `json:"mapped_addrs,omitempty"` + AssistedAddrs []string `json:"assisted_addrs,omitempty"` } type NatHoleClient struct { - ProxyName string `json:"proxy_name,omitempty"` - Sid string `json:"sid,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + ProxyName string `json:"proxy_name,omitempty"` + Sid string `json:"sid,omitempty"` + MappedAddrs []string `json:"mapped_addrs,omitempty"` + AssistedAddrs []string `json:"assisted_addrs,omitempty"` } -type NatHoleResp struct { - Sid string `json:"sid,omitempty"` - VisitorAddr string `json:"visitor_addr,omitempty"` - ClientAddr string `json:"client_addr,omitempty"` - Error string `json:"error,omitempty"` +type PortsRange struct { + From int `json:"from,omitempty"` + To int `json:"to,omitempty"` +} + +type NatHoleDetectBehavior struct { + Role string `json:"role,omitempty"` // sender or receiver + Mode int `json:"mode,omitempty"` // 0, 1, 2... + TTL int `json:"ttl,omitempty"` + SendDelayMs int `json:"send_delay_ms,omitempty"` + ReadTimeoutMs int `json:"read_timeout,omitempty"` + CandidatePorts []PortsRange `json:"candidate_ports,omitempty"` + SendRandomPorts int `json:"send_random_ports,omitempty"` + ListenRandomPorts int `json:"listen_random_ports,omitempty"` } -type NatHoleClientDetectOK struct { +type NatHoleResp struct { + TransactionID string `json:"transaction_id,omitempty"` + Sid string `json:"sid,omitempty"` + Protocol string `json:"protocol,omitempty"` + CandidateAddrs []string `json:"candidate_addrs,omitempty"` + AssistedAddrs []string `json:"assisted_addrs,omitempty"` + DetectBehavior NatHoleDetectBehavior `json:"detect_behavior,omitempty"` + Error string `json:"error,omitempty"` } type NatHoleSid struct { - Sid string `json:"sid,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + Sid string `json:"sid,omitempty"` + Response bool `json:"response,omitempty"` + Nonce string `json:"nonce,omitempty"` +} + +type NatHoleReport struct { + Sid string `json:"sid,omitempty"` + Success bool `json:"success,omitempty"` } diff --git a/pkg/msg/msg_test.go b/pkg/msg/msg_test.go new file mode 100644 index 00000000000..06272faf959 --- /dev/null +++ b/pkg/msg/msg_test.go @@ -0,0 +1,55 @@ +// 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 ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestV1MessageTypeIDsAreStable(t *testing.T) { + require.Equal(t, byte('o'), TypeLogin) + require.Equal(t, byte('1'), TypeLoginResp) + require.Equal(t, byte('p'), TypeNewProxy) + require.Equal(t, byte('2'), TypeNewProxyResp) + require.Equal(t, byte('c'), TypeCloseProxy) + require.Equal(t, byte('w'), TypeNewWorkConn) + require.Equal(t, byte('r'), TypeReqWorkConn) + require.Equal(t, byte('s'), TypeStartWorkConn) + require.Equal(t, byte('v'), TypeNewVisitorConn) + require.Equal(t, byte('3'), TypeNewVisitorConnResp) + require.Equal(t, byte('h'), TypePing) + require.Equal(t, byte('4'), TypePong) + require.Equal(t, byte('u'), TypeUDPPacket) + require.Equal(t, byte('i'), TypeNatHoleVisitor) + require.Equal(t, byte('n'), TypeNatHoleClient) + require.Equal(t, byte('m'), TypeNatHoleResp) + require.Equal(t, byte('5'), TypeNatHoleSid) + require.Equal(t, byte('6'), TypeNatHoleReport) +} + +func TestMessageTypeMapIsCompleteAndUnique(t *testing.T) { + require.Len(t, msgTypeMap, 18) + + msgTypes := make(map[reflect.Type]struct{}, len(msgTypeMap)) + + for _, m := range msgTypeMap { + msgType := reflect.TypeOf(m) + require.NotContains(t, msgTypes, msgType) + msgTypes[msgType] = struct{}{} + } +} diff --git a/pkg/msg/udp_benchmark_test.go b/pkg/msg/udp_benchmark_test.go new file mode 100644 index 00000000000..575938eced0 --- /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/pkg/msg/udp_binary.go b/pkg/msg/udp_binary.go new file mode 100644 index 00000000000..7493c70fcf2 --- /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 00000000000..7dac31fb350 --- /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 new file mode 100644 index 00000000000..f3da2330734 --- /dev/null +++ b/pkg/msg/wire_v2.go @@ -0,0 +1,193 @@ +// 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" + "encoding/json" + "fmt" + "io" + "reflect" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +const ( + V2TypeLogin uint16 = 1 + V2TypeLoginResp uint16 = 2 + V2TypeNewProxy uint16 = 3 + V2TypeNewProxyResp uint16 = 4 + V2TypeCloseProxy uint16 = 5 + V2TypeNewWorkConn uint16 = 6 + V2TypeReqWorkConn uint16 = 7 + V2TypeStartWorkConn uint16 = 8 + V2TypeNewVisitorConn uint16 = 9 + V2TypeNewVisitorConnResp uint16 = 10 + V2TypePing uint16 = 11 + V2TypePong uint16 = 12 + V2TypeUDPPacket uint16 = 13 + V2TypeNatHoleVisitor uint16 = 14 + V2TypeNatHoleClient uint16 = 15 + V2TypeNatHoleResp uint16 = 16 + V2TypeNatHoleSid uint16 = 17 + V2TypeNatHoleReport uint16 = 18 + V2TypeUDPPacketBinary uint16 = 19 +) + +var v2MsgTypeMap = map[uint16]any{ + V2TypeLogin: Login{}, + V2TypeLoginResp: LoginResp{}, + V2TypeNewProxy: NewProxy{}, + V2TypeNewProxyResp: NewProxyResp{}, + V2TypeCloseProxy: CloseProxy{}, + V2TypeNewWorkConn: NewWorkConn{}, + V2TypeReqWorkConn: ReqWorkConn{}, + V2TypeStartWorkConn: StartWorkConn{}, + V2TypeNewVisitorConn: NewVisitorConn{}, + V2TypeNewVisitorConnResp: NewVisitorConnResp{}, + V2TypePing: Ping{}, + V2TypePong: Pong{}, + V2TypeUDPPacket: UDPPacket{}, + V2TypeNatHoleVisitor: NatHoleVisitor{}, + V2TypeNatHoleClient: NatHoleClient{}, + V2TypeNatHoleResp: NatHoleResp{}, + V2TypeNatHoleSid: NatHoleSid{}, + V2TypeNatHoleReport: NatHoleReport{}, +} + +var v2MsgReflectTypeMap, v2MsgTypeIDMap = buildV2MsgTypeMaps() + +func buildV2MsgTypeMaps() (map[uint16]reflect.Type, map[reflect.Type]uint16) { + reflectTypeMap := make(map[uint16]reflect.Type, len(v2MsgTypeMap)) + typeIDMap := make(map[reflect.Type]uint16, len(v2MsgTypeMap)) + for typeID, m := range v2MsgTypeMap { + t := reflect.TypeOf(m) + reflectTypeMap[typeID] = t + typeIDMap[t] = typeID + } + return reflectTypeMap, typeIDMap +} + +type V2ReadWriter struct { + conn *wire.Conn +} + +func NewV2ReadWriter(rw io.ReadWriter) *V2ReadWriter { + return NewV2ReadWriterWithConn(wire.NewConn(rw)) +} + +func NewV2ReadWriterWithConn(conn *wire.Conn) *V2ReadWriter { + return &V2ReadWriter{conn: conn} +} + +func (rw *V2ReadWriter) WireConn() *wire.Conn { + return rw.conn +} + +func (rw *V2ReadWriter) ReadMsg() (Message, error) { + f, err := rw.conn.ReadFrame() + if err != nil { + return nil, err + } + return DecodeV2MessageFrame(f) +} + +func (rw *V2ReadWriter) ReadMsgInto(m Message) error { + f, err := rw.conn.ReadFrame() + if err != nil { + return err + } + return DecodeV2MessageFrameInto(f, m) +} + +func (rw *V2ReadWriter) WriteMsg(m Message) error { + f, err := EncodeV2MessageFrame(m) + if err != nil { + return err + } + return rw.conn.WriteFrame(f) +} + +func DecodeV2MessageFrame(f *wire.Frame) (Message, error) { + if f.Type != wire.FrameTypeMessage { + return nil, fmt.Errorf("unexpected frame type %d, want %d", f.Type, wire.FrameTypeMessage) + } + if len(f.Payload) < 2 { + return nil, fmt.Errorf("message frame payload too short") + } + typeID := binary.BigEndian.Uint16(f.Payload[:2]) + t, ok := v2MsgReflectTypeMap[typeID] + if !ok { + return nil, fmt.Errorf("unknown v2 message type %d", typeID) + } + m := reflect.New(t).Interface() + if err := json.Unmarshal(f.Payload[2:], m); err != nil { + return nil, err + } + return m, nil +} + +func DecodeV2MessageFrameInto(f *wire.Frame, out Message) error { + if f.Type != wire.FrameTypeMessage { + return fmt.Errorf("unexpected frame type %d, want %d", f.Type, wire.FrameTypeMessage) + } + if len(f.Payload) < 2 { + return fmt.Errorf("message frame payload too short") + } + + typeID := binary.BigEndian.Uint16(f.Payload[:2]) + outType := reflect.TypeOf(out) + if outType == nil || outType.Kind() != reflect.Pointer { + return fmt.Errorf("message target must be a pointer") + } + elemType := outType.Elem() + expectedTypeID, ok := v2MsgTypeIDMap[elemType] + if !ok { + return fmt.Errorf("unknown v2 message type %s", elemType.String()) + } + if typeID != expectedTypeID { + actualType, ok := v2MsgReflectTypeMap[typeID] + if !ok { + return fmt.Errorf("unknown v2 message type %d", typeID) + } + return fmt.Errorf("unexpected message type %s, want %s", actualType.String(), elemType.String()) + } + return json.Unmarshal(f.Payload[2:], out) +} + +func EncodeV2MessageFrame(m Message) (*wire.Frame, error) { + t := reflect.TypeOf(m) + if t == nil { + return nil, fmt.Errorf("nil message") + } + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + typeID, ok := v2MsgTypeIDMap[t] + if !ok { + return nil, fmt.Errorf("unknown v2 message type %s", t.String()) + } + content, err := json.Marshal(m) + if err != nil { + return nil, err + } + payload := make([]byte, 2+len(content)) + binary.BigEndian.PutUint16(payload[:2], typeID) + copy(payload[2:], content) + return &wire.Frame{ + Type: wire.FrameTypeMessage, + Payload: payload, + }, nil +} diff --git a/pkg/msg/wire_v2_test.go b/pkg/msg/wire_v2_test.go new file mode 100644 index 00000000000..ea6e8d5d22f --- /dev/null +++ b/pkg/msg/wire_v2_test.go @@ -0,0 +1,143 @@ +// 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" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestV2ReadWriterRoundTrip(t *testing.T) { + var buf bytes.Buffer + rw := NewV2ReadWriter(&buf) + + in := &Login{ + Version: "test-version", + RunID: "run-id", + User: "user", + } + require.NoError(t, rw.WriteMsg(in)) + + out, err := rw.ReadMsg() + require.NoError(t, err) + require.Equal(t, in, out) +} + +func TestNewReadWriter(t *testing.T) { + require.IsType(t, &V1ReadWriter{}, NewReadWriter(&bytes.Buffer{}, "")) + require.IsType(t, &V1ReadWriter{}, NewReadWriter(&bytes.Buffer{}, wire.ProtocolV1)) + require.IsType(t, &V1ReadWriter{}, NewReadWriter(&bytes.Buffer{}, "unknown")) + require.IsType(t, &V2ReadWriter{}, NewReadWriter(&bytes.Buffer{}, wire.ProtocolV2)) +} + +func TestNewReadWriterEncoding(t *testing.T) { + for _, wireProtocol := range []string{"", wire.ProtocolV1} { + var legacy bytes.Buffer + legacyRW := NewReadWriter(&legacy, wireProtocol) + require.NoError(t, legacyRW.WriteMsg(&UDPPacket{Content: []byte("legacy")})) + require.NotEmpty(t, legacy.Bytes()) + require.Equal(t, TypeUDPPacket, legacy.Bytes()[0]) + } + + var v2 bytes.Buffer + v2RW := NewReadWriter(&v2, wire.ProtocolV2) + require.NoError(t, v2RW.WriteMsg(&UDPPacket{Content: []byte("v2")})) + frame, err := wire.NewConn(&v2).ReadFrame() + require.NoError(t, err) + require.Equal(t, wire.FrameTypeMessage, frame.Type) + require.Equal(t, V2TypeUDPPacket, binary.BigEndian.Uint16(frame.Payload[:2])) +} + +func TestV2MessageTypeIDsAreStable(t *testing.T) { + require.Equal(t, uint16(1), V2TypeLogin) + require.Equal(t, uint16(2), V2TypeLoginResp) + require.Equal(t, uint16(3), V2TypeNewProxy) + require.Equal(t, uint16(4), V2TypeNewProxyResp) + require.Equal(t, uint16(5), V2TypeCloseProxy) + require.Equal(t, uint16(6), V2TypeNewWorkConn) + require.Equal(t, uint16(7), V2TypeReqWorkConn) + require.Equal(t, uint16(8), V2TypeStartWorkConn) + require.Equal(t, uint16(9), V2TypeNewVisitorConn) + require.Equal(t, uint16(10), V2TypeNewVisitorConnResp) + require.Equal(t, uint16(11), V2TypePing) + require.Equal(t, uint16(12), V2TypePong) + require.Equal(t, uint16(13), V2TypeUDPPacket) + require.Equal(t, uint16(14), V2TypeNatHoleVisitor) + require.Equal(t, uint16(15), V2TypeNatHoleClient) + 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) { + frame, err := EncodeV2MessageFrame(&ReqWorkConn{}) + require.NoError(t, err) + require.Equal(t, wire.FrameTypeMessage, frame.Type) + require.Len(t, frame.Payload, 4) + require.Equal(t, V2TypeReqWorkConn, binary.BigEndian.Uint16(frame.Payload[:2])) + + out, err := DecodeV2MessageFrame(frame) + require.NoError(t, err) + require.IsType(t, &ReqWorkConn{}, out) +} + +func TestDecodeV2MessageFrameInto(t *testing.T) { + in := &StartWorkConn{ProxyName: "tcp", SrcAddr: "127.0.0.1", SrcPort: 1234} + frame, err := EncodeV2MessageFrame(in) + require.NoError(t, err) + + var out StartWorkConn + require.NoError(t, DecodeV2MessageFrameInto(frame, &out)) + require.Equal(t, *in, out) +} + +func TestDecodeV2MessageFrameRejectsInvalidFrame(t *testing.T) { + _, err := DecodeV2MessageFrame(&wire.Frame{Type: wire.FrameTypeClientHello}) + require.ErrorContains(t, err, "unexpected frame type") + + _, err = DecodeV2MessageFrame(&wire.Frame{Type: wire.FrameTypeMessage, Payload: []byte{0}}) + require.ErrorContains(t, err, "payload too short") + + payload := make([]byte, 4) + binary.BigEndian.PutUint16(payload[:2], 65535) + copy(payload[2:], []byte("{}")) + _, err = DecodeV2MessageFrame(&wire.Frame{Type: wire.FrameTypeMessage, Payload: payload}) + require.ErrorContains(t, err, "unknown v2 message type") +} + +func TestDecodeV2MessageFrameIntoRejectsWrongTarget(t *testing.T) { + frame, err := EncodeV2MessageFrame(&ReqWorkConn{}) + require.NoError(t, err) + + var out StartWorkConn + err = DecodeV2MessageFrameInto(frame, &out) + require.ErrorContains(t, err, "unexpected message type") + + err = DecodeV2MessageFrameInto(frame, StartWorkConn{}) + require.ErrorContains(t, err, "must be a pointer") +} + +func TestEncodeV2MessageFrameRejectsUnknownMessage(t *testing.T) { + _, err := EncodeV2MessageFrame(struct{}{}) + require.ErrorContains(t, err, "unknown v2 message type") +} diff --git a/pkg/naming/names.go b/pkg/naming/names.go new file mode 100644 index 00000000000..3ca4dd9ca46 --- /dev/null +++ b/pkg/naming/names.go @@ -0,0 +1,32 @@ +package naming + +import "strings" + +// AddUserPrefix builds the wire-level proxy name for frps by prefixing user. +func AddUserPrefix(user, name string) string { + if user == "" { + return name + } + return user + "." + name +} + +// StripUserPrefix converts a wire-level proxy name to an internal raw name. +// It strips only one exact "{user}." prefix. +func StripUserPrefix(user, name string) string { + if user == "" { + return name + } + if trimmed, ok := strings.CutPrefix(name, user+"."); ok { + return trimmed + } + return name +} + +// BuildTargetServerProxyName resolves visitor target proxy name for wire-level +// protocol messages. serverUser overrides local user when set. +func BuildTargetServerProxyName(localUser, serverUser, serverName string) string { + if serverUser != "" { + return AddUserPrefix(serverUser, serverName) + } + return AddUserPrefix(localUser, serverName) +} diff --git a/pkg/naming/names_test.go b/pkg/naming/names_test.go new file mode 100644 index 00000000000..3792af42a49 --- /dev/null +++ b/pkg/naming/names_test.go @@ -0,0 +1,27 @@ +package naming + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAddUserPrefix(t *testing.T) { + require := require.New(t) + require.Equal("test", AddUserPrefix("", "test")) + require.Equal("alice.test", AddUserPrefix("alice", "test")) +} + +func TestStripUserPrefix(t *testing.T) { + require := require.New(t) + require.Equal("test", StripUserPrefix("", "test")) + require.Equal("test", StripUserPrefix("alice", "alice.test")) + require.Equal("alice.test", StripUserPrefix("alice", "alice.alice.test")) + require.Equal("bob.test", StripUserPrefix("alice", "bob.test")) +} + +func TestBuildTargetServerProxyName(t *testing.T) { + require := require.New(t) + require.Equal("alice.test", BuildTargetServerProxyName("alice", "", "test")) + require.Equal("bob.test", BuildTargetServerProxyName("alice", "bob", "test")) +} diff --git a/pkg/nathole/analysis.go b/pkg/nathole/analysis.go new file mode 100644 index 00000000000..2ff28e0ee99 --- /dev/null +++ b/pkg/nathole/analysis.go @@ -0,0 +1,348 @@ +// Copyright 2023 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 nathole + +import ( + "cmp" + "slices" + "sync" + "time" + + "github.com/samber/lo" + "k8s.io/utils/clock" +) + +var ( + // mode 0, both EasyNAT, PublicNetwork is always receiver + // sender | receiver, ttl 7 + // receiver, ttl 7 | sender + // sender | receiver, ttl 4 + // receiver, ttl 4 | sender + // sender | receiver + // receiver | sender + // sender, sendDelayMs 5000 | receiver + // sender, sendDelayMs 10000 | receiver + // receiver | sender, sendDelayMs 5000 + // receiver | sender, sendDelayMs 10000 + mode0Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{ + lo.T2(RecommandBehavior{Role: DetectRoleSender}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 7}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver, TTL: 7}, RecommandBehavior{Role: DetectRoleSender}), + lo.T2(RecommandBehavior{Role: DetectRoleSender}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 4}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver, TTL: 4}, RecommandBehavior{Role: DetectRoleSender}), + lo.T2(RecommandBehavior{Role: DetectRoleSender}, RecommandBehavior{Role: DetectRoleReceiver}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver}, RecommandBehavior{Role: DetectRoleSender}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 5000}, RecommandBehavior{Role: DetectRoleReceiver}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 10000}, RecommandBehavior{Role: DetectRoleReceiver}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver}, RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 5000}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver}, RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 10000}), + } + + // mode 1, HardNAT is sender, EasyNAT is receiver, port changes is regular + // sender | receiver, ttl 7, portsRangeNumber max 10 + // sender, sendDelayMs 2000 | receiver, ttl 7, portsRangeNumber max 10 + // sender | receiver, ttl 4, portsRangeNumber max 10 + // sender, sendDelayMs 2000 | receiver, ttl 4, portsRangeNumber max 10 + // sender | receiver, portsRangeNumber max 10 + // sender, sendDelayMs 2000 | receiver, portsRangeNumber max 10 + mode1Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{ + lo.T2(RecommandBehavior{Role: DetectRoleSender}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 7, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 2000}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 7, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 4, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 2000}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 4, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender}, RecommandBehavior{Role: DetectRoleReceiver, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, SendDelayMs: 2000}, RecommandBehavior{Role: DetectRoleReceiver, PortsRangeNumber: 10}), + } + + // mode 2, HardNAT is receiver, EasyNAT is sender + // sender, portsRandomNumber 1000, sendDelayMs 3000 | receiver, listen 256 ports, ttl 7 + // sender, portsRandomNumber 1000, sendDelayMs 3000 | receiver, listen 256 ports, ttl 4 + // sender, portsRandomNumber 1000, sendDelayMs 3000 | receiver, listen 256 ports + mode2Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{ + lo.T2( + RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000}, + RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 7}, + ), + lo.T2( + RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000}, + RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 4}, + ), + lo.T2( + RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000}, + RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256}, + ), + } + + // mode 3, For HardNAT & HardNAT, both changes in the ports are regular + // sender, portsRangeNumber 10 | receiver, ttl 7, portsRangeNumber 10 + // sender, portsRangeNumber 10 | receiver, ttl 4, portsRangeNumber 10 + // sender, portsRangeNumber 10 | receiver, portsRangeNumber 10 + // receiver, ttl 7, portsRangeNumber 10 | sender, portsRangeNumber 10 + // receiver, ttl 4, portsRangeNumber 10 | sender, portsRangeNumber 10 + // receiver, portsRangeNumber 10 | sender, portsRangeNumber 10 + mode3Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{ + lo.T2(RecommandBehavior{Role: DetectRoleSender, PortsRangeNumber: 10}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 7, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, PortsRangeNumber: 10}, RecommandBehavior{Role: DetectRoleReceiver, TTL: 4, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleSender, PortsRangeNumber: 10}, RecommandBehavior{Role: DetectRoleReceiver, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver, TTL: 7, PortsRangeNumber: 10}, RecommandBehavior{Role: DetectRoleSender, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver, TTL: 4, PortsRangeNumber: 10}, RecommandBehavior{Role: DetectRoleSender, PortsRangeNumber: 10}), + lo.T2(RecommandBehavior{Role: DetectRoleReceiver, PortsRangeNumber: 10}, RecommandBehavior{Role: DetectRoleSender, PortsRangeNumber: 10}), + } + + // mode 4, Regular ports changes are usually the sender. + // sender, portsRandomNumber 1000, sendDelayMs: 2000 | receiver, listen 256 ports, ttl 7, portsRangeNumber 2 + // sender, portsRandomNumber 1000, sendDelayMs: 2000 | receiver, listen 256 ports, ttl 4, portsRangeNumber 2 + // sender, portsRandomNumber 1000, SendDelayMs: 2000 | receiver, listen 256 ports, portsRangeNumber 2 + mode4Behaviors = []lo.Tuple2[RecommandBehavior, RecommandBehavior]{ + lo.T2( + RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000}, + RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 7, PortsRangeNumber: 2}, + ), + lo.T2( + RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000}, + RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, TTL: 4, PortsRangeNumber: 2}, + ), + lo.T2( + RecommandBehavior{Role: DetectRoleSender, PortsRandomNumber: 1000, SendDelayMs: 3000}, + RecommandBehavior{Role: DetectRoleReceiver, ListenRandomPorts: 256, PortsRangeNumber: 2}, + ), + } +) + +func getBehaviorByMode(mode int) []lo.Tuple2[RecommandBehavior, RecommandBehavior] { + switch mode { + case 0: + return mode0Behaviors + case 1: + return mode1Behaviors + case 2: + return mode2Behaviors + case 3: + return mode3Behaviors + case 4: + return mode4Behaviors + } + // default + return mode0Behaviors +} + +func getBehaviorByModeAndIndex(mode int, index int) (RecommandBehavior, RecommandBehavior) { + behaviors := getBehaviorByMode(mode) + if index >= len(behaviors) { + return RecommandBehavior{}, RecommandBehavior{} + } + return behaviors[index].A, behaviors[index].B +} + +func getBehaviorScoresByMode(mode int, defaultScore int) []*behaviorScore { + return getBehaviorScoresByMode2(mode, defaultScore, defaultScore) +} + +func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*behaviorScore { + behaviors := getBehaviorByMode(mode) + scores := make([]*behaviorScore, 0, len(behaviors)) + for i := range behaviors { + score := receiverScore + if behaviors[i].A.Role == DetectRoleSender { + score = senderScore + } + scores = append(scores, &behaviorScore{Mode: mode, Index: i, Score: score}) + } + return scores +} + +type RecommandBehavior struct { + Role string + TTL int + SendDelayMs int + PortsRangeNumber int + PortsRandomNumber int + ListenRandomPorts int +} + +type makeHoleRecords struct { + mu sync.Mutex + scores []*behaviorScore + clock clock.PassiveClock + lastUpdateTime time.Time +} + +func newMakeHoleRecordsWithClock(c, v *NatFeature, clk clock.PassiveClock) *makeHoleRecords { + if clk == nil { + clk = clock.RealClock{} + } + scores := []*behaviorScore{} + easyCount, hardCount, portsChangedRegularCount := ClassifyFeatureCount([]*NatFeature{c, v}) + appendMode0 := func() { + switch { + case c.PublicNetwork: + scores = append(scores, getBehaviorScoresByMode2(DetectMode0, 0, 1)...) + case v.PublicNetwork: + scores = append(scores, getBehaviorScoresByMode2(DetectMode0, 1, 0)...) + default: + scores = append(scores, getBehaviorScoresByMode(DetectMode0, 0)...) + } + } + + switch { + case easyCount == 2: + appendMode0() + case hardCount == 1 && portsChangedRegularCount == 1: + scores = append(scores, getBehaviorScoresByMode(DetectMode1, 0)...) + scores = append(scores, getBehaviorScoresByMode(DetectMode2, 0)...) + appendMode0() + case hardCount == 1 && portsChangedRegularCount == 0: + scores = append(scores, getBehaviorScoresByMode(DetectMode2, 0)...) + scores = append(scores, getBehaviorScoresByMode(DetectMode1, 0)...) + appendMode0() + case hardCount == 2 && portsChangedRegularCount == 2: + scores = append(scores, getBehaviorScoresByMode(DetectMode3, 0)...) + scores = append(scores, getBehaviorScoresByMode(DetectMode4, 0)...) + case hardCount == 2 && portsChangedRegularCount == 1: + scores = append(scores, getBehaviorScoresByMode(DetectMode4, 0)...) + default: + // hard to make hole, just trying it out. + scores = append(scores, getBehaviorScoresByMode(DetectMode0, 1)...) + scores = append(scores, getBehaviorScoresByMode(DetectMode1, 1)...) + scores = append(scores, getBehaviorScoresByMode(DetectMode3, 1)...) + } + return &makeHoleRecords{ + scores: scores, + clock: clk, + lastUpdateTime: clk.Now(), + } +} + +func (mhr *makeHoleRecords) reportSuccess(mode int, index int) { + mhr.mu.Lock() + defer mhr.mu.Unlock() + mhr.lastUpdateTime = mhr.clock.Now() + for i := range mhr.scores { + score := mhr.scores[i] + if score.Mode != mode || score.Index != index { + continue + } + + score.Score += 2 + score.Score = min(score.Score, 10) + return + } +} + +func (mhr *makeHoleRecords) recommand() (mode, index int) { + mhr.mu.Lock() + defer mhr.mu.Unlock() + + if len(mhr.scores) == 0 { + return 0, 0 + } + maxScore := slices.MaxFunc(mhr.scores, func(a, b *behaviorScore) int { + return cmp.Compare(a.Score, b.Score) + }) + maxScore.Score-- + mhr.lastUpdateTime = mhr.clock.Now() + return maxScore.Mode, maxScore.Index +} + +type behaviorScore struct { + Mode int + Index int + // between -10 and 10 + Score int +} + +type Analyzer struct { + // key is client ip + visitor ip + records map[string]*makeHoleRecords + dataReserveDuration time.Duration + clock clock.PassiveClock + + mu sync.Mutex +} + +func NewAnalyzer(dataReserveDuration time.Duration) *Analyzer { + return newAnalyzerWithClock(dataReserveDuration, clock.RealClock{}) +} + +func newAnalyzerWithClock(dataReserveDuration time.Duration, clk clock.PassiveClock) *Analyzer { + if clk == nil { + clk = clock.RealClock{} + } + return &Analyzer{ + records: make(map[string]*makeHoleRecords), + dataReserveDuration: dataReserveDuration, + clock: clk, + } +} + +func (a *Analyzer) GetRecommandBehaviors(key string, c, v *NatFeature) (mode, index int, _ RecommandBehavior, _ RecommandBehavior) { + a.mu.Lock() + records, ok := a.records[key] + if !ok { + records = newMakeHoleRecordsWithClock(c, v, a.clock) + a.records[key] = records + } + a.mu.Unlock() + + mode, index = records.recommand() + cBehavior, vBehavior := getBehaviorByModeAndIndex(mode, index) + + switch mode { + case DetectMode1: + // HardNAT is always the sender + if c.NatType == EasyNAT { + cBehavior, vBehavior = vBehavior, cBehavior + } + case DetectMode2: + // HardNAT is always the receiver + if c.NatType == HardNAT { + cBehavior, vBehavior = vBehavior, cBehavior + } + case DetectMode4: + // Regular ports changes is always the sender + if !c.RegularPortsChange { + cBehavior, vBehavior = vBehavior, cBehavior + } + } + return mode, index, cBehavior, vBehavior +} + +func (a *Analyzer) ReportSuccess(key string, mode, index int) { + a.mu.Lock() + records, ok := a.records[key] + a.mu.Unlock() + if !ok { + return + } + records.reportSuccess(mode, index) +} + +func (a *Analyzer) Clean() (int, int) { + now := a.clock.Now() + total := 0 + count := 0 + + // cleanup 10w records may take 5ms + a.mu.Lock() + defer a.mu.Unlock() + total = len(a.records) + // clean up records that have not been used for a period of time. + for key, records := range a.records { + if now.Sub(records.lastUpdateTime) > a.dataReserveDuration { + delete(a.records, key) + count++ + } + } + return count, total +} diff --git a/pkg/nathole/analysis_test.go b/pkg/nathole/analysis_test.go new file mode 100644 index 00000000000..05d0f8dcdca --- /dev/null +++ b/pkg/nathole/analysis_test.go @@ -0,0 +1,33 @@ +package nathole + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + clocktesting "k8s.io/utils/clock/testing" +) + +func TestAnalyzerUsesClockForRecordTimestamps(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + analyzer := newAnalyzerWithClock(time.Hour, clk) + clientFeature := &NatFeature{NatType: EasyNAT, Behavior: BehaviorNoChange} + visitorFeature := &NatFeature{NatType: EasyNAT, Behavior: BehaviorNoChange} + + mode, index, _, _ := analyzer.GetRecommandBehaviors("key", clientFeature, visitorFeature) + require.Equal(start, analyzer.records["key"].lastUpdateTime) + + updatedAt := start.Add(time.Minute) + clk.SetTime(updatedAt) + analyzer.ReportSuccess("key", mode, index) + require.Equal(updatedAt, analyzer.records["key"].lastUpdateTime) + + clk.SetTime(start.Add(2 * time.Hour)) + count, total := analyzer.Clean() + require.Equal(1, count) + require.Equal(1, total) + require.Empty(analyzer.records) +} diff --git a/pkg/nathole/classify.go b/pkg/nathole/classify.go new file mode 100644 index 00000000000..6e00a5836b4 --- /dev/null +++ b/pkg/nathole/classify.go @@ -0,0 +1,123 @@ +// Copyright 2023 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 nathole + +import ( + "fmt" + "net" + "slices" + "strconv" +) + +const ( + EasyNAT = "EasyNAT" + HardNAT = "HardNAT" + + BehaviorNoChange = "BehaviorNoChange" + BehaviorIPChanged = "BehaviorIPChanged" + BehaviorPortChanged = "BehaviorPortChanged" + BehaviorBothChanged = "BehaviorBothChanged" +) + +type NatFeature struct { + NatType string + Behavior string + PortsDifference int + RegularPortsChange bool + PublicNetwork bool +} + +func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, error) { + if len(addresses) <= 1 { + return nil, fmt.Errorf("not enough addresses") + } + natFeature := &NatFeature{} + ipChanged := false + portChanged := false + + var baseIP, basePort string + var portMax, portMin int + for _, addr := range addresses { + ip, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + portNum, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + if slices.Contains(localIPs, ip) { + natFeature.PublicNetwork = true + } + + if baseIP == "" { + baseIP = ip + basePort = port + portMax = portNum + portMin = portNum + continue + } + + portMax = max(portMax, portNum) + portMin = min(portMin, portNum) + if baseIP != ip { + ipChanged = true + } + if basePort != port { + portChanged = true + } + } + + switch { + case ipChanged && portChanged: + natFeature.NatType = HardNAT + natFeature.Behavior = BehaviorBothChanged + case ipChanged: + natFeature.NatType = HardNAT + natFeature.Behavior = BehaviorIPChanged + case portChanged: + natFeature.NatType = HardNAT + natFeature.Behavior = BehaviorPortChanged + default: + natFeature.NatType = EasyNAT + natFeature.Behavior = BehaviorNoChange + } + if natFeature.Behavior == BehaviorPortChanged { + natFeature.PortsDifference = portMax - portMin + if natFeature.PortsDifference <= 5 && natFeature.PortsDifference >= 1 { + natFeature.RegularPortsChange = true + } + } + return natFeature, nil +} + +func ClassifyFeatureCount(features []*NatFeature) (int, int, int) { + easyCount := 0 + hardCount := 0 + // for HardNAT + portsChangedRegularCount := 0 + for _, feature := range features { + if feature.NatType == EasyNAT { + easyCount++ + continue + } + + hardCount++ + if feature.RegularPortsChange { + portsChangedRegularCount++ + } + } + return easyCount, hardCount, portsChangedRegularCount +} diff --git a/pkg/nathole/controller.go b/pkg/nathole/controller.go new file mode 100644 index 00000000000..22a5ec5334a --- /dev/null +++ b/pkg/nathole/controller.go @@ -0,0 +1,402 @@ +// Copyright 2023 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 nathole + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + "net" + "slices" + "strconv" + "sync" + "time" + + "github.com/fatedier/golib/errors" + "github.com/samber/lo" + "golang.org/x/sync/errgroup" + + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/pkg/util/util" +) + +// NatHoleTimeout seconds. +var NatHoleTimeout int64 = 10 + +func NewTransactionID() string { + id, _ := util.RandID() + return fmt.Sprintf("%d%s", time.Now().Unix(), id) +} + +type ClientCfg struct { + name string + sk string + allowUsers []string + sidCh chan string +} + +type Session struct { + sid string + analysisKey string + recommandMode int + recommandIndex int + + visitorMsg *msg.NatHoleVisitor + visitorTransporter transport.MessageTransporter + vResp *msg.NatHoleResp + vNatFeature *NatFeature + vBehavior RecommandBehavior + + clientMsg *msg.NatHoleClient + clientTransporter transport.MessageTransporter + cResp *msg.NatHoleResp + cNatFeature *NatFeature + cBehavior RecommandBehavior + + notifyCh chan struct{} +} + +func (s *Session) genAnalysisKey() { + hash := md5.New() + vIPs := slices.Compact(parseIPs(s.visitorMsg.MappedAddrs)) + if len(vIPs) > 0 { + hash.Write([]byte(vIPs[0])) + } + hash.Write([]byte(s.vNatFeature.NatType)) + hash.Write([]byte(s.vNatFeature.Behavior)) + hash.Write([]byte(strconv.FormatBool(s.vNatFeature.RegularPortsChange))) + + cIPs := slices.Compact(parseIPs(s.clientMsg.MappedAddrs)) + if len(cIPs) > 0 { + hash.Write([]byte(cIPs[0])) + } + hash.Write([]byte(s.cNatFeature.NatType)) + hash.Write([]byte(s.cNatFeature.Behavior)) + hash.Write([]byte(strconv.FormatBool(s.cNatFeature.RegularPortsChange))) + s.analysisKey = hex.EncodeToString(hash.Sum(nil)) +} + +type Controller struct { + clientCfgs map[string]*ClientCfg + sessions map[string]*Session + analyzer *Analyzer + + mu sync.RWMutex +} + +func NewController(analysisDataReserveDuration time.Duration) (*Controller, error) { + return &Controller{ + clientCfgs: make(map[string]*ClientCfg), + sessions: make(map[string]*Session), + analyzer: NewAnalyzer(analysisDataReserveDuration), + }, nil +} + +func (c *Controller) CleanWorker(ctx context.Context) { + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + for { + select { + case <-ticker.C: + start := time.Now() + count, total := c.analyzer.Clean() + log.Tracef("clean %d/%d nathole analysis data, cost %v", count, total, time.Since(start)) + case <-ctx.Done(): + return + } + } +} + +func (c *Controller) ListenClient(name string, sk string, allowUsers []string) (chan string, error) { + cfg := &ClientCfg{ + name: name, + sk: sk, + allowUsers: allowUsers, + sidCh: make(chan string), + } + c.mu.Lock() + defer c.mu.Unlock() + if _, ok := c.clientCfgs[name]; ok { + return nil, fmt.Errorf("proxy [%s] is repeated", name) + } + c.clientCfgs[name] = cfg + return cfg.sidCh, nil +} + +func (c *Controller) CloseClient(name string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.clientCfgs, name) +} + +func (c *Controller) GenSid() string { + t := time.Now().Unix() + id, _ := util.RandID() + return fmt.Sprintf("%d%s", t, id) +} + +func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) { + if m.PreCheck { + c.mu.RLock() + cfg, ok := c.clientCfgs[m.ProxyName] + c.mu.RUnlock() + if !ok { + _ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName))) + return + } + if !slices.Contains(cfg.allowUsers, visitorUser) && !slices.Contains(cfg.allowUsers, "*") { + _ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp visitor user [%s] not allowed for [%s]", visitorUser, m.ProxyName))) + return + } + _ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, "")) + return + } + + sid := c.GenSid() + session := &Session{ + sid: sid, + visitorMsg: m, + visitorTransporter: transporter, + notifyCh: make(chan struct{}, 1), + } + var ( + clientCfg *ClientCfg + ok bool + ) + err := func() error { + c.mu.Lock() + defer c.mu.Unlock() + + clientCfg, ok = c.clientCfgs[m.ProxyName] + if !ok { + return fmt.Errorf("xtcp server for [%s] doesn't exist", m.ProxyName) + } + if !util.ConstantTimeEqString(m.SignKey, util.GetAuthKey(clientCfg.sk, m.Timestamp)) { + return fmt.Errorf("xtcp connection of [%s] auth failed", m.ProxyName) + } + c.sessions[sid] = session + return nil + }() + if err != nil { + log.Warnf("handle visitorMsg error: %v", err) + _ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, err.Error())) + return + } + log.Tracef("handle visitor message, sid [%s], server name: %s", sid, m.ProxyName) + + defer func() { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.sessions, sid) + }() + + if err := errors.PanicToError(func() { + clientCfg.sidCh <- sid + }); err != nil { + return + } + + // wait for NatHoleClient message + select { + case <-session.notifyCh: + case <-time.After(time.Duration(NatHoleTimeout) * time.Second): + log.Debugf("wait for NatHoleClient message timeout, sid [%s]", sid) + return + } + + // Make hole-punching decisions based on the NAT information of the client and visitor. + vResp, cResp, err := c.analysis(session) + if err != nil { + log.Debugf("sid [%s] analysis error: %v", err) + vResp = c.GenNatHoleResponse(session.visitorMsg.TransactionID, nil, err.Error()) + cResp = c.GenNatHoleResponse(session.clientMsg.TransactionID, nil, err.Error()) + } + session.cResp = cResp + session.vResp = vResp + + // send response to visitor and client + var g errgroup.Group + g.Go(func() error { + // if it's sender, wait for a while to make sure the client has send the detect messages + if vResp.DetectBehavior.Role == "sender" { + time.Sleep(1 * time.Second) + } + _ = session.visitorTransporter.Send(vResp) + return nil + }) + g.Go(func() error { + // if it's sender, wait for a while to make sure the client has send the detect messages + if cResp.DetectBehavior.Role == "sender" { + time.Sleep(1 * time.Second) + } + _ = session.clientTransporter.Send(cResp) + return nil + }) + _ = g.Wait() + + time.Sleep(time.Duration(cResp.DetectBehavior.ReadTimeoutMs+30000) * time.Millisecond) +} + +func (c *Controller) HandleClient(m *msg.NatHoleClient, transporter transport.MessageTransporter) { + c.mu.RLock() + session, ok := c.sessions[m.Sid] + c.mu.RUnlock() + if !ok { + return + } + log.Tracef("handle client message, sid [%s], server name: %s", session.sid, m.ProxyName) + session.clientMsg = m + session.clientTransporter = transporter + select { + case session.notifyCh <- struct{}{}: + default: + } +} + +func (c *Controller) HandleReport(m *msg.NatHoleReport) { + c.mu.RLock() + session, ok := c.sessions[m.Sid] + c.mu.RUnlock() + if !ok { + log.Tracef("sid [%s] report make hole success: %v, but session not found", m.Sid, m.Success) + return + } + if m.Success { + c.analyzer.ReportSuccess(session.analysisKey, session.recommandMode, session.recommandIndex) + } + log.Infof("sid [%s] report make hole success: %v, mode %v, index %v", + m.Sid, m.Success, session.recommandMode, session.recommandIndex) +} + +func (c *Controller) GenNatHoleResponse(transactionID string, session *Session, errInfo string) *msg.NatHoleResp { + var sid string + if session != nil { + sid = session.sid + } + return &msg.NatHoleResp{ + TransactionID: transactionID, + Sid: sid, + Error: errInfo, + } +} + +// analysis analyzes the NAT type and behavior of the visitor and client, then makes hole-punching decisions. +// return the response to the visitor and client. +func (c *Controller) analysis(session *Session) (*msg.NatHoleResp, *msg.NatHoleResp, error) { + cm := session.clientMsg + vm := session.visitorMsg + + cNatFeature, err := ClassifyNATFeature(cm.MappedAddrs, parseIPs(cm.AssistedAddrs)) + if err != nil { + return nil, nil, fmt.Errorf("classify client nat feature error: %v", err) + } + + vNatFeature, err := ClassifyNATFeature(vm.MappedAddrs, parseIPs(vm.AssistedAddrs)) + if err != nil { + return nil, nil, fmt.Errorf("classify visitor nat feature error: %v", err) + } + session.cNatFeature = cNatFeature + session.vNatFeature = vNatFeature + session.genAnalysisKey() + + mode, index, cBehavior, vBehavior := c.analyzer.GetRecommandBehaviors(session.analysisKey, cNatFeature, vNatFeature) + session.recommandMode = mode + session.recommandIndex = index + session.cBehavior = cBehavior + session.vBehavior = vBehavior + + timeoutMs := max(cBehavior.SendDelayMs, vBehavior.SendDelayMs) + 5000 + if cBehavior.ListenRandomPorts > 0 || vBehavior.ListenRandomPorts > 0 { + timeoutMs += 30000 + } + + protocol := vm.Protocol + vResp := newNatHoleResponse( + vm.TransactionID, session.sid, protocol, mode, + cm.MappedAddrs, cm.AssistedAddrs, vBehavior, + timeoutMs-vBehavior.SendDelayMs, cNatFeature.PortsDifference, + ) + cResp := newNatHoleResponse( + cm.TransactionID, session.sid, protocol, mode, + vm.MappedAddrs, vm.AssistedAddrs, cBehavior, + timeoutMs-cBehavior.SendDelayMs, vNatFeature.PortsDifference, + ) + + log.Debugf("sid [%s] visitor nat: %+v, candidateAddrs: %v; client nat: %+v, candidateAddrs: %v, protocol: %s", + session.sid, *vNatFeature, vm.MappedAddrs, *cNatFeature, cm.MappedAddrs, protocol) + log.Debugf("sid [%s] visitor detect behavior: %+v", session.sid, vResp.DetectBehavior) + log.Debugf("sid [%s] client detect behavior: %+v", session.sid, cResp.DetectBehavior) + return vResp, cResp, nil +} + +func newNatHoleResponse( + transactionID string, + sid string, + protocol string, + mode int, + candidateAddrs []string, + assistedAddrs []string, + behavior RecommandBehavior, + readTimeoutMs int, + portsDifference int, +) *msg.NatHoleResp { + compactCandidateAddrs := slices.Compact(candidateAddrs) + compactAssistedAddrs := slices.Compact(assistedAddrs) + return &msg.NatHoleResp{ + TransactionID: transactionID, + Sid: sid, + Protocol: protocol, + CandidateAddrs: compactCandidateAddrs, + AssistedAddrs: compactAssistedAddrs, + DetectBehavior: msg.NatHoleDetectBehavior{ + Mode: mode, + Role: behavior.Role, + TTL: behavior.TTL, + SendDelayMs: behavior.SendDelayMs, + ReadTimeoutMs: readTimeoutMs, + SendRandomPorts: behavior.PortsRandomNumber, + ListenRandomPorts: behavior.ListenRandomPorts, + CandidatePorts: getRangePorts(candidateAddrs, portsDifference, behavior.PortsRangeNumber), + }, + } +} + +func getRangePorts(addrs []string, difference, maxNumber int) []msg.PortsRange { + if maxNumber <= 0 { + return nil + } + + addr, isLast := lo.Last(addrs) + if !isLast { + return nil + } + ports := make([]msg.PortsRange, 0, 1) + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil + } + ports = append(ports, msg.PortsRange{ + From: max(port-difference-5, port-maxNumber, 1), + To: min(port+difference+5, port+maxNumber, 65535), + }) + return ports +} diff --git a/pkg/nathole/discovery.go b/pkg/nathole/discovery.go new file mode 100644 index 00000000000..241b0f6ccfc --- /dev/null +++ b/pkg/nathole/discovery.go @@ -0,0 +1,145 @@ +// Copyright 2023 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 nathole + +import ( + "errors" + "fmt" + "net" + "time" + + "github.com/fatedier/golib/net/stun" +) + +var responseTimeout = 3 * time.Second + +// If the localAddr is empty, it will listen on a random port. +func Discover(stunServers []string, localAddr string) ([]string, net.Addr, error) { + discoverConn, err := listen(localAddr) + if err != nil { + return nil, nil, err + } + defer discoverConn.Close() + + addresses := make([]string, 0, len(stunServers)) + for _, addr := range stunServers { + // get external address from stun server + externalAddrs, err := discoverConn.discoverFromStunServer(addr) + if err != nil { + return nil, nil, err + } + addresses = append(addresses, externalAddrs...) + } + return addresses, discoverConn.localAddr, nil +} + +type stunResponse struct { + externalAddr string + otherAddr string +} + +type discoverConn struct { + conn *net.UDPConn + client *stun.Client + localAddr net.Addr +} + +func listen(localAddr string) (*discoverConn, error) { + var local *net.UDPAddr + if localAddr != "" { + addr, err := net.ResolveUDPAddr("udp4", localAddr) + if err != nil { + return nil, err + } + local = addr + } + conn, err := net.ListenUDP("udp4", local) + if err != nil { + return nil, err + } + client, err := stun.NewClient(conn) + if err != nil { + _ = conn.Close() + return nil, err + } + + return &discoverConn{ + conn: conn, + client: client, + localAddr: conn.LocalAddr(), + }, nil +} + +func (c *discoverConn) Close() error { + return c.conn.Close() +} + +func (c *discoverConn) doSTUNRequest(addr string) (*stunResponse, error) { + serverAddr, err := net.ResolveUDPAddr("udp4", addr) + if err != nil { + return nil, err + } + transaction, err := stun.NewBindingTransaction(serverAddr) + if err != nil { + return nil, err + } + if err := c.conn.SetReadDeadline(time.Now().Add(responseTimeout)); err != nil { + return nil, err + } + response, err := c.client.Do(transaction) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return nil, fmt.Errorf("wait response from stun server timeout") + } + return nil, err + } + + resp := &stunResponse{} + if response.MappedAddr != nil { + resp.externalAddr = response.MappedAddr.String() + } + if response.OtherAddr != nil { + resp.otherAddr = response.OtherAddr.String() + } + return resp, nil +} + +func (c *discoverConn) discoverFromStunServer(addr string) ([]string, error) { + resp, err := c.doSTUNRequest(addr) + if err != nil { + return nil, err + } + if resp.externalAddr == "" { + return nil, fmt.Errorf("no external address found") + } + + externalAddrs := make([]string, 0, 2) + externalAddrs = append(externalAddrs, resp.externalAddr) + + if resp.otherAddr == "" { + return externalAddrs, nil + } + + // find external address from changed address + resp, err = c.doSTUNRequest(resp.otherAddr) + if err != nil { + return nil, err + } + if resp.externalAddr != "" { + externalAddrs = append(externalAddrs, resp.externalAddr) + } + return externalAddrs, nil +} diff --git a/pkg/nathole/discovery_test.go b/pkg/nathole/discovery_test.go new file mode 100644 index 00000000000..6596314d27a --- /dev/null +++ b/pkg/nathole/discovery_test.go @@ -0,0 +1,382 @@ +package nathole + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + "testing" + "time" + + "github.com/fatedier/golib/net/stun" + "github.com/stretchr/testify/require" +) + +const ( + testBindingRequest = 0x0001 + testBindingSuccess = 0x0101 + testBindingError = 0x0111 + testMagicCookie = 0x2112a442 + testAttrMapped = 0x0001 + testAttrChanged = 0x0005 + testAttrErrorCode = 0x0009 + testAttrXORMapped = 0x0020 + testAttrOther = 0x802c + testSTUNHeaderSize = 20 + testSTUNServerLimit = time.Second +) + +type testSTUNAttribute struct { + typ uint16 + value []byte +} + +type testSTUNExchange struct { + source *net.UDPAddr + err error +} + +func listenTestUDP4(t *testing.T) *net.UDPConn { + t.Helper() + + conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + return conn +} + +func serveOneSTUNRequest( + server *net.UDPConn, + buildResponse func([]byte, *net.UDPAddr) ([]byte, error), +) <-chan testSTUNExchange { + done := make(chan testSTUNExchange, 1) + go func() { + if err := server.SetDeadline(time.Now().Add(testSTUNServerLimit)); err != nil { + done <- testSTUNExchange{err: err} + return + } + buffer := make([]byte, 1024) + n, source, err := server.ReadFromUDP(buffer) + if err == nil && buildResponse != nil { + var response []byte + response, err = buildResponse(buffer[:n], source) + if err == nil && response != nil { + _, err = server.WriteToUDP(response, source) + } + } + done <- testSTUNExchange{source: source, err: err} + }() + return done +} + +func waitSTUNExchange(t *testing.T, done <-chan testSTUNExchange) *net.UDPAddr { + t.Helper() + + select { + case exchange := <-done: + require.NoError(t, exchange.err) + return exchange.source + case <-time.After(testSTUNServerLimit): + t.Fatal("timed out waiting for local STUN server") + return nil + } +} + +func makeTestSTUNResponse(request []byte, typ uint16, attributes ...testSTUNAttribute) ([]byte, error) { + if len(request) != testSTUNHeaderSize || binary.BigEndian.Uint16(request[0:2]) != testBindingRequest || + binary.BigEndian.Uint32(request[4:8]) != testMagicCookie { + return nil, fmt.Errorf("invalid Binding request") + } + + length := 0 + for _, attribute := range attributes { + length += 4 + (len(attribute.value)+3)&^3 + } + response := make([]byte, testSTUNHeaderSize, testSTUNHeaderSize+length) + binary.BigEndian.PutUint16(response[0:2], typ) + binary.BigEndian.PutUint16(response[2:4], uint16(length)) + binary.BigEndian.PutUint32(response[4:8], testMagicCookie) + copy(response[8:20], request[8:20]) + + for _, attribute := range attributes { + start := len(response) + paddedLength := (len(attribute.value) + 3) &^ 3 + response = append(response, make([]byte, 4+paddedLength)...) + binary.BigEndian.PutUint16(response[start:start+2], attribute.typ) + binary.BigEndian.PutUint16(response[start+2:start+4], uint16(len(attribute.value))) + copy(response[start+4:], attribute.value) + } + return response, nil +} + +func testIPv4AddressValue(ip net.IP, port int, xor bool) []byte { + value := make([]byte, 8) + value[1] = 0x01 + binary.BigEndian.PutUint16(value[2:4], uint16(port)) + copy(value[4:], ip.To4()) + if xor { + binary.BigEndian.PutUint16(value[2:4], binary.BigEndian.Uint16(value[2:4])^uint16(testMagicCookie>>16)) + for i := range 4 { + value[4+i] ^= byte(uint32(testMagicCookie) >> uint(24-8*i)) + } + } + return value +} + +func TestDiscoverReusesLocalPortAndPreservesNATClassification(t *testing.T) { + tests := []struct { + name string + secondMapped string + secondMappedPort int + wantNATType string + wantBehavior string + }{ + { + name: "same mapped address", + secondMapped: "198.51.100.10:40000", + secondMappedPort: 40000, + wantNATType: EasyNAT, + wantBehavior: BehaviorNoChange, + }, + { + name: "different mapped port", + secondMapped: "198.51.100.10:40001", + secondMappedPort: 40001, + wantNATType: HardNAT, + wantBehavior: BehaviorPortChanged, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + primary := listenTestUDP4(t) + alternate := listenTestUDP4(t) + alternateAddr := alternate.LocalAddr().(*net.UDPAddr) + + primaryDone := serveOneSTUNRequest(primary, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.10"), 40000, true)}, + testSTUNAttribute{typ: testAttrOther, value: testIPv4AddressValue(alternateAddr.IP, alternateAddr.Port, false)}, + ) + }) + alternateDone := serveOneSTUNRequest(alternate, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.10"), tt.secondMappedPort, true)}, + ) + }) + + addresses, localAddr, err := Discover([]string{primary.LocalAddr().String()}, "") + require.NoError(t, err) + require.Equal(t, []string{"198.51.100.10:40000", tt.secondMapped}, addresses) + + primarySource := waitSTUNExchange(t, primaryDone) + alternateSource := waitSTUNExchange(t, alternateDone) + require.Equal(t, primarySource.Port, alternateSource.Port) + require.Equal(t, localAddr.(*net.UDPAddr).Port, primarySource.Port) + + feature, err := ClassifyNATFeature(addresses, nil) + require.NoError(t, err) + require.Equal(t, tt.wantNATType, feature.NatType) + require.Equal(t, tt.wantBehavior, feature.Behavior) + }) + } +} + +func TestDoSTUNRequestMapsLegacyAndModernAddresses(t *testing.T) { + tests := []struct { + name string + attributes []testSTUNAttribute + wantExternal string + wantOther string + }{ + { + name: "legacy", + attributes: []testSTUNAttribute{ + {typ: testAttrMapped, value: testIPv4AddressValue(net.ParseIP("192.0.2.1"), 1000, false)}, + {typ: testAttrChanged, value: testIPv4AddressValue(net.ParseIP("192.0.2.2"), 2000, false)}, + }, + wantExternal: "192.0.2.1:1000", + wantOther: "192.0.2.2:2000", + }, + { + name: "modern takes precedence", + attributes: []testSTUNAttribute{ + {typ: testAttrMapped, value: testIPv4AddressValue(net.ParseIP("192.0.2.1"), 1000, false)}, + {typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.1"), 3000, true)}, + {typ: testAttrChanged, value: testIPv4AddressValue(net.ParseIP("192.0.2.2"), 2000, false)}, + {typ: testAttrOther, value: testIPv4AddressValue(net.ParseIP("198.51.100.2"), 4000, false)}, + }, + wantExternal: "198.51.100.1:3000", + wantOther: "198.51.100.2:4000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, tt.attributes...) + }) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + response, err := conn.doSTUNRequest(server.LocalAddr().String()) + require.NoError(t, err) + require.Equal(t, tt.wantExternal, response.externalAddr) + require.Equal(t, tt.wantOther, response.otherAddr) + waitSTUNExchange(t, done) + }) + } +} + +func TestSTUNResponseErrorsAndMissingAddresses(t *testing.T) { + tests := []struct { + name string + buildResponse func([]byte, *net.UDPAddr) ([]byte, error) + request func(*discoverConn, string) error + checkError func(*testing.T, error) + }{ + { + name: "correlated malformed response", + buildResponse: func(request []byte, _ *net.UDPAddr) ([]byte, error) { + response, err := makeTestSTUNResponse(request, testBindingSuccess) + if err == nil { + binary.BigEndian.PutUint16(response[2:4], 4) + } + return response, err + }, + request: func(conn *discoverConn, server string) error { + _, err := conn.doSTUNRequest(server) + return err + }, + checkError: func(t *testing.T, err error) { + require.ErrorIs(t, err, stun.ErrMalformedResponse) + }, + }, + { + name: "Binding error response", + buildResponse: func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingError, testSTUNAttribute{ + typ: testAttrErrorCode, + value: []byte{0, 0, 4, 20, 'U', 'n', 'k', 'n', 'o', 'w', 'n'}, + }) + }, + request: func(conn *discoverConn, server string) error { + _, err := conn.doSTUNRequest(server) + return err + }, + checkError: func(t *testing.T, err error) { + var responseErr *stun.ResponseError + require.ErrorAs(t, err, &responseErr) + require.Equal(t, 420, responseErr.Code) + }, + }, + { + name: "missing mapped address", + buildResponse: func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrOther, value: testIPv4AddressValue(net.ParseIP("192.0.2.2"), 2000, false)}, + ) + }, + request: func(conn *discoverConn, server string) error { + _, err := conn.discoverFromStunServer(server) + return err + }, + checkError: func(t *testing.T, err error) { + require.EqualError(t, err, "no external address found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, tt.buildResponse) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + err = tt.request(conn, server.LocalAddr().String()) + tt.checkError(t, err) + waitSTUNExchange(t, done) + }) + } + + t.Run("missing other address", func(t *testing.T) { + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.1"), 3000, true)}, + ) + }) + + _, err := Prepare([]string{server.LocalAddr().String()}, PrepareOptions{}) + require.EqualError(t, err, "discover error: not enough addresses") + waitSTUNExchange(t, done) + }) +} + +func TestSTUNTimeoutUsesCallerDeadlineWithoutRetry(t *testing.T) { + originalTimeout := responseTimeout + responseTimeout = 50 * time.Millisecond + t.Cleanup(func() { responseTimeout = originalTimeout }) + + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, nil) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + _, err = conn.doSTUNRequest(server.LocalAddr().String()) + require.EqualError(t, err, "wait response from stun server timeout") + waitSTUNExchange(t, done) + + require.NoError(t, server.SetReadDeadline(time.Now().Add(50*time.Millisecond))) + _, _, err = server.ReadFromUDP(make([]byte, 1)) + var netErr net.Error + require.ErrorAs(t, err, &netErr) + require.True(t, netErr.Timeout()) +} + +func TestSTUNClientLeavesSocketAndDeadlineWithCaller(t *testing.T) { + originalTimeout := responseTimeout + responseTimeout = 100 * time.Millisecond + t.Cleanup(func() { responseTimeout = originalTimeout }) + + server := listenTestUDP4(t) + unrelated := listenTestUDP4(t) + done := serveOneSTUNRequest(server, func(request []byte, source *net.UDPAddr) ([]byte, error) { + response, err := makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.5"), 5000, true)}, + ) + if err != nil { + return nil, err + } + if _, err := unrelated.WriteToUDP(response, source); err != nil { + return nil, err + } + return response, nil + }) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + response, err := conn.doSTUNRequest(server.LocalAddr().String()) + require.NoError(t, err) + require.Equal(t, "198.51.100.5:5000", response.externalAddr) + waitSTUNExchange(t, done) + + _, _, err = conn.conn.ReadFromUDP(make([]byte, 1)) + var netErr net.Error + require.True(t, errors.As(err, &netErr)) + require.True(t, netErr.Timeout()) + + require.NoError(t, conn.conn.SetDeadline(time.Time{})) + require.NoError(t, server.SetReadDeadline(time.Now().Add(testSTUNServerLimit))) + _, err = conn.conn.WriteToUDP([]byte{1}, server.LocalAddr().(*net.UDPAddr)) + require.NoError(t, err) + _, source, err := server.ReadFromUDP(make([]byte, 1)) + require.NoError(t, err) + require.Equal(t, conn.localAddr.(*net.UDPAddr).Port, source.Port) +} diff --git a/pkg/nathole/nathole.go b/pkg/nathole/nathole.go index 545ad7aa035..bea3d371908 100644 --- a/pkg/nathole/nathole.go +++ b/pkg/nathole/nathole.go @@ -1,212 +1,453 @@ +// Copyright 2023 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 nathole import ( - "bytes" + "context" "fmt" + "math/rand/v2" "net" - "sync" + "slices" + "strconv" + "strings" "time" + "github.com/fatedier/golib/pool" + "golang.org/x/net/ipv4" + "k8s.io/apimachinery/pkg/util/sets" + "github.com/fatedier/frp/pkg/msg" - "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/xlog" +) - "github.com/fatedier/golib/errors" - "github.com/fatedier/golib/pool" +var ( + // mode 0: simple detect mode, usually for both EasyNAT or HardNAT & EasyNAT(Public Network) + // a. receiver sends detect message with low TTL + // b. sender sends normal detect message to receiver + // c. receiver receives detect message and sends back a message to sender + // + // mode 1: For HardNAT & EasyNAT, send detect messages to multiple guessed ports. + // Usually applicable to scenarios where port changes are regular. + // Most of the steps are the same as mode 0, but EasyNAT is fixed as the receiver and will send detect messages + // with low TTL to multiple guessed ports of the sender. + // + // mode 2: For HardNAT & EasyNAT, ports changes are not regular. + // a. HardNAT machine will listen on multiple ports and send detect messages with low TTL to EasyNAT machine + // b. EasyNAT machine will send detect messages to random ports of HardNAT machine. + // + // mode 3: For HardNAT & HardNAT, both changes in the ports are regular. + // Most of the steps are the same as mode 1, but the sender also needs to send detect messages to multiple guessed + // ports of the receiver. + // + // mode 4: For HardNAT & HardNAT, one of the changes in the ports is regular. + // Regular port changes are usually on the sender side. + // a. Receiver listens on multiple ports and sends detect messages with low TTL to the sender's guessed range ports. + // b. Sender sends detect messages to random ports of the receiver. + SupportedModes = []int{DetectMode0, DetectMode1, DetectMode2, DetectMode3, DetectMode4} + SupportedRoles = []string{DetectRoleSender, DetectRoleReceiver} + + DetectMode0 = 0 + DetectMode1 = 1 + DetectMode2 = 2 + DetectMode3 = 3 + DetectMode4 = 4 + DetectRoleSender = "sender" + DetectRoleReceiver = "receiver" ) -// Timeout seconds. -var NatHoleTimeout int64 = 10 +// PrepareOptions defines options for NAT traversal preparation +type PrepareOptions struct { + // DisableAssistedAddrs disables the use of local network interfaces + // for assisted connections during NAT traversal + DisableAssistedAddrs bool +} -type SidRequest struct { - Sid string - NotifyCh chan struct{} +type PrepareResult struct { + Addrs []string + AssistedAddrs []string + ListenConn *net.UDPConn + NatType string + Behavior string } -type Controller struct { - listener *net.UDPConn +// PreCheck is used to check if the proxy is ready for penetration. +// Call this function before calling Prepare to avoid unnecessary preparation work. +func PreCheck( + ctx context.Context, transporter transport.MessageTransporter, + proxyName string, timeout time.Duration, +) error { + timeoutCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() - clientCfgs map[string]*ClientCfg - sessions map[string]*Session + var natHoleRespMsg *msg.NatHoleResp + transactionID := NewTransactionID() + m, err := transporter.Do(timeoutCtx, &msg.NatHoleVisitor{ + TransactionID: transactionID, + ProxyName: proxyName, + PreCheck: true, + }, transactionID, msg.TypeNameNatHoleResp) + if err != nil { + return fmt.Errorf("get natHoleRespMsg error: %v", err) + } + mm, ok := m.(*msg.NatHoleResp) + if !ok { + return fmt.Errorf("get natHoleRespMsg error: invalid message type") + } + natHoleRespMsg = mm - mu sync.RWMutex + if natHoleRespMsg.Error != "" { + return fmt.Errorf("%s", natHoleRespMsg.Error) + } + return nil } -func NewController(udpBindAddr string) (nc *Controller, err error) { - addr, err := net.ResolveUDPAddr("udp", udpBindAddr) +// Prepare is used to do some preparation work before penetration. +func Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) { + // discover for Nat type + addrs, localAddr, err := Discover(stunServers, "") if err != nil { - return nil, err + return nil, fmt.Errorf("discover error: %v", err) } - lconn, err := net.ListenUDP("udp", addr) + if len(addrs) < 2 { + return nil, fmt.Errorf("discover error: not enough addresses") + } + + localIPs, _ := ListLocalIPsForNatHole(10) + natFeature, err := ClassifyNATFeature(addrs, localIPs) + if err != nil { + return nil, fmt.Errorf("classify nat feature error: %v", err) + } + + laddr, err := net.ResolveUDPAddr("udp4", localAddr.String()) + if err != nil { + return nil, fmt.Errorf("resolve local udp addr error: %v", err) + } + listenConn, err := net.ListenUDP("udp4", laddr) if err != nil { - return nil, err + return nil, fmt.Errorf("listen local udp addr error: %v", err) } - nc = &Controller{ - listener: lconn, - clientCfgs: make(map[string]*ClientCfg), - sessions: make(map[string]*Session), + + // Apply NAT traversal options + var assistedAddrs []string + if !opts.DisableAssistedAddrs { + assistedAddrs = make([]string, 0, len(localIPs)) + for _, ip := range localIPs { + assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) + } } - return nc, nil + return &PrepareResult{ + Addrs: addrs, + AssistedAddrs: assistedAddrs, + ListenConn: listenConn, + NatType: natFeature.NatType, + Behavior: natFeature.Behavior, + }, nil } -func (nc *Controller) ListenClient(name string, sk string) (sidCh chan *SidRequest) { - clientCfg := &ClientCfg{ - Name: name, - Sk: sk, - SidCh: make(chan *SidRequest), +// ExchangeInfo is used to exchange information between client and visitor. +// 1. Send input message to server by msgTransporter. +// 2. Server will gather information from client and visitor and analyze it. Then send back a NatHoleResp message to them to tell them how to do next. +// 3. Receive NatHoleResp message from server. +func ExchangeInfo( + ctx context.Context, transporter transport.MessageTransporter, + laneKey string, m msg.Message, timeout time.Duration, +) (*msg.NatHoleResp, error) { + timeoutCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var natHoleRespMsg *msg.NatHoleResp + m, err := transporter.Do(timeoutCtx, m, laneKey, msg.TypeNameNatHoleResp) + if err != nil { + return nil, fmt.Errorf("get natHoleRespMsg error: %v", err) } - nc.mu.Lock() - nc.clientCfgs[name] = clientCfg - nc.mu.Unlock() - return clientCfg.SidCh + mm, ok := m.(*msg.NatHoleResp) + if !ok { + return nil, fmt.Errorf("get natHoleRespMsg error: invalid message type") + } + natHoleRespMsg = mm + + if natHoleRespMsg.Error != "" { + return nil, fmt.Errorf("natHoleRespMsg get error info: %s", natHoleRespMsg.Error) + } + if len(natHoleRespMsg.CandidateAddrs) == 0 { + return nil, fmt.Errorf("natHoleRespMsg get empty candidate addresses") + } + return natHoleRespMsg, nil } -func (nc *Controller) CloseClient(name string) { - nc.mu.Lock() - defer nc.mu.Unlock() - delete(nc.clientCfgs, name) +// MakeHole is used to make a NAT hole between client and visitor. +func MakeHole(ctx context.Context, listenConn *net.UDPConn, m *msg.NatHoleResp, key []byte) (*net.UDPConn, *net.UDPAddr, error) { + xl := xlog.FromContextSafe(ctx) + transactionID := NewTransactionID() + sendToRangePortsFunc := func(conn *net.UDPConn, addr string) error { + return sendSidMessage(ctx, conn, m.Sid, transactionID, addr, key, m.DetectBehavior.TTL) + } + + listenConns := []*net.UDPConn{listenConn} + var detectAddrs []string + if m.DetectBehavior.Role == DetectRoleSender { + // sender + if m.DetectBehavior.SendDelayMs > 0 { + time.Sleep(time.Duration(m.DetectBehavior.SendDelayMs) * time.Millisecond) + } + detectAddrs = m.AssistedAddrs + detectAddrs = append(detectAddrs, m.CandidateAddrs...) + } else { + // receiver + if len(m.DetectBehavior.CandidatePorts) == 0 { + detectAddrs = m.CandidateAddrs + } + + if m.DetectBehavior.ListenRandomPorts > 0 { + for i := 0; i < m.DetectBehavior.ListenRandomPorts; i++ { + tmpConn, err := net.ListenUDP("udp4", nil) + if err != nil { + xl.Warnf("listen random udp addr error: %v", err) + continue + } + listenConns = append(listenConns, tmpConn) + } + } + } + + detectAddrs = slices.Compact(detectAddrs) + for _, detectAddr := range detectAddrs { + for _, conn := range listenConns { + if err := sendSidMessage(ctx, conn, m.Sid, transactionID, detectAddr, key, m.DetectBehavior.TTL); err != nil { + xl.Tracef("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err) + } + } + } + if len(m.DetectBehavior.CandidatePorts) > 0 { + for _, conn := range listenConns { + sendSidMessageToRangePorts(ctx, conn, m.CandidateAddrs, m.DetectBehavior.CandidatePorts, sendToRangePortsFunc) + } + } + if m.DetectBehavior.SendRandomPorts > 0 { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + for i := range listenConns { + go sendSidMessageToRandomPorts(ctx, listenConns[i], m.CandidateAddrs, m.DetectBehavior.SendRandomPorts, sendToRangePortsFunc) + } + } + + timeout := 5 * time.Second + if m.DetectBehavior.ReadTimeoutMs > 0 { + timeout = time.Duration(m.DetectBehavior.ReadTimeoutMs) * time.Millisecond + } + + if len(listenConns) == 1 { + raddr, err := waitDetectMessage(ctx, listenConns[0], m.Sid, key, timeout, m.DetectBehavior.Role) + if err != nil { + return nil, nil, fmt.Errorf("wait detect message error: %v", err) + } + return listenConns[0], raddr, nil + } + + type result struct { + lConn *net.UDPConn + raddr *net.UDPAddr + } + resultCh := make(chan result) + for _, conn := range listenConns { + go func(lConn *net.UDPConn) { + addr, err := waitDetectMessage(ctx, lConn, m.Sid, key, timeout, m.DetectBehavior.Role) + if err != nil { + lConn.Close() + return + } + select { + case resultCh <- result{lConn: lConn, raddr: addr}: + default: + lConn.Close() + } + }(conn) + } + + select { + case result := <-resultCh: + return result.lConn, result.raddr, nil + case <-time.After(timeout): + return nil, nil, fmt.Errorf("wait detect message timeout") + case <-ctx.Done(): + return nil, nil, fmt.Errorf("wait detect message canceled") + } } -func (nc *Controller) Run() { +func waitDetectMessage( + ctx context.Context, conn *net.UDPConn, sid string, key []byte, + timeout time.Duration, role string, +) (*net.UDPAddr, error) { + xl := xlog.FromContextSafe(ctx) for { buf := pool.GetBuf(1024) - n, raddr, err := nc.listener.ReadFromUDP(buf) + _ = conn.SetReadDeadline(time.Now().Add(timeout)) + n, raddr, err := conn.ReadFromUDP(buf) + _ = conn.SetReadDeadline(time.Time{}) if err != nil { - log.Trace("nat hole listener read from udp error: %v", err) - return + pool.PutBuf(buf) + return nil, err } - - rd := bytes.NewReader(buf[:n]) - rawMsg, err := msg.ReadMsg(rd) - if err != nil { - log.Trace("read nat hole message error: %v", err) + xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr) + var m msg.NatHoleSid + if err := DecodeMessageInto(buf[:n], key, &m); err != nil { + pool.PutBuf(buf) + xl.Warnf("decode sid message error: %v", err) continue } + pool.PutBuf(buf) - switch m := rawMsg.(type) { - case *msg.NatHoleVisitor: - go nc.HandleVisitor(m, raddr) - case *msg.NatHoleClient: - go nc.HandleClient(m, raddr) - default: - log.Trace("error nat hole message type") + if m.Sid != sid { + xl.Warnf("get sid message with wrong sid: %s, expect: %s", m.Sid, sid) continue } - pool.PutBuf(buf) - } -} -func (nc *Controller) GenSid() string { - t := time.Now().Unix() - id, _ := util.RandID() - return fmt.Sprintf("%d%s", t, id) + if !m.Response { + // only wait for response messages if we are a sender + if role == DetectRoleSender { + continue + } + + m.Response = true + buf2, err := EncodeMessage(&m, key) + if err != nil { + xl.Warnf("encode sid message error: %v", err) + continue + } + _, _ = conn.WriteToUDP(buf2, raddr) + } + return raddr, nil + } } -func (nc *Controller) HandleVisitor(m *msg.NatHoleVisitor, raddr *net.UDPAddr) { - sid := nc.GenSid() - session := &Session{ - Sid: sid, - VisitorAddr: raddr, - NotifyCh: make(chan struct{}, 0), +func sendSidMessage( + ctx context.Context, conn *net.UDPConn, + sid string, transactionID string, addr string, key []byte, ttl int, +) error { + xl := xlog.FromContextSafe(ctx) + ttlStr := "" + if ttl > 0 { + ttlStr = fmt.Sprintf(" with ttl %d", ttl) } - nc.mu.Lock() - clientCfg, ok := nc.clientCfgs[m.ProxyName] - if !ok { - nc.mu.Unlock() - errInfo := fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName) - log.Debug(errInfo) - nc.listener.WriteToUDP(nc.GenNatHoleResponse(nil, errInfo), raddr) - return - } - if m.SignKey != util.GetAuthKey(clientCfg.Sk, m.Timestamp) { - nc.mu.Unlock() - errInfo := fmt.Sprintf("xtcp connection of [%s] auth failed", m.ProxyName) - log.Debug(errInfo) - nc.listener.WriteToUDP(nc.GenNatHoleResponse(nil, errInfo), raddr) - return - } - - nc.sessions[sid] = session - nc.mu.Unlock() - log.Trace("handle visitor message, sid [%s]", sid) - - defer func() { - nc.mu.Lock() - delete(nc.sessions, sid) - nc.mu.Unlock() - }() - - err := errors.PanicToError(func() { - clientCfg.SidCh <- &SidRequest{ - Sid: sid, - NotifyCh: session.NotifyCh, - } - }) + xl.Tracef("send sid message from %s to %s%s", conn.LocalAddr(), addr, ttlStr) + raddr, err := net.ResolveUDPAddr("udp4", addr) if err != nil { - return + return err } - - // Wait client connections. - select { - case <-session.NotifyCh: - resp := nc.GenNatHoleResponse(session, "") - log.Trace("send nat hole response to visitor") - nc.listener.WriteToUDP(resp, raddr) - case <-time.After(time.Duration(NatHoleTimeout) * time.Second): - return + if transactionID == "" { + transactionID = NewTransactionID() } -} + m := &msg.NatHoleSid{ + TransactionID: transactionID, + Sid: sid, + Response: false, + Nonce: strings.Repeat("0", rand.IntN(20)), + } + buf, err := EncodeMessage(m, key) + if err != nil { + return err + } + if ttl > 0 { + uConn := ipv4.NewConn(conn) + original, err := uConn.TTL() + if err != nil { + xl.Tracef("get ttl error %v", err) + return err + } + xl.Tracef("original ttl %d", original) -func (nc *Controller) HandleClient(m *msg.NatHoleClient, raddr *net.UDPAddr) { - nc.mu.RLock() - session, ok := nc.sessions[m.Sid] - nc.mu.RUnlock() - if !ok { - return + err = uConn.SetTTL(ttl) + if err != nil { + xl.Tracef("set ttl error %v", err) + } else { + defer func() { + _ = uConn.SetTTL(original) + }() + } } - log.Trace("handle client message, sid [%s]", session.Sid) - session.ClientAddr = raddr - resp := nc.GenNatHoleResponse(session, "") - log.Trace("send nat hole response to client") - nc.listener.WriteToUDP(resp, raddr) + if _, err := conn.WriteToUDP(buf, raddr); err != nil { + return err + } + return nil } -func (nc *Controller) GenNatHoleResponse(session *Session, errInfo string) []byte { - var ( - sid string - visitorAddr string - clientAddr string - ) - if session != nil { - sid = session.Sid - visitorAddr = session.VisitorAddr.String() - clientAddr = session.ClientAddr.String() - } - m := &msg.NatHoleResp{ - Sid: sid, - VisitorAddr: visitorAddr, - ClientAddr: clientAddr, - Error: errInfo, - } - b := bytes.NewBuffer(nil) - err := msg.WriteMsg(b, m) - if err != nil { - return []byte("") +func sendSidMessageToRangePorts( + ctx context.Context, conn *net.UDPConn, addrs []string, ports []msg.PortsRange, + sendFunc func(*net.UDPConn, string) error, +) { + xl := xlog.FromContextSafe(ctx) + for _, ip := range slices.Compact(parseIPs(addrs)) { + for _, portsRange := range ports { + for i := portsRange.From; i <= portsRange.To; i++ { + detectAddr := net.JoinHostPort(ip, strconv.Itoa(i)) + if err := sendFunc(conn, detectAddr); err != nil { + xl.Tracef("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err) + } + time.Sleep(2 * time.Millisecond) + } + } } - return b.Bytes() } -type Session struct { - Sid string - VisitorAddr *net.UDPAddr - ClientAddr *net.UDPAddr +func sendSidMessageToRandomPorts( + ctx context.Context, conn *net.UDPConn, addrs []string, count int, + sendFunc func(*net.UDPConn, string) error, +) { + xl := xlog.FromContextSafe(ctx) + used := sets.New[int]() + getUnusedPort := func() int { + for range 10 { + port := rand.IntN(65535-1024) + 1024 + if !used.Has(port) { + used.Insert(port) + return port + } + } + return 0 + } - NotifyCh chan struct{} + for range count { + select { + case <-ctx.Done(): + return + default: + } + + port := getUnusedPort() + if port == 0 { + continue + } + + for _, ip := range slices.Compact(parseIPs(addrs)) { + detectAddr := net.JoinHostPort(ip, strconv.Itoa(port)) + if err := sendFunc(conn, detectAddr); err != nil { + xl.Tracef("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err) + } + time.Sleep(time.Millisecond * 15) + } + } } -type ClientCfg struct { - Name string - Sk string - SidCh chan *SidRequest +func parseIPs(addrs []string) []string { + var ips []string + for _, addr := range addrs { + if ip, _, err := net.SplitHostPort(addr); err == nil { + ips = append(ips, ip) + } + } + return ips } diff --git a/pkg/nathole/utils.go b/pkg/nathole/utils.go new file mode 100644 index 00000000000..0d71692f2a7 --- /dev/null +++ b/pkg/nathole/utils.go @@ -0,0 +1,93 @@ +// Copyright 2023 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 nathole + +import ( + "bytes" + "fmt" + "net" + + "github.com/fatedier/golib/crypto" + + "github.com/fatedier/frp/pkg/msg" +) + +func EncodeMessage(m msg.Message, key []byte) ([]byte, error) { + buffer := bytes.NewBuffer(nil) + if err := msg.WriteMsg(buffer, m); err != nil { + return nil, err + } + + buf, err := crypto.Encode(buffer.Bytes(), key) + if err != nil { + return nil, err + } + return buf, nil +} + +func DecodeMessageInto(data, key []byte, m msg.Message) error { + buf, err := crypto.Decode(data, key) + if err != nil { + return err + } + + return msg.ReadMsgInto(bytes.NewReader(buf), m) +} + +func ListAllLocalIPs() ([]net.IP, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + ips := make([]net.IP, 0, len(addrs)) + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err != nil { + continue + } + ips = append(ips, ip) + } + return ips, nil +} + +func ListLocalIPsForNatHole(maxItems int) ([]string, error) { + if maxItems <= 0 { + return nil, fmt.Errorf("maxItems must be greater than 0") + } + + ips, err := ListAllLocalIPs() + if err != nil { + return nil, err + } + + filtered := make([]string, 0, maxItems) + for _, ip := range ips { + if len(filtered) >= maxItems { + break + } + + // ignore ipv6 address + if ip.To4() == nil { + continue + } + // ignore localhost IP + if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + continue + } + + filtered = append(filtered, ip.String()) + } + return filtered, nil +} diff --git a/pkg/plugin/client/http2http.go b/pkg/plugin/client/http2http.go new file mode 100644 index 00000000000..f92daddd654 --- /dev/null +++ b/pkg/plugin/client/http2http.go @@ -0,0 +1,56 @@ +// Copyright 2024 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 ( + "net/http/httputil" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func init() { + Register(v1.PluginHTTP2HTTP, NewHTTP2HTTPPlugin) +} + +type HTTP2HTTPPlugin struct { + opts *v1.HTTP2HTTPPluginOptions + + *httpBridgePlugin +} + +func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTP2HTTPPluginOptions) + + p := &HTTP2HTTPPlugin{ + opts: opts, + } + + rp := newHTTPBridgeReverseProxy( + func(r *httputil.ProxyRequest) { + req := r.Out + rewriteHTTPPluginRequest(req, "http", p.opts.LocalAddr, p.opts.HostHeaderRewrite, p.opts.RequestHeaders) + }, + nil, + ) + p.httpBridgePlugin = newHTTPBridgePluginServer(rp, false) + + return p, nil +} + +func (p *HTTP2HTTPPlugin) Name() string { + return v1.PluginHTTP2HTTP +} diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go index 3074cddf34e..f5a61602e25 100644 --- a/pkg/plugin/client/http2https.go +++ b/pkg/plugin/client/http2https.go @@ -12,100 +12,54 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( "crypto/tls" - "fmt" - "io" - "net" "net/http" "net/http/httputil" - "strings" - frpNet "github.com/fatedier/frp/pkg/util/net" + v1 "github.com/fatedier/frp/pkg/config/v1" ) -const PluginHTTP2HTTPS = "http2https" - func init() { - Register(PluginHTTP2HTTPS, NewHTTP2HTTPSPlugin) + Register(v1.PluginHTTP2HTTPS, NewHTTP2HTTPSPlugin) } type HTTP2HTTPSPlugin struct { - hostHeaderRewrite string - localAddr string - headers map[string]string + opts *v1.HTTP2HTTPSPluginOptions - l *Listener - s *http.Server + *httpBridgePlugin } -func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) { - localAddr := params["plugin_local_addr"] - hostHeaderRewrite := params["plugin_host_header_rewrite"] - headers := make(map[string]string) - for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { - continue - } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - headers[k] = v - } - } - - if localAddr == "" { - return nil, fmt.Errorf("plugin_local_addr is required") - } - - listener := NewProxyListener() +func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTP2HTTPSPluginOptions) p := &HTTP2HTTPSPlugin{ - localAddr: localAddr, - hostHeaderRewrite: hostHeaderRewrite, - headers: headers, - l: listener, + opts: opts, } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - rp := &httputil.ReverseProxy{ - Director: func(req *http.Request) { - req.URL.Scheme = "https" - req.URL.Host = p.localAddr - if p.hostHeaderRewrite != "" { - req.Host = p.hostHeaderRewrite - } - for k, v := range p.headers { - req.Header.Set(k, v) - } + rp := newHTTPBridgeReverseProxy( + func(r *httputil.ProxyRequest) { + r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] + r.Out.Header["X-Forwarded-Host"] = r.In.Header["X-Forwarded-Host"] + r.Out.Header["X-Forwarded-Proto"] = r.In.Header["X-Forwarded-Proto"] + req := r.Out + rewriteHTTPPluginRequest(req, "https", p.opts.LocalAddr, p.opts.HostHeaderRewrite, p.opts.RequestHeaders) }, - Transport: tr, - } - - p.s = &http.Server{ - Handler: rp, - } - - go p.s.Serve(listener) + tr, + ) + p.httpBridgePlugin = newHTTPBridgePluginServer(rp, false) return p, nil } -func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { - wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) - p.l.PutConn(wrapConn) -} - func (p *HTTP2HTTPSPlugin) Name() string { - return PluginHTTP2HTTPS -} - -func (p *HTTP2HTTPSPlugin) Close() error { - if err := p.s.Close(); err != nil { - return err - } - return nil + return v1.PluginHTTP2HTTPS } diff --git a/pkg/plugin/client/http_common.go b/pkg/plugin/client/http_common.go new file mode 100644 index 00000000000..5010a4e934a --- /dev/null +++ b/pkg/plugin/client/http_common.go @@ -0,0 +1,126 @@ +// 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" + stdlog "log" + "net/http" + "net/http/httputil" + "time" + + "github.com/fatedier/golib/pool" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/plugin/client/internal/httpsserver" + "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +const httpBridgeReadHeaderTimeout = 60 * time.Second + +func rewriteHTTPPluginRequest( + req *http.Request, + scheme string, + localAddr string, + hostHeaderRewrite string, + requestHeaders v1.HeaderOperations, +) { + req.URL.Scheme = scheme + req.URL.Host = localAddr + if hostHeaderRewrite != "" { + req.Host = hostHeaderRewrite + } + for k, v := range requestHeaders.Set { + req.Header.Set(k, v) + } +} + +type httpBridgePlugin struct { + l *Listener + s *http.Server + + useSourceRemoteAddr bool +} + +func newHTTPBridgePluginServer(handler http.Handler, useSourceRemoteAddr bool) *httpBridgePlugin { + listener := NewProxyListener() + p := &httpBridgePlugin{ + l: listener, + useSourceRemoteAddr: useSourceRemoteAddr, + } + p.s = &http.Server{ + Handler: handler, + ReadHeaderTimeout: httpBridgeReadHeaderTimeout, + } + go func() { + _ = p.s.Serve(listener) + }() + return p +} + +func newHTTPSBridgePluginServer( + handler http.Handler, + crtPath string, + keyPath string, + enableHTTP2 *bool, + useSourceRemoteAddr bool, +) (*httpBridgePlugin, error) { + listener := NewProxyListener() + server, err := httpsserver.New(handler, crtPath, keyPath, enableHTTP2) + if err != nil { + return nil, err + } + p := &httpBridgePlugin{ + l: listener, + s: server, + useSourceRemoteAddr: useSourceRemoteAddr, + } + go func() { + _ = p.s.ServeTLS(listener, "", "") + }() + return p, nil +} + +func newHTTPBridgeReverseProxy( + rewrite func(*httputil.ProxyRequest), + transport http.RoundTripper, +) *httputil.ReverseProxy { + rp := &httputil.ReverseProxy{ + Rewrite: rewrite, + BufferPool: pool.NewBuffer(32 * 1024), + ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0), + } + if transport != nil { + rp.Transport = transport + } + return rp +} + +func (p *httpBridgePlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + if p.useSourceRemoteAddr && connInfo.SrcAddr != nil { + wrapConn.SetRemoteAddr(connInfo.SrcAddr) + } + _ = p.l.PutConn(wrapConn) +} + +func (p *httpBridgePlugin) Close() error { + err := p.s.Close() + _ = p.l.Close() + return err +} diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go index 45bd3a9a792..0145f4445f5 100644 --- a/pkg/plugin/client/http_proxy.go +++ b/pkg/plugin/client/http_proxy.go @@ -12,82 +12,92 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( "bufio" + "context" "encoding/base64" "io" "net" "net/http" "strings" + "time" - frpNet "github.com/fatedier/frp/pkg/util/net" + libio "github.com/fatedier/golib/io" + libnet "github.com/fatedier/golib/net" - frpIo "github.com/fatedier/golib/io" - gnet "github.com/fatedier/golib/net" + v1 "github.com/fatedier/frp/pkg/config/v1" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" ) -const PluginHTTPProxy = "http_proxy" - func init() { - Register(PluginHTTPProxy, NewHTTPProxyPlugin) + Register(v1.PluginHTTPProxy, NewHTTPProxyPlugin) } type HTTPProxy struct { - l *Listener - s *http.Server - AuthUser string - AuthPasswd string + opts *v1.HTTPProxyPluginOptions + + l *Listener + s *http.Server } -func NewHTTPProxyPlugin(params map[string]string) (Plugin, error) { - user := params["plugin_http_user"] - passwd := params["plugin_http_passwd"] +const httpProxyReadHeaderTimeout = 60 * time.Second + +func NewHTTPProxyPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTPProxyPluginOptions) listener := NewProxyListener() hp := &HTTPProxy{ - l: listener, - AuthUser: user, - AuthPasswd: passwd, + l: listener, + opts: opts, } hp.s = &http.Server{ - Handler: hp, + Handler: hp, + ReadHeaderTimeout: httpProxyReadHeaderTimeout, } - go hp.s.Serve(listener) + go func() { + _ = hp.s.Serve(listener) + }() return hp, nil } func (hp *HTTPProxy) Name() string { - return PluginHTTPProxy + return v1.PluginHTTPProxy } -func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { - wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) +func (hp *HTTPProxy) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) - sc, rd := gnet.NewSharedConn(wrapConn) - firstBytes := make([]byte, 7) - _, err := rd.Read(firstBytes) + sc, rd := libnet.NewSharedConn(wrapConn) + firstBytes := make([]byte, len(http.MethodConnect)) + _ = wrapConn.SetReadDeadline(time.Now().Add(httpProxyReadHeaderTimeout)) + _, err := io.ReadFull(rd, firstBytes) if err != nil { + _ = wrapConn.SetReadDeadline(time.Time{}) wrapConn.Close() return } - if strings.ToUpper(string(firstBytes)) == "CONNECT" { + if strings.EqualFold(string(firstBytes), http.MethodConnect) { bufRd := bufio.NewReader(sc) request, err := http.ReadRequest(bufRd) + _ = wrapConn.SetReadDeadline(time.Time{}) if err != nil { wrapConn.Close() return } - hp.handleConnectReq(request, frpIo.WrapReadWriteCloser(bufRd, wrapConn, wrapConn.Close)) + hp.handleConnectReq(request, libio.WrapReadWriteCloser(bufRd, wrapConn, wrapConn.Close)) return } - hp.l.PutConn(sc) - return + _ = wrapConn.SetReadDeadline(time.Time{}) + _ = hp.l.PutConn(sc) } func (hp *HTTPProxy) Close() error { @@ -103,13 +113,7 @@ func (hp *HTTPProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - if req.Method == http.MethodConnect { - // deprecated - // Connect request is handled in Handle function. - hp.ConnectHandler(rw, req) - } else { - hp.HTTPHandler(rw, req) - } + hp.HTTPHandler(rw, req) } func (hp *HTTPProxy) HTTPHandler(rw http.ResponseWriter, req *http.Request) { @@ -131,35 +135,8 @@ func (hp *HTTPProxy) HTTPHandler(rw http.ResponseWriter, req *http.Request) { } } -// deprecated -// Hijack needs to SetReadDeadline on the Conn of the request, but if we use stream compression here, -// we may always get i/o timeout error. -func (hp *HTTPProxy) ConnectHandler(rw http.ResponseWriter, req *http.Request) { - hj, ok := rw.(http.Hijacker) - if !ok { - rw.WriteHeader(http.StatusInternalServerError) - return - } - - client, _, err := hj.Hijack() - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - return - } - - remote, err := net.Dial("tcp", req.URL.Host) - if err != nil { - http.Error(rw, "Failed", http.StatusBadRequest) - client.Close() - return - } - client.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) - - go frpIo.Join(remote, client) -} - func (hp *HTTPProxy) Auth(req *http.Request) bool { - if hp.AuthUser == "" && hp.AuthPasswd == "" { + if hp.opts.HTTPUser == "" && hp.opts.HTTPPassword == "" { return true } @@ -178,7 +155,9 @@ func (hp *HTTPProxy) Auth(req *http.Request) bool { return false } - if pair[0] != hp.AuthUser || pair[1] != hp.AuthPasswd { + if !util.ConstantTimeEqString(pair[0], hp.opts.HTTPUser) || + !util.ConstantTimeEqString(pair[1], hp.opts.HTTPPassword) { + time.Sleep(200 * time.Millisecond) return false } return true @@ -188,7 +167,10 @@ func (hp *HTTPProxy) handleConnectReq(req *http.Request, rwc io.ReadWriteCloser) defer rwc.Close() if ok := hp.Auth(req); !ok { res := getBadResponse() - res.Write(rwc) + _ = res.Write(rwc) + if res.Body != nil { + res.Body.Close() + } return } @@ -200,12 +182,12 @@ func (hp *HTTPProxy) handleConnectReq(req *http.Request, rwc io.ReadWriteCloser) ProtoMajor: 1, ProtoMinor: 1, } - res.Write(rwc) + _ = res.Write(rwc) return } - rwc.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) + _, _ = rwc.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) - frpIo.Join(remote, rwc) + libio.Join(remote, rwc) } func copyHeaders(dst, src http.Header) { diff --git a/pkg/plugin/client/http_proxy_test.go b/pkg/plugin/client/http_proxy_test.go new file mode 100644 index 00000000000..c4557ecee2c --- /dev/null +++ b/pkg/plugin/client/http_proxy_test.go @@ -0,0 +1,107 @@ +// 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 ( + "bufio" + "context" + "fmt" + "io" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestHTTPProxyHandleFragmentedConnectMethod(t *testing.T) { + require := require.New(t) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(err) + defer ln.Close() + + const payload = "ping" + echoErr := make(chan error, 1) + go func() { + conn, err := ln.Accept() + if err != nil { + echoErr <- err + return + } + defer conn.Close() + + buf := make([]byte, len(payload)) + if _, err = io.ReadFull(conn, buf); err != nil { + echoErr <- err + return + } + if string(buf) != payload { + echoErr <- fmt.Errorf("unexpected payload %q", string(buf)) + return + } + _, err = conn.Write([]byte("echo:" + payload)) + echoErr <- err + }() + + hp := &HTTPProxy{ + opts: &v1.HTTPProxyPluginOptions{}, + l: NewProxyListener(), + } + + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + + go hp.Handle(context.Background(), &ConnectionInfo{ + Conn: serverConn, + UnderlyingConn: serverConn, + }) + + require.NoError(clientConn.SetDeadline(time.Now().Add(5 * time.Second))) + + targetAddr := ln.Addr().String() + req := "CONNECT " + targetAddr + " HTTP/1.1\r\nHost: " + targetAddr + "\r\n\r\n" + _, err = clientConn.Write([]byte("CON")) + require.NoError(err) + _, err = clientConn.Write([]byte(req[len("CON"):])) + require.NoError(err) + + rd := bufio.NewReader(clientConn) + status, err := rd.ReadString('\n') + require.NoError(err) + require.Equal("HTTP/1.1 200 OK\r\n", status) + line, err := rd.ReadString('\n') + require.NoError(err) + require.Equal("\r\n", line) + + _, err = clientConn.Write([]byte(payload)) + require.NoError(err) + + got := make([]byte, len("echo:"+payload)) + _, err = io.ReadFull(rd, got) + require.NoError(err) + require.Equal("echo:"+payload, string(got)) + + select { + case err := <-echoErr: + require.NoError(err) + case <-time.After(time.Second): + t.Fatal("timed out waiting for echo server") + } +} diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go index 5aec067b17c..8722bec94a3 100644 --- a/pkg/plugin/client/https2http.go +++ b/pkg/plugin/client/https2http.go @@ -12,126 +12,52 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( - "crypto/tls" - "fmt" - "io" - "net" - "net/http" "net/http/httputil" - "strings" - "github.com/fatedier/frp/pkg/transport" - frpNet "github.com/fatedier/frp/pkg/util/net" + v1 "github.com/fatedier/frp/pkg/config/v1" ) -const PluginHTTPS2HTTP = "https2http" - func init() { - Register(PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin) + Register(v1.PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin) } type HTTPS2HTTPPlugin struct { - crtPath string - keyPath string - hostHeaderRewrite string - localAddr string - headers map[string]string + opts *v1.HTTPS2HTTPPluginOptions - l *Listener - s *http.Server + *httpBridgePlugin } -func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) { - crtPath := params["plugin_crt_path"] - keyPath := params["plugin_key_path"] - localAddr := params["plugin_local_addr"] - hostHeaderRewrite := params["plugin_host_header_rewrite"] - headers := make(map[string]string) - for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { - continue - } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - headers[k] = v - } - } - - if localAddr == "" { - return nil, fmt.Errorf("plugin_local_addr is required") - } - - listener := NewProxyListener() +func NewHTTPS2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTPS2HTTPPluginOptions) p := &HTTPS2HTTPPlugin{ - crtPath: crtPath, - keyPath: keyPath, - localAddr: localAddr, - hostHeaderRewrite: hostHeaderRewrite, - headers: headers, - l: listener, + opts: opts, } - rp := &httputil.ReverseProxy{ - Director: func(req *http.Request) { - req.URL.Scheme = "http" - req.URL.Host = p.localAddr - if p.hostHeaderRewrite != "" { - req.Host = p.hostHeaderRewrite - } - for k, v := range p.headers { - req.Header.Set(k, v) - } + rp := newHTTPBridgeReverseProxy( + func(r *httputil.ProxyRequest) { + r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] + r.SetXForwarded() + req := r.Out + rewriteHTTPPluginRequest(req, "http", p.opts.LocalAddr, p.opts.HostHeaderRewrite, p.opts.RequestHeaders) }, - } - - p.s = &http.Server{ - Handler: rp, - } - - var ( - tlsConfig *tls.Config - err error + nil, ) - if crtPath != "" || keyPath != "" { - tlsConfig, err = p.genTLSConfig() - } else { - tlsConfig, err = transport.NewServerTLSConfig("", "", "") - tlsConfig.InsecureSkipVerify = true - } - if err != nil { - return nil, fmt.Errorf("gen TLS config error: %v", err) - } - ln := tls.NewListener(listener, tlsConfig) - - go p.s.Serve(ln) - return p, nil -} -func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath) + server, err := newHTTPSBridgePluginServer(rp, p.opts.CrtPath, p.opts.KeyPath, opts.EnableHTTP2, true) if err != nil { return nil, err } + p.httpBridgePlugin = server - config := &tls.Config{Certificates: []tls.Certificate{cert}} - return config, nil -} - -func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { - wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) - p.l.PutConn(wrapConn) + return p, nil } func (p *HTTPS2HTTPPlugin) Name() string { - return PluginHTTPS2HTTP -} - -func (p *HTTPS2HTTPPlugin) Close() error { - if err := p.s.Close(); err != nil { - return err - } - return nil + return v1.PluginHTTPS2HTTP } diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go index cefa203017a..7b9e455e8ae 100644 --- a/pkg/plugin/client/https2https.go +++ b/pkg/plugin/client/https2https.go @@ -12,131 +12,58 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( "crypto/tls" - "fmt" - "io" - "net" "net/http" "net/http/httputil" - "strings" - "github.com/fatedier/frp/pkg/transport" - frpNet "github.com/fatedier/frp/pkg/util/net" + v1 "github.com/fatedier/frp/pkg/config/v1" ) -const PluginHTTPS2HTTPS = "https2https" - func init() { - Register(PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin) + Register(v1.PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin) } type HTTPS2HTTPSPlugin struct { - crtPath string - keyPath string - hostHeaderRewrite string - localAddr string - headers map[string]string + opts *v1.HTTPS2HTTPSPluginOptions - l *Listener - s *http.Server + *httpBridgePlugin } -func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) { - crtPath := params["plugin_crt_path"] - keyPath := params["plugin_key_path"] - localAddr := params["plugin_local_addr"] - hostHeaderRewrite := params["plugin_host_header_rewrite"] - headers := make(map[string]string) - for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { - continue - } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - headers[k] = v - } - } - - if localAddr == "" { - return nil, fmt.Errorf("plugin_local_addr is required") - } - - listener := NewProxyListener() +func NewHTTPS2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTPS2HTTPSPluginOptions) p := &HTTPS2HTTPSPlugin{ - crtPath: crtPath, - keyPath: keyPath, - localAddr: localAddr, - hostHeaderRewrite: hostHeaderRewrite, - headers: headers, - l: listener, + opts: opts, } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - rp := &httputil.ReverseProxy{ - Director: func(req *http.Request) { - req.URL.Scheme = "https" - req.URL.Host = p.localAddr - if p.hostHeaderRewrite != "" { - req.Host = p.hostHeaderRewrite - } - for k, v := range p.headers { - req.Header.Set(k, v) - } + rp := newHTTPBridgeReverseProxy( + func(r *httputil.ProxyRequest) { + r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] + r.SetXForwarded() + req := r.Out + rewriteHTTPPluginRequest(req, "https", p.opts.LocalAddr, p.opts.HostHeaderRewrite, p.opts.RequestHeaders) }, - Transport: tr, - } - - p.s = &http.Server{ - Handler: rp, - } - - var ( - tlsConfig *tls.Config - err error + tr, ) - if crtPath != "" || keyPath != "" { - tlsConfig, err = p.genTLSConfig() - } else { - tlsConfig, err = transport.NewServerTLSConfig("", "", "") - tlsConfig.InsecureSkipVerify = true - } - if err != nil { - return nil, fmt.Errorf("gen TLS config error: %v", err) - } - ln := tls.NewListener(listener, tlsConfig) - - go p.s.Serve(ln) - return p, nil -} -func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath) + server, err := newHTTPSBridgePluginServer(rp, p.opts.CrtPath, p.opts.KeyPath, opts.EnableHTTP2, true) if err != nil { return nil, err } + p.httpBridgePlugin = server - config := &tls.Config{Certificates: []tls.Certificate{cert}} - return config, nil -} - -func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { - wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) - p.l.PutConn(wrapConn) + return p, nil } func (p *HTTPS2HTTPSPlugin) Name() string { - return PluginHTTPS2HTTPS -} - -func (p *HTTPS2HTTPSPlugin) Close() error { - if err := p.s.Close(); err != nil { - return err - } - return nil + return v1.PluginHTTPS2HTTPS } diff --git a/pkg/plugin/client/internal/httpsserver/server.go b/pkg/plugin/client/internal/httpsserver/server.go new file mode 100644 index 00000000000..a2047b9008d --- /dev/null +++ b/pkg/plugin/client/internal/httpsserver/server.go @@ -0,0 +1,60 @@ +// 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 httpsserver + +import ( + "crypto/tls" + "fmt" + "net/http" + "time" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/transport" + httppkg "github.com/fatedier/frp/pkg/util/http" +) + +func New(handler http.Handler, crtPath, keyPath string, enableHTTP2 *bool) (*http.Server, error) { + tlsConfig, err := transport.NewServerTLSConfig(crtPath, keyPath, "") + if err != nil { + return nil, fmt.Errorf("gen TLS config error: %v", err) + } + + server := &http.Server{ + Handler: withMisdirectedRequestCheck(handler), + ReadHeaderTimeout: 60 * time.Second, + TLSConfig: tlsConfig, + } + if !lo.FromPtr(enableHTTP2) { + server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + } + return server, nil +} + +func withMisdirectedRequestCheck(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS != nil { + tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName) + host, _ := httppkg.CanonicalHost(r.Host) + if tlsServerName != "" && tlsServerName != host { + w.WriteHeader(http.StatusMisdirectedRequest) + return + } + } + handler.ServeHTTP(w, r) + }) +} diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go index 6850919a3c1..7bcd04893f5 100644 --- a/pkg/plugin/client/plugin.go +++ b/pkg/plugin/client/plugin.go @@ -12,41 +12,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package client import ( + "context" "fmt" "io" "net" "sync" "github.com/fatedier/golib/errors" + pp "github.com/pires/go-proxyproto" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/vnet" ) +type PluginContext struct { + Name string + VnetController *vnet.Controller +} + // Creators is used for create plugins to handle connections. var creators = make(map[string]CreatorFn) -// params has prefix "plugin_" -type CreatorFn func(params map[string]string) (Plugin, error) +type CreatorFn func(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) func Register(name string, fn CreatorFn) { + if _, exist := creators[name]; exist { + panic(fmt.Sprintf("plugin [%s] is already registered", name)) + } creators[name] = fn } -func Create(name string, params map[string]string) (p Plugin, err error) { - if fn, ok := creators[name]; ok { - p, err = fn(params) +func Create(pluginName string, pluginCtx PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) { + if fn, ok := creators[pluginName]; ok { + p, err = fn(pluginCtx, options) } else { - err = fmt.Errorf("plugin [%s] is not registered", name) + err = fmt.Errorf("plugin [%s] is not registered", pluginName) } return } +type ConnectionInfo struct { + Conn io.ReadWriteCloser + UnderlyingConn net.Conn + + ProxyProtocolHeader *pp.Header + SrcAddr net.Addr + DstAddr net.Addr +} + type Plugin interface { Name() string - // extraBufToLocal will send to local connection first, then join conn with local connection - Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) + Handle(ctx context.Context, connInfo *ConnectionInfo) Close() error } diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go index 3a543d9d2d4..752dd1e198e 100644 --- a/pkg/plugin/client/socks5.go +++ b/pkg/plugin/client/socks5.go @@ -12,40 +12,37 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( + "context" "io" "log" - "net" - - frpNet "github.com/fatedier/frp/pkg/util/net" gosocks5 "github.com/armon/go-socks5" -) -const PluginSocks5 = "socks5" + v1 "github.com/fatedier/frp/pkg/config/v1" + netpkg "github.com/fatedier/frp/pkg/util/net" +) func init() { - Register(PluginSocks5, NewSocks5Plugin) + Register(v1.PluginSocks5, NewSocks5Plugin) } type Socks5Plugin struct { Server *gosocks5.Server - - user string - passwd string } -func NewSocks5Plugin(params map[string]string) (p Plugin, err error) { - user := params["plugin_user"] - passwd := params["plugin_passwd"] +func NewSocks5Plugin(_ PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) { + opts := options.(*v1.Socks5PluginOptions) cfg := &gosocks5.Config{ Logger: log.New(io.Discard, "", log.LstdFlags), } - if user != "" || passwd != "" { - cfg.Credentials = gosocks5.StaticCredentials(map[string]string{user: passwd}) + if opts.Username != "" || opts.Password != "" { + cfg.Credentials = gosocks5.StaticCredentials(map[string]string{opts.Username: opts.Password}) } sp := &Socks5Plugin{} sp.Server, err = gosocks5.New(cfg) @@ -53,14 +50,14 @@ func NewSocks5Plugin(params map[string]string) (p Plugin, err error) { return } -func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { - defer conn.Close() - wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) - sp.Server.ServeConn(wrapConn) +func (sp *Socks5Plugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + defer connInfo.Conn.Close() + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + _ = sp.Server.ServeConn(wrapConn) } func (sp *Socks5Plugin) Name() string { - return PluginSocks5 + return v1.PluginSocks5 } func (sp *Socks5Plugin) Close() error { diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go index 1eeea3ba92e..0bab120aa22 100644 --- a/pkg/plugin/client/static_file.go +++ b/pkg/plugin/client/static_file.go @@ -12,74 +12,69 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( - "io" - "net" + "context" "net/http" - - frpNet "github.com/fatedier/frp/pkg/util/net" + "time" "github.com/gorilla/mux" -) -const PluginStaticFile = "static_file" + v1 "github.com/fatedier/frp/pkg/config/v1" + netpkg "github.com/fatedier/frp/pkg/util/net" +) func init() { - Register(PluginStaticFile, NewStaticFilePlugin) + Register(v1.PluginStaticFile, NewStaticFilePlugin) } type StaticFilePlugin struct { - localPath string - stripPrefix string - httpUser string - httpPasswd string + opts *v1.StaticFilePluginOptions l *Listener s *http.Server } -func NewStaticFilePlugin(params map[string]string) (Plugin, error) { - localPath := params["plugin_local_path"] - stripPrefix := params["plugin_strip_prefix"] - httpUser := params["plugin_http_user"] - httpPasswd := params["plugin_http_passwd"] +func NewStaticFilePlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.StaticFilePluginOptions) listener := NewProxyListener() sp := &StaticFilePlugin{ - localPath: localPath, - stripPrefix: stripPrefix, - httpUser: httpUser, - httpPasswd: httpPasswd, + opts: opts, l: listener, } var prefix string - if stripPrefix != "" { - prefix = "/" + stripPrefix + "/" + if opts.StripPrefix != "" { + prefix = "/" + opts.StripPrefix + "/" } else { prefix = "/" } router := mux.NewRouter() - router.Use(frpNet.NewHTTPAuthMiddleware(httpUser, httpPasswd).Middleware) - router.PathPrefix(prefix).Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(localPath))))).Methods("GET") + router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware) + router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET") sp.s = &http.Server{ - Handler: router, + Handler: router, + ReadHeaderTimeout: 60 * time.Second, } - go sp.s.Serve(listener) + go func() { + _ = sp.s.Serve(listener) + }() return sp, nil } -func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { - wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) - sp.l.PutConn(wrapConn) +func (sp *StaticFilePlugin) Handle(_ context.Context, connInfo *ConnectionInfo) { + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + _ = sp.l.PutConn(wrapConn) } func (sp *StaticFilePlugin) Name() string { - return PluginStaticFile + return v1.PluginStaticFile } func (sp *StaticFilePlugin) Close() error { diff --git a/pkg/plugin/client/tls2raw.go b/pkg/plugin/client/tls2raw.go new file mode 100644 index 00000000000..6ce05420cdf --- /dev/null +++ b/pkg/plugin/client/tls2raw.go @@ -0,0 +1,93 @@ +// Copyright 2024 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" + "crypto/tls" + "net" + + libio "github.com/fatedier/golib/io" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/transport" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/xlog" +) + +func init() { + Register(v1.PluginTLS2Raw, NewTLS2RawPlugin) +} + +type TLS2RawPlugin struct { + opts *v1.TLS2RawPluginOptions + + tlsConfig *tls.Config +} + +func NewTLS2RawPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.TLS2RawPluginOptions) + + p := &TLS2RawPlugin{ + opts: opts, + } + + tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "") + if err != nil { + return nil, err + } + p.tlsConfig = tlsConfig + return p, nil +} + +func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { + xl := xlog.FromContextSafe(ctx) + + wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn) + tlsConn := tls.Server(wrapConn, p.tlsConfig) + + if err := tlsConn.Handshake(); err != nil { + xl.Warnf("tls handshake error: %v", err) + tlsConn.Close() + return + } + rawConn, err := net.Dial("tcp", p.opts.LocalAddr) + if err != nil { + xl.Warnf("dial to local addr error: %v", err) + tlsConn.Close() + return + } + + if connInfo.ProxyProtocolHeader != nil { + if _, err := connInfo.ProxyProtocolHeader.WriteTo(rawConn); err != nil { + xl.Warnf("tls2raw write proxy protocol header to local conn error: %v", err) + rawConn.Close() + tlsConn.Close() + return + } + } + + libio.Join(tlsConn, rawConn) +} + +func (p *TLS2RawPlugin) Name() string { + return v1.PluginTLS2Raw +} + +func (p *TLS2RawPlugin) Close() error { + return nil +} diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go index a85ada702f8..3046aadeade 100644 --- a/pkg/plugin/client/unix_domain_socket.go +++ b/pkg/plugin/client/unix_domain_socket.go @@ -12,34 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +//go:build !frps + +package client import ( - "fmt" - "io" + "context" "net" - frpIo "github.com/fatedier/golib/io" -) + libio "github.com/fatedier/golib/io" -const PluginUnixDomainSocket = "unix_domain_socket" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/xlog" +) func init() { - Register(PluginUnixDomainSocket, NewUnixDomainSocketPlugin) + Register(v1.PluginUnixDomainSocket, NewUnixDomainSocketPlugin) } type UnixDomainSocketPlugin struct { UnixAddr *net.UnixAddr } -func NewUnixDomainSocketPlugin(params map[string]string) (p Plugin, err error) { - unixPath, ok := params["plugin_unix_path"] - if !ok { - err = fmt.Errorf("plugin_unix_path not found") - return - } +func NewUnixDomainSocketPlugin(_ PluginContext, options v1.ClientPluginOptions) (p Plugin, err error) { + opts := options.(*v1.UnixDomainSocketPluginOptions) - unixAddr, errRet := net.ResolveUnixAddr("unix", unixPath) + unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath) if errRet != nil { err = errRet return @@ -51,20 +49,27 @@ func NewUnixDomainSocketPlugin(params map[string]string) (p Plugin, err error) { return } -func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { +func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { + xl := xlog.FromContextSafe(ctx) localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) if err != nil { + xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err) + connInfo.Conn.Close() return } - if len(extraBufToLocal) > 0 { - localConn.Write(extraBufToLocal) + if connInfo.ProxyProtocolHeader != nil { + if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { + localConn.Close() + connInfo.Conn.Close() + return + } } - frpIo.Join(localConn, conn) + libio.Join(localConn, connInfo.Conn) } func (uds *UnixDomainSocketPlugin) Name() string { - return PluginUnixDomainSocket + return v1.PluginUnixDomainSocket } func (uds *UnixDomainSocketPlugin) Close() error { diff --git a/pkg/plugin/client/virtual_net.go b/pkg/plugin/client/virtual_net.go new file mode 100644 index 00000000000..53570035c55 --- /dev/null +++ b/pkg/plugin/client/virtual_net.go @@ -0,0 +1,92 @@ +// Copyright 2025 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" + "io" + "sync" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func init() { + Register(v1.PluginVirtualNet, NewVirtualNetPlugin) +} + +type VirtualNetPlugin struct { + pluginCtx PluginContext + opts *v1.VirtualNetPluginOptions + mu sync.Mutex + conns map[io.ReadWriteCloser]struct{} +} + +func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.VirtualNetPluginOptions) + + p := &VirtualNetPlugin{ + pluginCtx: pluginCtx, + opts: opts, + } + return p, nil +} + +func (p *VirtualNetPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) { + // Verify if virtual network controller is available + if p.pluginCtx.VnetController == nil { + return + } + + // Add the connection before starting the read loop to avoid race condition + // where RemoveConn might be called before the connection is added. + p.mu.Lock() + if p.conns == nil { + p.conns = make(map[io.ReadWriteCloser]struct{}) + } + p.conns[connInfo.Conn] = struct{}{} + p.mu.Unlock() + + // Register the connection with the controller and pass the cleanup function + p.pluginCtx.VnetController.StartServerConnReadLoop(ctx, connInfo.Conn, func() { + p.RemoveConn(connInfo.Conn) + }) +} + +func (p *VirtualNetPlugin) RemoveConn(conn io.ReadWriteCloser) { + p.mu.Lock() + defer p.mu.Unlock() + // Check if the map exists, as Close might have set it to nil concurrently + if p.conns != nil { + delete(p.conns, conn) + } +} + +func (p *VirtualNetPlugin) Name() string { + return v1.PluginVirtualNet +} + +func (p *VirtualNetPlugin) Close() error { + p.mu.Lock() + defer p.mu.Unlock() + + // Close any remaining connections + for conn := range p.conns { + _ = conn.Close() + } + p.conns = nil + return nil +} diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go index 5ca8c0635bb..b05b909e59a 100644 --- a/pkg/plugin/server/http.go +++ b/pkg/plugin/server/http.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "bytes" @@ -24,31 +24,26 @@ import ( "net/http" "net/url" "reflect" + "slices" "strings" -) -type HTTPPluginOptions struct { - Name string `ini:"name"` - Addr string `ini:"addr"` - Path string `ini:"path"` - Ops []string `ini:"ops"` - TLSVerify bool `ini:"tls_verify"` -} + v1 "github.com/fatedier/frp/pkg/config/v1" +) type httpPlugin struct { - options HTTPPluginOptions + options v1.HTTPPluginOptions url string client *http.Client } -func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin { - var url = fmt.Sprintf("%s%s", options.Addr, options.Path) +func NewHTTPPluginOptions(options v1.HTTPPluginOptions) Plugin { + url := fmt.Sprintf("%s%s", options.Addr, options.Path) var client *http.Client if strings.HasPrefix(url, "https://") { tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: options.TLSVerify == false}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: !options.TLSVerify}, } client = &http.Client{Transport: tr} } else { @@ -70,15 +65,10 @@ func (p *httpPlugin) Name() string { } func (p *httpPlugin) IsSupport(op string) bool { - for _, v := range p.options.Ops { - if v == op { - return true - } - } - return false + return slices.Contains(p.options.Ops, op) } -func (p *httpPlugin) Handle(ctx context.Context, op string, content interface{}) (*Response, interface{}, error) { +func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) { r := &Request{ Version: APIVersion, Op: op, @@ -120,8 +110,5 @@ func (p *httpPlugin) do(ctx context.Context, r *Request, res *Response) error { if err != nil { return err } - if err = json.Unmarshal(buf, res); err != nil { - return err - } - return nil + return json.Unmarshal(buf, res) } diff --git a/pkg/plugin/server/manager.go b/pkg/plugin/server/manager.go index 47d11d1c245..6f1061c69aa 100644 --- a/pkg/plugin/server/manager.go +++ b/pkg/plugin/server/manager.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "context" @@ -44,29 +44,36 @@ func NewManager() *Manager { } } -func (m *Manager) Register(p Plugin) { - if p.IsSupport(OpLogin) { - m.loginPlugins = append(m.loginPlugins, p) - } - if p.IsSupport(OpNewProxy) { - m.newProxyPlugins = append(m.newProxyPlugins, p) - } - if p.IsSupport(OpCloseProxy) { - m.closeProxyPlugins = append(m.closeProxyPlugins, p) - } - if p.IsSupport(OpPing) { - m.pingPlugins = append(m.pingPlugins, p) - } - if p.IsSupport(OpNewWorkConn) { - m.newWorkConnPlugins = append(m.newWorkConnPlugins, p) - } - if p.IsSupport(OpNewUserConn) { - m.newUserConnPlugins = append(m.newUserConnPlugins, p) +func newPluginRequestContext() (context.Context, *xlog.Logger) { + reqid, _ := util.RandID() + xl := xlog.New().AppendPrefix("reqid: " + reqid) + ctx := xlog.NewContext(context.Background(), xl) + return NewReqidContext(ctx, reqid), xl +} + +type pluginErrorLogMode bool + +const ( + // Warn is the zero value because it is the default for mutable plugin operations. + pluginErrorLogWarn pluginErrorLogMode = false + pluginErrorLogInfo pluginErrorLogMode = true +) + +func logPluginError(xl *xlog.Logger, p Plugin, op string, err error, mode pluginErrorLogMode) { + if mode == pluginErrorLogInfo { + xl.Infof("send %s request to plugin [%s] error: %v", op, p.Name(), err) + return } + xl.Warnf("send %s request to plugin [%s] error: %v", op, p.Name(), err) } -func (m *Manager) Login(content *LoginContent) (*LoginContent, error) { - if len(m.loginPlugins) == 0 { +func handleMutableContent[T any]( + plugins []Plugin, + op string, + content *T, + logMode pluginErrorLogMode, +) (*T, error) { + if len(plugins) == 0 { return content, nil } @@ -75,62 +82,56 @@ func (m *Manager) Login(content *LoginContent) (*LoginContent, error) { Reject: false, Unchange: true, } - retContent interface{} + retContent any err error ) - reqid, _ := util.RandID() - xl := xlog.New().AppendPrefix("reqid: " + reqid) - ctx := xlog.NewContext(context.Background(), xl) - ctx = NewReqidContext(ctx, reqid) + ctx, xl := newPluginRequestContext() - for _, p := range m.loginPlugins { - res, retContent, err = p.Handle(ctx, OpLogin, *content) + for _, p := range plugins { + res, retContent, err = p.Handle(ctx, op, *content) if err != nil { - xl.Warn("send Login request to plugin [%s] error: %v", p.Name(), err) - return nil, errors.New("send Login request to plugin error") + logPluginError(xl, p, op, err, logMode) + return nil, errors.New("send " + op + " request to plugin error") } if res.Reject { return nil, fmt.Errorf("%s", res.RejectReason) } if !res.Unchange { - content = retContent.(*LoginContent) + // Preserve the existing Plugin contract: changed content must be *T. + // Buggy Plugin implementations still panic here, by design. + content = retContent.(*T) } } return content, nil } -func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) { - if len(m.newProxyPlugins) == 0 { - return content, nil +func (m *Manager) Register(p Plugin) { + if p.IsSupport(OpLogin) { + m.loginPlugins = append(m.loginPlugins, p) } + if p.IsSupport(OpNewProxy) { + m.newProxyPlugins = append(m.newProxyPlugins, p) + } + if p.IsSupport(OpCloseProxy) { + m.closeProxyPlugins = append(m.closeProxyPlugins, p) + } + if p.IsSupport(OpPing) { + m.pingPlugins = append(m.pingPlugins, p) + } + if p.IsSupport(OpNewWorkConn) { + m.newWorkConnPlugins = append(m.newWorkConnPlugins, p) + } + if p.IsSupport(OpNewUserConn) { + m.newUserConnPlugins = append(m.newUserConnPlugins, p) + } +} - var ( - res = &Response{ - Reject: false, - Unchange: true, - } - retContent interface{} - err error - ) - reqid, _ := util.RandID() - xl := xlog.New().AppendPrefix("reqid: " + reqid) - ctx := xlog.NewContext(context.Background(), xl) - ctx = NewReqidContext(ctx, reqid) +func (m *Manager) Login(content *LoginContent) (*LoginContent, error) { + return handleMutableContent(m.loginPlugins, OpLogin, content, pluginErrorLogWarn) +} - for _, p := range m.newProxyPlugins { - res, retContent, err = p.Handle(ctx, OpNewProxy, *content) - if err != nil { - xl.Warn("send NewProxy request to plugin [%s] error: %v", p.Name(), err) - return nil, errors.New("send NewProxy request to plugin error") - } - if res.Reject { - return nil, fmt.Errorf("%s", res.RejectReason) - } - if !res.Unchange { - content = retContent.(*NewProxyContent) - } - } - return content, nil +func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) { + return handleMutableContent(m.newProxyPlugins, OpNewProxy, content, pluginErrorLogWarn) } func (m *Manager) CloseProxy(content *CloseProxyContent) error { @@ -139,124 +140,31 @@ func (m *Manager) CloseProxy(content *CloseProxyContent) error { } errs := make([]string, 0) - reqid, _ := util.RandID() - xl := xlog.New().AppendPrefix("reqid: " + reqid) - ctx := xlog.NewContext(context.Background(), xl) - ctx = NewReqidContext(ctx, reqid) + ctx, xl := newPluginRequestContext() for _, p := range m.closeProxyPlugins { _, _, err := p.Handle(ctx, OpCloseProxy, *content) if err != nil { - xl.Warn("send CloseProxy request to plugin [%s] error: %v", p.Name(), err) + xl.Warnf("send CloseProxy request to plugin [%s] error: %v", p.Name(), err) errs = append(errs, fmt.Sprintf("[%s]: %v", p.Name(), err)) } } if len(errs) > 0 { return fmt.Errorf("send CloseProxy request to plugin errors: %s", strings.Join(errs, "; ")) - } else { - return nil } + return nil } func (m *Manager) Ping(content *PingContent) (*PingContent, error) { - if len(m.pingPlugins) == 0 { - return content, nil - } - - var ( - res = &Response{ - Reject: false, - Unchange: true, - } - retContent interface{} - err error - ) - reqid, _ := util.RandID() - xl := xlog.New().AppendPrefix("reqid: " + reqid) - ctx := xlog.NewContext(context.Background(), xl) - ctx = NewReqidContext(ctx, reqid) - - for _, p := range m.pingPlugins { - res, retContent, err = p.Handle(ctx, OpPing, *content) - if err != nil { - xl.Warn("send Ping request to plugin [%s] error: %v", p.Name(), err) - return nil, errors.New("send Ping request to plugin error") - } - if res.Reject { - return nil, fmt.Errorf("%s", res.RejectReason) - } - if !res.Unchange { - content = retContent.(*PingContent) - } - } - return content, nil + return handleMutableContent(m.pingPlugins, OpPing, content, pluginErrorLogWarn) } func (m *Manager) NewWorkConn(content *NewWorkConnContent) (*NewWorkConnContent, error) { - if len(m.newWorkConnPlugins) == 0 { - return content, nil - } - - var ( - res = &Response{ - Reject: false, - Unchange: true, - } - retContent interface{} - err error - ) - reqid, _ := util.RandID() - xl := xlog.New().AppendPrefix("reqid: " + reqid) - ctx := xlog.NewContext(context.Background(), xl) - ctx = NewReqidContext(ctx, reqid) - - for _, p := range m.newWorkConnPlugins { - res, retContent, err = p.Handle(ctx, OpPing, *content) - if err != nil { - xl.Warn("send NewWorkConn request to plugin [%s] error: %v", p.Name(), err) - return nil, errors.New("send NewWorkConn request to plugin error") - } - if res.Reject { - return nil, fmt.Errorf("%s", res.RejectReason) - } - if !res.Unchange { - content = retContent.(*NewWorkConnContent) - } - } - return content, nil + return handleMutableContent(m.newWorkConnPlugins, OpNewWorkConn, content, pluginErrorLogWarn) } func (m *Manager) NewUserConn(content *NewUserConnContent) (*NewUserConnContent, error) { - if len(m.newUserConnPlugins) == 0 { - return content, nil - } - - var ( - res = &Response{ - Reject: false, - Unchange: true, - } - retContent interface{} - err error - ) - reqid, _ := util.RandID() - xl := xlog.New().AppendPrefix("reqid: " + reqid) - ctx := xlog.NewContext(context.Background(), xl) - ctx = NewReqidContext(ctx, reqid) - - for _, p := range m.newUserConnPlugins { - res, retContent, err = p.Handle(ctx, OpNewUserConn, *content) - if err != nil { - xl.Info("send NewUserConn request to plugin [%s] error: %v", p.Name(), err) - return nil, errors.New("send NewUserConn request to plugin error") - } - if res.Reject { - return nil, fmt.Errorf("%s", res.RejectReason) - } - if !res.Unchange { - content = retContent.(*NewUserConnContent) - } - } - return content, nil + // Preserve the pre-refactor log level for NewUserConn plugin errors. + return handleMutableContent(m.newUserConnPlugins, OpNewUserConn, content, pluginErrorLogInfo) } diff --git a/pkg/plugin/server/manager_test.go b/pkg/plugin/server/manager_test.go new file mode 100644 index 00000000000..92391d4ba04 --- /dev/null +++ b/pkg/plugin/server/manager_test.go @@ -0,0 +1,336 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// 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 server + +import ( + "bytes" + "context" + "errors" + "strings" + "sync" + "testing" + "time" + + goliblog "github.com/fatedier/golib/log" + + "github.com/fatedier/frp/pkg/msg" + frplog "github.com/fatedier/frp/pkg/util/log" +) + +type testPlugin struct { + name string + ops map[string]bool + handler func(context.Context, string, any) (*Response, any, error) +} + +// Log-capturing subtests serialize global logger swaps; do not use t.Parallel. +var logCaptureMu sync.Mutex + +type logCapture struct { + bytes.Buffer + levels []goliblog.Level +} + +func (p testPlugin) Name() string { + return p.name +} + +func (p testPlugin) IsSupport(op string) bool { + return p.ops[op] +} + +func (p testPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) { + return p.handler(ctx, op, content) +} + +func (w *logCapture) WriteLog(p []byte, level goliblog.Level, _ time.Time) (int, error) { + w.levels = append(w.levels, level) + return w.Write(p) +} + +func captureLogOutput(t *testing.T) *logCapture { + t.Helper() + + logCaptureMu.Lock() + logOutput := &logCapture{} + oldLogger := frplog.Logger + frplog.Logger = goliblog.New( + goliblog.WithOutput(logOutput), + goliblog.WithLevel(goliblog.TraceLevel), + goliblog.WithCaller(false), + ) + t.Cleanup(func() { + frplog.Logger = oldLogger + logCaptureMu.Unlock() + }) + return logOutput +} + +var mutablePluginOps = []struct { + name string + op string +}{ + {name: "login", op: OpLogin}, + {name: "new proxy", op: OpNewProxy}, + {name: "ping", op: OpPing}, + {name: "new work conn", op: OpNewWorkConn}, + {name: "new user conn", op: OpNewUserConn}, +} + +func callMutableWithUser(m *Manager, op string, user string) (string, error) { + switch op { + case OpLogin: + got, err := m.Login(&LoginContent{Login: msg.Login{User: user}}) + if got == nil { + return "", err + } + return got.User, err + case OpNewProxy: + got, err := m.NewProxy(&NewProxyContent{User: UserInfo{User: user}}) + if got == nil { + return "", err + } + return got.User.User, err + case OpPing: + got, err := m.Ping(&PingContent{User: UserInfo{User: user}}) + if got == nil { + return "", err + } + return got.User.User, err + case OpNewWorkConn: + got, err := m.NewWorkConn(&NewWorkConnContent{User: UserInfo{User: user}}) + if got == nil { + return "", err + } + return got.User.User, err + case OpNewUserConn: + got, err := m.NewUserConn(&NewUserConnContent{User: UserInfo{User: user}}) + if got == nil { + return "", err + } + return got.User.User, err + default: + panic("unsupported mutable op: " + op) + } +} + +func mutableUser(t *testing.T, op string, content any) string { + t.Helper() + + switch op { + case OpLogin: + return content.(LoginContent).User + case OpNewProxy: + return content.(NewProxyContent).User.User + case OpPing: + return content.(PingContent).User.User + case OpNewWorkConn: + return content.(NewWorkConnContent).User.User + case OpNewUserConn: + return content.(NewUserConnContent).User.User + default: + t.Fatalf("unsupported mutable op: %s", op) + return "" + } +} + +func mutateMutableContent(t *testing.T, op string, content any, user string) any { + t.Helper() + + switch op { + case OpLogin: + got := content.(LoginContent) + got.User = user + return &got + case OpNewProxy: + got := content.(NewProxyContent) + got.User.User = user + return &got + case OpPing: + got := content.(PingContent) + got.User.User = user + return &got + case OpNewWorkConn: + got := content.(NewWorkConnContent) + got.User.User = user + return &got + case OpNewUserConn: + got := content.(NewUserConnContent) + got.User.User = user + return &got + default: + t.Fatalf("unsupported mutable op: %s", op) + return nil + } +} + +func TestManagerMutableContentAcrossOps(t *testing.T) { + for _, tt := range mutablePluginOps { + t.Run(tt.name, func(t *testing.T) { + m := NewManager() + m.Register(testPlugin{ + name: "mutate", + ops: map[string]bool{tt.op: true}, + handler: func(ctx context.Context, op string, content any) (*Response, any, error) { + if op != tt.op { + t.Fatalf("unexpected op: %s", op) + } + if GetReqidFromContext(ctx) == "" { + t.Fatal("expected request id in context") + } + if got := mutableUser(t, tt.op, content); got != "initial" { + t.Fatalf("expected initial user, got %q", got) + } + return &Response{Unchange: false}, mutateMutableContent(t, tt.op, content, "mutated"), nil + }, + }) + m.Register(testPlugin{ + name: "observe", + ops: map[string]bool{tt.op: true}, + handler: func(ctx context.Context, op string, content any) (*Response, any, error) { + if op != tt.op { + t.Fatalf("unexpected op: %s", op) + } + if GetReqidFromContext(ctx) == "" { + t.Fatal("expected request id in context") + } + if got := mutableUser(t, tt.op, content); got != "mutated" { + t.Fatalf("expected mutated user, got %q", got) + } + return &Response{Unchange: true}, mutateMutableContent(t, tt.op, content, "ignored"), nil + }, + }) + + got, err := callMutableWithUser(m, tt.op, "initial") + if err != nil { + t.Fatalf("mutable op failed: %v", err) + } + if got != "mutated" { + t.Fatalf("expected mutated user, got %q", got) + } + }) + } +} + +func TestManagerMutableContentRejectStopsChain(t *testing.T) { + m := NewManager() + + var called bool + m.Register(testPlugin{ + name: "reject", + ops: map[string]bool{OpPing: true}, + handler: func(context.Context, string, any) (*Response, any, error) { + return &Response{Reject: true, RejectReason: "blocked"}, nil, nil + }, + }) + m.Register(testPlugin{ + name: "unused", + ops: map[string]bool{OpPing: true}, + handler: func(context.Context, string, any) (*Response, any, error) { + called = true + return &Response{Unchange: true}, nil, nil + }, + }) + + got, err := m.Ping(&PingContent{}) + if err == nil { + t.Fatal("expected reject error") + } + if got != nil { + t.Fatalf("expected no returned content, got %#v", got) + } + if err.Error() != "blocked" { + t.Fatalf("unexpected error: %v", err) + } + if called { + t.Fatal("expected plugin chain to stop after reject") + } +} + +func TestManagerMutableContentPluginErrorLogLevel(t *testing.T) { + tests := []struct { + name string + op string + level goliblog.Level + }{ + {name: "default warning", op: OpLogin, level: goliblog.WarnLevel}, + {name: "new user conn info", op: OpNewUserConn, level: goliblog.InfoLevel}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logOutput := captureLogOutput(t) + m := NewManager() + m.Register(testPlugin{ + name: "error", + ops: map[string]bool{tt.op: true}, + handler: func(context.Context, string, any) (*Response, any, error) { + return nil, nil, errors.New("boom") + }, + }) + + _, err := callMutableWithUser(m, tt.op, "initial") + if err == nil { + t.Fatal("expected plugin error") + } + if want := "send " + tt.op + " request to plugin error"; err.Error() != want { + t.Fatalf("unexpected error: %v", err) + } + if len(logOutput.levels) != 1 || logOutput.levels[0] != tt.level { + t.Fatalf("expected log level %v, got %v in %q", tt.level, logOutput.levels, logOutput.String()) + } + }) + } +} + +func TestManagerCloseProxyAggregatesErrors(t *testing.T) { + logOutput := captureLogOutput(t) + m := NewManager() + + for _, name := range []string{"first", "second"} { + m.Register(testPlugin{ + name: name, + ops: map[string]bool{OpCloseProxy: true}, + handler: func(ctx context.Context, op string, content any) (*Response, any, error) { + if GetReqidFromContext(ctx) == "" { + t.Fatal("expected request id in context") + } + if op != OpCloseProxy { + t.Fatalf("unexpected op: %s", op) + } + return nil, nil, errors.New(name + " error") + }, + }) + } + + err := m.CloseProxy(&CloseProxyContent{}) + if err == nil { + t.Fatal("expected close proxy error") + } + if !strings.HasPrefix(err.Error(), "send CloseProxy request to plugin errors: ") { + t.Fatalf("unexpected close proxy error prefix: %v", err) + } + if !strings.Contains(err.Error(), "[first]: first error") || !strings.Contains(err.Error(), "[second]: second error") { + t.Fatalf("missing aggregated errors: %v", err) + } + if len(logOutput.levels) != 2 { + t.Fatalf("expected two warning logs, got %v", logOutput.levels) + } + for _, level := range logOutput.levels { + if level != goliblog.WarnLevel { + t.Fatalf("expected warning log level, got %v", logOutput.levels) + } + } +} diff --git a/pkg/plugin/server/plugin.go b/pkg/plugin/server/plugin.go index 0d34de5467d..3d3c8cfdd65 100644 --- a/pkg/plugin/server/plugin.go +++ b/pkg/plugin/server/plugin.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "context" @@ -32,5 +32,5 @@ const ( type Plugin interface { Name() string IsSupport(op string) bool - Handle(ctx context.Context, op string, content interface{}) (res *Response, retContent interface{}, err error) + Handle(ctx context.Context, op string, content any) (res *Response, retContent any, err error) } diff --git a/pkg/plugin/server/tracer.go b/pkg/plugin/server/tracer.go index 2f4f2ccc25d..5b6ede182ec 100644 --- a/pkg/plugin/server/tracer.go +++ b/pkg/plugin/server/tracer.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "context" diff --git a/pkg/plugin/server/types.go b/pkg/plugin/server/types.go index d7d98cb6535..4a5b7527185 100644 --- a/pkg/plugin/server/types.go +++ b/pkg/plugin/server/types.go @@ -12,23 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package server import ( "github.com/fatedier/frp/pkg/msg" ) type Request struct { - Version string `json:"version"` - Op string `json:"op"` - Content interface{} `json:"content"` + Version string `json:"version"` + Op string `json:"op"` + Content any `json:"content"` } type Response struct { - Reject bool `json:"reject"` - RejectReason string `json:"reject_reason"` - Unchange bool `json:"unchange"` - Content interface{} `json:"content"` + Reject bool `json:"reject"` + RejectReason string `json:"reject_reason"` + Unchange bool `json:"unchange"` + Content any `json:"content"` } type LoginContent struct { diff --git a/pkg/plugin/visitor/plugin.go b/pkg/plugin/visitor/plugin.go new file mode 100644 index 00000000000..27eecc82fd1 --- /dev/null +++ b/pkg/plugin/visitor/plugin.go @@ -0,0 +1,67 @@ +// Copyright 2025 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 visitor + +import ( + "context" + "fmt" + "net" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/vnet" +) + +// PluginContext provides the necessary context and callbacks for visitor plugins. +type PluginContext struct { + // Name is the unique identifier for this visitor, used for logging and routing. + Name string + + // Ctx manages the plugin's lifecycle and carries the logger for structured logging. + Ctx context.Context + + // VnetController manages TUN device routing. May be nil if virtual networking is disabled. + VnetController *vnet.Controller + + // SendConnToVisitor sends a connection to the visitor's internal processing queue. + // Does not return error; failures are handled by closing the connection. + SendConnToVisitor func(net.Conn) +} + +// Creators is used for create plugins to handle connections. +var creators = make(map[string]CreatorFn) + +type CreatorFn func(pluginCtx PluginContext, options v1.VisitorPluginOptions) (Plugin, error) + +func Register(name string, fn CreatorFn) { + if _, exist := creators[name]; exist { + panic(fmt.Sprintf("plugin [%s] is already registered", name)) + } + creators[name] = fn +} + +func Create(pluginName string, pluginCtx PluginContext, options v1.VisitorPluginOptions) (p Plugin, err error) { + if fn, ok := creators[pluginName]; ok { + p, err = fn(pluginCtx, options) + } else { + err = fmt.Errorf("plugin [%s] is not registered", pluginName) + } + return +} + +type Plugin interface { + Name() string + Start() + Close() error +} diff --git a/pkg/plugin/visitor/virtual_net.go b/pkg/plugin/visitor/virtual_net.go new file mode 100644 index 00000000000..fa8c700e460 --- /dev/null +++ b/pkg/plugin/visitor/virtual_net.go @@ -0,0 +1,217 @@ +// Copyright 2025 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 visitor + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "time" + + v1 "github.com/fatedier/frp/pkg/config/v1" + netutil "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/xlog" +) + +func init() { + Register(v1.VisitorPluginVirtualNet, NewVirtualNetPlugin) +} + +type VirtualNetPlugin struct { + pluginCtx PluginContext + + routes []net.IPNet + + mu sync.Mutex + controllerConn net.Conn + closeSignal chan struct{} + + consecutiveErrors int // Tracks consecutive connection errors for exponential backoff + + ctx context.Context + cancel context.CancelFunc +} + +func NewVirtualNetPlugin(pluginCtx PluginContext, options v1.VisitorPluginOptions) (Plugin, error) { + opts := options.(*v1.VirtualNetVisitorPluginOptions) + + p := &VirtualNetPlugin{ + pluginCtx: pluginCtx, + routes: make([]net.IPNet, 0), + } + + p.ctx, p.cancel = context.WithCancel(pluginCtx.Ctx) + + if opts.DestinationIP == "" { + return nil, errors.New("destinationIP is required") + } + + // Parse DestinationIP and create a host route. + ip := net.ParseIP(opts.DestinationIP) + if ip == nil { + return nil, fmt.Errorf("invalid destination IP address [%s]", opts.DestinationIP) + } + + var mask net.IPMask + if ip.To4() != nil { + mask = net.CIDRMask(32, 32) // /32 for IPv4 + } else { + mask = net.CIDRMask(128, 128) // /128 for IPv6 + } + p.routes = append(p.routes, net.IPNet{IP: ip, Mask: mask}) + + return p, nil +} + +func (p *VirtualNetPlugin) Name() string { + return v1.VisitorPluginVirtualNet +} + +func (p *VirtualNetPlugin) Start() { + xl := xlog.FromContextSafe(p.pluginCtx.Ctx) + if p.pluginCtx.VnetController == nil { + return + } + + routeStr := "unknown" + if len(p.routes) > 0 { + routeStr = p.routes[0].String() + } + xl.Infof("starting VirtualNetPlugin for visitor [%s], attempting to register routes for %s", p.pluginCtx.Name, routeStr) + + go p.run() +} + +func (p *VirtualNetPlugin) run() { + xl := xlog.FromContextSafe(p.ctx) + + for { + currentCloseSignal := make(chan struct{}) + + p.mu.Lock() + p.closeSignal = currentCloseSignal + p.mu.Unlock() + + select { + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin run loop for visitor [%s] stopping (context cancelled before pipe creation).", p.pluginCtx.Name) + p.cleanupControllerConn(xl) + return + default: + } + + controllerConn, pluginConn := net.Pipe() + + p.mu.Lock() + p.controllerConn = controllerConn + p.mu.Unlock() + + // Wrap with CloseNotifyConn which supports both close notification and error recording + var closeErr error + pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) { + closeErr = err + close(currentCloseSignal) // Signal the run loop on close. + }) + + xl.Infof("attempting to register client route for visitor [%s]", p.pluginCtx.Name) + p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn) + xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name) + + // Pass the CloseNotifyConn to the visitor for handling. + // The visitor can call CloseWithError to record the failure reason. + p.pluginCtx.SendConnToVisitor(pluginNotifyConn) + + // Wait for context cancellation or connection close. + select { + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin run loop stopping for visitor [%s] (context cancelled while waiting).", p.pluginCtx.Name) + p.cleanupControllerConn(xl) + return + case <-currentCloseSignal: + // Determine reconnect delay based on error with exponential backoff + var reconnectDelay time.Duration + if closeErr != nil { + p.consecutiveErrors++ + xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v", + p.pluginCtx.Name, p.consecutiveErrors, closeErr) + + // Exponential backoff: 60s, 120s, 240s, 300s (capped) + baseDelay := 60 * time.Second + reconnectDelay = min(baseDelay*time.Duration(1< 0 { + xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)", + p.pluginCtx.Name, p.consecutiveErrors) + p.consecutiveErrors = 0 + } else { + xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name) + } + reconnectDelay = 10 * time.Second + } + + // The visitor closed the plugin side. Close the controller side. + p.cleanupControllerConn(xl) + + xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name) + select { + case <-time.After(reconnectDelay): + case <-p.ctx.Done(): + xl.Infof("VirtualNetPlugin reconnection delay interrupted for visitor [%s]", p.pluginCtx.Name) + return + } + } + + xl.Infof("re-establishing virtual connection for visitor [%s]...", p.pluginCtx.Name) + } +} + +// cleanupControllerConn closes the current controllerConn (if it exists) under lock. +func (p *VirtualNetPlugin) cleanupControllerConn(xl *xlog.Logger) { + p.mu.Lock() + defer p.mu.Unlock() + if p.controllerConn != nil { + xl.Debugf("cleaning up controllerConn for visitor [%s]", p.pluginCtx.Name) + p.controllerConn.Close() + p.controllerConn = nil + } + p.closeSignal = nil +} + +// Close initiates the plugin shutdown. +func (p *VirtualNetPlugin) Close() error { + xl := xlog.FromContextSafe(p.pluginCtx.Ctx) + xl.Infof("closing VirtualNetPlugin for visitor [%s]", p.pluginCtx.Name) + + // Signal the run loop goroutine to stop. + p.cancel() + + // Unregister the route from the controller. + if p.pluginCtx.VnetController != nil { + p.pluginCtx.VnetController.UnregisterClientRoute(p.pluginCtx.Name) + xl.Infof("unregistered client route for visitor [%s]", p.pluginCtx.Name) + } + + // Explicitly close the controller side of the pipe. + // This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end. + p.cleanupControllerConn(xl) + xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name) + + return nil +} diff --git a/pkg/policy/featuregate/feature_gate.go b/pkg/policy/featuregate/feature_gate.go new file mode 100644 index 00000000000..f27aecf3fd3 --- /dev/null +++ b/pkg/policy/featuregate/feature_gate.go @@ -0,0 +1,209 @@ +// Copyright 2025 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 featuregate + +import ( + "fmt" + "maps" + "sort" + "strings" + "sync" + "sync/atomic" +) + +// Feature represents a feature gate name +type Feature string + +// FeatureStage represents the maturity level of a feature +type FeatureStage string + +const ( + // Alpha means the feature is experimental and disabled by default + Alpha FeatureStage = "ALPHA" + // Beta means the feature is more stable but still might change and is disabled by default + Beta FeatureStage = "BETA" + // GA means the feature is generally available and enabled by default + GA FeatureStage = "" +) + +// FeatureSpec describes a feature and its properties +type FeatureSpec struct { + // Default is the default enablement state for the feature + Default bool + // LockToDefault indicates the feature cannot be changed from its default + LockToDefault bool + // Stage indicates the maturity level of the feature + Stage FeatureStage +} + +// Define all available features here +var ( + VirtualNet = Feature("VirtualNet") +) + +// defaultFeatures defines default features with their specifications +var defaultFeatures = map[Feature]FeatureSpec{ + // Actual features + VirtualNet: {Default: false, Stage: Alpha}, +} + +// FeatureGate indicates whether a given feature is enabled or not +type FeatureGate interface { + // Enabled returns true if the key is enabled + Enabled(key Feature) bool + // KnownFeatures returns a slice of strings describing the known features + KnownFeatures() []string +} + +// MutableFeatureGate allows for dynamic feature gate configuration +type MutableFeatureGate interface { + FeatureGate + + // SetFromMap sets feature gate values from a map[string]bool + SetFromMap(m map[string]bool) error + // Add adds features to the feature gate + Add(features map[Feature]FeatureSpec) error + // String returns a string representing the feature gate configuration + String() string +} + +// featureGate implements the FeatureGate and MutableFeatureGate interfaces +type featureGate struct { + // lock guards writes to known, enabled, and reads/writes of closed + lock sync.Mutex + // known holds a map[Feature]FeatureSpec + known atomic.Value + // enabled holds a map[Feature]bool + enabled atomic.Value + // closed is set to true once the feature gates are considered immutable + closed bool +} + +// NewFeatureGate creates a new feature gate with the default features +func NewFeatureGate() MutableFeatureGate { + known := maps.Clone(defaultFeatures) + + f := &featureGate{} + f.known.Store(known) + f.enabled.Store(map[Feature]bool{}) + return f +} + +// SetFromMap sets feature gate values from a map[string]bool +func (f *featureGate) SetFromMap(m map[string]bool) error { + f.lock.Lock() + defer f.lock.Unlock() + + // Copy existing state + known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) + enabled := maps.Clone(f.enabled.Load().(map[Feature]bool)) + + // Apply the new settings + for k, v := range m { + k := Feature(k) + featureSpec, ok := known[k] + if !ok { + return fmt.Errorf("unrecognized feature gate: %s", k) + } + if featureSpec.LockToDefault && featureSpec.Default != v { + return fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default) + } + enabled[k] = v + } + + // Persist the changes + f.known.Store(known) + f.enabled.Store(enabled) + return nil +} + +// Add adds features to the feature gate +func (f *featureGate) Add(features map[Feature]FeatureSpec) error { + f.lock.Lock() + defer f.lock.Unlock() + + if f.closed { + return fmt.Errorf("cannot add feature gates after the feature gate is closed") + } + + // Copy existing state + known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec)) + + // Add new features + for name, spec := range features { + if existingSpec, found := known[name]; found { + if existingSpec == spec { + continue + } + return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec) + } + known[name] = spec + } + + // Persist changes + f.known.Store(known) + + return nil +} + +// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..." +func (f *featureGate) String() string { + enabled := f.enabled.Load().(map[Feature]bool) + pairs := make([]string, 0, len(enabled)) + for k, v := range enabled { + pairs = append(pairs, fmt.Sprintf("%s=%t", k, v)) + } + sort.Strings(pairs) + return strings.Join(pairs, ",") +} + +// Enabled returns true if the key is enabled +func (f *featureGate) Enabled(key Feature) bool { + if v, ok := f.enabled.Load().(map[Feature]bool)[key]; ok { + return v + } + if v, ok := f.known.Load().(map[Feature]FeatureSpec)[key]; ok { + return v.Default + } + return false +} + +// KnownFeatures returns a slice of strings describing the FeatureGate's known features +// GA features are hidden from the list +func (f *featureGate) KnownFeatures() []string { + knownFeatures := f.known.Load().(map[Feature]FeatureSpec) + known := make([]string, 0, len(knownFeatures)) + for k, v := range knownFeatures { + if v.Stage == GA { + continue + } + known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v.Stage, v.Default)) + } + sort.Strings(known) + return known +} + +// Default feature gates instance +var DefaultFeatureGates = NewFeatureGate() + +// Enabled checks if a feature is enabled in the default feature gates +func Enabled(name Feature) bool { + return DefaultFeatureGates.Enabled(name) +} + +// SetFromMap sets feature gate values from a map in the default feature gates +func SetFromMap(featureMap map[string]bool) error { + return DefaultFeatureGates.SetFromMap(featureMap) +} diff --git a/pkg/policy/security/unsafe.go b/pkg/policy/security/unsafe.go new file mode 100644 index 00000000000..b3f84a85278 --- /dev/null +++ b/pkg/policy/security/unsafe.go @@ -0,0 +1,34 @@ +package security + +const ( + TokenSourceExec = "TokenSourceExec" +) + +var ( + ClientUnsafeFeatures = []string{ + TokenSourceExec, + } + + ServerUnsafeFeatures = []string{ + TokenSourceExec, + } +) + +type UnsafeFeatures struct { + features map[string]bool +} + +func NewUnsafeFeatures(allowed []string) *UnsafeFeatures { + features := make(map[string]bool) + for _, f := range allowed { + features[f] = true + } + return &UnsafeFeatures{features: features} +} + +func (u *UnsafeFeatures) IsEnabled(feature string) bool { + if u == nil { + return false + } + return u.features[feature] +} diff --git a/pkg/proto/udp/udp.go b/pkg/proto/udp/udp.go index 97f6ea4098f..ec3bd94f874 100644 --- a/pkg/proto/udp/udp.go +++ b/pkg/proto/udp/udp.go @@ -15,28 +15,29 @@ package udp import ( - "encoding/base64" "net" "sync" "time" - "github.com/fatedier/frp/pkg/msg" - "github.com/fatedier/golib/errors" "github.com/fatedier/golib/pool" + + "github.com/fatedier/frp/pkg/msg" + netpkg "github.com/fatedier/frp/pkg/util/net" ) func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket { + content := make([]byte, len(buf)) + copy(content, buf) return &msg.UDPPacket{ - Content: base64.StdEncoding.EncodeToString(buf), + Content: content, LocalAddr: laddr, RemoteAddr: raddr, } } func GetContent(m *msg.UDPPacket) (buf []byte, err error) { - buf, err = base64.StdEncoding.DecodeString(m.Content) - return + return m.Content, nil } func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) { @@ -47,7 +48,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh if err != nil { continue } - udpConn.WriteToUDP(buf, udpMsg.RemoteAddr) + _, _ = udpConn.WriteToUDP(buf, udpMsg.RemoteAddr) } }() @@ -59,20 +60,22 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh if err != nil { return } - // buf[:n] will be encoded to string, so the bytes can be reused + // 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 } } } -func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int) { - var ( - mu sync.RWMutex - ) +func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- msg.Message, bufSize int, proxyProtocolVersion string) { + var mu sync.RWMutex udpConnMap := make(map[string]*net.UDPConn) // read from dstAddr and write to sendCh @@ -86,8 +89,9 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- }() buf := pool.GetBuf(bufSize) + defer pool.PutBuf(buf) for { - udpConn.SetReadDeadline(time.Now().Add(30 * time.Second)) + _ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second)) n, _, err := udpConn.ReadFromUDP(buf) if err != nil { return @@ -112,6 +116,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- if err != nil { continue } + mu.Lock() udpConn, ok := udpConnMap[udpMsg.RemoteAddr.String()] if !ok { @@ -124,9 +129,23 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<- } mu.Unlock() + // Add proxy protocol header if configured (only for the first packet of a new connection) + if !ok && proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil { + ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion) + if err == nil { + // Prepend proxy protocol header to the UDP payload + finalBuf := make([]byte, len(ppBuf)+len(buf)) + copy(finalBuf, ppBuf) + copy(finalBuf[len(ppBuf):], buf) + buf = finalBuf + } + } + _, err = udpConn.Write(buf) if err != nil { udpConn.Close() + } else { + _ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second)) } if !ok { diff --git a/pkg/proto/udp/udp_test.go b/pkg/proto/udp/udp_test.go index 0e61d9e65a5..b959a26a791 100644 --- a/pkg/proto/udp/udp_test.go +++ b/pkg/proto/udp/udp_test.go @@ -1,18 +1,52 @@ package udp import ( + "net" "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/msg" ) func TestUdpPacket(t *testing.T) { - assert := assert.New(t) + require := require.New(t) buf := []byte("hello world") udpMsg := NewUDPPacket(buf, nil, nil) newBuf, err := GetContent(udpMsg) - assert.NoError(err) - assert.EqualValues(buf, newBuf) + 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") + } } diff --git a/pkg/proto/wire/crypto.go b/pkg/proto/wire/crypto.go new file mode 100644 index 00000000000..38916a28b6c --- /dev/null +++ b/pkg/proto/wire/crypto.go @@ -0,0 +1,214 @@ +// 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 wire + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "encoding/json" + "fmt" + "hash" + "runtime" + + "golang.org/x/sys/cpu" +) + +const ( + AEADAlgorithmAES256GCM = "aes-256-gcm" + AEADAlgorithmXChaCha20Poly1305 = "xchacha20-poly1305" + + CryptoRandomSize = 32 + + cryptoTranscriptLabel = "frp wire v2 crypto transcript" +) + +var supportedAEADAlgorithms = []string{ + AEADAlgorithmAES256GCM, + AEADAlgorithmXChaCha20Poly1305, +} + +type CryptoContext struct { + Algorithm string + TranscriptHash []byte +} + +func NewClientHello(bootstrap BootstrapInfo) (ClientHello, error) { + clientRandom, err := newCryptoRandom() + if err != nil { + return ClientHello{}, err + } + return clientHelloWithCryptoRandom(bootstrap, clientRandom), nil +} + +func NewServerHello(clientHello ClientHello) (ServerHello, error) { + if err := ValidateClientHello(clientHello); err != nil { + return ServerHello{}, err + } + algorithm, ok := SelectAEADAlgorithm(clientHello.Capabilities.Crypto.Algorithms) + if !ok { + return ServerHello{}, fmt.Errorf("no supported crypto algorithm") + } + serverRandom, err := newCryptoRandom() + if err != nil { + return ServerHello{}, err + } + return ServerHello{ + Selected: ServerSelection{ + Message: MessageSelection{ + Codec: MessageCodecJSON, + UDPPacketCodec: selectUDPPacketCodec(clientHello.Capabilities.Message.UDPPacketCodecs), + }, + Crypto: CryptoSelection{ + Algorithm: algorithm, + ServerRandom: serverRandom, + }, + }, + }, nil +} + +func ValidateCryptoCapabilities(c CryptoCapabilities) error { + if len(c.ClientRandom) != CryptoRandomSize { + return fmt.Errorf("invalid crypto client random length %d, want %d", len(c.ClientRandom), CryptoRandomSize) + } + if _, ok := SelectAEADAlgorithm(c.Algorithms); !ok { + return fmt.Errorf("no supported crypto algorithm") + } + return nil +} + +func ValidateServerHelloForClient(clientHello ClientHello, serverHello ServerHello) error { + 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) + } + if !Supports(clientHello.Capabilities.Crypto.Algorithms, cryptoSelection.Algorithm) { + return fmt.Errorf("selected crypto algorithm was not advertised by client: %s", cryptoSelection.Algorithm) + } + if len(cryptoSelection.ServerRandom) != CryptoRandomSize { + return fmt.Errorf("invalid crypto server random length %d, want %d", len(cryptoSelection.ServerRandom), CryptoRandomSize) + } + 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, + TranscriptHash: HashCryptoTranscript(clientHelloPayload, serverHelloPayload), + } +} + +func NewClientCryptoContext(clientHelloPayload, serverHelloPayload []byte) (*CryptoContext, error) { + var clientHello ClientHello + if err := json.Unmarshal(clientHelloPayload, &clientHello); err != nil { + return nil, fmt.Errorf("decode ClientHello transcript: %w", err) + } + var serverHello ServerHello + if err := json.Unmarshal(serverHelloPayload, &serverHello); err != nil { + return nil, fmt.Errorf("decode ServerHello transcript: %w", err) + } + if err := ValidateServerHelloForClient(clientHello, serverHello); err != nil { + return nil, err + } + + return NewCryptoContext(serverHello.Selected.Crypto.Algorithm, clientHelloPayload, serverHelloPayload), nil +} + +func HashCryptoTranscript(clientHelloPayload, serverHelloPayload []byte) []byte { + h := sha256.New() + _, _ = h.Write([]byte(cryptoTranscriptLabel)) + writeCryptoTranscriptPart(h, "client hello", clientHelloPayload) + writeCryptoTranscriptPart(h, "server hello", serverHelloPayload) + return h.Sum(nil) +} + +func writeCryptoTranscriptPart(h hash.Hash, label string, payload []byte) { + var length [8]byte + binary.BigEndian.PutUint64(length[:], uint64(len(payload))) + _, _ = h.Write([]byte{0}) + _, _ = h.Write([]byte(label)) + _, _ = h.Write([]byte{0}) + _, _ = h.Write(length[:]) + _, _ = h.Write(payload) +} + +func PreferredAEADAlgorithms() []string { + if hasFastAESGCM() { + return []string{AEADAlgorithmAES256GCM, AEADAlgorithmXChaCha20Poly1305} + } + return []string{AEADAlgorithmXChaCha20Poly1305, AEADAlgorithmAES256GCM} +} + +func SelectAEADAlgorithm(clientAlgorithms []string) (string, bool) { + for _, algorithm := range clientAlgorithms { + if IsSupportedAEADAlgorithm(algorithm) { + return algorithm, true + } + } + return "", false +} + +func IsSupportedAEADAlgorithm(algorithm string) bool { + return Supports(supportedAEADAlgorithms, algorithm) +} + +func newCryptoRandom() ([]byte, error) { + b := make([]byte, CryptoRandomSize) + if _, err := rand.Read(b); err != nil { + return nil, fmt.Errorf("generate crypto random: %w", err) + } + return b, nil +} + +func hasFastAESGCM() bool { + switch runtime.GOARCH { + case "amd64": + return cpu.X86.HasAES && + cpu.X86.HasPCLMULQDQ && + cpu.X86.HasSSE41 && + cpu.X86.HasSSSE3 + case "arm64": + return cpu.ARM64.HasAES && cpu.ARM64.HasPMULL + case "s390x": + return cpu.S390X.HasAES && + cpu.S390X.HasAESCTR && + cpu.S390X.HasGHASH + case "ppc64", "ppc64le": + // Go's ppc64/ppc64le port targets POWER8+, which has AES instructions; + // x/sys/cpu does not expose a PPC64 AES feature flag. + return true + default: + return false + } +} diff --git a/pkg/proto/wire/wire.go b/pkg/proto/wire/wire.go new file mode 100644 index 00000000000..5a109d67ddf --- /dev/null +++ b/pkg/proto/wire/wire.go @@ -0,0 +1,250 @@ +// 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 wire + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net" + "slices" + + libnet "github.com/fatedier/golib/net" +) + +const ( + ProtocolV1 = "v1" + ProtocolV2 = "v2" + + WireVersionV2 = 2 + + FrameTypeClientHello uint16 = 1 + FrameTypeServerHello uint16 = 2 + FrameTypeMessage uint16 = 16 + + MessageCodecJSON = "json" + UDPPacketCodecBinary = "binary-v1" + DefaultMaxFramePayloadSize = 64 * 1024 + + MagicV2 = "FRP\x00\x02\r\n" +) + +type Frame struct { + Type uint16 + Flags uint16 + Payload []byte +} + +type Conn struct { + rw io.ReadWriter + maxFramePayloadSize uint32 +} + +func NewConn(rw io.ReadWriter) *Conn { + return &Conn{ + rw: rw, + maxFramePayloadSize: DefaultMaxFramePayloadSize, + } +} + +func (c *Conn) ReadFrame() (*Frame, error) { + header := make([]byte, 8) + if _, err := io.ReadFull(c.rw, header); err != nil { + return nil, err + } + + frameType := binary.BigEndian.Uint16(header[0:2]) + flags := binary.BigEndian.Uint16(header[2:4]) + length := binary.BigEndian.Uint32(header[4:8]) + if flags != 0 { + return nil, fmt.Errorf("unsupported frame flags: %d", flags) + } + if length > c.maxFramePayloadSize { + return nil, fmt.Errorf("frame payload length %d exceeds limit %d", length, c.maxFramePayloadSize) + } + + payload := make([]byte, length) + if _, err := io.ReadFull(c.rw, payload); err != nil { + return nil, err + } + return &Frame{ + Type: frameType, + Flags: flags, + Payload: payload, + }, nil +} + +func (c *Conn) WriteFrame(f *Frame) error { + if f.Flags != 0 { + return fmt.Errorf("unsupported frame flags: %d", f.Flags) + } + if len(f.Payload) > int(c.maxFramePayloadSize) { + return fmt.Errorf("frame payload length %d exceeds limit %d", len(f.Payload), c.maxFramePayloadSize) + } + + header := make([]byte, 8) + binary.BigEndian.PutUint16(header[0:2], f.Type) + binary.BigEndian.PutUint16(header[2:4], f.Flags) + binary.BigEndian.PutUint32(header[4:8], uint32(len(f.Payload))) + if _, err := c.rw.Write(header); err != nil { + return err + } + _, err := c.rw.Write(f.Payload) + return err +} + +func (c *Conn) ReadJSONFrame(frameType uint16, out any) error { + f, err := c.ReadFrame() + if err != nil { + return err + } + if f.Type != frameType { + return fmt.Errorf("unexpected frame type %d, want %d", f.Type, frameType) + } + return c.UnmarshalFrame(f, out) +} + +func (c *Conn) UnmarshalFrame(f *Frame, out any) error { + return json.Unmarshal(f.Payload, out) +} + +func NewJSONFrame(frameType uint16, in any) (*Frame, error) { + payload, err := json.Marshal(in) + if err != nil { + return nil, err + } + return &Frame{ + Type: frameType, + Payload: payload, + }, nil +} + +func (c *Conn) WriteJSONFrame(frameType uint16, in any) error { + f, err := NewJSONFrame(frameType, in) + if err != nil { + return err + } + return c.WriteFrame(f) +} + +func WriteMagic(w io.Writer) error { + _, err := io.WriteString(w, MagicV2) + return err +} + +func WriteMagicIfV2(w io.Writer, wireProtocol string) error { + if wireProtocol != ProtocolV2 { + return nil + } + return WriteMagic(w) +} + +func CheckMagic(conn net.Conn) (out net.Conn, isV2 bool, err error) { + sharedConn, r := libnet.NewSharedConnSize(conn, len(MagicV2)) + buf := make([]byte, len(MagicV2)) + if _, err = io.ReadFull(r, buf); err != nil { + return nil, false, err + } + for i := range MagicV2 { + if buf[i] != MagicV2[i] { + return sharedConn, false, nil + } + } + return conn, true, nil +} + +type BootstrapInfo struct { + Transport string `json:"transport,omitempty"` + TLS bool `json:"tls,omitempty"` + TCPMux bool `json:"tcpMux,omitempty"` +} + +type ClientHello struct { + Bootstrap BootstrapInfo `json:"bootstrap,omitempty"` + Capabilities ClientCapabilities `json:"capabilities,omitempty"` +} + +type ClientCapabilities struct { + Message MessageCapabilities `json:"message,omitempty"` + Crypto CryptoCapabilities `json:"crypto,omitempty"` +} + +type MessageCapabilities struct { + Codecs []string `json:"codecs,omitempty"` + UDPPacketCodecs []string `json:"udpPacketCodecs,omitempty"` +} + +type CryptoCapabilities struct { + Algorithms []string `json:"algorithms,omitempty"` + ClientRandom []byte `json:"clientRandom,omitempty"` +} + +type ServerHello struct { + Selected ServerSelection `json:"selected,omitempty"` + Error string `json:"error,omitempty"` +} + +type ServerSelection struct { + Message MessageSelection `json:"message,omitempty"` + Crypto CryptoSelection `json:"crypto,omitempty"` +} + +type MessageSelection struct { + Codec string `json:"codec,omitempty"` + UDPPacketCodec string `json:"udpPacketCodec,omitempty"` +} + +type CryptoSelection struct { + Algorithm string `json:"algorithm,omitempty"` + ServerRandom []byte `json:"serverRandom,omitempty"` +} + +func clientHelloWithCryptoRandom(bootstrap BootstrapInfo, clientRandom []byte) ClientHello { + return ClientHello{ + Bootstrap: bootstrap, + Capabilities: ClientCapabilities{ + Message: MessageCapabilities{ + Codecs: []string{MessageCodecJSON}, + UDPPacketCodecs: []string{UDPPacketCodecBinary}, + }, + Crypto: CryptoCapabilities{ + Algorithms: PreferredAEADAlgorithms(), + ClientRandom: clientRandom, + }, + }, + } +} + +func DefaultServerHello() ServerHello { + return ServerHello{ + Selected: ServerSelection{ + Message: MessageSelection{ + Codec: MessageCodecJSON, + }, + }, + } +} + +func Supports(list []string, value string) bool { + return slices.Contains(list, value) +} + +func ValidateClientHello(h ClientHello) error { + if !Supports(h.Capabilities.Message.Codecs, MessageCodecJSON) { + return fmt.Errorf("unsupported message codec") + } + return ValidateCryptoCapabilities(h.Capabilities.Crypto) +} diff --git a/pkg/proto/wire/wire_test.go b/pkg/proto/wire/wire_test.go new file mode 100644 index 00000000000..b1c1b062656 --- /dev/null +++ b/pkg/proto/wire/wire_test.go @@ -0,0 +1,263 @@ +// 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 wire + +import ( + "bytes" + "encoding/binary" + "io" + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFrameRoundTrip(t *testing.T) { + var buf bytes.Buffer + conn := NewConn(&buf) + + in := mustClientHello(t, BootstrapInfo{ + Transport: "tcp", + TLS: true, + TCPMux: true, + }) + require.NoError(t, conn.WriteJSONFrame(FrameTypeClientHello, in)) + + var out ClientHello + require.NoError(t, conn.ReadJSONFrame(FrameTypeClientHello, &out)) + require.Equal(t, in, out) +} + +func TestReadFrameRejectsUnsupportedFlags(t *testing.T) { + var buf bytes.Buffer + header := make([]byte, 8) + binary.BigEndian.PutUint16(header[0:2], FrameTypeMessage) + binary.BigEndian.PutUint16(header[2:4], 1) + binary.BigEndian.PutUint32(header[4:8], 0) + buf.Write(header) + + _, err := NewConn(&buf).ReadFrame() + require.ErrorContains(t, err, "unsupported frame flags") +} + +func TestReadFrameRejectsOversizedPayload(t *testing.T) { + var buf bytes.Buffer + header := make([]byte, 8) + binary.BigEndian.PutUint16(header[0:2], FrameTypeMessage) + binary.BigEndian.PutUint32(header[4:8], DefaultMaxFramePayloadSize+1) + buf.Write(header) + + _, err := NewConn(&buf).ReadFrame() + require.ErrorContains(t, err, "exceeds limit") +} + +func TestCheckMagicV2ConsumesMagic(t *testing.T) { + client, server := net.Pipe() + defer server.Close() + + want := []byte("payload") + go func() { + defer client.Close() + _, _ = client.Write(append([]byte(MagicV2), want...)) + }() + + out, isV2, err := CheckMagic(server) + require.NoError(t, err) + require.True(t, isV2) + + got := make([]byte, len(want)) + _, err = io.ReadFull(out, got) + require.NoError(t, err) + require.Equal(t, want, got) +} + +func TestWriteMagicIfV2(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, WriteMagicIfV2(&buf, ProtocolV1)) + require.Empty(t, buf.Bytes()) + + require.NoError(t, WriteMagicIfV2(&buf, ProtocolV2)) + require.Equal(t, []byte(MagicV2), buf.Bytes()) +} + +func TestCheckMagicV1PreservesReadBytes(t *testing.T) { + client, server := net.Pipe() + defer server.Close() + + want := []byte("legacy payload") + go func() { + defer client.Close() + _, _ = client.Write(want) + }() + + out, isV2, err := CheckMagic(server) + require.NoError(t, err) + require.False(t, isV2) + + got, err := io.ReadAll(out) + require.NoError(t, err) + require.Equal(t, want, got) +} + +func TestValidateClientHello(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{}) + require.NoError(t, ValidateClientHello(hello)) + require.Len(t, hello.Capabilities.Crypto.ClientRandom, CryptoRandomSize) + require.ElementsMatch(t, []string{ + AEADAlgorithmAES256GCM, + AEADAlgorithmXChaCha20Poly1305, + }, hello.Capabilities.Crypto.Algorithms) + + hello.Capabilities.Message.Codecs = []string{"unknown"} + require.ErrorContains(t, ValidateClientHello(hello), "unsupported message codec") +} + +func TestValidateClientHelloRejectsInvalidCrypto(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{}) + hello.Capabilities.Crypto.ClientRandom = hello.Capabilities.Crypto.ClientRandom[:CryptoRandomSize-1] + require.ErrorContains(t, ValidateClientHello(hello), "invalid crypto client random length") + + hello = mustClientHello(t, BootstrapInfo{}) + hello.Capabilities.Crypto.Algorithms = []string{"unknown"} + require.ErrorContains(t, ValidateClientHello(hello), "no supported crypto algorithm") +} + +func TestPreferredAEADAlgorithms(t *testing.T) { + require.ElementsMatch(t, []string{ + AEADAlgorithmAES256GCM, + AEADAlgorithmXChaCha20Poly1305, + }, PreferredAEADAlgorithms()) +} + +func TestNewServerHelloSelectsFirstSupportedAEADAlgorithm(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{}) + hello.Capabilities.Crypto.Algorithms = []string{"future-aead", AEADAlgorithmXChaCha20Poly1305, AEADAlgorithmAES256GCM} + + 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) + require.NoError(t, err) + clientHelloPayload, serverHelloPayload := mustCryptoTranscriptPayloads(t, hello, serverHello) + + ctx, err := NewClientCryptoContext(clientHelloPayload, serverHelloPayload) + require.NoError(t, err) + require.Equal(t, serverHello.Selected.Crypto.Algorithm, ctx.Algorithm) + require.Len(t, ctx.TranscriptHash, 32) + + tampered := serverHello + tampered.Selected.Crypto.ServerRandom = append([]byte(nil), serverHello.Selected.Crypto.ServerRandom...) + tampered.Selected.Crypto.ServerRandom[0] ^= 0xff + _, tamperedServerHelloPayload := mustCryptoTranscriptPayloads(t, hello, tampered) + tamperedCtx, err := NewClientCryptoContext(clientHelloPayload, tamperedServerHelloPayload) + require.NoError(t, err) + require.NotEqual(t, ctx.TranscriptHash, tamperedCtx.TranscriptHash) +} + +func TestNewCryptoContextBindsFullClientHelloPayload(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{ + Transport: "tcp", + TLS: true, + TCPMux: true, + }) + serverHello, err := NewServerHello(hello) + require.NoError(t, err) + clientHelloPayload, serverHelloPayload := mustCryptoTranscriptPayloads(t, hello, serverHello) + + ctx := NewCryptoContext(serverHello.Selected.Crypto.Algorithm, clientHelloPayload, serverHelloPayload) + + tamperedHello := hello + tamperedHello.Bootstrap.TLS = false + tamperedClientHelloPayload, _ := mustCryptoTranscriptPayloads(t, tamperedHello, serverHello) + tamperedCtx := NewCryptoContext(serverHello.Selected.Crypto.Algorithm, tamperedClientHelloPayload, serverHelloPayload) + require.NotEqual(t, ctx.TranscriptHash, tamperedCtx.TranscriptHash) +} + +func TestNewClientCryptoContextRejectsUnknownServerSelection(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{}) + serverHello, err := NewServerHello(hello) + require.NoError(t, err) + + serverHello.Selected.Crypto.Algorithm = "unknown" + clientHelloPayload, serverHelloPayload := mustCryptoTranscriptPayloads(t, hello, serverHello) + _, err = NewClientCryptoContext(clientHelloPayload, serverHelloPayload) + require.ErrorContains(t, err, "unknown selected crypto algorithm") +} + +func TestNewClientCryptoContextRejectsUnadvertisedServerSelection(t *testing.T) { + hello := mustClientHello(t, BootstrapInfo{}) + hello.Capabilities.Crypto.Algorithms = []string{AEADAlgorithmAES256GCM} + serverHello, err := NewServerHello(hello) + require.NoError(t, err) + + serverHello.Selected.Crypto.Algorithm = AEADAlgorithmXChaCha20Poly1305 + clientHelloPayload, serverHelloPayload := mustCryptoTranscriptPayloads(t, hello, serverHello) + _, err = NewClientCryptoContext(clientHelloPayload, serverHelloPayload) + require.ErrorContains(t, err, "selected crypto algorithm was not advertised by client") +} + +func mustClientHello(t *testing.T, bootstrap BootstrapInfo) ClientHello { + t.Helper() + + hello, err := NewClientHello(bootstrap) + require.NoError(t, err) + return hello +} + +func mustCryptoTranscriptPayloads(t *testing.T, hello ClientHello, serverHello ServerHello) ([]byte, []byte) { + t.Helper() + + clientHelloFrame, err := NewJSONFrame(FrameTypeClientHello, hello) + require.NoError(t, err) + serverHelloFrame, err := NewJSONFrame(FrameTypeServerHello, serverHello) + require.NoError(t, err) + return clientHelloFrame.Payload, serverHelloFrame.Payload +} diff --git a/pkg/sdk/client/client.go b/pkg/sdk/client/client.go new file mode 100644 index 00000000000..088d3f8ff0e --- /dev/null +++ b/pkg/sdk/client/client.go @@ -0,0 +1,140 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/fatedier/frp/client/http/model" + httppkg "github.com/fatedier/frp/pkg/util/http" +) + +type Client struct { + address string + authUser string + authPwd string +} + +func New(host string, port int) *Client { + return &Client{ + address: net.JoinHostPort(host, strconv.Itoa(port)), + } +} + +func (c *Client) SetAuth(user, pwd string) { + c.authUser = user + c.authPwd = pwd +} + +func (c *Client) GetProxyStatus(ctx context.Context, name string) (*model.ProxyStatusResp, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil) + if err != nil { + return nil, err + } + content, err := c.do(req) + if err != nil { + return nil, err + } + allStatus := make(model.StatusResp) + if err = json.Unmarshal([]byte(content), &allStatus); err != nil { + return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content)) + } + for _, pss := range allStatus { + for _, ps := range pss { + if ps.Name == name { + return &ps, nil + } + } + } + return nil, fmt.Errorf("no proxy status found") +} + +func (c *Client) GetAllProxyStatus(ctx context.Context) (model.StatusResp, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil) + if err != nil { + return nil, err + } + content, err := c.do(req) + if err != nil { + return nil, err + } + allStatus := make(model.StatusResp) + if err = json.Unmarshal([]byte(content), &allStatus); err != nil { + return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content)) + } + return allStatus, nil +} + +func (c *Client) Reload(ctx context.Context, strictMode bool) error { + v := url.Values{} + if strictMode { + v.Set("strictConfig", "true") + } + queryStr := "" + if len(v) > 0 { + queryStr = "?" + v.Encode() + } + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/reload"+queryStr, nil) + if err != nil { + return err + } + _, err = c.do(req) + return err +} + +func (c *Client) Stop(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, "POST", "http://"+c.address+"/api/stop", nil) + if err != nil { + return err + } + _, err = c.do(req) + return err +} + +func (c *Client) GetConfig(ctx context.Context) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/config", nil) + if err != nil { + return "", err + } + return c.do(req) +} + +func (c *Client) UpdateConfig(ctx context.Context, content string) error { + req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+c.address+"/api/config", strings.NewReader(content)) + if err != nil { + return err + } + _, err = c.do(req) + return err +} + +func (c *Client) setAuthHeader(req *http.Request) { + if c.authUser != "" || c.authPwd != "" { + req.Header.Set("Authorization", httppkg.BasicAuth(c.authUser, c.authPwd)) + } +} + +func (c *Client) do(req *http.Request) (string, error) { + c.setAuthHeader(req) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "", fmt.Errorf("api status code [%d]", resp.StatusCode) + } + buf, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(buf), nil +} diff --git a/pkg/ssh/gateway.go b/pkg/ssh/gateway.go new file mode 100644 index 00000000000..4d90f338385 --- /dev/null +++ b/pkg/ssh/gateway.go @@ -0,0 +1,147 @@ +// Copyright 2023 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 ssh + +import ( + "fmt" + "net" + "os" + "strconv" + "strings" + + "golang.org/x/crypto/ssh" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +type Gateway struct { + bindPort int + ln net.Listener + + peerServerListener *netpkg.InternalListener + + sshConfig *ssh.ServerConfig +} + +func NewGateway( + cfg v1.SSHTunnelGateway, bindAddr string, + peerServerListener *netpkg.InternalListener, +) (*Gateway, error) { + sshConfig := &ssh.ServerConfig{} + + // privateKey + var ( + privateKeyBytes []byte + err error + ) + if cfg.PrivateKeyFile != "" { + privateKeyBytes, err = os.ReadFile(cfg.PrivateKeyFile) + } else { + if cfg.AutoGenPrivateKeyPath != "" { + privateKeyBytes, _ = os.ReadFile(cfg.AutoGenPrivateKeyPath) + } + if len(privateKeyBytes) == 0 { + privateKeyBytes, err = transport.NewRandomPrivateKey() + if err == nil && cfg.AutoGenPrivateKeyPath != "" { + err = os.WriteFile(cfg.AutoGenPrivateKeyPath, privateKeyBytes, 0o600) + } + } + } + if err != nil { + return nil, err + } + privateKey, err := ssh.ParsePrivateKey(privateKeyBytes) + if err != nil { + return nil, err + } + sshConfig.AddHostKey(privateKey) + + sshConfig.NoClientAuth = cfg.AuthorizedKeysFile == "" + sshConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile) + if err != nil { + log.Errorf("load authorized keys file error: %v", err) + return nil, fmt.Errorf("internal error") + } + + user, ok := authorizedKeysMap[string(key.Marshal())] + if !ok { + return nil, fmt.Errorf("unknown public key for remoteAddr %q", conn.RemoteAddr()) + } + return &ssh.Permissions{ + Extensions: map[string]string{ + "user": user, + }, + }, nil + } + + ln, err := net.Listen("tcp", net.JoinHostPort(bindAddr, strconv.Itoa(cfg.BindPort))) + if err != nil { + return nil, err + } + return &Gateway{ + bindPort: cfg.BindPort, + ln: ln, + peerServerListener: peerServerListener, + sshConfig: sshConfig, + }, nil +} + +func (g *Gateway) Run() { + for { + conn, err := g.ln.Accept() + if err != nil { + return + } + go g.handleConn(conn) + } +} + +func (g *Gateway) Close() error { + return g.ln.Close() +} + +func (g *Gateway) handleConn(conn net.Conn) { + defer conn.Close() + + ts, err := NewTunnelServer(conn, g.sshConfig, g.peerServerListener) + if err != nil { + return + } + if err := ts.Run(); err != nil { + log.Errorf("ssh tunnel server run error: %v", err) + } +} + +func loadAuthorizedKeysFromFile(path string) (map[string]string, error) { + authorizedKeysMap := make(map[string]string) // value is username + authorizedKeysBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + for len(authorizedKeysBytes) > 0 { + pubKey, comment, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes) + if err != nil { + return nil, err + } + + authorizedKeysMap[string(pubKey.Marshal())] = strings.TrimSpace(comment) + authorizedKeysBytes = rest + } + return authorizedKeysMap, nil +} diff --git a/pkg/ssh/server.go b/pkg/ssh/server.go new file mode 100644 index 00000000000..8a69fc30f71 --- /dev/null +++ b/pkg/ssh/server.go @@ -0,0 +1,406 @@ +// Copyright 2023 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 ssh + +import ( + "context" + "errors" + "fmt" + "net" + "slices" + "strings" + "sync" + "time" + + libio "github.com/fatedier/golib/io" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "golang.org/x/crypto/ssh" + + "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/log" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/pkg/util/xlog" + "github.com/fatedier/frp/pkg/virtual" +) + +const ( + // https://datatracker.ietf.org/doc/html/rfc4254#page-16 + ChannelTypeServerOpenChannel = "forwarded-tcpip" + RequestTypeForward = "tcpip-forward" +) + +type tcpipForward struct { + Host string + Port uint32 +} + +// https://datatracker.ietf.org/doc/html/rfc4254#section-6.5 +type execPayload struct { + Command string +} + +// https://datatracker.ietf.org/doc/html/rfc4254#page-16 +type forwardedTCPPayload struct { + Addr string + Port uint32 + + OriginAddr string + OriginPort uint32 +} + +type TunnelServer struct { + underlyingConn net.Conn + sshConn *ssh.ServerConn + sc *ssh.ServerConfig + firstChannel ssh.Channel + firstChannelMu sync.Mutex + + vc *virtual.Client + peerServerListener *netpkg.InternalListener + doneCh chan struct{} + closeDoneChOnce sync.Once +} + +func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, peerServerListener *netpkg.InternalListener) (*TunnelServer, error) { + s := &TunnelServer{ + underlyingConn: conn, + sc: sc, + peerServerListener: peerServerListener, + doneCh: make(chan struct{}), + } + return s, nil +} + +func (s *TunnelServer) Run() error { + sshConn, channels, requests, err := ssh.NewServerConn(s.underlyingConn, s.sc) + if err != nil { + return err + } + + s.sshConn = sshConn + + addr, extraPayload, err := s.waitForwardAddrAndExtraPayload(channels, requests, 3*time.Second) + if err != nil { + return err + } + + clientCfg, pc, helpMessage, err := s.parseClientAndProxyConfigurer(addr, extraPayload) + if err != nil { + if errors.Is(err, flag.ErrHelp) { + s.writeToClient(helpMessage) + return nil + } + s.writeToClient(err.Error()) + return fmt.Errorf("parse flags from ssh client error: %v", err) + } + if err := clientCfg.Complete(); err != nil { + s.writeToClient(fmt.Sprintf("failed to complete client config: %v", err)) + return fmt.Errorf("complete client config error: %v", err) + } + if sshConn.Permissions != nil { + clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User) + } + pc.Complete() + + vc, err := virtual.NewClient(virtual.ClientOptions{ + Common: clientCfg, + Spec: &msg.ClientSpec{ + Type: "ssh-tunnel", + // If ssh does not require authentication, then the virtual client needs to authenticate through a token. + // Otherwise, once ssh authentication is passed, the virtual client does not need to authenticate again. + AlwaysAuthPass: !s.sc.NoClientAuth, + }, + HandleWorkConnCb: func(base *v1.ProxyBaseConfig, workConn net.Conn, m *msg.StartWorkConn) bool { + // join workConn and ssh channel + c, err := s.openConn(addr) + if err != nil { + log.Tracef("open conn error: %v", err) + workConn.Close() + return false + } + libio.Join(c, workConn) + return false + }, + }) + if err != nil { + return err + } + s.vc = vc + + // transfer connection from virtual client to server peer listener + go func() { + l := s.vc.PeerListener() + for { + conn, err := l.Accept() + if err != nil { + return + } + _ = s.peerServerListener.PutConn(conn) + } + }() + xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100}) + ctx := xlog.NewContext(context.Background(), xl) + go func() { + vcErr := s.vc.Run(ctx) + if vcErr != nil { + s.writeToClient(vcErr.Error()) + } + + // If vc.Run returns, it means that the virtual client has been closed, and the ssh tunnel connection should be closed. + // One scenario is that the virtual client exits due to login failure. + s.closeDoneChOnce.Do(func() { + _ = sshConn.Close() + close(s.doneCh) + }) + }() + + s.vc.UpdateProxyConfigurer([]v1.ProxyConfigurer{pc}) + + if ps, err := s.waitProxyStatusReady(pc.GetBaseConfig().Name, time.Second); err != nil { + s.writeToClient(err.Error()) + log.Warnf("wait proxy status ready error: %v", err) + } else { + // success + s.writeToClient(createSuccessInfo(clientCfg.User, pc, ps)) + _ = sshConn.Wait() + } + + s.vc.Close() + log.Tracef("ssh tunnel connection from %v closed", sshConn.RemoteAddr()) + s.closeDoneChOnce.Do(func() { + _ = sshConn.Close() + close(s.doneCh) + }) + return nil +} + +func (s *TunnelServer) writeToClient(data string) { + s.firstChannelMu.Lock() + defer s.firstChannelMu.Unlock() + if s.firstChannel == nil { + return + } + _, _ = s.firstChannel.Write([]byte(data + "\n")) +} + +func (s *TunnelServer) waitForwardAddrAndExtraPayload( + channels <-chan ssh.NewChannel, + requests <-chan *ssh.Request, + timeout time.Duration, +) (*tcpipForward, string, error) { + addrCh := make(chan *tcpipForward, 1) + extraPayloadCh := make(chan string, 1) + + // get forward address + go func() { + addrGot := false + for req := range requests { + if req.Type == RequestTypeForward && !addrGot { + payload := tcpipForward{} + if err := ssh.Unmarshal(req.Payload, &payload); err != nil { + return + } + addrGot = true + addrCh <- &payload + } + if req.WantReply { + _ = req.Reply(true, nil) + } + } + }() + + // get extra payload + go func() { + for newChannel := range channels { + // extraPayload will send to extraPayloadCh + go s.handleNewChannel(newChannel, extraPayloadCh) + } + }() + + var ( + addr *tcpipForward + extraPayload string + ) + + timer := time.NewTimer(timeout) + defer timer.Stop() + for { + select { + case v := <-addrCh: + addr = v + case extra := <-extraPayloadCh: + extraPayload = extra + case <-timer.C: + return nil, "", fmt.Errorf("get addr and extra payload timeout") + } + if addr != nil && extraPayload != "" { + break + } + } + return addr, extraPayload, nil +} + +func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPayload string) (*v1.ClientCommonConfig, v1.ProxyConfigurer, string, error) { + helpMessage := "" + cmd := &cobra.Command{ + Use: "ssh v0@{address} [command]", + Short: "ssh v0@{address} [command]", + Run: func(*cobra.Command, []string) {}, + } + cmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc) + + args := strings.Split(extraPayload, " ") + if len(args) < 1 { + return nil, nil, helpMessage, fmt.Errorf("invalid extra payload") + } + proxyType := strings.TrimSpace(args[0]) + supportTypes := []string{"tcp", "http", "https", "tcpmux", "stcp"} + if !slices.Contains(supportTypes, proxyType) { + return nil, nil, helpMessage, fmt.Errorf("invalid proxy type: %s, support types: %v", proxyType, supportTypes) + } + pc := v1.NewProxyConfigurerByType(v1.ProxyType(proxyType)) + if pc == nil { + return nil, nil, helpMessage, fmt.Errorf("new proxy configurer error") + } + config.RegisterProxyFlags(cmd, pc, config.WithSSHMode()) + + clientCfg := v1.ClientCommonConfig{} + config.RegisterClientCommonConfigFlags(cmd, &clientCfg, config.WithSSHMode()) + + cmd.InitDefaultHelpCmd() + if err := cmd.ParseFlags(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + helpMessage = cmd.UsageString() + } + return nil, nil, helpMessage, err + } + // if name is not set, generate a random one + if pc.GetBaseConfig().Name == "" { + id, err := util.RandIDWithLen(8) + if err != nil { + return nil, nil, helpMessage, fmt.Errorf("generate random id error: %v", err) + } + pc.GetBaseConfig().Name = fmt.Sprintf("sshtunnel-%s-%s", proxyType, id) + } + return &clientCfg, pc, helpMessage, nil +} + +func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh chan string) { + ch, reqs, err := channel.Accept() + 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 { + if req.WantReply { + _ = req.Reply(true, nil) + } + if req.Type != "exec" { + continue + } + extraPayload, ok := parseExecPayload(req.Payload) + if !ok { + continue + } + select { + case extraPayloadCh <- extraPayload: + default: + } + } +} + +func parseExecPayload(payload []byte) (string, bool) { + var msg execPayload + if err := ssh.Unmarshal(payload, &msg); err != nil { + return "", false + } + return msg.Command, true +} + +func (s *TunnelServer) keepAlive(ch ssh.Channel) { + tk := time.NewTicker(time.Second * 30) + defer tk.Stop() + + for { + select { + case <-tk.C: + _, err := ch.SendRequest("heartbeat", false, nil) + if err != nil { + return + } + case <-s.doneCh: + return + } + } +} + +func (s *TunnelServer) openConn(addr *tcpipForward) (net.Conn, error) { + payload := forwardedTCPPayload{ + Addr: addr.Host, + Port: addr.Port, + // Note: Here is just for compatibility, not the real source address. + OriginAddr: addr.Host, + OriginPort: addr.Port, + } + channel, reqs, err := s.sshConn.OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(&payload)) + if err != nil { + return nil, fmt.Errorf("open ssh channel error: %v", err) + } + go ssh.DiscardRequests(reqs) + + conn := netpkg.WrapReadWriteCloserToConn(channel, s.underlyingConn) + return conn, nil +} + +func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration) (*proxy.WorkingStatus, error) { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + timer := time.NewTimer(timeout) + defer timer.Stop() + + statusExporter := s.vc.Service().StatusExporter() + + for { + select { + case <-ticker.C: + ps, ok := statusExporter.GetProxyStatus(name) + if !ok { + continue + } + switch ps.Phase { + case proxy.ProxyPhaseRunning: + return ps, nil + case proxy.ProxyPhaseStartErr, proxy.ProxyPhaseClosed: + return ps, errors.New(ps.Err) + } + case <-timer.C: + return nil, fmt.Errorf("wait proxy status ready timeout") + case <-s.doneCh: + return nil, fmt.Errorf("ssh tunnel server closed") + } + } +} diff --git a/pkg/ssh/server_test.go b/pkg/ssh/server_test.go new file mode 100644 index 00000000000..56b1ba5b359 --- /dev/null +++ b/pkg/ssh/server_test.go @@ -0,0 +1,115 @@ +// 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 ssh + +import ( + "encoding/binary" + "io" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + cryptossh "golang.org/x/crypto/ssh" +) + +func TestParseExecPayload(t *testing.T) { + payload := cryptossh.Marshal(&execPayload{Command: "tcp --remote_port 6000"}) + + got, ok := parseExecPayload(payload) + + require.True(t, ok) + require.Equal(t, "tcp --remote_port 6000", got) +} + +func TestParseExecPayloadRejectsMalformedPayloads(t *testing.T) { + overflowLength := make([]byte, 5) + binary.BigEndian.PutUint32(overflowLength[:4], ^uint32(0)) + + for _, tc := range []struct { + name string + payload []byte + }{ + { + name: "empty", + payload: nil, + }, + { + name: "short length prefix", + payload: []byte{0, 0, 0}, + }, + { + name: "declared length exceeds remaining payload", + payload: []byte{0, 0, 0, 2, 'x'}, + }, + { + name: "overflow length", + payload: overflowLength, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var ( + got string + ok bool + ) + require.NotPanics(t, func() { + got, ok = parseExecPayload(tc.payload) + }) + require.False(t, ok) + require.Empty(t, got) + }) + } +} + +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") + } +} diff --git a/pkg/ssh/terminal.go b/pkg/ssh/terminal.go new file mode 100644 index 00000000000..a2e9a6ff362 --- /dev/null +++ b/pkg/ssh/terminal.go @@ -0,0 +1,31 @@ +// Copyright 2023 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 ssh + +import ( + "github.com/fatedier/frp/client/proxy" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func createSuccessInfo(user string, pc v1.ProxyConfigurer, ps *proxy.WorkingStatus) string { + base := pc.GetBaseConfig() + out := "\n" + out += "frp (via SSH) (Ctrl+C to quit)\n\n" + out += "User: " + user + "\n" + out += "ProxyName: " + base.Name + "\n" + out += "Type: " + base.Type + "\n" + out += "RemoteAddress: " + ps.RemoteAddr + "\n" + return out +} diff --git a/pkg/transport/message.go b/pkg/transport/message.go new file mode 100644 index 00000000000..40165f5dc4a --- /dev/null +++ b/pkg/transport/message.go @@ -0,0 +1,123 @@ +// Copyright 2023 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 transport + +import ( + "context" + "reflect" + "sync" + + "github.com/fatedier/golib/errors" + + "github.com/fatedier/frp/pkg/msg" +) + +type MessageTransporter interface { + Send(msg.Message) error + // Recv(ctx context.Context, laneKey string, msgType string) (Message, error) + // Do will first send msg, then recv msg with the same laneKey and specified msgType. + Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) + // Dispatch will dispatch message to related channel registered in Do function by its message type and laneKey. + Dispatch(m msg.Message, laneKey string) bool + // Same with Dispatch but with specified message type. + DispatchWithType(m msg.Message, msgType, laneKey string) bool +} + +type MessageSender interface { + Send(msg.Message) error +} + +func NewMessageTransporter(sender MessageSender) MessageTransporter { + return &transporterImpl{ + sender: sender, + registry: make(map[string]map[string]chan msg.Message), + } +} + +type transporterImpl struct { + sender MessageSender + + // First key is message type and second key is lane key. + // Dispatch will dispatch message to related channel by its message type + // and lane key. + registry map[string]map[string]chan msg.Message + mu sync.RWMutex +} + +func (impl *transporterImpl) Send(m msg.Message) error { + return impl.sender.Send(m) +} + +func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) { + ch := make(chan msg.Message, 1) + defer close(ch) + unregisterFn := impl.registerMsgChan(ch, laneKey, recvMsgType) + defer unregisterFn() + + if err := impl.Send(req); err != nil { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case resp := <-ch: + return resp, nil + } +} + +func (impl *transporterImpl) DispatchWithType(m msg.Message, msgType, laneKey string) bool { + var ch chan msg.Message + impl.mu.RLock() + byLaneKey, ok := impl.registry[msgType] + if ok { + ch = byLaneKey[laneKey] + } + impl.mu.RUnlock() + + if ch == nil { + return false + } + + if err := errors.PanicToError(func() { + ch <- m + }); err != nil { + return false + } + return true +} + +func (impl *transporterImpl) Dispatch(m msg.Message, laneKey string) bool { + msgType := reflect.TypeOf(m).Elem().Name() + return impl.DispatchWithType(m, msgType, laneKey) +} + +func (impl *transporterImpl) registerMsgChan(recvCh chan msg.Message, laneKey string, msgType string) (unregister func()) { + impl.mu.Lock() + byLaneKey, ok := impl.registry[msgType] + if !ok { + byLaneKey = make(map[string]chan msg.Message) + impl.registry[msgType] = byLaneKey + } + byLaneKey[laneKey] = recvCh + impl.mu.Unlock() + + unregister = func() { + impl.mu.Lock() + delete(byLaneKey, laneKey) + impl.mu.Unlock() + } + return +} diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go index 38201f8e900..19ebca73b38 100644 --- a/pkg/transport/tls.go +++ b/pkg/transport/tls.go @@ -1,3 +1,17 @@ +// Copyright 2023 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 transport import ( @@ -6,8 +20,10 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "fmt" "math/big" "os" + "time" ) func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { @@ -18,12 +34,30 @@ func newCustomTLSKeyPair(certfile, keyfile string) (*tls.Certificate, error) { return &tlsCert, nil } -func newRandomTLSKeyPair() *tls.Certificate { - key, err := rsa.GenerateKey(rand.Reader, 1024) +func newRandomTLSKeyPair() (*tls.Certificate, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + // Generate a random positive serial number with 128 bits of entropy. + // RFC 5280 requires serial numbers to be positive integers (not zero). + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - panic(err) + return nil, err } - template := x509.Certificate{SerialNumber: big.NewInt(1)} + // Ensure serial number is positive (not zero) + if serialNumber.Sign() == 0 { + serialNumber = big.NewInt(1) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour * 10), + } + certDER, err := x509.CreateCertificate( rand.Reader, &template, @@ -31,16 +65,16 @@ func newRandomTLSKeyPair() *tls.Certificate { &key.PublicKey, key) if err != nil { - panic(err) + return nil, err } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { - panic(err) + return nil, err } - return &tlsCert + return &tlsCert, nil } // Only support one ca file to add @@ -52,17 +86,22 @@ func newCertPool(caPath string) (*x509.CertPool, error) { return nil, err } - pool.AppendCertsFromPEM(caCrt) + if !pool.AppendCertsFromPEM(caCrt) { + return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath) + } return pool, nil } func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { - var base = &tls.Config{} + base := &tls.Config{} if certPath == "" || keyPath == "" { // server will generate tls conf by itself - cert := newRandomTLSKeyPair() + cert, err := newRandomTLSKeyPair() + if err != nil { + return nil, err + } base.Certificates = []tls.Certificate{*cert} } else { cert, err := newCustomTLSKeyPair(certPath, keyPath) @@ -87,11 +126,9 @@ func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { } func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Config, error) { - var base = &tls.Config{} + base := &tls.Config{} - if certPath == "" || keyPath == "" { - // client will not generate tls conf by itself - } else { + if certPath != "" && keyPath != "" { cert, err := newCustomTLSKeyPair(certPath, keyPath) if err != nil { return nil, err @@ -116,3 +153,15 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf return base, nil } + +func NewRandomPrivateKey() ([]byte, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + keyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + return keyPEM, nil +} diff --git a/pkg/util/http/context.go b/pkg/util/http/context.go new file mode 100644 index 00000000000..ae4b7ea638f --- /dev/null +++ b/pkg/util/http/context.go @@ -0,0 +1,57 @@ +// Copyright 2025 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 http + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/gorilla/mux" +) + +type Context struct { + Req *http.Request + Resp http.ResponseWriter + vars map[string]string +} + +func NewContext(w http.ResponseWriter, r *http.Request) *Context { + return &Context{ + Req: r, + Resp: w, + vars: mux.Vars(r), + } +} + +func (c *Context) Param(key string) string { + return c.vars[key] +} + +func (c *Context) Query(key string) string { + return c.Req.URL.Query().Get(key) +} + +func (c *Context) BindJSON(obj any) error { + body, err := io.ReadAll(c.Req.Body) + if err != nil { + return err + } + return json.Unmarshal(body, obj) +} + +func (c *Context) Body() ([]byte, error) { + return io.ReadAll(c.Req.Body) +} diff --git a/pkg/util/http/error.go b/pkg/util/http/error.go new file mode 100644 index 00000000000..8206141bd2d --- /dev/null +++ b/pkg/util/http/error.go @@ -0,0 +1,33 @@ +// Copyright 2025 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 http + +import "fmt" + +type Error struct { + Code int + Err error +} + +func (e *Error) Error() string { + return e.Err.Error() +} + +func NewError(code int, msg string) *Error { + return &Error{ + Code: code, + Err: fmt.Errorf("%s", msg), + } +} diff --git a/pkg/util/http/handler.go b/pkg/util/http/handler.go new file mode 100644 index 00000000000..55de29323bb --- /dev/null +++ b/pkg/util/http/handler.go @@ -0,0 +1,96 @@ +// Copyright 2025 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 http + +import ( + "encoding/json" + "net/http" + + "github.com/fatedier/frp/pkg/util/log" +) + +type GeneralResponse struct { + Code int + Msg string +} + +type V2Response struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data any `json:"data"` +} + +// APIHandler is a handler function that returns a response object or an error. +type APIHandler func(ctx *Context) (any, error) + +// MakeHTTPHandlerFunc turns a normal APIHandler into a http.HandlerFunc. +func MakeHTTPHandlerFunc(handler APIHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := NewContext(w, r) + res, err := handler(ctx) + if err != nil { + log.Warnf("http response [%s]: error: %v", r.URL.Path, err) + code := http.StatusInternalServerError + if e, ok := err.(*Error); ok { + code = e.Code + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(GeneralResponse{Code: code, Msg: err.Error()}) + return + } + + if res == nil { + w.WriteHeader(http.StatusOK) + return + } + + switch v := res.(type) { + case []byte: + _, _ = w.Write(v) + case string: + _, _ = w.Write([]byte(v)) + default: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(v) + } + } +} + +// MakeHTTPHandlerFuncV2 wraps a handler response in the dashboard API v2 envelope. +func MakeHTTPHandlerFuncV2(handler APIHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := NewContext(w, r) + res, err := handler(ctx) + if err != nil { + log.Warnf("http response [%s]: error: %v", r.URL.Path, err) + code := http.StatusInternalServerError + if e, ok := err.(*Error); ok { + code = e.Code + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(V2Response{Code: code, Msg: err.Error(), Data: nil}) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(V2Response{Code: http.StatusOK, Msg: "success", Data: res}) + } +} diff --git a/pkg/util/util/http.go b/pkg/util/http/http.go similarity index 85% rename from pkg/util/util/http.go rename to pkg/util/http/http.go index 3b8117346f9..8a763727c8e 100644 --- a/pkg/util/util/http.go +++ b/pkg/util/http/http.go @@ -1,4 +1,4 @@ -// Copyright 2020 guylewin, guy@lewin.co.il +// Copyright 2023 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. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package http import ( "encoding/base64" @@ -60,10 +60,8 @@ func CanonicalHost(host string) (string, error) { return "", err } } - if strings.HasSuffix(host, ".") { - // Strip trailing dot from fully qualified domain names. - host = host[:len(host)-1] - } + // Strip trailing dot from fully qualified domain names. + host = strings.TrimSuffix(host, ".") return host, nil } @@ -91,9 +89,14 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) { return } cs := string(c) - s := strings.IndexByte(cs, ':') - if s < 0 { + before, after, found := strings.Cut(cs, ":") + if !found { return } - return cs[:s], cs[s+1:], true + return before, after, true +} + +func BasicAuth(username, passwd string) string { + auth := username + ":" + passwd + return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) } diff --git a/pkg/util/http/middleware.go b/pkg/util/http/middleware.go new file mode 100644 index 00000000000..40009a403e4 --- /dev/null +++ b/pkg/util/http/middleware.go @@ -0,0 +1,40 @@ +// Copyright 2025 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 http + +import ( + "net/http" + + "github.com/fatedier/frp/pkg/util/log" +) + +type responseWriter struct { + http.ResponseWriter + code int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.code = code + rw.ResponseWriter.WriteHeader(code) +} + +func NewRequestLogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Infof("http request: [%s]", r.URL.Path) + rw := &responseWriter{ResponseWriter: w, code: http.StatusOK} + next.ServeHTTP(rw, r) + log.Infof("http response [%s]: code [%d]", r.URL.Path, rw.code) + }) +} diff --git a/pkg/util/http/server.go b/pkg/util/http/server.go new file mode 100644 index 00000000000..0bca899311a --- /dev/null +++ b/pkg/util/http/server.go @@ -0,0 +1,130 @@ +// Copyright 2023 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 http + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/pprof" + "strconv" + "time" + + "github.com/gorilla/mux" + + "github.com/fatedier/frp/assets" + v1 "github.com/fatedier/frp/pkg/config/v1" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +var ( + defaultReadTimeout = 60 * time.Second + defaultWriteTimeout = 60 * time.Second +) + +type Server struct { + addr string + ln net.Listener + tlsCfg *tls.Config + + router *mux.Router + hs *http.Server + + authMiddleware mux.MiddlewareFunc +} + +func NewServer(cfg v1.WebServerConfig) (*Server, error) { + assets.Load(cfg.AssetsDir) + + addr := net.JoinHostPort(cfg.Addr, strconv.Itoa(cfg.Port)) + if addr == ":" { + addr = ":http" + } + + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + router := mux.NewRouter() + hs := &http.Server{ + Addr: addr, + Handler: router, + ReadTimeout: defaultReadTimeout, + WriteTimeout: defaultWriteTimeout, + } + s := &Server{ + addr: addr, + ln: ln, + hs: hs, + router: router, + } + if cfg.PprofEnable { + s.registerPprofHandlers() + } + if cfg.TLS != nil { + cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile) + if err != nil { + return nil, err + } + s.tlsCfg = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + } + s.authMiddleware = netpkg.NewHTTPAuthMiddleware(cfg.User, cfg.Password).SetAuthFailDelay(200 * time.Millisecond).Middleware + return s, nil +} + +func (s *Server) Address() string { + return s.addr +} + +func (s *Server) Run() error { + ln := s.ln + if s.tlsCfg != nil { + ln = tls.NewListener(ln, s.tlsCfg) + } + return s.hs.Serve(ln) +} + +func (s *Server) Close() error { + err := s.hs.Close() + if s.ln != nil { + _ = s.ln.Close() + } + return err +} + +type RouterRegisterHelper struct { + Router *mux.Router + AssetsFS http.FileSystem + AuthMiddleware mux.MiddlewareFunc +} + +func (s *Server) RouteRegister(register func(helper *RouterRegisterHelper)) { + register(&RouterRegisterHelper{ + Router: s.router, + AssetsFS: assets.FileSystem, + AuthMiddleware: s.authMiddleware, + }) +} + +func (s *Server) registerPprofHandlers() { + s.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + s.router.HandleFunc("/debug/pprof/profile", pprof.Profile) + s.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + s.router.HandleFunc("/debug/pprof/trace", pprof.Trace) + s.router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) +} diff --git a/pkg/util/jsonx/json_v1.go b/pkg/util/jsonx/json_v1.go new file mode 100644 index 00000000000..1fb982903d9 --- /dev/null +++ b/pkg/util/jsonx/json_v1.go @@ -0,0 +1,45 @@ +// 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 jsonx + +import ( + "bytes" + "encoding/json" +) + +type DecodeOptions struct { + RejectUnknownMembers bool +} + +func Marshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +func MarshalIndent(v any, prefix, indent string) ([]byte, error) { + return json.MarshalIndent(v, prefix, indent) +} + +func Unmarshal(data []byte, out any) error { + return json.Unmarshal(data, out) +} + +func UnmarshalWithOptions(data []byte, out any, options DecodeOptions) error { + if !options.RejectUnknownMembers { + return json.Unmarshal(data, out) + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + return decoder.Decode(out) +} diff --git a/pkg/util/jsonx/raw_message.go b/pkg/util/jsonx/raw_message.go new file mode 100644 index 00000000000..3eda462839d --- /dev/null +++ b/pkg/util/jsonx/raw_message.go @@ -0,0 +1,36 @@ +// 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 jsonx + +import "fmt" + +// RawMessage stores a raw encoded JSON value. +// It is equivalent to encoding/json.RawMessage behavior. +type RawMessage []byte + +func (m RawMessage) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + return m, nil +} + +func (m *RawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return fmt.Errorf("jsonx.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[:0], data...) + return nil +} diff --git a/pkg/util/limit/limiter.go b/pkg/util/limit/limiter.go new file mode 100644 index 00000000000..43ba7434550 --- /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 00000000000..f245bcfc7cd --- /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 efa828f48d4..eccca5a558e 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 5256d1e2276..56357228e92 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/pkg/util/log/log.go b/pkg/util/log/log.go index 1ddf4cdc042..327d4ef654d 100644 --- a/pkg/util/log/log.go +++ b/pkg/util/log/log.go @@ -15,79 +15,95 @@ package log import ( - "fmt" + "bytes" + "os" - "github.com/fatedier/beego/logs" + "github.com/fatedier/golib/log" ) -// Log is the under log object -var Log *logs.BeeLogger +var ( + TraceLevel = log.TraceLevel + DebugLevel = log.DebugLevel + InfoLevel = log.InfoLevel + WarnLevel = log.WarnLevel + ErrorLevel = log.ErrorLevel +) -func init() { - Log = logs.NewLogger(200) - Log.EnableFuncCallDepth(true) - Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1) -} +var Logger *log.Logger -func InitLog(logWay string, logFile string, logLevel string, maxdays int64, disableLogColor bool) { - SetLogFile(logWay, logFile, maxdays, disableLogColor) - SetLogLevel(logLevel) +func init() { + Logger = log.New( + log.WithCaller(true), + log.AddCallerSkip(1), + log.WithLevel(log.InfoLevel), + ) } -// SetLogFile to configure log params -// logWay: file or console -func SetLogFile(logWay string, logFile string, maxdays int64, disableLogColor bool) { - if logWay == "console" { - params := "" - if disableLogColor { - params = fmt.Sprintf(`{"color": false}`) +func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) { + options := []log.Option{} + if logPath == "console" { + if !disableLogColor { + options = append(options, + log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{ + Colorful: true, + }, os.Stdout)), + ) } - Log.SetLogger("console", params) } else { - params := fmt.Sprintf(`{"filename": "%s", "maxdays": %d}`, logFile, maxdays) - Log.SetLogger("file", params) + writer := log.NewRotateFileWriter(log.RotateFileConfig{ + FileName: logPath, + Mode: log.RotateFileModeDaily, + MaxDays: maxDays, + }) + writer.Init() + options = append(options, log.WithOutput(writer)) } -} -// SetLogLevel set log level, default is warning -// value: error, warning, info, debug, trace -func SetLogLevel(logLevel string) { - level := 4 // warning - switch logLevel { - case "error": - level = 3 - case "warn": - level = 4 - case "info": - level = 6 - case "debug": - level = 7 - case "trace": - level = 8 - default: - level = 4 + level, err := log.ParseLevel(levelStr) + if err != nil { + level = log.InfoLevel } - Log.SetLevel(level) + options = append(options, log.WithLevel(level)) + Logger = Logger.WithOptions(options...) } -// wrap log +func Errorf(format string, v ...any) { + Logger.Errorf(format, v...) +} -func Error(format string, v ...interface{}) { - Log.Error(format, v...) +func Warnf(format string, v ...any) { + Logger.Warnf(format, v...) } -func Warn(format string, v ...interface{}) { - Log.Warn(format, v...) +func Infof(format string, v ...any) { + Logger.Infof(format, v...) } -func Info(format string, v ...interface{}) { - Log.Info(format, v...) +func Debugf(format string, v ...any) { + Logger.Debugf(format, v...) } -func Debug(format string, v ...interface{}) { - Log.Debug(format, v...) +func Tracef(format string, v ...any) { + Logger.Tracef(format, v...) +} + +func Logf(level log.Level, offset int, format string, v ...any) { + Logger.Logf(level, offset, format, v...) +} + +type WriteLogger struct { + level log.Level + offset int +} + +func NewWriteLogger(level log.Level, offset int) *WriteLogger { + return &WriteLogger{ + level: level, + offset: offset, + } } -func Trace(format string, v ...interface{}) { - Log.Trace(format, v...) +func (w *WriteLogger) Write(p []byte) (n int, err error) { + Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n"))) + return len(p), nil } diff --git a/pkg/util/metric/counter_test.go b/pkg/util/metric/counter_test.go index 4925c25b523..dca72052ed3 100644 --- a/pkg/util/metric/counter_test.go +++ b/pkg/util/metric/counter_test.go @@ -3,21 +3,21 @@ package metric import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCounter(t *testing.T) { - assert := assert.New(t) + require := require.New(t) c := NewCounter() c.Inc(10) - assert.EqualValues(10, c.Count()) + require.EqualValues(10, c.Count()) c.Dec(5) - assert.EqualValues(5, c.Count()) + require.EqualValues(5, c.Count()) cTmp := c.Snapshot() - assert.EqualValues(5, cTmp.Count()) + require.EqualValues(5, cTmp.Count()) c.Clear() - assert.EqualValues(0, c.Count()) + require.EqualValues(0, c.Count()) } diff --git a/pkg/util/metric/date_counter.go b/pkg/util/metric/date_counter.go index 4524fec874c..5e6b889f92f 100644 --- a/pkg/util/metric/date_counter.go +++ b/pkg/util/metric/date_counter.go @@ -17,6 +17,8 @@ package metric import ( "sync" "time" + + "k8s.io/utils/clock" ) type DateCounter interface { @@ -38,27 +40,33 @@ func NewDateCounter(reserveDays int64) DateCounter { type StandardDateCounter struct { reserveDays int64 counts []int64 + clock clock.PassiveClock lastUpdateDate time.Time mu sync.Mutex } func newStandardDateCounter(reserveDays int64) *StandardDateCounter { - now := time.Now() - now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - s := &StandardDateCounter{ + return newStandardDateCounterWithClock(reserveDays, clock.RealClock{}) +} + +func newStandardDateCounterWithClock(reserveDays int64, clk clock.PassiveClock) *StandardDateCounter { + if clk == nil { + clk = clock.RealClock{} + } + return &StandardDateCounter{ reserveDays: reserveDays, counts: make([]int64, reserveDays), - lastUpdateDate: now, + clock: clk, + lastUpdateDate: startOfDay(clk.Now()), } - return s } func (c *StandardDateCounter) TodayCount() int64 { c.mu.Lock() defer c.mu.Unlock() - c.rotate(time.Now()) + c.rotate(c.clock.Now()) return c.counts[0] } @@ -70,65 +78,61 @@ func (c *StandardDateCounter) GetLastDaysCount(lastdays int64) []int64 { c.mu.Lock() defer c.mu.Unlock() - c.rotate(time.Now()) - for i := 0; i < int(lastdays); i++ { - counts[i] = c.counts[i] - } + c.rotate(c.clock.Now()) + copy(counts, c.counts) return counts } func (c *StandardDateCounter) Inc(count int64) { c.mu.Lock() defer c.mu.Unlock() - c.rotate(time.Now()) + c.rotate(c.clock.Now()) c.counts[0] += count } func (c *StandardDateCounter) Dec(count int64) { c.mu.Lock() defer c.mu.Unlock() - c.rotate(time.Now()) + c.rotate(c.clock.Now()) c.counts[0] -= count } func (c *StandardDateCounter) Snapshot() DateCounter { c.mu.Lock() defer c.mu.Unlock() - tmp := newStandardDateCounter(c.reserveDays) - for i := 0; i < int(c.reserveDays); i++ { - tmp.counts[i] = c.counts[i] - } + tmp := newStandardDateCounterWithClock(c.reserveDays, c.clock) + copy(tmp.counts, c.counts) return tmp } func (c *StandardDateCounter) Clear() { c.mu.Lock() defer c.mu.Unlock() - for i := 0; i < int(c.reserveDays); i++ { - c.counts[i] = 0 - } + clear(c.counts) } // rotate // Must hold the lock before calling this function. func (c *StandardDateCounter) rotate(now time.Time) { - now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + now = startOfDay(now) days := int(now.Sub(c.lastUpdateDate).Hours() / 24) - - defer func() { - c.lastUpdateDate = now - }() + reserveDays := int(c.reserveDays) if days <= 0 { return - } else if days >= int(c.reserveDays) { + } else if days >= reserveDays { c.counts = make([]int64, c.reserveDays) + c.lastUpdateDate = now return } newCounts := make([]int64, c.reserveDays) - for i := days; i < int(c.reserveDays); i++ { - newCounts[i] = c.counts[i-days] - } + copy(newCounts[days:], c.counts[:reserveDays-days]) c.counts = newCounts + c.lastUpdateDate = now +} + +// startOfDay returns midnight in t's location. +func startOfDay(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) } diff --git a/pkg/util/metric/date_counter_test.go b/pkg/util/metric/date_counter_test.go index c9997c70fe7..cbd29386f7e 100644 --- a/pkg/util/metric/date_counter_test.go +++ b/pkg/util/metric/date_counter_test.go @@ -1,27 +1,134 @@ package metric import ( + "sync" "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + clocktesting "k8s.io/utils/clock/testing" ) func TestDateCounter(t *testing.T) { - assert := assert.New(t) + require := require.New(t) dc := NewDateCounter(3) dc.Inc(10) - assert.EqualValues(10, dc.TodayCount()) + require.EqualValues(10, dc.TodayCount()) dc.Dec(5) - assert.EqualValues(5, dc.TodayCount()) + require.EqualValues(5, dc.TodayCount()) counts := dc.GetLastDaysCount(3) - assert.EqualValues(3, len(counts)) - assert.EqualValues(5, counts[0]) - assert.EqualValues(0, counts[1]) - assert.EqualValues(0, counts[2]) + require.EqualValues(3, len(counts)) + require.EqualValues(5, counts[0]) + require.EqualValues(0, counts[1]) + require.EqualValues(0, counts[2]) dcTmp := dc.Snapshot() - assert.EqualValues(5, dcTmp.TodayCount()) + require.EqualValues(5, dcTmp.TodayCount()) +} + +func TestDateCounterRotate(t *testing.T) { + loc := time.FixedZone("test", 8*60*60) + lastUpdateDate := time.Date(2026, time.May, 8, 0, 0, 0, 0, loc) + + tests := []struct { + name string + now time.Time + want []int64 + wantLastUpdateDate time.Time + }{ + { + name: "same day", + now: time.Date(2026, time.May, 8, 12, 30, 0, 0, loc), + want: []int64{10, 7, 3}, + wantLastUpdateDate: lastUpdateDate, + }, + { + name: "clock skew", + now: time.Date(2026, time.May, 7, 12, 30, 0, 0, loc), + want: []int64{10, 7, 3}, + wantLastUpdateDate: lastUpdateDate, + }, + { + name: "one day", + now: time.Date(2026, time.May, 9, 12, 30, 0, 0, loc), + want: []int64{0, 10, 7}, + wantLastUpdateDate: time.Date(2026, time.May, 9, 0, 0, 0, 0, loc), + }, + { + name: "two days", + now: time.Date(2026, time.May, 10, 12, 30, 0, 0, loc), + want: []int64{0, 0, 10}, + wantLastUpdateDate: time.Date(2026, time.May, 10, 0, 0, 0, 0, loc), + }, + { + name: "all reserved days elapsed", + now: time.Date(2026, time.May, 11, 12, 30, 0, 0, loc), + want: []int64{0, 0, 0}, + wantLastUpdateDate: time.Date(2026, time.May, 11, 0, 0, 0, 0, loc), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + dc := newStandardDateCounter(3) + dc.counts = []int64{10, 7, 3} + dc.lastUpdateDate = lastUpdateDate + + dc.mu.Lock() + dc.rotate(tt.now) + dc.mu.Unlock() + + require.Equal(tt.want, dc.counts) + require.Equal(tt.wantLastUpdateDate, dc.lastUpdateDate) + }) + } +} + +func TestDateCounterGetLastDaysCountReturnsCopy(t *testing.T) { + require := require.New(t) + + clk := clocktesting.NewFakeClock(time.Date(2026, time.May, 8, 12, 30, 0, 0, time.Local)) + dc := newStandardDateCounterWithClock(3, clk) + dc.counts = []int64{10, 7, 3} + + counts := dc.GetLastDaysCount(2) + require.Equal([]int64{10, 7}, counts) + + counts[0] = 100 + require.Equal([]int64{10, 7}, dc.GetLastDaysCount(2)) +} + +func TestDateCounterClear(t *testing.T) { + require := require.New(t) + + dc := newStandardDateCounter(3) + dc.counts = []int64{10, 7, 3} + + dc.Clear() + + require.Equal([]int64{0, 0, 0}, dc.counts) +} + +func TestDateCounterConcurrentAccess(t *testing.T) { + clk := clocktesting.NewFakeClock(time.Date(2026, time.May, 8, 12, 30, 0, 0, time.Local)) + dc := newStandardDateCounterWithClock(3, clk) + + var wg sync.WaitGroup + for range 8 { + wg.Go(func() { + for range 100 { + dc.Inc(1) + dc.Dec(1) + _ = dc.TodayCount() + _ = dc.GetLastDaysCount(3) + _ = dc.Snapshot() + } + }) + } + wg.Wait() } diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go index f3d8caee400..fd71162ecf1 100644 --- a/pkg/util/net/conn.go +++ b/pkg/util/net/conn.go @@ -16,12 +16,17 @@ package net import ( "context" + "crypto/hkdf" + "crypto/sha256" "errors" "io" "net" "sync/atomic" "time" + libcrypto "github.com/fatedier/golib/crypto" + quic "github.com/quic-go/quic-go" + "github.com/fatedier/frp/pkg/util/xlog" ) @@ -73,9 +78,11 @@ type WrapReadWriteCloserConn struct { io.ReadWriteCloser underConn net.Conn + + remoteAddr net.Addr } -func WrapReadWriteCloserToConn(rwc io.ReadWriteCloser, underConn net.Conn) net.Conn { +func WrapReadWriteCloserToConn(rwc io.ReadWriteCloser, underConn net.Conn) *WrapReadWriteCloserConn { return &WrapReadWriteCloserConn{ ReadWriteCloser: rwc, underConn: underConn, @@ -89,7 +96,14 @@ func (conn *WrapReadWriteCloserConn) LocalAddr() net.Addr { return (*net.TCPAddr)(nil) } +func (conn *WrapReadWriteCloserConn) SetRemoteAddr(addr net.Addr) { + conn.remoteAddr = addr +} + func (conn *WrapReadWriteCloserConn) RemoteAddr() net.Addr { + if conn.remoteAddr != nil { + return conn.remoteAddr + } if conn.underConn != nil { return conn.underConn.RemoteAddr() } @@ -121,13 +135,13 @@ type CloseNotifyConn struct { net.Conn // 1 means closed - closeFlag int32 + closeFlag atomic.Int32 - closeFn func() + closeFn func(error) } -// closeFn will be only called once -func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn { +// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called) +func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn { return &CloseNotifyConn{ Conn: c, closeFn: closeFn, @@ -135,20 +149,33 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn { } func (cc *CloseNotifyConn) Close() (err error) { - pflag := atomic.SwapInt32(&cc.closeFlag, 1) + pflag := cc.closeFlag.Swap(1) if pflag == 0 { - err = cc.Close() + err = cc.Conn.Close() if cc.closeFn != nil { - cc.closeFn() + cc.closeFn(nil) } } return } +// CloseWithError closes the connection and passes the error to the close callback. +func (cc *CloseNotifyConn) CloseWithError(err error) error { + pflag := cc.closeFlag.Swap(1) + if pflag == 0 { + closeErr := cc.Conn.Close() + if cc.closeFn != nil { + cc.closeFn(err) + } + return closeErr + } + return nil +} + type StatsConn struct { net.Conn - closed int64 // 1 means closed + closed atomic.Int64 // 1 means closed totalRead int64 totalWrite int64 statsFunc func(totalRead, totalWrite int64) @@ -174,7 +201,7 @@ func (statsConn *StatsConn) Write(p []byte) (n int, err error) { } func (statsConn *StatsConn) Close() (err error) { - old := atomic.SwapInt64(&statsConn.closed, 1) + old := statsConn.closed.Swap(1) if old != 1 { err = statsConn.Conn.Close() if statsConn.statsFunc != nil { @@ -183,3 +210,131 @@ func (statsConn *StatsConn) Close() (err error) { } return } + +type wrapQuicStream struct { + *quic.Stream + c *quic.Conn +} + +func QuicStreamToNetConn(s *quic.Stream, c *quic.Conn) net.Conn { + return &wrapQuicStream{ + Stream: s, + c: c, + } +} + +func (conn *wrapQuicStream) LocalAddr() net.Addr { + if conn.c != nil { + return conn.c.LocalAddr() + } + return (*net.TCPAddr)(nil) +} + +func (conn *wrapQuicStream) RemoteAddr() net.Addr { + if conn.c != nil { + return conn.c.RemoteAddr() + } + return (*net.TCPAddr)(nil) +} + +func (conn *wrapQuicStream) Close() error { + conn.CancelRead(0) + return conn.Stream.Close() +} + +func NewCryptoReadWriter(rw io.ReadWriter, key []byte) (io.ReadWriter, error) { + encReader := libcrypto.NewReader(rw, key) + encWriter, err := libcrypto.NewWriter(rw, key) + if err != nil { + return nil, err + } + return struct { + io.Reader + io.Writer + }{ + Reader: encReader, + Writer: encWriter, + }, nil +} + +type AEADCryptoRole int + +const ( + AEADCryptoRoleClient AEADCryptoRole = iota + 1 + AEADCryptoRoleServer +) + +const ( + aeadControlHKDFInfoPrefix = "frp wire v2 control aead" + aeadDirectionClientToServer = "client-to-server" + aeadDirectionServerToClient = "server-to-client" +) + +// NewAEADCryptoReadWriter wraps rw with framed AEAD encryption for the v2 +// control channel. Frames and their order are authenticated, but end-of-stream +// is not: a clean EOF at a frame boundary is returned as normal EOF by the +// underlying AEAD stream. Protocols that need truncation detection for finite +// objects must add their own authenticated final message. +func NewAEADCryptoReadWriter( + rw io.ReadWriter, + key []byte, + role AEADCryptoRole, + algorithm string, + transcriptHash []byte, +) (io.ReadWriter, error) { + clientToServerKey, serverToClientKey, err := deriveAEADControlKeys(key, algorithm, transcriptHash) + if err != nil { + return nil, err + } + + var readKey, writeKey []byte + switch role { + case AEADCryptoRoleClient: + readKey = serverToClientKey + writeKey = clientToServerKey + case AEADCryptoRoleServer: + readKey = clientToServerKey + writeKey = serverToClientKey + default: + return nil, errors.New("invalid aead crypto role") + } + + encReader, err := libcrypto.NewAEADStreamReader(rw, libcrypto.AEADStreamOptions{ + Algorithm: libcrypto.AEADAlgorithm(algorithm), + Key: readKey, + }) + if err != nil { + return nil, err + } + encWriter, err := libcrypto.NewAEADStreamWriter(rw, libcrypto.AEADStreamOptions{ + Algorithm: libcrypto.AEADAlgorithm(algorithm), + Key: writeKey, + }) + if err != nil { + return nil, err + } + return struct { + io.Reader + io.Writer + }{ + Reader: encReader, + Writer: encWriter, + }, nil +} + +func deriveAEADControlKeys(key []byte, algorithm string, transcriptHash []byte) (clientToServerKey, serverToClientKey []byte, err error) { + clientToServerKey, err = deriveAEADControlKey(key, algorithm, transcriptHash, aeadDirectionClientToServer) + if err != nil { + return nil, nil, err + } + serverToClientKey, err = deriveAEADControlKey(key, algorithm, transcriptHash, aeadDirectionServerToClient) + if err != nil { + return nil, nil, err + } + return clientToServerKey, serverToClientKey, nil +} + +func deriveAEADControlKey(key []byte, algorithm string, transcriptHash []byte, direction string) ([]byte, error) { + info := aeadControlHKDFInfoPrefix + " " + algorithm + " " + direction + return hkdf.Key(sha256.New, key, transcriptHash, info, libcrypto.AEADKeySize) +} diff --git a/pkg/util/net/conn_test.go b/pkg/util/net/conn_test.go new file mode 100644 index 00000000000..93387384bd9 --- /dev/null +++ b/pkg/util/net/conn_test.go @@ -0,0 +1,124 @@ +// 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 net + +import ( + "bytes" + "io" + stdnet "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestNewAEADCryptoReadWriterRoundTrip(t *testing.T) { + clientConn, serverConn := stdnet.Pipe() + defer clientConn.Close() + defer serverConn.Close() + + key := []byte("token") + transcriptHash := bytes.Repeat([]byte{0x11}, 32) + clientRW, err := NewAEADCryptoReadWriter( + clientConn, + key, + AEADCryptoRoleClient, + wire.AEADAlgorithmXChaCha20Poly1305, + transcriptHash, + ) + require.NoError(t, err) + serverRW, err := NewAEADCryptoReadWriter( + serverConn, + key, + AEADCryptoRoleServer, + wire.AEADAlgorithmXChaCha20Poly1305, + transcriptHash, + ) + require.NoError(t, err) + + clientErrCh := make(chan error, 1) + go func() { + if _, err := clientRW.Write([]byte("ping")); err != nil { + clientErrCh <- err + return + } + buf := make([]byte, len("pong")) + _, err := io.ReadFull(clientRW, buf) + clientErrCh <- err + }() + + buf := make([]byte, len("ping")) + _, err = io.ReadFull(serverRW, buf) + require.NoError(t, err) + require.Equal(t, "ping", string(buf)) + _, err = serverRW.Write([]byte("pong")) + require.NoError(t, err) + require.NoError(t, <-clientErrCh) +} + +func TestNewAEADCryptoReadWriterRejectsDifferentTranscript(t *testing.T) { + clientConn, serverConn := stdnet.Pipe() + defer clientConn.Close() + defer serverConn.Close() + require.NoError(t, clientConn.SetDeadline(time.Now().Add(time.Second))) + require.NoError(t, serverConn.SetDeadline(time.Now().Add(time.Second))) + + key := []byte("token") + clientRW, err := NewAEADCryptoReadWriter( + clientConn, + key, + AEADCryptoRoleClient, + wire.AEADAlgorithmAES256GCM, + bytes.Repeat([]byte{0x22}, 32), + ) + require.NoError(t, err) + serverRW, err := NewAEADCryptoReadWriter( + serverConn, + key, + AEADCryptoRoleServer, + wire.AEADAlgorithmAES256GCM, + bytes.Repeat([]byte{0x33}, 32), + ) + require.NoError(t, err) + + writeErrCh := make(chan error, 1) + go func() { + _, err := clientRW.Write([]byte("ping")) + writeErrCh <- err + }() + + buf := make([]byte, len("ping")) + _, err = io.ReadFull(serverRW, buf) + require.Error(t, err) + require.NoError(t, <-writeErrCh) +} + +func TestDeriveAEADControlKeysUsesDistinctDirections(t *testing.T) { + clientToServerKey, serverToClientKey, err := deriveAEADControlKeys( + []byte("token"), + wire.AEADAlgorithmXChaCha20Poly1305, + bytes.Repeat([]byte{0x44}, 32), + ) + require.NoError(t, err) + require.Equal(t, []byte{ + 0xa0, 0x58, 0xcd, 0x02, 0x5d, 0x96, 0x98, 0x5f, + 0xeb, 0xeb, 0xff, 0x79, 0xa1, 0x9f, 0x62, 0xb7, + 0x15, 0xe0, 0x53, 0x91, 0x3d, 0xfc, 0x74, 0x77, + 0x05, 0x91, 0x4c, 0x62, 0x4b, 0xf3, 0xd4, 0x95, + }, clientToServerKey) + require.NotEqual(t, clientToServerKey, serverToClientKey) +} diff --git a/pkg/util/net/dial.go b/pkg/util/net/dial.go index 4f9bae3242e..b0a5e43758e 100644 --- a/pkg/util/net/dial.go +++ b/pkg/util/net/dial.go @@ -5,11 +5,11 @@ import ( "net" "net/url" - libdial "github.com/fatedier/golib/net/dial" + libnet "github.com/fatedier/golib/net" "golang.org/x/net/websocket" ) -func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libdial.AfterHookFunc { +func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libnet.AfterHookFunc { return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) { if enableTLS && !disableCustomTLSHeadByte { _, err := c.Write([]byte{byte(FRPTLSHeadByte)}) @@ -21,21 +21,21 @@ func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) li } } -func DialHookWebsocket(isSecure bool) libdial.AfterHookFunc { +func DialHookWebsocket(protocol string, host string) libnet.AfterHookFunc { return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) { - addrScheme := "ws" - originScheme := "http" - if isSecure { - addrScheme = "wss" - originScheme = "https" + if protocol != "wss" { + protocol = "ws" } - addr = addrScheme + "://" + addr + FrpWebsocketPath + if host == "" { + host = addr + } + addr = protocol + "://" + host + FrpWebsocketPath uri, err := url.Parse(addr) if err != nil { return nil, nil, err } - origin := originScheme + "://" + uri.Host + origin := "http://" + uri.Host cfg, err := websocket.NewConfig(addr, origin) if err != nil { return nil, nil, err @@ -45,6 +45,11 @@ func DialHookWebsocket(isSecure bool) libdial.AfterHookFunc { if err != nil { return nil, nil, err } + // The tunnel payload is a raw byte stream (yamux), not UTF-8 text. + // Send it as binary frames; otherwise RFC 6455-compliant intermediaries + // (e.g. API gateways/reverse proxies) UTF-8-validate the default text + // frames and close the connection on invalid bytes. + conn.PayloadType = websocket.BinaryFrame return ctx, conn, nil } } diff --git a/pkg/util/net/dns.go b/pkg/util/net/dns.go new file mode 100644 index 00000000000..441b1e67a3f --- /dev/null +++ b/pkg/util/net/dns.go @@ -0,0 +1,33 @@ +// Copyright 2023 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 net + +import ( + "context" + "net" +) + +func SetDefaultDNSAddress(dnsAddress string) { + if _, _, err := net.SplitHostPort(dnsAddress); err != nil { + dnsAddress = net.JoinHostPort(dnsAddress, "53") + } + // Change default dns server + net.DefaultResolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, _ string) (net.Conn, error) { + return net.Dial(network, dnsAddress) + }, + } +} diff --git a/pkg/util/net/http.go b/pkg/util/net/http.go index fa1c34afd44..e9fc52609fc 100644 --- a/pkg/util/net/http.go +++ b/pkg/util/net/http.go @@ -19,35 +19,15 @@ import ( "io" "net/http" "strings" -) - -type HTTPAuthWraper struct { - h http.Handler - user string - passwd string -} + "time" -func NewHTTPBasicAuthWraper(h http.Handler, user, passwd string) http.Handler { - return &HTTPAuthWraper{ - h: h, - user: user, - passwd: passwd, - } -} - -func (aw *HTTPAuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) { - user, passwd, hasAuth := r.BasicAuth() - if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) { - aw.h.ServeHTTP(w, r) - } else { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } -} + "github.com/fatedier/frp/pkg/util/util" +) type HTTPAuthMiddleware struct { - user string - passwd string + user string + passwd string + authFailDelay time.Duration } func NewHTTPAuthMiddleware(user, passwd string) *HTTPAuthMiddleware { @@ -57,37 +37,33 @@ func NewHTTPAuthMiddleware(user, passwd string) *HTTPAuthMiddleware { } } +func (authMid *HTTPAuthMiddleware) SetAuthFailDelay(delay time.Duration) *HTTPAuthMiddleware { + authMid.authFailDelay = delay + return authMid +} + func (authMid *HTTPAuthMiddleware) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reqUser, reqPasswd, hasAuth := r.BasicAuth() if (authMid.user == "" && authMid.passwd == "") || - (hasAuth && reqUser == authMid.user && reqPasswd == authMid.passwd) { + (hasAuth && util.ConstantTimeEqString(reqUser, authMid.user) && + util.ConstantTimeEqString(reqPasswd, authMid.passwd)) { next.ServeHTTP(w, r) } else { + if authMid.authFailDelay > 0 { + time.Sleep(authMid.authFailDelay) + } w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) } }) } -func HTTPBasicAuth(h http.HandlerFunc, user, passwd string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - reqUser, reqPasswd, hasAuth := r.BasicAuth() - if (user == "" && passwd == "") || - (hasAuth && reqUser == user && reqPasswd == passwd) { - h.ServeHTTP(w, r) - } else { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } - } -} - -type HTTPGzipWraper struct { +type HTTPGzipWrapper struct { h http.Handler } -func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (gw *HTTPGzipWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { gw.h.ServeHTTP(w, r) return @@ -100,7 +76,7 @@ func (gw *HTTPGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func MakeHTTPGzipHandler(h http.Handler) http.Handler { - return &HTTPGzipWraper{ + return &HTTPGzipWrapper{ h: h, } } diff --git a/pkg/util/net/kcp.go b/pkg/util/net/kcp.go index e9e0b1269b6..b1d1fb93a48 100644 --- a/pkg/util/net/kcp.go +++ b/pkg/util/net/kcp.go @@ -18,7 +18,7 @@ import ( "fmt" "net" - kcp "github.com/fatedier/kcp-go" + kcp "github.com/xtaci/kcp-go/v5" ) type KCPListener struct { @@ -32,8 +32,8 @@ func ListenKcp(address string) (l *KCPListener, err error) { if err != nil { return l, err } - listener.SetReadBuffer(4194304) - listener.SetWriteBuffer(4194304) + _ = listener.SetReadBuffer(4194304) + _ = listener.SetWriteBuffer(4194304) l = &KCPListener{ listener: listener, @@ -85,7 +85,15 @@ func (l *KCPListener) Addr() net.Addr { } func NewKCPConnFromUDP(conn *net.UDPConn, connected bool, raddr string) (net.Conn, error) { - kcpConn, err := kcp.NewConnEx(1, connected, raddr, nil, 10, 3, conn) + udpAddr, err := net.ResolveUDPAddr("udp", raddr) + if err != nil { + return nil, err + } + var pConn net.PacketConn = conn + if connected { + pConn = &ConnectedUDPConn{conn} + } + kcpConn, err := kcp.NewConn3(1, udpAddr, nil, 10, 3, pConn) if err != nil { return nil, err } diff --git a/pkg/util/net/listener.go b/pkg/util/net/listener.go index 3b199c839f0..c3aebcd6f6b 100644 --- a/pkg/util/net/listener.go +++ b/pkg/util/net/listener.go @@ -22,20 +22,21 @@ import ( "github.com/fatedier/golib/errors" ) -// Custom listener -type CustomListener struct { +// InternalListener is a listener that can be used to accept connections from +// other goroutines. +type InternalListener struct { acceptCh chan net.Conn closed bool mu sync.Mutex } -func NewCustomListener() *CustomListener { - return &CustomListener{ - acceptCh: make(chan net.Conn, 64), +func NewInternalListener() *InternalListener { + return &InternalListener{ + acceptCh: make(chan net.Conn, 128), } } -func (l *CustomListener) Accept() (net.Conn, error) { +func (l *InternalListener) Accept() (net.Conn, error) { conn, ok := <-l.acceptCh if !ok { return nil, fmt.Errorf("listener closed") @@ -43,7 +44,7 @@ func (l *CustomListener) Accept() (net.Conn, error) { return conn, nil } -func (l *CustomListener) PutConn(conn net.Conn) error { +func (l *InternalListener) PutConn(conn net.Conn) error { err := errors.PanicToError(func() { select { case l.acceptCh <- conn: @@ -51,10 +52,13 @@ func (l *CustomListener) PutConn(conn net.Conn) error { conn.Close() } }) - return err + if err != nil { + return fmt.Errorf("put conn error: listener is closed") + } + return nil } -func (l *CustomListener) Close() error { +func (l *InternalListener) Close() error { l.mu.Lock() defer l.mu.Unlock() if !l.closed { @@ -64,6 +68,16 @@ func (l *CustomListener) Close() error { return nil } -func (l *CustomListener) Addr() net.Addr { - return (*net.TCPAddr)(nil) +func (l *InternalListener) Addr() net.Addr { + return &InternalAddr{} +} + +type InternalAddr struct{} + +func (ia *InternalAddr) Network() string { + return "internal" +} + +func (ia *InternalAddr) String() string { + return "internal" } diff --git a/pkg/util/net/proxyprotocol.go b/pkg/util/net/proxyprotocol.go new file mode 100644 index 00000000000..5f0cd51fab8 --- /dev/null +++ b/pkg/util/net/proxyprotocol.go @@ -0,0 +1,45 @@ +// Copyright 2025 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 net + +import ( + "bytes" + "fmt" + "net" + + pp "github.com/pires/go-proxyproto" +) + +func BuildProxyProtocolHeaderStruct(srcAddr, dstAddr net.Addr, version string) *pp.Header { + var versionByte byte + if version == "v1" { + versionByte = 1 + } else { + versionByte = 2 // default to v2 + } + return pp.HeaderProxyFromAddrs(versionByte, srcAddr, dstAddr) +} + +func BuildProxyProtocolHeader(srcAddr, dstAddr net.Addr, version string) ([]byte, error) { + h := BuildProxyProtocolHeaderStruct(srcAddr, dstAddr, version) + + // Convert header to bytes using a buffer + var buf bytes.Buffer + _, err := h.WriteTo(&buf) + if err != nil { + return nil, fmt.Errorf("failed to write proxy protocol header: %v", err) + } + return buf.Bytes(), nil +} diff --git a/pkg/util/net/proxyprotocol_test.go b/pkg/util/net/proxyprotocol_test.go new file mode 100644 index 00000000000..187801f67aa --- /dev/null +++ b/pkg/util/net/proxyprotocol_test.go @@ -0,0 +1,178 @@ +package net + +import ( + "net" + "testing" + + pp "github.com/pires/go-proxyproto" + "github.com/stretchr/testify/require" +) + +func TestBuildProxyProtocolHeader(t *testing.T) { + require := require.New(t) + + tests := []struct { + name string + srcAddr net.Addr + dstAddr net.Addr + version string + expectError bool + }{ + { + name: "UDP IPv4 v2", + srcAddr: &net.UDPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectError: false, + }, + { + name: "TCP IPv4 v1", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}, + version: "v1", + expectError: false, + }, + { + name: "UDP IPv6 v2", + srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306}, + version: "v2", + expectError: false, + }, + { + name: "TCP IPv6 v1", + srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80}, + version: "v1", + expectError: false, + }, + { + name: "nil source address", + srcAddr: nil, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectError: false, + }, + { + name: "nil destination address", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: nil, + version: "v2", + expectError: false, + }, + { + name: "unsupported address type", + srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectError: false, + }, + } + + for _, tt := range tests { + header, err := BuildProxyProtocolHeader(tt.srcAddr, tt.dstAddr, tt.version) + + if tt.expectError { + require.Error(err, "test case: %s", tt.name) + continue + } + + require.NoError(err, "test case: %s", tt.name) + require.NotEmpty(header, "test case: %s", tt.name) + } +} + +func TestBuildProxyProtocolHeaderStruct(t *testing.T) { + require := require.New(t) + + tests := []struct { + name string + srcAddr net.Addr + dstAddr net.Addr + version string + expectedProtocol pp.AddressFamilyAndProtocol + expectedVersion byte + expectedCommand pp.ProtocolVersionAndCommand + expectedSourceAddr net.Addr + expectedDestAddr net.Addr + }{ + { + name: "TCP IPv4 v2", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}, + version: "v2", + expectedProtocol: pp.TCPv4, + expectedVersion: 2, + expectedCommand: pp.PROXY, + expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}, + }, + { + name: "UDP IPv6 v1", + srcAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306}, + version: "v1", + expectedProtocol: pp.UDPv6, + expectedVersion: 1, + expectedCommand: pp.PROXY, + expectedSourceAddr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 12345}, + expectedDestAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 3306}, + }, + { + name: "TCP IPv6 default version", + srcAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345}, + dstAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80}, + version: "", + expectedProtocol: pp.TCPv6, + expectedVersion: 2, // default to v2 + expectedCommand: pp.PROXY, + expectedSourceAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 12345}, + expectedDestAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 80}, + }, + { + name: "nil source address", + srcAddr: nil, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectedProtocol: pp.UNSPEC, + expectedVersion: 2, + expectedCommand: pp.LOCAL, + expectedSourceAddr: nil, // go-proxyproto sets both to nil when srcAddr is nil + expectedDestAddr: nil, + }, + { + name: "nil destination address", + srcAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 12345}, + dstAddr: nil, + version: "v2", + expectedProtocol: pp.UNSPEC, + expectedVersion: 2, + expectedCommand: pp.LOCAL, + expectedSourceAddr: nil, // go-proxyproto sets both to nil when dstAddr is nil + expectedDestAddr: nil, + }, + { + name: "unsupported address type", + srcAddr: &net.UnixAddr{Name: "/tmp/test.sock", Net: "unix"}, + dstAddr: &net.UDPAddr{IP: net.ParseIP("10.0.0.1"), Port: 3306}, + version: "v2", + expectedProtocol: pp.UNSPEC, + expectedVersion: 2, + expectedCommand: pp.LOCAL, + expectedSourceAddr: nil, // go-proxyproto sets both to nil for unsupported types + expectedDestAddr: nil, + }, + } + + for _, tt := range tests { + header := BuildProxyProtocolHeaderStruct(tt.srcAddr, tt.dstAddr, tt.version) + + require.NotNil(header, "test case: %s", tt.name) + + require.Equal(tt.expectedCommand, header.Command, "test case: %s", tt.name) + require.Equal(tt.expectedSourceAddr, header.SourceAddr, "test case: %s", tt.name) + require.Equal(tt.expectedDestAddr, header.DestinationAddr, "test case: %s", tt.name) + require.Equal(tt.expectedProtocol, header.TransportProtocol, "test case: %s", tt.name) + require.Equal(tt.expectedVersion, header.Version, "test case: %s", tt.name) + } +} diff --git a/pkg/util/net/tls.go b/pkg/util/net/tls.go index 1bae196adb8..6645dfaf546 100644 --- a/pkg/util/net/tls.go +++ b/pkg/util/net/tls.go @@ -20,35 +20,33 @@ import ( "net" "time" - gnet "github.com/fatedier/golib/net" + libnet "github.com/fatedier/golib/net" ) -var ( - FRPTLSHeadByte = 0x17 -) +var FRPTLSHeadByte = 0x17 func CheckAndEnableTLSServerConnWithTimeout( c net.Conn, tlsConfig *tls.Config, tlsOnly bool, timeout time.Duration, ) (out net.Conn, isTLS bool, custom bool, err error) { - - sc, r := gnet.NewSharedConnSize(c, 2) + sc, r := libnet.NewSharedConnSize(c, 2) buf := make([]byte, 1) var n int - c.SetReadDeadline(time.Now().Add(timeout)) + _ = c.SetReadDeadline(time.Now().Add(timeout)) n, err = r.Read(buf) - c.SetReadDeadline(time.Time{}) + _ = c.SetReadDeadline(time.Time{}) if err != nil { return } - if n == 1 && int(buf[0]) == FRPTLSHeadByte { + switch { + case n == 1 && int(buf[0]) == FRPTLSHeadByte: out = tls.Server(c, tlsConfig) isTLS = true custom = true - } else if n == 1 && int(buf[0]) == 0x16 { + case n == 1 && int(buf[0]) == 0x16: out = tls.Server(sc, tlsConfig) isTLS = true - } else { + default: if tlsOnly { err = fmt.Errorf("non-TLS connection received on a TlsOnly server") return diff --git a/pkg/util/net/udp.go b/pkg/util/net/udp.go index 6689732e40c..aa4d9834227 100644 --- a/pkg/util/net/udp.go +++ b/pkg/util/net/udp.go @@ -55,7 +55,7 @@ func NewFakeUDPConn(l *UDPListener, laddr, raddr net.Addr) *FakeUDPConn { for { time.Sleep(5 * time.Second) fc.mu.RLock() - if time.Now().Sub(fc.lastActive) > 10*time.Second { + if time.Since(fc.lastActive) > 10*time.Second { fc.mu.RUnlock() fc.Close() break @@ -68,8 +68,7 @@ func NewFakeUDPConn(l *UDPListener, laddr, raddr net.Addr) *FakeUDPConn { func (c *FakeUDPConn) putPacket(content []byte) { defer func() { - if err := recover(); err != nil { - } + _ = recover() }() select { @@ -87,11 +86,7 @@ func (c *FakeUDPConn) Read(b []byte) (n int, err error) { c.lastActive = time.Now() c.mu.Unlock() - if len(b) < len(content) { - n = len(b) - } else { - n = len(content) - } + n = min(len(b), len(content)) copy(b, content) return n, nil } @@ -109,7 +104,7 @@ func (c *FakeUDPConn) Write(b []byte) (n int, err error) { LocalAddr: c.localAddr, RemoteAddr: c.remoteAddr, } - c.l.writeUDPPacket(packet) + _ = c.l.writeUDPPacket(packet) c.mu.Lock() c.lastActive = time.Now() @@ -141,15 +136,15 @@ func (c *FakeUDPConn) RemoteAddr() net.Addr { return c.remoteAddr } -func (c *FakeUDPConn) SetDeadline(t time.Time) error { +func (c *FakeUDPConn) SetDeadline(_ time.Time) error { return nil } -func (c *FakeUDPConn) SetReadDeadline(t time.Time) error { +func (c *FakeUDPConn) SetReadDeadline(_ time.Time) error { return nil } -func (c *FakeUDPConn) SetWriteDeadline(t time.Time) error { +func (c *FakeUDPConn) SetWriteDeadline(_ time.Time) error { return nil } @@ -169,11 +164,15 @@ func ListenUDP(bindAddr string, bindPort int) (l *UDPListener, err error) { return l, err } readConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return l, err + } l = &UDPListener{ addr: udpAddr, acceptCh: make(chan net.Conn), writeCh: make(chan *UDPPacket, 1000), + readConn: readConn, fakeConns: make(map[string]*FakeUDPConn), } @@ -208,7 +207,7 @@ func ListenUDP(bindAddr string, bindPort int) (l *UDPListener, err error) { } if addr, ok := packet.RemoteAddr.(*net.UDPAddr); ok { - readConn.WriteToUDP(packet.Buf, addr) + _, _ = readConn.WriteToUDP(packet.Buf, addr) } } }() @@ -257,3 +256,11 @@ func (l *UDPListener) Close() error { func (l *UDPListener) Addr() net.Addr { return l.addr } + +// ConnectedUDPConn is a wrapper for net.UDPConn which converts WriteTo syscalls +// to Write syscalls that are 4 times faster on some OS'es. This should only be +// used for connections that were produced by a net.Dial* call. +type ConnectedUDPConn struct{ *net.UDPConn } + +// WriteTo redirects all writes to the Write syscall, which is 4 times faster. +func (c *ConnectedUDPConn) WriteTo(b []byte, _ net.Addr) (int, error) { return c.Write(b) } diff --git a/pkg/util/net/websocket.go b/pkg/util/net/websocket.go index 4ec5c9fe079..57641b31186 100644 --- a/pkg/util/net/websocket.go +++ b/pkg/util/net/websocket.go @@ -4,14 +4,12 @@ import ( "errors" "net" "net/http" - "strconv" + "time" "golang.org/x/net/websocket" ) -var ( - ErrWebsocketListenerClosed = errors.New("websocket listener closed") -) +var ErrWebsocketListenerClosed = errors.New("websocket listener closed") const ( FrpWebsocketPath = "/~!frp" @@ -21,21 +19,26 @@ type WebsocketListener struct { ln net.Listener acceptCh chan net.Conn - server *http.Server - httpMutex *http.ServeMux + server *http.Server } // NewWebsocketListener to handle websocket connections // ln: tcp listener for websocket connections func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) { wl = &WebsocketListener{ + ln: ln, acceptCh: make(chan net.Conn), } muxer := http.NewServeMux() muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) { + // The tunnel payload is a raw byte stream (yamux), not UTF-8 text. + // Send it as binary frames; otherwise RFC 6455-compliant intermediaries + // (e.g. API gateways/reverse proxies) UTF-8-validate the default text + // frames and close the connection on invalid bytes. + c.PayloadType = websocket.BinaryFrame notifyCh := make(chan struct{}) - conn := WrapCloseNotifyConn(c, func() { + conn := WrapCloseNotifyConn(c, func(_ error) { close(notifyCh) }) wl.acceptCh <- conn @@ -43,23 +46,17 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) { })) wl.server = &http.Server{ - Addr: ln.Addr().String(), - Handler: muxer, + Addr: ln.Addr().String(), + Handler: muxer, + ReadHeaderTimeout: 60 * time.Second, } - go wl.server.Serve(ln) + go func() { + _ = wl.server.Serve(ln) + }() return } -func ListenWebsocket(bindAddr string, bindPort int) (*WebsocketListener, error) { - tcpLn, err := net.Listen("tcp", net.JoinHostPort(bindAddr, strconv.Itoa(bindPort))) - if err != nil { - return nil, err - } - l := NewWebsocketListener(tcpLn) - return l, nil -} - func (p *WebsocketListener) Accept() (net.Conn, error) { c, ok := <-p.acceptCh if !ok { diff --git a/pkg/util/system/system.go b/pkg/util/system/system.go new file mode 100644 index 00000000000..ae40e85e214 --- /dev/null +++ b/pkg/util/system/system.go @@ -0,0 +1,22 @@ +// Copyright 2024 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 !android + +package system + +// EnableCompatibilityMode enables compatibility mode for different system. +// For example, on Android, the inability to obtain the correct time zone will result in incorrect log time output. +func EnableCompatibilityMode() { +} diff --git a/pkg/util/system/system_android.go b/pkg/util/system/system_android.go new file mode 100644 index 00000000000..6fcfdbc1361 --- /dev/null +++ b/pkg/util/system/system_android.go @@ -0,0 +1,70 @@ +// Copyright 2024 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 system + +import ( + "context" + "net" + "os/exec" + "strings" + "time" +) + +func EnableCompatibilityMode() { + fixTimezone() + fixDNSResolver() +} + +// fixTimezone is used to try our best to fix timezone issue on some Android devices. +func fixTimezone() { + out, err := exec.Command("/system/bin/getprop", "persist.sys.timezone").Output() + if err != nil { + return + } + loc, err := time.LoadLocation(strings.TrimSpace(string(out))) + if err != nil { + return + } + time.Local = loc +} + +// fixDNSResolver will first attempt to resolve google.com to check if the current DNS is available. +// If it is not available, it will default to using 8.8.8.8 as the DNS server. +// This is a workaround for the issue that golang can't get the default DNS servers on Android. +func fixDNSResolver() { + // First, we attempt to resolve a domain. If resolution is successful, no modifications are necessary. + // In real-world scenarios, users may have already configured /etc/resolv.conf, or compiled directly + // in the Android environment instead of using cross-platform compilation, so this issue does not arise. + if net.DefaultResolver != nil { + timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + _, err := net.DefaultResolver.LookupHost(timeoutCtx, "google.com") + if err == nil { + return + } + } + // If the resolution fails, use 8.8.8.8 as the DNS server. + // Note: If there are other methods to obtain the default DNS servers, the default DNS servers should be used preferentially. + net.DefaultResolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, addr string) (net.Conn, error) { + if addr == "127.0.0.1:53" || addr == "[::1]:53" { + addr = "8.8.8.8:53" + } + var d net.Dialer + return d.DialContext(ctx, network, addr) + }, + } +} diff --git a/pkg/util/tcpmux/httpconnect.go b/pkg/util/tcpmux/httpconnect.go index 2639c493644..d43b5904e4e 100644 --- a/pkg/util/tcpmux/httpconnect.go +++ b/pkg/util/tcpmux/httpconnect.go @@ -22,26 +22,34 @@ import ( "net/http" "time" - "github.com/fatedier/frp/pkg/util/util" + libnet "github.com/fatedier/golib/net" + + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/vhost" - gnet "github.com/fatedier/golib/net" ) type HTTPConnectTCPMuxer struct { *vhost.Muxer - passthrough bool - authRequired bool // Not supported until we really need this. + // If passthrough is set to true, the CONNECT request will be forwarded to the backend service. + // Otherwise, it will return an OK response to the client and forward the remaining content to the backend service. + passthrough bool } func NewHTTPConnectTCPMuxer(listener net.Listener, passthrough bool, timeout time.Duration) (*HTTPConnectTCPMuxer, error) { - ret := &HTTPConnectTCPMuxer{passthrough: passthrough, authRequired: false} - mux, err := vhost.NewMuxer(listener, ret.getHostFromHTTPConnect, nil, ret.sendConnectResponse, nil, timeout) + ret := &HTTPConnectTCPMuxer{passthrough: passthrough} + mux, err := vhost.NewMuxer(listener, ret.getHostFromHTTPConnect, timeout) + if err != nil { + return nil, err + } + mux.SetCheckAuthFunc(ret.auth). + SetSuccessHookFunc(ret.sendConnectResponse). + SetFailHookFunc(vhostFailed) ret.Muxer = mux - return ret, err + return ret, nil } -func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host string, httpUser string, err error) { +func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host, httpUser, httpPwd string, err error) { bufioReader := bufio.NewReader(rd) req, err := http.ReadRequest(bufioReader) @@ -54,26 +62,54 @@ func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host str return } - host, _ = util.CanonicalHost(req.Host) + host, _ = httppkg.CanonicalHost(req.Host) proxyAuth := req.Header.Get("Proxy-Authorization") if proxyAuth != "" { - httpUser, _, _ = util.ParseBasicAuth(proxyAuth) + httpUser, httpPwd, _ = httppkg.ParseBasicAuth(proxyAuth) } return } -func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, reqInfo map[string]string) error { +func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, _ map[string]string) error { if muxer.passthrough { return nil } - return util.OkResponse().Write(c) + res := httppkg.OkResponse() + if res.Body != nil { + defer res.Body.Close() + } + return res.Write(c) +} + +func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, reqInfo map[string]string) (bool, error) { + reqUsername := reqInfo["HTTPUser"] + reqPassword := reqInfo["HTTPPwd"] + if username == reqUsername && password == reqPassword { + return true, nil + } + + resp := httppkg.ProxyUnauthorizedResponse() + if resp.Body != nil { + defer resp.Body.Close() + } + _ = resp.Write(c) + return false, nil +} + +func vhostFailed(c net.Conn) { + res := vhost.NotFoundResponse() + if res.Body != nil { + defer res.Body.Close() + } + _ = res.Write(c) + _ = c.Close() } func (muxer *HTTPConnectTCPMuxer) getHostFromHTTPConnect(c net.Conn) (net.Conn, map[string]string, error) { reqInfoMap := make(map[string]string, 0) - sc, rd := gnet.NewSharedConn(c) + sc, rd := libnet.NewSharedConn(c) - host, httpUser, err := muxer.readHTTPConnectRequest(rd) + host, httpUser, httpPwd, err := muxer.readHTTPConnectRequest(rd) if err != nil { return nil, reqInfoMap, err } @@ -81,14 +117,11 @@ func (muxer *HTTPConnectTCPMuxer) getHostFromHTTPConnect(c net.Conn) (net.Conn, reqInfoMap["Host"] = host reqInfoMap["Scheme"] = "tcp" reqInfoMap["HTTPUser"] = httpUser + reqInfoMap["HTTPPwd"] = httpPwd - var outConn net.Conn = c + outConn := c if muxer.passthrough { outConn = sc - if muxer.authRequired && httpUser == "" { - util.ProxyUnauthorizedResponse().Write(c) - outConn = c - } } return outConn, reqInfoMap, nil } diff --git a/pkg/util/util/types.go b/pkg/util/util/types.go new file mode 100644 index 00000000000..784b8f193ad --- /dev/null +++ b/pkg/util/util/types.go @@ -0,0 +1,23 @@ +// Copyright 2023 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 util + +func EmptyOr[T comparable](v T, fallback T) T { + var zero T + if zero == v { + return fallback + } + return v +} diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 032675fb761..3199748516d 100644 --- a/pkg/util/util/util.go +++ b/pkg/util/util/util.go @@ -17,9 +17,10 @@ package util import ( "crypto/md5" "crypto/rand" + "crypto/subtle" "encoding/hex" "fmt" - mathrand "math/rand" + mathrand "math/rand/v2" "net" "strconv" "strings" @@ -28,25 +29,28 @@ import ( // RandID return a rand string used in frp. func RandID() (id string, err error) { - return RandIDWithLen(8) + return RandIDWithLen(16) } // RandIDWithLen return a rand string with idLen length. func RandIDWithLen(idLen int) (id string, err error) { - b := make([]byte, idLen) + if idLen <= 0 { + return "", nil + } + b := make([]byte, idLen/2+1) _, err = rand.Read(b) if err != nil { return } id = fmt.Sprintf("%x", b) - return + return id[:idLen], nil } func GetAuthKey(token string, timestamp int64) (key string) { - token = token + fmt.Sprintf("%d", timestamp) md5Ctx := md5.New() md5Ctx.Write([]byte(token)) + md5Ctx.Write([]byte(strconv.FormatInt(timestamp, 10))) data := md5Ctx.Sum(nil) return hex.EncodeToString(data) } @@ -64,13 +68,14 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) { rangeStr = strings.TrimSpace(rangeStr) numbers = make([]int64, 0) // e.g. 1000-2000,2001,2002,3000-4000 - numRanges := strings.Split(rangeStr, ",") - for _, numRangeStr := range numRanges { + numRanges := strings.SplitSeq(rangeStr, ",") + for numRangeStr := range numRanges { // 1000-2000 or 2001 numArray := strings.Split(numRangeStr, "-") // length: only 1 or 2 is correct rangeType := len(numArray) - if rangeType == 1 { + switch rangeType { + case 1: // single number singleNum, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) if errRet != nil { @@ -78,26 +83,26 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) { return } numbers = append(numbers, singleNum) - } else if rangeType == 2 { + case 2: // range numbers - min, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + minValue, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) if errRet != nil { err = fmt.Errorf("range number is invalid, %v", errRet) return } - max, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) + maxValue, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) if errRet != nil { err = fmt.Errorf("range number is invalid, %v", errRet) return } - if max < min { + if maxValue < minValue { err = fmt.Errorf("range number is invalid") return } - for i := min; i <= max; i++ { + for i := minValue; i <= maxValue; i++ { numbers = append(numbers, i) } - } else { + default: err = fmt.Errorf("range number is invalid") return } @@ -113,15 +118,28 @@ func GenerateResponseErrorString(summary string, err error, detailed bool) strin } func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Duration { - min := int64(minRatio * 1000.0) - max := int64(maxRatio * 1000.0) + minValue := int64(minRatio * 1000.0) + maxValue := int64(maxRatio * 1000.0) var n int64 - if max <= min { - n = min + if maxValue <= minValue { + n = minValue } else { - n = mathrand.Int63n(max-min) + min + n = mathrand.Int64N(maxValue-minValue) + minValue } d := duration * time.Duration(n) / time.Duration(1000) time.Sleep(d) return d } + +func ConstantTimeEqString(a, b string) bool { + return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 +} + +// ClonePtr returns a pointer to a copied value. If v is nil, it returns nil. +func ClonePtr[T any](v *T) *T { + if v == nil { + return nil + } + out := *v + return &out +} diff --git a/pkg/util/util/util_test.go b/pkg/util/util/util_test.go index 311732b421f..bd53f48ea2c 100644 --- a/pkg/util/util/util_test.go +++ b/pkg/util/util/util_test.go @@ -3,46 +3,54 @@ package util import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRandId(t *testing.T) { - assert := assert.New(t) + require := require.New(t) id, err := RandID() - assert.NoError(err) + require.NoError(err) t.Log(id) - assert.Equal(16, len(id)) + require.Equal(16, len(id)) } func TestGetAuthKey(t *testing.T) { - assert := assert.New(t) + require := require.New(t) key := GetAuthKey("1234", 1488720000) - t.Log(key) - assert.Equal("6df41a43725f0c770fd56379e12acf8c", key) + require.Equal("6df41a43725f0c770fd56379e12acf8c", key) } func TestParseRangeNumbers(t *testing.T) { - assert := assert.New(t) + require := require.New(t) numbers, err := ParseRangeNumbers("2-5") - if assert.NoError(err) { - assert.Equal([]int64{2, 3, 4, 5}, numbers) - } + require.NoError(err) + require.Equal([]int64{2, 3, 4, 5}, numbers) numbers, err = ParseRangeNumbers("1") - if assert.NoError(err) { - assert.Equal([]int64{1}, numbers) - } + require.NoError(err) + require.Equal([]int64{1}, numbers) numbers, err = ParseRangeNumbers("3-5,8") - if assert.NoError(err) { - assert.Equal([]int64{3, 4, 5, 8}, numbers) - } + require.NoError(err) + require.Equal([]int64{3, 4, 5, 8}, numbers) numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ") - if assert.NoError(err) { - assert.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers) - } + require.NoError(err) + require.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers) _, err = ParseRangeNumbers("3-a") - assert.Error(err) + require.Error(err) +} + +func TestClonePtr(t *testing.T) { + require := require.New(t) + + var nilInt *int + require.Nil(ClonePtr(nilInt)) + + v := 42 + cloned := ClonePtr(&v) + require.NotNil(cloned) + require.Equal(v, *cloned) + require.NotSame(&v, cloned) } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index b5eca31735b..5c232712bbf 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,69 +14,8 @@ package version -import ( - "strconv" - "strings" -) - -var version string = "0.44.0" +var version = "0.71.0" func Full() string { return version } - -func getSubVersion(v string, position int) int64 { - arr := strings.Split(v, ".") - if len(arr) < 3 { - return 0 - } - res, _ := strconv.ParseInt(arr[position], 10, 64) - return res -} - -func Proto(v string) int64 { - return getSubVersion(v, 0) -} - -func Major(v string) int64 { - return getSubVersion(v, 1) -} - -func Minor(v string) int64 { - return getSubVersion(v, 2) -} - -// add every case there if server will not accept client's protocol and return false -func Compat(client string) (ok bool, msg string) { - if LessThan(client, "0.18.0") { - return false, "Please upgrade your frpc version to at least 0.18.0" - } - return true, "" -} - -func LessThan(client string, server string) bool { - vc := Proto(client) - vs := Proto(server) - if vc > vs { - return false - } else if vc < vs { - return true - } - - vc = Major(client) - vs = Major(server) - if vc > vs { - return false - } else if vc < vs { - return true - } - - vc = Minor(client) - vs = Minor(server) - if vc > vs { - return false - } else if vc < vs { - return true - } - return false -} diff --git a/pkg/util/version/version_test.go b/pkg/util/version/version_test.go deleted file mode 100644 index a77bf421832..00000000000 --- a/pkg/util/version/version_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2016 fatedier, fatedier@gmail.com -// -// 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 version - -import ( - "fmt" - "strconv" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFull(t *testing.T) { - assert := assert.New(t) - version := Full() - arr := strings.Split(version, ".") - assert.Equal(3, len(arr)) - - proto, err := strconv.ParseInt(arr[0], 10, 64) - assert.NoError(err) - assert.True(proto >= 0) - - major, err := strconv.ParseInt(arr[1], 10, 64) - assert.NoError(err) - assert.True(major >= 0) - - minor, err := strconv.ParseInt(arr[2], 10, 64) - assert.NoError(err) - assert.True(minor >= 0) -} - -func TestVersion(t *testing.T) { - assert := assert.New(t) - proto := Proto(Full()) - major := Major(Full()) - minor := Minor(Full()) - parseVerion := fmt.Sprintf("%d.%d.%d", proto, major, minor) - version := Full() - assert.Equal(parseVerion, version) -} - -func TestCompact(t *testing.T) { - assert := assert.New(t) - ok, _ := Compat("0.9.0") - assert.False(ok) - - ok, _ = Compat("10.0.0") - assert.True(ok) - - ok, _ = Compat("0.10.0") - assert.False(ok) -} diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index b9555ba9879..bfd3976ed06 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -15,35 +15,33 @@ package vhost import ( - "bytes" "context" "encoding/base64" "errors" "fmt" - "log" + stdlog "log" "net" "net/http" + "net/http/httputil" "net/url" "strings" "time" - frpLog "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/pkg/util/util" - frpIo "github.com/fatedier/golib/io" - + libio "github.com/fatedier/golib/io" "github.com/fatedier/golib/pool" -) -var ( - ErrNoRouteFound = errors.New("no route found") + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/pkg/util/log" ) +var ErrNoRouteFound = errors.New("no route found") + type HTTPReverseProxyOptions struct { ResponseHeaderTimeoutS int64 } type HTTPReverseProxy struct { - proxy *ReverseProxy + proxy http.Handler vhostRouter *Routers responseHeaderTimeout time.Duration @@ -57,23 +55,35 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * responseHeaderTimeout: time.Duration(option.ResponseHeaderTimeoutS) * time.Second, vhostRouter: vhostRouter, } - proxy := &ReverseProxy{ + proxy := &httputil.ReverseProxy{ // Modify incoming requests by route policies. - Director: func(req *http.Request) { + Rewrite: func(r *httputil.ProxyRequest) { + r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] + r.SetXForwarded() + req := r.Out req.URL.Scheme = "http" - url := req.Context().Value(RouteInfoURL).(string) - routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string) - oldHost, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string)) - rc := rp.GetRouteConfig(oldHost, url, routeByHTTPUser) + reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo) + originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host) + + rc := req.Context().Value(RouteConfigKey).(*RouteConfig) if rc != nil { if rc.RewriteHost != "" { req.Host = rc.RewriteHost } - // Set {domain}.{location}.{routeByHTTPUser} as URL host here to let http transport reuse connections. - // TODO(fatedier): use proxy name instead? + + var endpoint string + if rc.ChooseEndpointFn != nil { + // ignore error here, it will use CreateConnFn instead later + endpoint, _ = rc.ChooseEndpointFn() + reqRouteInfo.Endpoint = endpoint + log.Tracef("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]", + endpoint, originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) + } + // Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections. req.URL.Host = rc.Domain + "." + base64.StdEncoding.EncodeToString([]byte(rc.Location)) + "." + - base64.StdEncoding.EncodeToString([]byte(rc.RouteByHTTPUser)) + base64.StdEncoding.EncodeToString([]byte(rc.RouteByHTTPUser)) + "." + + base64.StdEncoding.EncodeToString([]byte(endpoint)) for k, v := range rc.Headers { req.Header.Set(k, v) @@ -81,18 +91,23 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * } else { req.URL.Host = req.Host } - + }, + ModifyResponse: func(r *http.Response) error { + rc := r.Request.Context().Value(RouteConfigKey).(*RouteConfig) + if rc != nil { + for k, v := range rc.ResponseHeaders { + r.Header.Set(k, v) + } + } + return nil }, // Create a connection to one proxy routed by route policy. Transport: &http.Transport{ ResponseHeaderTimeout: rp.responseHeaderTimeout, IdleConnTimeout: 60 * time.Second, + MaxIdleConnsPerHost: 5, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - url := ctx.Value(RouteInfoURL).(string) - host, _ := util.CanonicalHost(ctx.Value(RouteInfoHost).(string)) - routerByHTTPUser := ctx.Value(RouteInfoHTTPUser).(string) - remote := ctx.Value(RouteInfoRemote).(string) - return rp.CreateConnection(host, url, routerByHTTPUser, remote) + return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true) }, Proxy: func(req *http.Request) (*url.URL, error) { // Use proxy mode if there is host in HTTP first request line. @@ -102,19 +117,25 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) * // Normal: // GET / HTTP/1.1 // Host: example.com - urlHost := req.Context().Value(RouteInfoURLHost).(string) + urlHost := req.Context().Value(RouteInfoKey).(*RequestRouteInfo).URLHost if urlHost != "" { return req.URL, nil } return nil, nil }, }, - BufferPool: newWrapPool(), - ErrorLog: log.New(newWrapLogger(), "", 0), + BufferPool: pool.NewBuffer(32 * 1024), + ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0), ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { - frpLog.Warn("do http proxy request [host: %s] error: %v", req.Host, err) + log.Logf(log.WarnLevel, 1, "do http proxy request [host: %s] error: %v", req.Host, err) + if err != nil { + if e, ok := err.(net.Error); ok && e.Timeout() { + rw.WriteHeader(http.StatusGatewayTimeout) + return + } + } rw.WriteHeader(http.StatusNotFound) - rw.Write(getNotFoundPageContent()) + _, _ = rw.Write(getNotFoundPageContent()) }, } rp.proxy = proxy @@ -136,121 +157,71 @@ func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) { rp.vhostRouter.Del(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser) } -func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig { - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) - if ok { - frpLog.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) - return vr.payload.(*RouteConfig) +// CheckClientOriginIPAddr checks addr against the ip allow list configured for the matched route. +// To prevent IP spoofing, be sure to delete any pre-existing X-Forwarded-For header coming from +// the client or an untrusted proxy before it reaches frps. +func (rp *HTTPReverseProxy) CheckClientOriginIPAddr(domain, location, routeByHTTPUser, addr string) bool { + if addr != "" { + // Selecting the first ip in the list; it's safe to take it once we ensure the first ip + // cannot be set by untrusted proxies or the client. + if ips := strings.Split(addr, ", "); len(ips) > 1 { + addr = ips[0] + } } - return nil -} - -func (rp *HTTPReverseProxy) GetRealHost(domain, location, routeByHTTPUser string) (host string) { - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) - if ok { - host = vr.payload.(*RouteConfig).RewriteHost + vr, ok := rp.vhostRouter.getByRoute(domain, location, routeByHTTPUser) + if ok && vr.ipFilter != nil { + return vr.ipFilter.Allowed(addr) } - return + return true } -func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) { - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) +func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig { + vr, ok := rp.vhostRouter.getByRoute(domain, location, routeByHTTPUser) if ok { - headers = vr.payload.(*RouteConfig).Headers + log.Debugf("get new http request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser) + return vr.payload.(*RouteConfig) } - return + return nil } // CreateConnection create a new connection by route config -func (rp *HTTPReverseProxy) CreateConnection(domain, location, routeByHTTPUser string, remoteAddr string) (net.Conn, error) { - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) +func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) { + host, _ := httppkg.CanonicalHost(reqRouteInfo.Host) + vr, ok := rp.vhostRouter.getByRoute(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser) if ok { + if byEndpoint { + fn := vr.payload.(*RouteConfig).CreateConnByEndpointFn + if fn != nil { + return fn(reqRouteInfo.Endpoint, reqRouteInfo.RemoteAddr) + } + } fn := vr.payload.(*RouteConfig).CreateConnFn if fn != nil { - return fn(remoteAddr) - } - } - return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, domain, location, routeByHTTPUser) -} - -func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool { - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) - if ok { - checkUser := vr.payload.(*RouteConfig).Username - checkPasswd := vr.payload.(*RouteConfig).Password - if (checkUser != "" || checkPasswd != "") && (checkUser != user || checkPasswd != passwd) { - return false - } - } - return true -} - -// CheckClientOriginIpAddr to prevent IP spoofing, be sure to delete any pre-existing X-Forwarded-For header coming from the client or an untrusted proxy. -func (rp *HTTPReverseProxy) CheckClientOriginIpAddr(domain, location, routeByHTTPUser, addr string) bool { - if addr != "" { - frpLog.Debug("Received client ip addr: %s", addr) - ips := strings.Split(addr, ", ") - if len(ips) > 1 { - // Selecting the first ip in the list, it's safe to take it once we ensured the first ip cannot be set by untrusted proxies or the client - addr = ips[0] - } - } - vr, ok := rp.getVhost(domain, location, routeByHTTPUser) - if ok { - if vr.ipFilter != nil { - frpLog.Debug("validating client origin ip %s", addr) - return vr.ipFilter.Allowed(addr) + return fn(reqRouteInfo.RemoteAddr) } } - return true + return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser) } -// getVhost trys to get vhost router by route policy. -func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) { - findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) { - vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser) - if ok { - return vr, ok - } - // Try to check if there is one proxy that doesn't specify routerByHTTPUser, it means match all. - vr, ok = rp.vhostRouter.Get(inDomain, inLocation, "") - if ok { - return vr, ok - } - return nil, false +func checkRouteAuthByRequest(req *http.Request, rc *RouteConfig) bool { + if rc == nil { + return true } - - // First we check the full hostname - // if not exist, then check the wildcard_domain such as *.example.com - vr, ok := findRouter(domain, location, routeByHTTPUser) - if ok { - return vr, ok + if rc.Username == "" && rc.Password == "" { + return true } - // e.g. domain = test.example.com, try to match wildcard domains. - // *.example.com - // *.com - domainSplit := strings.Split(domain, ".") - for { - if len(domainSplit) < 3 { - break - } - - domainSplit[0] = "*" - domain = strings.Join(domainSplit, ".") - vr, ok = findRouter(domain, location, routeByHTTPUser) - if ok { - return vr, true + if req.URL.Host != "" { + proxyAuth := req.Header.Get("Proxy-Authorization") + if proxyAuth == "" { + return false } - domainSplit = domainSplit[1:] + user, passwd, ok := httppkg.ParseBasicAuth(proxyAuth) + return ok && user == rc.Username && passwd == rc.Password } - // Finally, try to check if there is one proxy that domain is "*" means match all domains. - vr, ok = findRouter("*", location, routeByHTTPUser) - if ok { - return vr, true - } - return nil, false + user, passwd, ok := req.BasicAuth() + return ok && user == rc.Username && passwd == rc.Password } func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Request) { @@ -266,85 +237,82 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req return } - url := req.Context().Value(RouteInfoURL).(string) - routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string) - domain, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string)) - remoteAddr := req.Context().Value(RouteInfoRemote).(string) - - remote, err := rp.CreateConnection(domain, url, routeByHTTPUser, remoteAddr) + remote, err := rp.CreateConnection(req.Context().Value(RouteInfoKey).(*RequestRouteInfo), false) if err != nil { - http.Error(rw, "Failed", http.StatusBadRequest) + _ = NotFoundResponse().Write(client) client.Close() return } - req.Write(remote) - go frpIo.Join(remote, client) + _ = req.Write(remote) + go libio.Join(remote, client) } -func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request { - newctx := req.Context() - newctx = context.WithValue(newctx, RouteInfoURL, req.URL.Path) - newctx = context.WithValue(newctx, RouteInfoHost, req.Host) - newctx = context.WithValue(newctx, RouteInfoURLHost, req.URL.Host) - - user := "" - // If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header. +func getRequestRouteUser(req *http.Request) string { if req.URL.Host != "" { proxyAuth := req.Header.Get("Proxy-Authorization") - if proxyAuth != "" { - user, _, _ = parseBasicAuth(proxyAuth) + if proxyAuth == "" { + // Preserve legacy proxy-mode routing when clients send only Authorization, + // so requests still hit the matched route and return 407 instead of 404. + // Auth validation intentionally does not share this fallback. + user, _, _ := req.BasicAuth() + return user } + user, _, _ := httppkg.ParseBasicAuth(proxyAuth) + return user } - if user == "" { - user, _, _ = req.BasicAuth() + user, _, _ := req.BasicAuth() + return user +} + +func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request { + user := getRequestRouteUser(req) + + reqRouteInfo := &RequestRouteInfo{ + URL: req.URL.Path, + Host: req.Host, + HTTPUser: user, + RemoteAddr: req.RemoteAddr, + URLHost: req.URL.Host, } - newctx = context.WithValue(newctx, RouteInfoHTTPUser, user) - newctx = context.WithValue(newctx, RouteInfoRemote, req.RemoteAddr) + + originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host) + rc := rp.GetRouteConfig(originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser) + + newctx := req.Context() + newctx = context.WithValue(newctx, RouteInfoKey, reqRouteInfo) + newctx = context.WithValue(newctx, RouteConfigKey, rc) return req.Clone(newctx) } func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - domain, _ := util.CanonicalHost(req.Host) - location := req.URL.Path - user, passwd, _ := req.BasicAuth() - if !rp.CheckAuth(domain, location, user, user, passwd) { - rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + newreq := rp.injectRequestInfoToCtx(req) + rc := newreq.Context().Value(RouteConfigKey).(*RouteConfig) + if !checkRouteAuthByRequest(req, rc) { + if req.URL.Host != "" { + rw.Header().Set("Proxy-Authenticate", `Basic realm="Restricted"`) + http.Error(rw, http.StatusText(http.StatusProxyAuthRequired), http.StatusProxyAuthRequired) + } else { + rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } return } - // Identifying the originating IP address of a client connecting to a web server through a proxy server - addr := req.Header.Get("X-Forwarded-For") - if addr == "" { - // For server direct access, remote address is in "IP:port" format - addr, _, _ = net.SplitHostPort(req.RemoteAddr) - } - if !rp.CheckClientOriginIpAddr(domain, location, user, addr) { - http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) - return + if rc != nil { + // Identifying the originating IP address of a client connecting through a proxy server. + addr := req.Header.Get("X-Forwarded-For") + if addr == "" { + addr, _, _ = net.SplitHostPort(req.RemoteAddr) + } + if !rp.CheckClientOriginIPAddr(rc.Domain, rc.Location, rc.RouteByHTTPUser, addr) { + http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } } - newreq := rp.injectRequestInfoToCtx(req) if req.Method == http.MethodConnect { rp.connectHandler(rw, newreq) } else { rp.proxy.ServeHTTP(rw, newreq) } } - -type wrapPool struct{} - -func newWrapPool() *wrapPool { return &wrapPool{} } - -func (p *wrapPool) Get() []byte { return pool.GetBuf(32 * 1024) } - -func (p *wrapPool) Put(buf []byte) { pool.PutBuf(buf) } - -type wrapLogger struct{} - -func newWrapLogger() *wrapLogger { return &wrapLogger{} } - -func (l *wrapLogger) Write(p []byte) (n int, err error) { - frpLog.Warn("%s", string(bytes.TrimRight(p, "\n"))) - return len(p), nil -} diff --git a/pkg/util/vhost/http_test.go b/pkg/util/vhost/http_test.go new file mode 100644 index 00000000000..a6493b08bf9 --- /dev/null +++ b/pkg/util/vhost/http_test.go @@ -0,0 +1,182 @@ +package vhost + +import ( + "bufio" + "fmt" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" + + httppkg "github.com/fatedier/frp/pkg/util/http" +) + +func TestHTTPServerProtocols(t *testing.T) { + rp := NewHTTPReverseProxy(HTTPReverseProxyOptions{}, NewRouters()) + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetUnencryptedHTTP2(true) + server := &http.Server{ + Handler: rp, + ReadHeaderTimeout: time.Second, + Protocols: protocols, + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + serveErr := make(chan error, 1) + go func() { + serveErr <- server.Serve(listener) + }() + defer func() { + require.NoError(t, server.Close()) + require.ErrorIs(t, <-serveErr, http.ErrServerClosed) + }() + + require.True(t, server.Protocols.HTTP1()) + require.True(t, server.Protocols.UnencryptedHTTP2()) + + t.Run("HTTP/1.1", func(t *testing.T) { + transport := &http.Transport{Protocols: httpProtocols(true, false)} + defer transport.CloseIdleConnections() + client := &http.Client{Transport: transport} + response, err := client.Get("http://" + listener.Addr().String() + "/") + require.NoError(t, err) + defer response.Body.Close() + + require.Equal(t, "HTTP/1.1", response.Proto) + require.Equal(t, http.StatusNotFound, response.StatusCode) + }) + + t.Run("HTTP/2 prior knowledge", func(t *testing.T) { + transport := &http.Transport{Protocols: httpProtocols(false, true)} + defer transport.CloseIdleConnections() + client := &http.Client{Transport: transport} + response, err := client.Get("http://" + listener.Addr().String() + "/") + require.NoError(t, err) + defer response.Body.Close() + + require.Equal(t, "HTTP/2.0", response.Proto) + require.Equal(t, http.StatusNotFound, response.StatusCode) + }) + + t.Run("HTTP/1.1 Upgrade h2c", func(t *testing.T) { + conn, err := net.Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer conn.Close() + + _, err = fmt.Fprintf(conn, + "GET / HTTP/1.1\r\nHost: %s\r\n"+ + "Connection: Upgrade, HTTP2-Settings\r\nUpgrade: h2c\r\n"+ + "HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA\r\n\r\n", + listener.Addr()) + require.NoError(t, err) + response, err := http.ReadResponse(bufio.NewReader(conn), nil) + require.NoError(t, err) + defer response.Body.Close() + + require.NotEqual(t, http.StatusSwitchingProtocols, response.StatusCode) + }) +} + +func httpProtocols(http1, unencryptedHTTP2 bool) *http.Protocols { + protocols := new(http.Protocols) + protocols.SetHTTP1(http1) + protocols.SetUnencryptedHTTP2(unencryptedHTTP2) + return protocols +} + +func TestCheckRouteAuthByRequest(t *testing.T) { + rc := &RouteConfig{ + Username: "alice", + Password: "secret", + } + + t.Run("accepts nil route config", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + require.True(t, checkRouteAuthByRequest(req, nil)) + }) + + t.Run("accepts route without credentials", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + require.True(t, checkRouteAuthByRequest(req, &RouteConfig{})) + }) + + t.Run("accepts authorization header", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.SetBasicAuth("alice", "secret") + require.True(t, checkRouteAuthByRequest(req, rc)) + }) + + t.Run("accepts proxy authorization header", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://target.example.com/", nil) + req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret")) + require.True(t, checkRouteAuthByRequest(req, rc)) + }) + + t.Run("rejects authorization fallback for proxy request", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://target.example.com/", nil) + req.SetBasicAuth("alice", "secret") + require.False(t, checkRouteAuthByRequest(req, rc)) + }) + + t.Run("rejects wrong proxy authorization even when authorization matches", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://target.example.com/", nil) + req.SetBasicAuth("alice", "secret") + req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong")) + require.False(t, checkRouteAuthByRequest(req, rc)) + }) + + t.Run("rejects when neither header matches", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://target.example.com/", nil) + req.SetBasicAuth("alice", "wrong") + req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong")) + require.False(t, checkRouteAuthByRequest(req, rc)) + }) + + t.Run("rejects proxy authorization on direct request", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret")) + require.False(t, checkRouteAuthByRequest(req, rc)) + }) +} + +func TestGetRequestRouteUser(t *testing.T) { + t.Run("proxy request uses proxy authorization username", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://target.example.com/", nil) + req.Host = "target.example.com" + req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass")) + req.SetBasicAuth("direct-user", "direct-pass") + + require.Equal(t, "proxy-user", getRequestRouteUser(req)) + }) + + t.Run("connect request keeps proxy authorization routing", func(t *testing.T) { + req := httptest.NewRequest("CONNECT", "http://target.example.com:443", nil) + req.Host = "target.example.com:443" + req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass")) + req.SetBasicAuth("direct-user", "direct-pass") + + require.Equal(t, "proxy-user", getRequestRouteUser(req)) + }) + + t.Run("direct request uses authorization username", func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.Host = "example.com" + req.SetBasicAuth("direct-user", "direct-pass") + + require.Equal(t, "direct-user", getRequestRouteUser(req)) + }) + + t.Run("proxy request does not fall back when proxy authorization is invalid", func(t *testing.T) { + req := httptest.NewRequest("GET", "http://target.example.com/", nil) + req.Host = "target.example.com" + req.Header.Set("Proxy-Authorization", "Basic !!!") + req.SetBasicAuth("direct-user", "direct-pass") + + require.Empty(t, getRequestRouteUser(req)) + }) +} diff --git a/pkg/util/vhost/https.go b/pkg/util/vhost/https.go index dd20739ea27..857a10d2da2 100644 --- a/pkg/util/vhost/https.go +++ b/pkg/util/vhost/https.go @@ -20,7 +20,7 @@ import ( "net" "time" - gnet "github.com/fatedier/golib/net" + libnet "github.com/fatedier/golib/net" ) type HTTPSMuxer struct { @@ -28,13 +28,17 @@ type HTTPSMuxer struct { } func NewHTTPSMuxer(listener net.Listener, timeout time.Duration) (*HTTPSMuxer, error) { - mux, err := NewMuxer(listener, GetHTTPSHostname, nil, nil, nil, timeout) - return &HTTPSMuxer{mux}, err + mux, err := NewMuxer(listener, GetHTTPSHostname, timeout) + if err != nil { + return nil, err + } + mux.SetFailHookFunc(vhostFailed) + return &HTTPSMuxer{mux}, nil } func GetHTTPSHostname(c net.Conn) (_ net.Conn, _ map[string]string, err error) { reqInfoMap := make(map[string]string, 0) - sc, rd := gnet.NewSharedConn(c) + sc, rd := libnet.NewSharedConn(c) clientHello, err := readClientHello(rd) if err != nil { @@ -66,15 +70,21 @@ func readClientHello(reader io.Reader) (*tls.ClientHelloInfo, error) { return hello, nil } +func vhostFailed(c net.Conn) { + // Alert with alertUnrecognizedName + _ = tls.Server(c, &tls.Config{}).Handshake() + c.Close() +} + type readOnlyConn struct { reader io.Reader } func (conn readOnlyConn) Read(p []byte) (int, error) { return conn.reader.Read(p) } -func (conn readOnlyConn) Write(p []byte) (int, error) { return 0, io.ErrClosedPipe } +func (conn readOnlyConn) Write(_ []byte) (int, error) { return 0, io.ErrClosedPipe } func (conn readOnlyConn) Close() error { return nil } func (conn readOnlyConn) LocalAddr() net.Addr { return nil } func (conn readOnlyConn) RemoteAddr() net.Addr { return nil } -func (conn readOnlyConn) SetDeadline(t time.Time) error { return nil } -func (conn readOnlyConn) SetReadDeadline(t time.Time) error { return nil } -func (conn readOnlyConn) SetWriteDeadline(t time.Time) error { return nil } +func (conn readOnlyConn) SetDeadline(_ time.Time) error { return nil } +func (conn readOnlyConn) SetReadDeadline(_ time.Time) error { return nil } +func (conn readOnlyConn) SetWriteDeadline(_ time.Time) error { return nil } diff --git a/pkg/util/vhost/https_test.go b/pkg/util/vhost/https_test.go index 47fb9dae4ed..6c30368747b 100644 --- a/pkg/util/vhost/https_test.go +++ b/pkg/util/vhost/https_test.go @@ -12,27 +12,57 @@ import ( func TestGetHTTPSHostname(t *testing.T) { require := require.New(t) - l, err := net.Listen("tcp", ":") + l, err := net.Listen("tcp", "127.0.0.1:") require.NoError(err) defer l.Close() - var conn net.Conn + connCh := make(chan net.Conn, 1) + acceptErrCh := make(chan error, 1) go func() { - conn, _ = l.Accept() - require.NotNil(conn) + conn, err := l.Accept() + if err != nil { + acceptErrCh <- err + return + } + connCh <- conn }() + clientErrCh := make(chan error, 1) go func() { time.Sleep(100 * time.Millisecond) - tls.Dial("tcp", l.Addr().String(), &tls.Config{ + conn, err := tls.Dial("tcp", l.Addr().String(), &tls.Config{ InsecureSkipVerify: true, ServerName: "example.com", }) + if conn != nil { + _ = conn.Close() + } + clientErrCh <- err }() - time.Sleep(200 * time.Millisecond) - _, infos, err := GetHTTPSHostname(conn) + var conn net.Conn + select { + case conn = <-connCh: + case err := <-acceptErrCh: + require.NoError(err) + case <-time.After(time.Second): + t.Fatal("timed out waiting for accepted connection") + } + require.NotNil(conn) + + serverConn, infos, err := GetHTTPSHostname(conn) + if serverConn != nil { + _ = serverConn.Close() + } else { + _ = conn.Close() + } require.NoError(err) require.Equal("example.com", infos["Host"]) require.Equal("https", infos["Scheme"]) + + select { + case <-clientErrCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for TLS client") + } } diff --git a/pkg/util/vhost/resource.go b/pkg/util/vhost/resource.go index 446f40f91f2..a65e29973ea 100644 --- a/pkg/util/vhost/resource.go +++ b/pkg/util/vhost/resource.go @@ -20,13 +20,11 @@ import ( "net/http" "os" - frpLog "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" ) -var ( - NotFoundPagePath = "" -) +var NotFoundPagePath = "" const ( NotFound = ` @@ -60,7 +58,7 @@ func getNotFoundPageContent() []byte { if NotFoundPagePath != "" { buf, err = os.ReadFile(NotFoundPagePath) if err != nil { - frpLog.Warn("read custom 404 page error: %v", err) + log.Warnf("read custom 404 page error: %v", err) buf = []byte(NotFound) } } else { @@ -69,33 +67,21 @@ func getNotFoundPageContent() []byte { return buf } -func notFoundResponse() *http.Response { +func NotFoundResponse() *http.Response { header := make(http.Header) header.Set("server", "frp/"+version.Full()) header.Set("Content-Type", "text/html") + content := getNotFoundPageContent() res := &http.Response{ - Status: "Not Found", - StatusCode: 404, - Proto: "HTTP/1.0", - ProtoMajor: 1, - ProtoMinor: 0, - Header: header, - Body: io.NopCloser(bytes.NewReader(getNotFoundPageContent())), - } - return res -} - -func noAuthResponse() *http.Response { - header := make(map[string][]string) - header["WWW-Authenticate"] = []string{`Basic realm="Restricted"`} - res := &http.Response{ - Status: "401 Not authorized", - StatusCode: 401, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - Header: header, + Status: "Not Found", + StatusCode: 404, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: header, + Body: io.NopCloser(bytes.NewReader(content)), + ContentLength: int64(len(content)), } return res } diff --git a/pkg/util/vhost/reverseproxy.go b/pkg/util/vhost/reverseproxy.go deleted file mode 100644 index 975f1d14ecf..00000000000 --- a/pkg/util/vhost/reverseproxy.go +++ /dev/null @@ -1,636 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// HTTP reverse proxy handler - -package vhost - -import ( - "context" - "encoding/base64" - "fmt" - "io" - "log" - "net" - "net/http" - "net/textproto" - "net/url" - "strings" - "sync" - "time" - - "golang.org/x/net/http/httpguts" -) - -// ReverseProxy is an HTTP Handler that takes an incoming request and -// sends it to another server, proxying the response back to the -// client. -// -// ReverseProxy by default sets the client IP as the value of the -// X-Forwarded-For header. -// -// If an X-Forwarded-For header already exists, the client IP is -// appended to the existing values. As a special case, if the header -// exists in the Request.Header map but has a nil value (such as when -// set by the Director func), the X-Forwarded-For header is -// not modified. -// -// To prevent IP spoofing, be sure to delete any pre-existing -// X-Forwarded-For header coming from the client or -// an untrusted proxy. -type ReverseProxy struct { - // Director must be a function which modifies - // the request into a new request to be sent - // using Transport. Its response is then copied - // back to the original client unmodified. - // Director must not access the provided Request - // after returning. - Director func(*http.Request) - - // The transport used to perform proxy requests. - // If nil, http.DefaultTransport is used. - Transport http.RoundTripper - - // FlushInterval specifies the flush interval - // to flush to the client while copying the - // response body. - // If zero, no periodic flushing is done. - // A negative value means to flush immediately - // after each write to the client. - // The FlushInterval is ignored when ReverseProxy - // recognizes a response as a streaming response, or - // if its ContentLength is -1; for such responses, writes - // are flushed to the client immediately. - FlushInterval time.Duration - - // ErrorLog specifies an optional logger for errors - // that occur when attempting to proxy the request. - // If nil, logging is done via the log package's standard logger. - ErrorLog *log.Logger - - // BufferPool optionally specifies a buffer pool to - // get byte slices for use by io.CopyBuffer when - // copying HTTP response bodies. - BufferPool BufferPool - - // ModifyResponse is an optional function that modifies the - // Response from the backend. It is called if the backend - // returns a response at all, with any HTTP status code. - // If the backend is unreachable, the optional ErrorHandler is - // called without any call to ModifyResponse. - // - // If ModifyResponse returns an error, ErrorHandler is called - // with its error value. If ErrorHandler is nil, its default - // implementation is used. - ModifyResponse func(*http.Response) error - - // ErrorHandler is an optional function that handles errors - // reaching the backend or errors from ModifyResponse. - // - // If nil, the default is to log the provided error and return - // a 502 Status Bad Gateway response. - ErrorHandler func(http.ResponseWriter, *http.Request, error) -} - -// A BufferPool is an interface for getting and returning temporary -// byte slices for use by io.CopyBuffer. -type BufferPool interface { - Get() []byte - Put([]byte) -} - -func singleJoiningSlash(a, b string) string { - aslash := strings.HasSuffix(a, "/") - bslash := strings.HasPrefix(b, "/") - switch { - case aslash && bslash: - return a + b[1:] - case !aslash && !bslash: - return a + "/" + b - } - return a + b -} - -func joinURLPath(a, b *url.URL) (path, rawpath string) { - if a.RawPath == "" && b.RawPath == "" { - return singleJoiningSlash(a.Path, b.Path), "" - } - // Same as singleJoiningSlash, but uses EscapedPath to determine - // whether a slash should be added - apath := a.EscapedPath() - bpath := b.EscapedPath() - - aslash := strings.HasSuffix(apath, "/") - bslash := strings.HasPrefix(bpath, "/") - - switch { - case aslash && bslash: - return a.Path + b.Path[1:], apath + bpath[1:] - case !aslash && !bslash: - return a.Path + "/" + b.Path, apath + "/" + bpath - } - return a.Path + b.Path, apath + bpath -} - -// NewSingleHostReverseProxy returns a new ReverseProxy that routes -// URLs to the scheme, host, and base path provided in target. If the -// target's path is "/base" and the incoming request was for "/dir", -// the target request will be for /base/dir. -// NewSingleHostReverseProxy does not rewrite the Host header. -// To rewrite Host headers, use ReverseProxy directly with a custom -// Director policy. -func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy { - targetQuery := target.RawQuery - director := func(req *http.Request) { - req.URL.Scheme = target.Scheme - req.URL.Host = target.Host - req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL) - if targetQuery == "" || req.URL.RawQuery == "" { - req.URL.RawQuery = targetQuery + req.URL.RawQuery - } else { - req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery - } - if _, ok := req.Header["User-Agent"]; !ok { - // explicitly disable User-Agent so it's not set to default value - req.Header.Set("User-Agent", "") - } - } - return &ReverseProxy{Director: director} -} - -func copyHeader(dst, src http.Header) { - for k, vv := range src { - for _, v := range vv { - dst.Add(k, v) - } - } -} - -// Hop-by-hop headers. These are removed when sent to the backend. -// As of RFC 7230, hop-by-hop headers are required to appear in the -// Connection header field. These are the headers defined by the -// obsoleted RFC 2616 (section 13.5.1) and are used for backward -// compatibility. -var hopHeaders = []string{ - "Connection", - "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google - "Keep-Alive", - "Proxy-Authenticate", - "Proxy-Authorization", - "Te", // canonicalized version of "TE" - "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522 - "Transfer-Encoding", - "Upgrade", -} - -func (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { - p.logf("http: proxy error: %v", err) - rw.WriteHeader(http.StatusBadGateway) -} - -func (p *ReverseProxy) getErrorHandler() func(http.ResponseWriter, *http.Request, error) { - if p.ErrorHandler != nil { - return p.ErrorHandler - } - return p.defaultErrorHandler -} - -// modifyResponse conditionally runs the optional ModifyResponse hook -// and reports whether the request should proceed. -func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response, req *http.Request) bool { - if p.ModifyResponse == nil { - return true - } - if err := p.ModifyResponse(res); err != nil { - res.Body.Close() - p.getErrorHandler()(rw, req, err) - return false - } - return true -} - -func parseBasicAuth(auth string) (username, password string, ok bool) { - const prefix = "Basic " - // Case insensitive prefix match. See Issue 22736. - if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { - return - } - c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) - if err != nil { - return - } - cs := string(c) - s := strings.IndexByte(cs, ':') - if s < 0 { - return - } - return cs[:s], cs[s+1:], true -} - -func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - transport := p.Transport - if transport == nil { - transport = http.DefaultTransport - } - - ctx := req.Context() - if cn, ok := rw.(http.CloseNotifier); ok { - var cancel context.CancelFunc - ctx, cancel = context.WithCancel(ctx) - defer cancel() - notifyChan := cn.CloseNotify() - go func() { - select { - case <-notifyChan: - cancel() - case <-ctx.Done(): - } - }() - } - - outreq := req.Clone(ctx) - if req.ContentLength == 0 { - outreq.Body = nil // Issue 16036: nil Body for http.Transport retries - } - if outreq.Header == nil { - outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate - } - - p.Director(outreq) - outreq.Close = false - - reqUpType := upgradeType(outreq.Header) - removeConnectionHeaders(outreq.Header) - - // Remove hop-by-hop headers to the backend. Especially - // important is "Connection" because we want a persistent - // connection, regardless of what the client sent to us. - for _, h := range hopHeaders { - hv := outreq.Header.Get(h) - if hv == "" { - continue - } - if h == "Te" && hv == "trailers" { - // Issue 21096: tell backend applications that - // care about trailer support that we support - // trailers. (We do, but we don't go out of - // our way to advertise that unless the - // incoming client request thought it was - // worth mentioning) - continue - } - outreq.Header.Del(h) - } - - // After stripping all the hop-by-hop connection headers above, add back any - // necessary for protocol upgrades, such as for websockets. - if reqUpType != "" { - outreq.Header.Set("Connection", "Upgrade") - outreq.Header.Set("Upgrade", reqUpType) - } - - if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { - // If we aren't the first proxy retain prior - // X-Forwarded-For information as a comma+space - // separated list and fold multiple headers into one. - prior, ok := outreq.Header["X-Forwarded-For"] - omit := ok && prior == nil // Issue 38079: nil now means don't populate the header - if len(prior) > 0 { - clientIP = strings.Join(prior, ", ") + ", " + clientIP - } - if !omit { - outreq.Header.Set("X-Forwarded-For", clientIP) - } - } - - res, err := transport.RoundTrip(outreq) - if err != nil { - p.getErrorHandler()(rw, outreq, err) - return - } - - // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc) - if res.StatusCode == http.StatusSwitchingProtocols { - if !p.modifyResponse(rw, res, outreq) { - return - } - p.handleUpgradeResponse(rw, outreq, res) - return - } - - removeConnectionHeaders(res.Header) - - for _, h := range hopHeaders { - res.Header.Del(h) - } - - if !p.modifyResponse(rw, res, outreq) { - return - } - - copyHeader(rw.Header(), res.Header) - - // The "Trailer" header isn't included in the Transport's response, - // at least for *http.Transport. Build it up from Trailer. - announcedTrailers := len(res.Trailer) - if announcedTrailers > 0 { - trailerKeys := make([]string, 0, len(res.Trailer)) - for k := range res.Trailer { - trailerKeys = append(trailerKeys, k) - } - rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) - } - - rw.WriteHeader(res.StatusCode) - - err = p.copyResponse(rw, res.Body, p.flushInterval(res)) - if err != nil { - defer res.Body.Close() - // Since we're streaming the response, if we run into an error all we can do - // is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler - // on read error while copying body. - if !shouldPanicOnCopyError(req) { - p.logf("suppressing panic for copyResponse error in test; copy error: %v", err) - return - } - panic(http.ErrAbortHandler) - } - res.Body.Close() // close now, instead of defer, to populate res.Trailer - - if len(res.Trailer) > 0 { - // Force chunking if we saw a response trailer. - // This prevents net/http from calculating the length for short - // bodies and adding a Content-Length. - if fl, ok := rw.(http.Flusher); ok { - fl.Flush() - } - } - - if len(res.Trailer) == announcedTrailers { - copyHeader(rw.Header(), res.Trailer) - return - } - - for k, vv := range res.Trailer { - k = http.TrailerPrefix + k - for _, v := range vv { - rw.Header().Add(k, v) - } - } -} - -var inOurTests bool // whether we're in our own tests - -// shouldPanicOnCopyError reports whether the reverse proxy should -// panic with http.ErrAbortHandler. This is the right thing to do by -// default, but Go 1.10 and earlier did not, so existing unit tests -// weren't expecting panics. Only panic in our own tests, or when -// running under the HTTP server. -func shouldPanicOnCopyError(req *http.Request) bool { - if inOurTests { - // Our tests know to handle this panic. - return true - } - if req.Context().Value(http.ServerContextKey) != nil { - // We seem to be running under an HTTP server, so - // it'll recover the panic. - return true - } - // Otherwise act like Go 1.10 and earlier to not break - // existing tests. - return false -} - -// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h. -// See RFC 7230, section 6.1 -func removeConnectionHeaders(h http.Header) { - for _, f := range h["Connection"] { - for _, sf := range strings.Split(f, ",") { - if sf = textproto.TrimString(sf); sf != "" { - h.Del(sf) - } - } - } -} - -// flushInterval returns the p.FlushInterval value, conditionally -// overriding its value for a specific request/response. -func (p *ReverseProxy) flushInterval(res *http.Response) time.Duration { - resCT := res.Header.Get("Content-Type") - - // For Server-Sent Events responses, flush immediately. - // The MIME type is defined in https://www.w3.org/TR/eventsource/#text-event-stream - if resCT == "text/event-stream" { - return -1 // negative means immediately - } - - // We might have the case of streaming for which Content-Length might be unset. - if res.ContentLength == -1 { - return -1 - } - - return p.FlushInterval -} - -func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error { - if flushInterval != 0 { - if wf, ok := dst.(writeFlusher); ok { - mlw := &maxLatencyWriter{ - dst: wf, - latency: flushInterval, - } - defer mlw.stop() - - // set up initial timer so headers get flushed even if body writes are delayed - mlw.flushPending = true - mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush) - - dst = mlw - } - } - - var buf []byte - if p.BufferPool != nil { - buf = p.BufferPool.Get() - defer p.BufferPool.Put(buf) - } - _, err := p.copyBuffer(dst, src, buf) - return err -} - -// copyBuffer returns any write errors or non-EOF read errors, and the amount -// of bytes written. -func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) { - if len(buf) == 0 { - buf = make([]byte, 32*1024) - } - var written int64 - for { - nr, rerr := src.Read(buf) - if rerr != nil && rerr != io.EOF && rerr != context.Canceled { - p.logf("httputil: ReverseProxy read error during body copy: %v", rerr) - } - if nr > 0 { - nw, werr := dst.Write(buf[:nr]) - if nw > 0 { - written += int64(nw) - } - if werr != nil { - return written, werr - } - if nr != nw { - return written, io.ErrShortWrite - } - } - if rerr != nil { - if rerr == io.EOF { - rerr = nil - } - return written, rerr - } - } -} - -func (p *ReverseProxy) logf(format string, args ...interface{}) { - if p.ErrorLog != nil { - p.ErrorLog.Printf(format, args...) - } else { - log.Printf(format, args...) - } -} - -type writeFlusher interface { - io.Writer - http.Flusher -} - -type maxLatencyWriter struct { - dst writeFlusher - latency time.Duration // non-zero; negative means to flush immediately - - mu sync.Mutex // protects t, flushPending, and dst.Flush - t *time.Timer - flushPending bool -} - -func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { - m.mu.Lock() - defer m.mu.Unlock() - n, err = m.dst.Write(p) - if m.latency < 0 { - m.dst.Flush() - return - } - if m.flushPending { - return - } - if m.t == nil { - m.t = time.AfterFunc(m.latency, m.delayedFlush) - } else { - m.t.Reset(m.latency) - } - m.flushPending = true - return -} - -func (m *maxLatencyWriter) delayedFlush() { - m.mu.Lock() - defer m.mu.Unlock() - if !m.flushPending { // if stop was called but AfterFunc already started this goroutine - return - } - m.dst.Flush() - m.flushPending = false -} - -func (m *maxLatencyWriter) stop() { - m.mu.Lock() - defer m.mu.Unlock() - m.flushPending = false - if m.t != nil { - m.t.Stop() - } -} - -func upgradeType(h http.Header) string { - if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") { - return "" - } - return strings.ToLower(h.Get("Upgrade")) -} - -func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) { - reqUpType := upgradeType(req.Header) - resUpType := upgradeType(res.Header) - if reqUpType != resUpType { - p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType)) - return - } - - hj, ok := rw.(http.Hijacker) - if !ok { - p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)) - return - } - backConn, ok := res.Body.(io.ReadWriteCloser) - if !ok { - p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body")) - return - } - - backConnCloseCh := make(chan bool) - go func() { - // Ensure that the cancelation of a request closes the backend. - // See issue https://golang.org/issue/35559. - select { - case <-req.Context().Done(): - case <-backConnCloseCh: - } - backConn.Close() - }() - - defer close(backConnCloseCh) - - conn, brw, err := hj.Hijack() - if err != nil { - p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err)) - return - } - defer conn.Close() - - copyHeader(rw.Header(), res.Header) - - res.Header = rw.Header() - res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above - if err := res.Write(brw); err != nil { - p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err)) - return - } - if err := brw.Flush(); err != nil { - p.getErrorHandler()(rw, req, fmt.Errorf("response flush: %v", err)) - return - } - errc := make(chan error, 1) - spc := switchProtocolCopier{user: conn, backend: backConn} - go spc.copyToBackend(errc) - go spc.copyFromBackend(errc) - <-errc - return -} - -// switchProtocolCopier exists so goroutines proxying data back and -// forth have nice names in stacks. -type switchProtocolCopier struct { - user, backend io.ReadWriter -} - -func (c switchProtocolCopier) copyFromBackend(errc chan<- error) { - _, err := io.Copy(c.user, c.backend) - errc <- err -} - -func (c switchProtocolCopier) copyToBackend(errc chan<- error) { - _, err := io.Copy(c.backend, c.user) - errc <- err -} diff --git a/pkg/util/vhost/router.go b/pkg/util/vhost/router.go index f9828689533..4703c31c842 100644 --- a/pkg/util/vhost/router.go +++ b/pkg/util/vhost/router.go @@ -1,18 +1,16 @@ package vhost import ( + "cmp" "errors" - frpLog "github.com/fatedier/frp/pkg/util/log" - "sort" + "slices" "strings" "sync" "github.com/jpillora/ipfilter" ) -var ( - ErrRouterConfigConflict = errors.New("router config conflict") -) +var ErrRouterConfigConflict = errors.New("router config conflict") type routerByHTTPUser map[string][]*Router @@ -29,7 +27,7 @@ type Router struct { ipFilter *ipfilter.IPFilter // store any object here - payload interface{} + payload any } func NewRouters() *Routers { @@ -38,7 +36,9 @@ func NewRouters() *Routers { } } -func (r *Routers) Add(domain, location, httpUser string, ipsAllowList []string, payload interface{}) error { +func (r *Routers) Add(domain, location, httpUser string, ipsAllowList []string, payload any) error { + domain = strings.ToLower(domain) + r.mutex.Lock() defer r.mutex.Unlock() @@ -56,8 +56,7 @@ func (r *Routers) Add(domain, location, httpUser string, ipsAllowList []string, } var ipFilter *ipfilter.IPFilter - frpLog.Debug("adding allow list %s", ipsAllowList) - if ipsAllowList != nil { + if len(ipsAllowList) > 0 { ipFilter = ipfilter.New(ipfilter.Options{ AllowedIPs: ipsAllowList, BlockByDefault: true, @@ -72,7 +71,10 @@ func (r *Routers) Add(domain, location, httpUser string, ipsAllowList []string, payload: payload, } vrs = append(vrs, vr) - sort.Sort(sort.Reverse(ByLocation(vrs))) + + slices.SortFunc(vrs, func(a, b *Router) int { + return -cmp.Compare(a.location, b.location) + }) routersByHTTPUser[httpUser] = vrs r.indexByDomain[domain] = routersByHTTPUser @@ -80,6 +82,8 @@ func (r *Routers) Add(domain, location, httpUser string, ipsAllowList []string, } func (r *Routers) Del(domain, location, httpUser string) { + domain = strings.ToLower(domain) + r.mutex.Lock() defer r.mutex.Unlock() @@ -101,10 +105,19 @@ func (r *Routers) Del(domain, location, httpUser string) { routersByHTTPUser[httpUser] = newVrs } +// Get returns the best location match for an exact host and exact HTTP user. +// It does not apply all-users, wildcard-domain, or catch-all-domain fallback. func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) { + host = strings.ToLower(host) + r.mutex.RLock() defer r.mutex.RUnlock() + return r.getLocked(host, path, httpUser) +} + +// getLocked performs an exact-host lookup; host must already be lower-cased. +func (r *Routers) getLocked(host, path, httpUser string) (vr *Router, exist bool) { routersByHTTPUser, found := r.indexByDomain[host] if !found { return @@ -123,6 +136,49 @@ func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) { return } +func (r *Routers) getByRoute(host, path, httpUser string) (*Router, bool) { + host = strings.ToLower(host) + + r.mutex.RLock() + defer r.mutex.RUnlock() + + // First we check the full hostname; if it doesn't exist, then check wildcard domains. + // For example, test.example.com checks *.example.com before falling back to "*". + vr, ok := r.getExactOrAllUsersLocked(host, path, httpUser) + if ok { + return vr, true + } + + hostSplit := strings.Split(host, ".") + // Keep two-label hosts out of the wildcard walk, so example.com does not match *.com. + for len(hostSplit) >= 3 { + // Replace the leftmost remaining label with the wildcard marker. + hostSplit[0] = "*" + host = strings.Join(hostSplit, ".") + vr, ok = r.getExactOrAllUsersLocked(host, path, httpUser) + if ok { + return vr, true + } + hostSplit = hostSplit[1:] + } + + // Finally, try to check if there is one proxy whose domain is "*", which means match all domains. + return r.getExactOrAllUsersLocked("*", path, httpUser) +} + +func (r *Routers) getExactOrAllUsersLocked(host, path, httpUser string) (*Router, bool) { + vr, ok := r.getLocked(host, path, httpUser) + if ok { + return vr, true + } + // Try to check if there is one proxy that doesn't specify routeByHTTPUser, it means match all. + vr, ok = r.getLocked(host, path, "") + if ok { + return vr, true + } + return nil, false +} + func (r *Routers) exist(host, path, httpUser string) (route *Router, exist bool) { routersByHTTPUser, found := r.indexByDomain[host] if !found { @@ -140,16 +196,3 @@ func (r *Routers) exist(host, path, httpUser string) (route *Router, exist bool) } return } - -// sort by location -type ByLocation []*Router - -func (a ByLocation) Len() int { - return len(a) -} -func (a ByLocation) Swap(i, j int) { - a[i], a[j] = a[j], a[i] -} -func (a ByLocation) Less(i, j int) bool { - return strings.Compare(a[i].location, a[j].location) < 0 -} diff --git a/pkg/util/vhost/router_test.go b/pkg/util/vhost/router_test.go new file mode 100644 index 00000000000..7d3e7139e09 --- /dev/null +++ b/pkg/util/vhost/router_test.go @@ -0,0 +1,257 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 vhost + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRoutersGet(t *testing.T) { + routers := NewRouters() + require.NoError(t, routers.Add("example.com", "/api", "alice", nil, "exact-user")) + require.NoError(t, routers.Add("example.com", "/public", "", nil, "exact-all-users")) + require.NoError(t, routers.Add("*.example.com", "/api", "", nil, "wildcard-all-users")) + require.NoError(t, routers.Add("*", "/api", "", nil, "all-domains")) + + t.Run("exact host and user match with normalized domain", func(t *testing.T) { + router, ok := routers.Get("EXAMPLE.COM", "/api/users", "alice") + require.True(t, ok) + require.Equal(t, "exact-user", router.payload) + }) + + t.Run("exact host and empty user match", func(t *testing.T) { + router, ok := routers.Get("EXAMPLE.COM", "/public/docs", "") + require.True(t, ok) + require.Equal(t, "exact-all-users", router.payload) + }) + + t.Run("does not fall back from named user to empty user", func(t *testing.T) { + // Get intentionally requires an exact HTTP user; route-level fallbacks live in getByRoute. + _, ok := routers.Get("EXAMPLE.COM", "/public/docs", "alice") + require.False(t, ok) + }) + + t.Run("does not fall back to wildcard domains", func(t *testing.T) { + _, ok := routers.Get("foo.example.com", "/api/users", "") + require.False(t, ok) + }) + + t.Run("does not fall back to catch-all domain", func(t *testing.T) { + _, ok := routers.Get("missing.test", "/api/users", "") + require.False(t, ok) + }) +} + +func TestRoutersGetByRoute(t *testing.T) { + routers := NewRouters() + require.NoError(t, routers.Add("example.com", "/api", "alice", nil, "exact-user")) + require.NoError(t, routers.Add("example.com", "/api", "", nil, "exact-all-users")) + require.NoError(t, routers.Add("exact.example.com", "/api", "", nil, "exact-subdomain")) + require.NoError(t, routers.Add("*.example.com", "/api", "", nil, "wildcard-all-users")) + require.NoError(t, routers.Add("*.foo.example.com", "/api", "", nil, "specific-wildcard")) + require.NoError(t, routers.Add("*.bar.com", "/api", "", nil, "wildcard-parent-domain")) + require.NoError(t, routers.Add("*", "/admin", "root", nil, "all-domains-user")) + require.NoError(t, routers.Add("*", "/", "", nil, "all-domains")) + + tests := []struct { + name string + domain string + location string + httpUser string + want string + }{ + { + name: "exact domain and http user", + domain: "example.com", + location: "/api/users", + httpUser: "alice", + want: "exact-user", + }, + { + name: "exact domain falls back to all users", + domain: "example.com", + location: "/api/users", + httpUser: "bob", + want: "exact-all-users", + }, + { + name: "wildcard domain uses all users fallback", + domain: "foo.example.com", + location: "/api/users", + httpUser: "bob", + want: "wildcard-all-users", + }, + { + name: "mixed-case domain is normalized", + domain: "Foo.Example.Com", + location: "/api/users", + httpUser: "bob", + want: "wildcard-all-users", + }, + { + name: "exact domain wins over wildcard domain", + domain: "exact.example.com", + location: "/api/users", + httpUser: "bob", + want: "exact-subdomain", + }, + { + name: "more specific wildcard wins over broader wildcard", + domain: "bar.foo.example.com", + location: "/api/users", + httpUser: "bob", + want: "specific-wildcard", + }, + { + name: "wildcard walk checks parent domains", + domain: "a.b.bar.com", + location: "/api/users", + httpUser: "bob", + want: "wildcard-parent-domain", + }, + { + name: "catch-all domain fallback", + domain: "foo.test.com", + location: "/other", + httpUser: "bob", + want: "all-domains", + }, + { + name: "catch-all domain honors http user", + domain: "foo.test.com", + location: "/admin/panel", + httpUser: "root", + want: "all-domains-user", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router, ok := routers.getByRoute(tt.domain, tt.location, tt.httpUser) + require.True(t, ok) + require.Equal(t, tt.want, router.payload) + }) + } +} + +func TestRoutersGetByRouteNoMatch(t *testing.T) { + routers := NewRouters() + require.NoError(t, routers.Add("*.example.com", "/api", "", nil, "wildcard-all-users")) + require.NoError(t, routers.Add("*.com", "/api", "", nil, "top-level-wildcard")) + + tests := []struct { + name string + domain string + location string + }{ + { + name: "two-label domain does not enter wildcard walk", + domain: "example.com", + location: "/api/users", + }, + { + name: "missing catch-all remains no match", + domain: "foo.test.com", + location: "/api/users", + }, + { + name: "wrong path remains no match", + domain: "foo.example.com", + location: "/other", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router, ok := routers.getByRoute(tt.domain, tt.location, "bob") + require.False(t, ok) + require.Nil(t, router) + }) + } +} + +func TestRoutersConcurrentGetByRouteAndAdd(t *testing.T) { + routers := NewRouters() + require.NoError(t, routers.Add("*.example.com", "/api", "", nil, "wildcard")) + + const readers = 8 + const iterations = 200 + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + start := make(chan struct{}) + errCh := make(chan error, readers+1) + var wg sync.WaitGroup + + for id := range readers { + wg.Go(func() { + <-start + for j := range iterations { + select { + case <-ctx.Done(): + return + default: + } + router, ok := routers.getByRoute("foo.example.com", "/api/users", "") + if !ok || router == nil || router.payload != "wildcard" { + errCh <- fmt.Errorf("reader %d iteration %d got router=%v ok=%v", id, j, router, ok) + return + } + } + }) + } + + wg.Go(func() { + <-start + for i := range iterations { + select { + case <-ctx.Done(): + return + default: + } + err := routers.Add(fmt.Sprintf("host-%d.example.com", i), "/api", "", nil, i) + if err != nil { + errCh <- err + return + } + } + }) + + close(start) + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + cancel() + t.Fatal("concurrent route lookup and add timed out") + } + + close(errCh) + for err := range errCh { + require.NoError(t, err) + } +} diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 4e0a23b62bc..da1bd79684e 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -19,63 +19,97 @@ import ( "strings" "time" + "github.com/fatedier/golib/errors" + "github.com/fatedier/frp/pkg/util/log" - frpNet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" - - "github.com/fatedier/golib/errors" ) type RouteInfo string const ( - RouteInfoURL RouteInfo = "url" - RouteInfoHost RouteInfo = "host" - RouteInfoHTTPUser RouteInfo = "httpUser" - RouteInfoRemote RouteInfo = "remote" - RouteInfoURLHost RouteInfo = "urlHost" + RouteInfoKey RouteInfo = "routeInfo" + RouteConfigKey RouteInfo = "routeConfig" ) -type muxFunc func(net.Conn) (net.Conn, map[string]string, error) -type httpAuthFunc func(net.Conn, string, string, string) (bool, error) -type hostRewriteFunc func(net.Conn, string) (net.Conn, error) -type successFunc func(net.Conn, map[string]string) error +type RequestRouteInfo struct { + URL string + Host string + HTTPUser string + RemoteAddr string + URLHost string + Endpoint string +} -// Muxer is only used for https and tcpmux proxy. +type ( + muxFunc func(net.Conn) (net.Conn, map[string]string, error) + authFunc func(conn net.Conn, username, password string, reqInfoMap map[string]string) (bool, error) + hostRewriteFunc func(net.Conn, string) (net.Conn, error) + successHookFunc func(net.Conn, map[string]string) error + failHookFunc func(net.Conn) +) + +// Muxer is a functional component used for https and tcpmux proxies. +// It accepts connections and extracts vhost information from the beginning of the connection data. +// It then routes the connection to its appropriate listener. type Muxer struct { - listener net.Listener - timeout time.Duration + listener net.Listener + timeout time.Duration + vhostFunc muxFunc - authFunc httpAuthFunc - successFunc successFunc - rewriteFunc hostRewriteFunc + checkAuth authFunc + successHook successHookFunc + failHook failHookFunc + rewriteHost hostRewriteFunc registryRouter *Routers } func NewMuxer( listener net.Listener, vhostFunc muxFunc, - authFunc httpAuthFunc, - successFunc successFunc, - rewriteFunc hostRewriteFunc, timeout time.Duration, ) (mux *Muxer, err error) { - mux = &Muxer{ listener: listener, timeout: timeout, vhostFunc: vhostFunc, - authFunc: authFunc, - successFunc: successFunc, - rewriteFunc: rewriteFunc, registryRouter: NewRouters(), } go mux.run() return mux, nil } +func (v *Muxer) SetCheckAuthFunc(f authFunc) *Muxer { + v.checkAuth = f + return v +} + +func (v *Muxer) SetSuccessHookFunc(f successHookFunc) *Muxer { + v.successHook = f + return v +} + +func (v *Muxer) SetFailHookFunc(f failHookFunc) *Muxer { + v.failHook = f + return v +} + +func (v *Muxer) SetRewriteHostFunc(f hostRewriteFunc) *Muxer { + v.rewriteHost = f + return v +} + +func (v *Muxer) Close() error { + return v.listener.Close() +} + +type ChooseEndpointFunc func() (string, error) + type CreateConnFunc func(remoteAddr string) (net.Conn, error) +type CreateConnByEndpointFunc func(endpoint, remoteAddr string) (net.Conn, error) + // RouteConfig is the params used to match HTTP requests type RouteConfig struct { Domain string @@ -84,13 +118,16 @@ type RouteConfig struct { Username string Password string Headers map[string]string + ResponseHeaders map[string]string IpsAllowList []string RouteByHTTPUser string - CreateConnFn CreateConnFunc + CreateConnFn CreateConnFunc + ChooseEndpointFn ChooseEndpointFunc + CreateConnByEndpointFn CreateConnByEndpointFunc } -// listen for a new domain name, if rewriteHost is not empty and rewriteFunc is not nil +// listen for a new domain name, if rewriteHost is not empty and rewriteHost func is not nil, // then rewrite the host header to rewriteHost func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err error) { l = &Listener{ @@ -98,9 +135,8 @@ func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err location: cfg.Location, routeByHTTPUser: cfg.RouteByHTTPUser, rewriteHost: cfg.RewriteHost, - userName: cfg.Username, - ipsAllowList: cfg.IpsAllowList, - passWord: cfg.Password, + username: cfg.Username, + password: cfg.Password, mux: v, accept: make(chan net.Conn), ctx: ctx, @@ -113,46 +149,9 @@ func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err } func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) { - - findRouter := func(inName, inPath, inHTTPUser string) (*Listener, bool) { - vr, ok := v.registryRouter.Get(inName, inPath, httpUser) - if ok { - return vr.payload.(*Listener), true - } - // Try to check if there is one proxy that doesn't specify routerByHTTPUser, it means match all. - vr, ok = v.registryRouter.Get(inName, inPath, "") - if ok { - return vr.payload.(*Listener), true - } - return nil, false - } - - // first we check the full hostname - // if not exist, then check the wildcard_domain such as *.example.com - l, ok := findRouter(name, path, httpUser) - if ok { - return l, true - } - - domainSplit := strings.Split(name, ".") - for { - if len(domainSplit) < 3 { - break - } - - domainSplit[0] = "*" - name = strings.Join(domainSplit, ".") - - l, ok = findRouter(name, path, httpUser) - if ok { - return l, true - } - domainSplit = domainSplit[1:] - } - // Finally, try to check if there is one proxy that domain is "*" means match all domains. - l, ok = findRouter("*", path, httpUser) + vr, ok := v.registryRouter.getByRoute(name, path, httpUser) if ok { - return l, true + return vr.payload.(*Listener), true } return nil, false } @@ -169,14 +168,14 @@ func (v *Muxer) run() { func (v *Muxer) handle(c net.Conn) { if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil { - c.Close() + _ = c.Close() return } sConn, reqInfoMap, err := v.vhostFunc(c) if err != nil { - log.Debug("get hostname from http/https request error: %v", err) - c.Close() + log.Debugf("get hostname from http/https request error: %v", err) + _ = c.Close() return } @@ -185,47 +184,43 @@ func (v *Muxer) handle(c net.Conn) { httpUser := reqInfoMap["HTTPUser"] l, ok := v.getListener(name, path, httpUser) if !ok { - res := notFoundResponse() - res.Write(c) - log.Debug("http request for host [%s] path [%s] httpUser [%s] not found", name, path, httpUser) - c.Close() + log.Debugf("http request for host [%s] path [%s] httpUser [%s] not found", name, path, httpUser) + v.failHook(sConn) return } xl := xlog.FromContextSafe(l.ctx) - if v.successFunc != nil { - if err := v.successFunc(c, reqInfoMap); err != nil { - xl.Info("success func failure on vhost connection: %v", err) - c.Close() + if v.successHook != nil { + if err := v.successHook(c, reqInfoMap); err != nil { + xl.Infof("success func failure on vhost connection: %v", err) + _ = c.Close() return } } - // if authFunc is exist and username/password is set + // if checkAuth func is exist and username/password is set // then verify user access - if l.mux.authFunc != nil && l.userName != "" && l.passWord != "" { - bAccess, err := l.mux.authFunc(c, l.userName, l.passWord, reqInfoMap["Authorization"]) - if bAccess == false || err != nil { - xl.Debug("check http Authorization failed") - res := noAuthResponse() - res.Write(c) - c.Close() + if l.mux.checkAuth != nil && l.username != "" { + ok, err := l.mux.checkAuth(c, l.username, l.password, reqInfoMap) + if !ok || err != nil { + xl.Debugf("auth failed for user: %s", l.username) + _ = c.Close() return } } if err = sConn.SetDeadline(time.Time{}); err != nil { - c.Close() + _ = c.Close() return } c = sConn - xl.Debug("new request host [%s] path [%s] httpUser [%s]", name, path, httpUser) + xl.Debugf("new request host [%s] path [%s] httpUser [%s]", name, path, httpUser) err = errors.PanicToError(func() { l.accept <- c }) if err != nil { - xl.Warn("listener is already closed, ignore this request") + xl.Warnf("listener is already closed, ignore this request") } } @@ -234,9 +229,8 @@ type Listener struct { location string routeByHTTPUser string rewriteHost string - userName string - passWord string - ipsAllowList []string + username string + password string mux *Muxer // for closing Muxer accept chan net.Conn ctx context.Context @@ -246,22 +240,22 @@ func (l *Listener) Accept() (net.Conn, error) { xl := xlog.FromContextSafe(l.ctx) conn, ok := <-l.accept if !ok { - return nil, fmt.Errorf("Listener closed") + return nil, fmt.Errorf("listener closed") } - // if rewriteFunc is exist + // if rewriteHost func is exist // rewrite http requests with a modified host header // if l.rewriteHost is empty, nothing to do - if l.mux.rewriteFunc != nil { - sConn, err := l.mux.rewriteFunc(conn, l.rewriteHost) + if l.mux.rewriteHost != nil { + sConn, err := l.mux.rewriteHost(conn, l.rewriteHost) if err != nil { - xl.Warn("host header rewrite failed: %v", err) + xl.Warnf("host header rewrite failed: %v", err) return nil, fmt.Errorf("host header rewrite failed") } - xl.Debug("rewrite host to [%s] success", l.rewriteHost) + xl.Debugf("rewrite host to [%s] success", l.rewriteHost) conn = sConn } - return frpNet.NewContextConn(l.ctx, conn), nil + return netpkg.NewContextConn(l.ctx, conn), nil } func (l *Listener) Close() error { diff --git a/pkg/util/wait/backoff.go b/pkg/util/wait/backoff.go new file mode 100644 index 00000000000..0ee5db71bab --- /dev/null +++ b/pkg/util/wait/backoff.go @@ -0,0 +1,186 @@ +// Copyright 2023 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 wait + +import ( + "math/rand/v2" + "time" + + "k8s.io/utils/clock" + + "github.com/fatedier/frp/pkg/util/util" +) + +type BackoffFunc func(previousDuration time.Duration, previousConditionError bool) time.Duration + +func (f BackoffFunc) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration { + return f(previousDuration, previousConditionError) +} + +type BackoffManager interface { + Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration +} + +type FastBackoffOptions struct { + Duration time.Duration + Factor float64 + Jitter float64 + MaxDuration time.Duration + InitDurationIfFail time.Duration + + // If FastRetryCount > 0, then within the FastRetryWindow time window, + // the retry will be performed with a delay of FastRetryDelay for the first FastRetryCount calls. + FastRetryCount int + FastRetryDelay time.Duration + FastRetryJitter float64 + FastRetryWindow time.Duration +} + +type fastBackoffImpl struct { + options FastBackoffOptions + clock clock.PassiveClock + + lastCalledTime time.Time + consecutiveErrCount int + + fastRetryCutoffTime time.Time + countsInFastRetryWindow int +} + +func NewFastBackoffManager(options FastBackoffOptions) BackoffManager { + return newFastBackoffManagerWithClock(options, clock.RealClock{}) +} + +func newFastBackoffManagerWithClock(options FastBackoffOptions, clk clock.PassiveClock) BackoffManager { + if clk == nil { + clk = clock.RealClock{} + } + return &fastBackoffImpl{ + options: options, + clock: clk, + countsInFastRetryWindow: 1, + } +} + +func (f *fastBackoffImpl) Backoff(previousDuration time.Duration, previousConditionError bool) time.Duration { + if f.lastCalledTime.IsZero() { + f.lastCalledTime = f.clock.Now() + return f.options.Duration + } + now := f.clock.Now() + f.lastCalledTime = now + + if previousConditionError { + f.consecutiveErrCount++ + } else { + f.consecutiveErrCount = 0 + } + + if f.options.FastRetryCount > 0 && previousConditionError { + f.countsInFastRetryWindow++ + if f.countsInFastRetryWindow <= f.options.FastRetryCount { + return Jitter(f.options.FastRetryDelay, f.options.FastRetryJitter) + } + if now.After(f.fastRetryCutoffTime) { + // reset + f.fastRetryCutoffTime = now.Add(f.options.FastRetryWindow) + f.countsInFastRetryWindow = 0 + } + } + + if previousConditionError { + var duration time.Duration + if f.consecutiveErrCount == 1 { + duration = util.EmptyOr(f.options.InitDurationIfFail, previousDuration) + } else { + duration = previousDuration + } + + duration = util.EmptyOr(duration, time.Second) + if f.options.Factor != 0 { + duration = time.Duration(float64(duration) * f.options.Factor) + } + if f.options.Jitter > 0 { + duration = Jitter(duration, f.options.Jitter) + } + if f.options.MaxDuration > 0 && duration > f.options.MaxDuration { + duration = f.options.MaxDuration + } + return duration + } + return f.options.Duration +} + +func BackoffUntil(f func() (bool, error), backoff BackoffManager, sliding bool, stopCh <-chan struct{}) { + var delay time.Duration + previousError := false + + ticker := time.NewTicker(backoff.Backoff(delay, previousError)) + defer ticker.Stop() + + for { + select { + case <-stopCh: + return + default: + } + + if !sliding { + delay = backoff.Backoff(delay, previousError) + } + + if done, err := f(); done { + return + } else if err != nil { + previousError = true + } else { + previousError = false + } + + if sliding { + delay = backoff.Backoff(delay, previousError) + } + + ticker.Reset(delay) + select { + case <-stopCh: + return + case <-ticker.C: + } + } +} + +// Jitter returns a time.Duration between duration and duration + maxFactor * +// duration. +// +// This allows clients to avoid converging on periodic behavior. If maxFactor +// is 0.0, a suggested default value will be chosen. +func Jitter(duration time.Duration, maxFactor float64) time.Duration { + if maxFactor <= 0.0 { + maxFactor = 1.0 + } + wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration)) + return wait +} + +func Until(f func(), period time.Duration, stopCh <-chan struct{}) { + ff := func() (bool, error) { + f() + return false, nil + } + BackoffUntil(ff, BackoffFunc(func(time.Duration, bool) time.Duration { + return period + }), true, stopCh) +} diff --git a/pkg/util/wait/backoff_test.go b/pkg/util/wait/backoff_test.go new file mode 100644 index 00000000000..edea3669381 --- /dev/null +++ b/pkg/util/wait/backoff_test.go @@ -0,0 +1,27 @@ +package wait + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + clocktesting "k8s.io/utils/clock/testing" +) + +func TestFastBackoffManagerUsesClock(t *testing.T) { + require := require.New(t) + + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + backoff := newFastBackoffManagerWithClock(FastBackoffOptions{ + Duration: time.Second, + }, clk).(*fastBackoffImpl) + + require.Equal(time.Second, backoff.Backoff(0, false)) + require.Equal(start, backoff.lastCalledTime) + + next := start.Add(time.Minute) + clk.SetTime(next) + require.Equal(time.Second, backoff.Backoff(time.Second, false)) + require.Equal(next, backoff.lastCalledTime) +} diff --git a/pkg/util/xlog/log_writer.go b/pkg/util/xlog/log_writer.go new file mode 100644 index 00000000000..28ddc22bbc3 --- /dev/null +++ b/pkg/util/xlog/log_writer.go @@ -0,0 +1,59 @@ +// Copyright 2025 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 "strings" + +// LogWriter forwards writes to frp's logger at configurable level. +// It is safe for concurrent use as long as the underlying Logger is thread-safe. +type LogWriter struct { + logFunc func(string) +} + +func (w LogWriter) Write(p []byte) (n int, err error) { + msg := strings.TrimSpace(string(p)) + w.logFunc(msg) + return len(p), nil +} + +func NewTraceWriter(xl *Logger) LogWriter { + return LogWriter{ + logFunc: func(msg string) { xl.Tracef("%s", msg) }, + } +} + +func NewDebugWriter(xl *Logger) LogWriter { + return LogWriter{ + logFunc: func(msg string) { xl.Debugf("%s", msg) }, + } +} + +func NewInfoWriter(xl *Logger) LogWriter { + return LogWriter{ + logFunc: func(msg string) { xl.Infof("%s", msg) }, + } +} + +func NewWarnWriter(xl *Logger) LogWriter { + return LogWriter{ + logFunc: func(msg string) { xl.Warnf("%s", msg) }, + } +} + +func NewErrorWriter(xl *Logger) LogWriter { + return LogWriter{ + logFunc: func(msg string) { xl.Errorf("%s", msg) }, + } +} diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go index b5746f9dfb8..1021d44af58 100644 --- a/pkg/util/xlog/xlog.go +++ b/pkg/util/xlog/xlog.go @@ -15,59 +15,102 @@ package xlog import ( + "cmp" + "slices" + "github.com/fatedier/frp/pkg/util/log" ) +type LogPrefix struct { + // Name is the name of the prefix, it won't be displayed in log but used to identify the prefix. + Name string + // Value is the value of the prefix, it will be displayed in log. + Value string + // The prefix with higher priority will be displayed first, default is 10. + Priority int +} + // Logger is not thread safety for operations on prefix type Logger struct { - prefixes []string + prefixes []LogPrefix prefixString string } func New() *Logger { return &Logger{ - prefixes: make([]string, 0), + prefixes: make([]LogPrefix, 0), } } -func (l *Logger) ResetPrefixes() (old []string) { +func (l *Logger) ResetPrefixes() (old []LogPrefix) { old = l.prefixes - l.prefixes = make([]string, 0) + l.prefixes = make([]LogPrefix, 0) l.prefixString = "" return } func (l *Logger) AppendPrefix(prefix string) *Logger { - l.prefixes = append(l.prefixes, prefix) - l.prefixString += "[" + prefix + "] " + return l.AddPrefix(LogPrefix{ + Name: prefix, + Value: prefix, + Priority: 10, + }) +} + +func (l *Logger) AddPrefix(prefix LogPrefix) *Logger { + found := false + if prefix.Priority <= 0 { + prefix.Priority = 10 + } + for i, p := range l.prefixes { + if p.Name == prefix.Name { + found = true + l.prefixes[i].Value = prefix.Value + l.prefixes[i].Priority = prefix.Priority + break + } + } + if !found { + l.prefixes = append(l.prefixes, prefix) + } + l.renderPrefixString() return l } -func (l *Logger) Spawn() *Logger { - nl := New() +func (l *Logger) renderPrefixString() { + slices.SortStableFunc(l.prefixes, func(a, b LogPrefix) int { + return cmp.Compare(a.Priority, b.Priority) + }) + l.prefixString = "" for _, v := range l.prefixes { - nl.AppendPrefix(v) + l.prefixString += "[" + v.Value + "] " } +} + +func (l *Logger) Spawn() *Logger { + nl := New() + nl.prefixes = append(nl.prefixes, l.prefixes...) + nl.renderPrefixString() return nl } -func (l *Logger) Error(format string, v ...interface{}) { - log.Log.Error(l.prefixString+format, v...) +func (l *Logger) Errorf(format string, v ...any) { + log.Logger.WithPrefix(l.prefixString).Errorf(format, v...) } -func (l *Logger) Warn(format string, v ...interface{}) { - log.Log.Warn(l.prefixString+format, v...) +func (l *Logger) Warnf(format string, v ...any) { + log.Logger.WithPrefix(l.prefixString).Warnf(format, v...) } -func (l *Logger) Info(format string, v ...interface{}) { - log.Log.Info(l.prefixString+format, v...) +func (l *Logger) Infof(format string, v ...any) { + log.Logger.WithPrefix(l.prefixString).Infof(format, v...) } -func (l *Logger) Debug(format string, v ...interface{}) { - log.Log.Debug(l.prefixString+format, v...) +func (l *Logger) Debugf(format string, v ...any) { + log.Logger.WithPrefix(l.prefixString).Debugf(format, v...) } -func (l *Logger) Trace(format string, v ...interface{}) { - log.Log.Trace(l.prefixString+format, v...) +func (l *Logger) Tracef(format string, v ...any) { + 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 00000000000..13d006ef56d --- /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/pkg/virtual/client.go b/pkg/virtual/client.go new file mode 100644 index 00000000000..c7006ce7157 --- /dev/null +++ b/pkg/virtual/client.go @@ -0,0 +1,113 @@ +// Copyright 2023 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 virtual + +import ( + "context" + "net" + + "github.com/fatedier/frp/client" + "github.com/fatedier/frp/pkg/config/source" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + netpkg "github.com/fatedier/frp/pkg/util/net" +) + +type ClientOptions struct { + Common *v1.ClientCommonConfig + Spec *msg.ClientSpec + HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool +} + +type Client struct { + l *netpkg.InternalListener + svr *client.Service +} + +func NewClient(options ClientOptions) (*Client, error) { + if options.Common != nil { + if err := options.Common.Complete(); err != nil { + return nil, err + } + } + + ln := netpkg.NewInternalListener() + configSource := source.NewConfigSource() + aggregator := source.NewAggregator(configSource) + + serviceOptions := client.ServiceOptions{ + Common: options.Common, + ConfigSourceAggregator: aggregator, + ClientSpec: options.Spec, + ConnectorCreator: func(context.Context, *v1.ClientCommonConfig) client.Connector { + return &pipeConnector{ + peerListener: ln, + } + }, + HandleWorkConnCb: options.HandleWorkConnCb, + } + svr, err := client.NewService(serviceOptions) + if err != nil { + return nil, err + } + return &Client{ + l: ln, + svr: svr, + }, nil +} + +func (c *Client) PeerListener() net.Listener { + return c.l +} + +func (c *Client) UpdateProxyConfigurer(proxyCfgs []v1.ProxyConfigurer) { + _ = c.svr.UpdateAllConfigurer(proxyCfgs, nil) +} + +func (c *Client) Run(ctx context.Context) error { + return c.svr.Run(ctx) +} + +func (c *Client) Service() *client.Service { + return c.svr +} + +func (c *Client) Close() { + c.svr.Close() + c.l.Close() +} + +type pipeConnector struct { + peerListener *netpkg.InternalListener +} + +func (pc *pipeConnector) Open() error { + return nil +} + +func (pc *pipeConnector) Connect() (net.Conn, error) { + c1, c2 := net.Pipe() + if err := pc.peerListener.PutConn(c1); err != nil { + c1.Close() + c2.Close() + return nil, err + } + return c2, nil +} + +func (pc *pipeConnector) Close() error { + pc.peerListener.Close() + return nil +} diff --git a/pkg/vnet/controller.go b/pkg/vnet/controller.go new file mode 100644 index 00000000000..348d50d2612 --- /dev/null +++ b/pkg/vnet/controller.go @@ -0,0 +1,387 @@ +// Copyright 2025 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 vnet + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net" + "sync" + + "github.com/fatedier/golib/pool" + "github.com/songgao/water/waterutil" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/pkg/util/xlog" +) + +const ( + maxPacketSize = 1420 +) + +type Controller struct { + addr string + + tun io.ReadWriteCloser + clientRouter *clientRouter // Route based on destination IP (client mode) + serverRouter *serverRouter // Route based on source IP (server mode) +} + +func NewController(cfg v1.VirtualNetConfig) *Controller { + return &Controller{ + addr: cfg.Address, + clientRouter: newClientRouter(), + serverRouter: newServerRouter(), + } +} + +func (c *Controller) Init() error { + tunDevice, err := OpenTun(context.Background(), c.addr) + if err != nil { + return err + } + c.tun = tunDevice + return nil +} + +func (c *Controller) Run() error { + conn := c.tun + + for { + buf := pool.GetBuf(maxPacketSize) + n, err := conn.Read(buf) + if err != nil { + pool.PutBuf(buf) + log.Warnf("vnet read from tun error: %v", err) + return err + } + + c.handlePacket(buf[:n]) + pool.PutBuf(buf) + } +} + +// handlePacket processes a single packet. The caller is responsible for managing the buffer. +func (c *Controller) handlePacket(buf []byte) { + log.Tracef("vnet read from tun [%d]: %s", len(buf), base64.StdEncoding.EncodeToString(buf)) + + var src, dst net.IP + switch { + case waterutil.IsIPv4(buf): + header, err := ipv4.ParseHeader(buf) + if err != nil { + log.Warnf("parse ipv4 header error: %v", err) + return + } + src = header.Src + dst = header.Dst + log.Tracef("%s >> %s %d/%-4d %-4x %d", + header.Src, header.Dst, + header.Len, header.TotalLen, header.ID, header.Flags) + case waterutil.IsIPv6(buf): + header, err := ipv6.ParseHeader(buf) + if err != nil { + log.Warnf("parse ipv6 header error: %v", err) + return + } + src = header.Src + dst = header.Dst + log.Tracef("%s >> %s %d %d", + header.Src, header.Dst, + header.PayloadLen, header.TrafficClass) + default: + log.Tracef("unknown packet, discarded(%d)", len(buf)) + return + } + + targetConn, err := c.clientRouter.findConn(dst) + if err == nil { + if err := WriteMessage(targetConn, buf); err != nil { + log.Warnf("write to client target conn error: %v", err) + } + return + } + + targetConn, err = c.serverRouter.findConnBySrc(dst) + if err == nil { + if err := WriteMessage(targetConn, buf); err != nil { + log.Warnf("write to server target conn error: %v", err) + } + return + } + + log.Tracef("no route found for packet from %s to %s", src, dst) +} + +func (c *Controller) Stop() error { + if c.tun == nil { + return nil + } + return c.tun.Close() +} + +// Client connection read loop +func (c *Controller) readLoopClient(ctx context.Context, conn io.ReadWriteCloser) { + xl := xlog.FromContextSafe(ctx) + defer func() { + // Remove the route when read loop ends (connection closed) + c.clientRouter.removeConnRoute(conn) + conn.Close() + }() + + for { + data, err := ReadMessage(conn) + if err != nil { + xl.Warnf("client read error: %v", err) + return + } + + if len(data) == 0 { + continue + } + + switch { + case waterutil.IsIPv4(data): + header, err := ipv4.ParseHeader(data) + if err != nil { + xl.Warnf("parse ipv4 header error: %v", err) + continue + } + xl.Tracef("%s >> %s %d/%-4d %-4x %d", + header.Src, header.Dst, + header.Len, header.TotalLen, header.ID, header.Flags) + case waterutil.IsIPv6(data): + header, err := ipv6.ParseHeader(data) + if err != nil { + xl.Warnf("parse ipv6 header error: %v", err) + continue + } + xl.Tracef("%s >> %s %d %d", + header.Src, header.Dst, + header.PayloadLen, header.TrafficClass) + default: + xl.Tracef("unknown packet, discarded(%d)", len(data)) + continue + } + + xl.Tracef("vnet write to tun (client) [%d]: %s", len(data), base64.StdEncoding.EncodeToString(data)) + _, err = c.tun.Write(data) + if err != nil { + xl.Warnf("client write tun error: %v", err) + } + } +} + +// Server connection read loop +func (c *Controller) readLoopServer(ctx context.Context, conn io.ReadWriteCloser, onClose func()) { + xl := xlog.FromContextSafe(ctx) + defer func() { + // Clean up all IP mappings associated with this connection when it closes + c.serverRouter.cleanupConnIPs(conn) + // Call the provided callback upon closure + if onClose != nil { + onClose() + } + conn.Close() + }() + + for { + data, err := ReadMessage(conn) + if err != nil { + xl.Warnf("server read error: %v", err) + return + } + + if len(data) == 0 { + continue + } + + // Register source IP to connection mapping + if waterutil.IsIPv4(data) || waterutil.IsIPv6(data) { + var src net.IP + if waterutil.IsIPv4(data) { + header, err := ipv4.ParseHeader(data) + if err == nil { + src = header.Src + c.serverRouter.registerSrcIP(src, conn) + } + } else { + header, err := ipv6.ParseHeader(data) + if err == nil { + src = header.Src + c.serverRouter.registerSrcIP(src, conn) + } + } + } + + xl.Tracef("vnet write to tun (server) [%d]: %s", len(data), base64.StdEncoding.EncodeToString(data)) + _, err = c.tun.Write(data) + if err != nil { + xl.Warnf("server write tun error: %v", err) + } + } +} + +// RegisterClientRoute registers a client route (based on destination IP CIDR) +// and starts the read loop +func (c *Controller) RegisterClientRoute(ctx context.Context, name string, routes []net.IPNet, conn io.ReadWriteCloser) { + c.clientRouter.addRoute(name, routes, conn) + go c.readLoopClient(ctx, conn) +} + +// UnregisterClientRoute Remove client route from routing table +func (c *Controller) UnregisterClientRoute(name string) { + c.clientRouter.delRoute(name) +} + +// StartServerConnReadLoop starts the read loop for a server connection +// (dynamically associates with source IPs) +func (c *Controller) StartServerConnReadLoop(ctx context.Context, conn io.ReadWriteCloser, onClose func()) { + go c.readLoopServer(ctx, conn, onClose) +} + +// ParseRoutes Convert route strings to IPNet objects +func ParseRoutes(routeStrings []string) ([]net.IPNet, error) { + routes := make([]net.IPNet, 0, len(routeStrings)) + for _, r := range routeStrings { + _, ipNet, err := net.ParseCIDR(r) + if err != nil { + return nil, fmt.Errorf("parse route %s error: %v", r, err) + } + routes = append(routes, *ipNet) + } + return routes, nil +} + +// Client router (based on destination IP routing) +type clientRouter struct { + routes map[string]*routeElement + mu sync.RWMutex +} + +func newClientRouter() *clientRouter { + return &clientRouter{ + routes: make(map[string]*routeElement), + } +} + +func (r *clientRouter) addRoute(name string, routes []net.IPNet, conn io.ReadWriteCloser) { + r.mu.Lock() + defer r.mu.Unlock() + r.routes[name] = &routeElement{ + routes: routes, + conn: conn, + } +} + +func (r *clientRouter) findConn(dst net.IP) (io.Writer, error) { + r.mu.RLock() + defer r.mu.RUnlock() + for _, re := range r.routes { + for _, route := range re.routes { + if route.Contains(dst) { + return re.conn, nil + } + } + } + return nil, fmt.Errorf("no route found for destination %s", dst) +} + +func (r *clientRouter) delRoute(name string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.routes, name) +} + +func (r *clientRouter) removeConnRoute(conn io.Writer) { + r.mu.Lock() + defer r.mu.Unlock() + for name, re := range r.routes { + if re.conn == conn { + delete(r.routes, name) + return + } + } +} + +// Server router (based solely on source IP routing) +type serverRouter struct { + srcIPConns map[string]io.Writer // Source IP string to connection mapping + mu sync.RWMutex +} + +func newServerRouter() *serverRouter { + return &serverRouter{ + srcIPConns: make(map[string]io.Writer), + } +} + +func (r *serverRouter) findConnBySrc(src net.IP) (io.Writer, error) { + r.mu.RLock() + defer r.mu.RUnlock() + conn, exists := r.srcIPConns[src.String()] + if !exists { + return nil, fmt.Errorf("no route found for source %s", src) + } + return conn, nil +} + +func (r *serverRouter) registerSrcIP(src net.IP, conn io.Writer) { + key := src.String() + + r.mu.RLock() + existingConn, ok := r.srcIPConns[key] + r.mu.RUnlock() + + // If the entry exists and the connection is the same, no need to do anything. + if ok && existingConn == conn { + return + } + + // Acquire write lock to update the map. + r.mu.Lock() + defer r.mu.Unlock() + + // Double-check after acquiring the write lock to handle potential race conditions. + existingConn, ok = r.srcIPConns[key] + if ok && existingConn == conn { + return + } + + r.srcIPConns[key] = conn +} + +// cleanupConnIPs removes all IP mappings associated with the specified connection +func (r *serverRouter) cleanupConnIPs(conn io.Writer) { + r.mu.Lock() + defer r.mu.Unlock() + + // Find and delete all IP mappings pointing to this connection + for ip, mappedConn := range r.srcIPConns { + if mappedConn == conn { + delete(r.srcIPConns, ip) + } + } +} + +type routeElement struct { + routes []net.IPNet + conn io.ReadWriteCloser +} diff --git a/pkg/vnet/message.go b/pkg/vnet/message.go new file mode 100644 index 00000000000..68ac7704c12 --- /dev/null +++ b/pkg/vnet/message.go @@ -0,0 +1,81 @@ +// Copyright 2025 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 vnet + +import ( + "encoding/binary" + "fmt" + "io" +) + +// Maximum message size +const ( + maxMessageSize = 1024 * 1024 // 1MB +) + +// Format: [length(4 bytes)][data(length bytes)] + +// ReadMessage reads a framed message from the reader +func ReadMessage(r io.Reader) ([]byte, error) { + // Read length (4 bytes) + var length uint32 + err := binary.Read(r, binary.LittleEndian, &length) + if err != nil { + return nil, fmt.Errorf("read message length error: %w", err) + } + + // Check length to prevent DoS + if length == 0 { + return nil, fmt.Errorf("message length is 0") + } + if length > maxMessageSize { + return nil, fmt.Errorf("message too large: %d > %d", length, maxMessageSize) + } + + // Read message data + data := make([]byte, length) + _, err = io.ReadFull(r, data) + if err != nil { + return nil, fmt.Errorf("read message data error: %w", err) + } + + return data, nil +} + +// WriteMessage writes a framed message to the writer +func WriteMessage(w io.Writer, data []byte) error { + // Get data length + length := uint32(len(data)) + if length == 0 { + return fmt.Errorf("message data length is 0") + } + if length > maxMessageSize { + return fmt.Errorf("message too large: %d > %d", length, maxMessageSize) + } + + // Write length + err := binary.Write(w, binary.LittleEndian, length) + if err != nil { + return fmt.Errorf("write message length error: %w", err) + } + + // Write message data + _, err = w.Write(data) + if err != nil { + return fmt.Errorf("write message data error: %w", err) + } + + return nil +} diff --git a/pkg/vnet/tun.go b/pkg/vnet/tun.go new file mode 100644 index 00000000000..d26314d053e --- /dev/null +++ b/pkg/vnet/tun.go @@ -0,0 +1,109 @@ +// Copyright 2025 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 vnet + +import ( + "context" + "io" + + "github.com/fatedier/golib/pool" + "golang.zx2c4.com/wireguard/tun" +) + +const ( + offset = 16 + defaultPacketSize = 1420 +) + +type TunDevice interface { + io.ReadWriteCloser +} + +func OpenTun(ctx context.Context, addr string) (TunDevice, error) { + td, err := openTun(ctx, addr) + if err != nil { + return nil, err + } + + mtu, err := td.MTU() + if err != nil { + mtu = defaultPacketSize + } + + bufferSize := max(mtu, defaultPacketSize) + batchSize := td.BatchSize() + + device := &tunDeviceWrapper{ + dev: td, + bufferSize: bufferSize, + readBuffers: make([][]byte, batchSize), + sizeBuffer: make([]int, batchSize), + } + + for i := range device.readBuffers { + device.readBuffers[i] = make([]byte, offset+bufferSize) + } + + return device, nil +} + +type tunDeviceWrapper struct { + dev tun.Device + bufferSize int + readBuffers [][]byte + packetBuffers [][]byte + sizeBuffer []int +} + +func (d *tunDeviceWrapper) Read(p []byte) (int, error) { + if len(d.packetBuffers) > 0 { + n := copy(p, d.packetBuffers[0]) + d.packetBuffers = d.packetBuffers[1:] + return n, nil + } + + n, err := d.dev.Read(d.readBuffers, d.sizeBuffer, offset) + if err != nil { + return 0, err + } + if n == 0 { + return 0, io.EOF + } + + for i := range n { + if d.sizeBuffer[i] <= 0 { + continue + } + d.packetBuffers = append(d.packetBuffers, d.readBuffers[i][offset:offset+d.sizeBuffer[i]]) + } + + dataSize := copy(p, d.packetBuffers[0]) + d.packetBuffers = d.packetBuffers[1:] + + return dataSize, nil +} + +func (d *tunDeviceWrapper) Write(p []byte) (int, error) { + buf := pool.GetBuf(offset + d.bufferSize) + defer pool.PutBuf(buf) + + n := copy(buf[offset:], p) + _, err := d.dev.Write([][]byte{buf[:offset+n]}, offset) + return n, err +} + +func (d *tunDeviceWrapper) Close() error { + return d.dev.Close() +} diff --git a/pkg/vnet/tun_darwin.go b/pkg/vnet/tun_darwin.go new file mode 100644 index 00000000000..9fa7d42c58f --- /dev/null +++ b/pkg/vnet/tun_darwin.go @@ -0,0 +1,85 @@ +// Copyright 2025 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 vnet + +import ( + "context" + "fmt" + "net" + "os/exec" + + "golang.zx2c4.com/wireguard/tun" +) + +const ( + defaultTunName = "utun" + defaultMTU = 1420 +) + +func openTun(_ context.Context, addr string) (tun.Device, error) { + dev, err := tun.CreateTUN(defaultTunName, defaultMTU) + if err != nil { + return nil, err + } + + name, err := dev.Name() + if err != nil { + return nil, err + } + + ip, ipNet, err := net.ParseCIDR(addr) + if err != nil { + return nil, err + } + + // Calculate a peer IP for the point-to-point tunnel + peerIP := generatePeerIP(ip) + + // Configure the interface with proper point-to-point addressing + if err = exec.Command("ifconfig", name, "inet", ip.String(), peerIP.String(), "mtu", fmt.Sprint(defaultMTU), "up").Run(); err != nil { + return nil, err + } + + // Add default route for the tunnel subnet + routes := []net.IPNet{*ipNet} + if err = addRoutes(name, routes); err != nil { + return nil, err + } + return dev, nil +} + +// generatePeerIP creates a peer IP for the point-to-point tunnel +// by incrementing the last octet of the IP +func generatePeerIP(ip net.IP) net.IP { + // Make a copy to avoid modifying the original + peerIP := make(net.IP, len(ip)) + copy(peerIP, ip) + + // Increment the last octet + peerIP[len(peerIP)-1]++ + + return peerIP +} + +// addRoutes configures system routes for the TUN interface +func addRoutes(ifName string, routes []net.IPNet) error { + for _, route := range routes { + routeStr := route.String() + if err := exec.Command("route", "add", "-net", routeStr, "-interface", ifName).Run(); err != nil { + return err + } + } + return nil +} diff --git a/pkg/vnet/tun_linux.go b/pkg/vnet/tun_linux.go new file mode 100644 index 00000000000..2e0cc56b234 --- /dev/null +++ b/pkg/vnet/tun_linux.go @@ -0,0 +1,131 @@ +// Copyright 2025 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 vnet + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + + "github.com/vishvananda/netlink" + "golang.zx2c4.com/wireguard/tun" +) + +const ( + baseTunName = "utun" + defaultMTU = 1420 +) + +func openTun(_ context.Context, addr string) (tun.Device, error) { + name, err := findNextTunName(baseTunName) + if err != nil { + name = getFallbackTunName(baseTunName, addr) + } + + tunDevice, err := tun.CreateTUN(name, defaultMTU) + if err != nil { + return nil, fmt.Errorf("failed to create TUN device '%s': %w", name, err) + } + + actualName, err := tunDevice.Name() + if err != nil { + return nil, err + } + + ifn, err := net.InterfaceByName(actualName) + if err != nil { + return nil, err + } + + link, err := netlink.LinkByName(actualName) + if err != nil { + return nil, err + } + + ip, cidr, err := net.ParseCIDR(addr) + if err != nil { + return nil, err + } + if err := netlink.AddrAdd(link, &netlink.Addr{ + IPNet: &net.IPNet{ + IP: ip, + Mask: cidr.Mask, + }, + }); err != nil { + return nil, err + } + + if err := netlink.LinkSetUp(link); err != nil { + return nil, err + } + + if err = addRoutes(ifn, cidr); err != nil { + return nil, err + } + return tunDevice, nil +} + +func findNextTunName(basename string) (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("failed to get network interfaces: %w", err) + } + maxSuffix := -1 + + for _, iface := range interfaces { + name := iface.Name + if strings.HasPrefix(name, basename) { + suffix := name[len(basename):] + if suffix == "" { + continue + } + + numSuffix, err := strconv.Atoi(suffix) + if err == nil && numSuffix > maxSuffix { + maxSuffix = numSuffix + } + } + } + + nextSuffix := maxSuffix + 1 + name := fmt.Sprintf("%s%d", basename, nextSuffix) + return name, nil +} + +func addRoutes(ifn *net.Interface, cidr *net.IPNet) error { + r := netlink.Route{ + Dst: cidr, + LinkIndex: ifn.Index, + } + if err := netlink.RouteReplace(&r); err != nil { + return fmt.Errorf("add route to %v error: %v", r.Dst, err) + } + return nil +} + +// getFallbackTunName generates a deterministic fallback TUN device name +// based on the base name and the provided address string using a hash. +func getFallbackTunName(baseName, addr string) string { + hasher := sha256.New() + hasher.Write([]byte(addr)) + hashBytes := hasher.Sum(nil) + // Use first 4 bytes -> 8 hex chars for brevity, respecting IFNAMSIZ limit. + shortHash := hex.EncodeToString(hashBytes[:4]) + return fmt.Sprintf("%s%s", baseName, shortHash) +} diff --git a/pkg/vnet/tun_unsupported.go b/pkg/vnet/tun_unsupported.go new file mode 100644 index 00000000000..731a06cda9e --- /dev/null +++ b/pkg/vnet/tun_unsupported.go @@ -0,0 +1,29 @@ +// Copyright 2025 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 !darwin && !linux + +package vnet + +import ( + "context" + "fmt" + "runtime" + + "golang.zx2c4.com/wireguard/tun" +) + +func openTun(_ context.Context, _ string) (tun.Device, error) { + return nil, fmt.Errorf("virtual net is not supported on this platform (%s/%s)", runtime.GOOS, runtime.GOARCH) +} diff --git a/server/api_router.go b/server/api_router.go new file mode 100644 index 00000000000..59a55016ca4 --- /dev/null +++ b/server/api_router.go @@ -0,0 +1,75 @@ +// Copyright 2017 fatedier, fatedier@gmail.com +// +// 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 server + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + httppkg "github.com/fatedier/frp/pkg/util/http" + netpkg "github.com/fatedier/frp/pkg/util/net" + adminapi "github.com/fatedier/frp/server/http" +) + +func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) { + helper.Router.HandleFunc("/healthz", healthz) + subRouter := helper.Router.NewRoute().Subrouter() + + subRouter.Use(helper.AuthMiddleware) + subRouter.Use(httppkg.NewRequestLogger) + + // metrics + if svr.cfg.EnablePrometheus { + subRouter.Handle("/metrics", promhttp.Handler()) + } + + apiController := adminapi.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager) + + // apis + subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET") + subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET") + subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET") + subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET") + subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET") + subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET") + subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET") + subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE") + + subRouter.HandleFunc("/api/v2/users", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2UserList)).Methods("GET") + subRouter.HandleFunc("/api/v2/system/info", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2SystemInfo)).Methods("GET") + subRouter.HandleFunc("/api/v2/system/prune", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2SystemPrune)).Methods("POST") + subRouter.HandleFunc("/api/v2/clients", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2ClientList)).Methods("GET") + v2EncodedPathRouter := subRouter.NewRoute().Subrouter() + v2EncodedPathRouter.UseEncodedPath() + v2EncodedPathRouter.HandleFunc("/api/v2/clients/{key}", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2ClientDetail)).Methods("GET") + subRouter.HandleFunc("/api/v2/proxies", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2ProxyList)).Methods("GET") + v2EncodedPathRouter.HandleFunc("/api/v2/proxies/{name}/traffic", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2ProxyTraffic)).Methods("GET") + v2EncodedPathRouter.HandleFunc("/api/v2/proxies/{name}", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2ProxyDetail)).Methods("GET") + + // view + subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") + subRouter.PathPrefix("/static/").Handler( + netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))), + ).Methods("GET") + + subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/static/", http.StatusMovedPermanently) + }) +} + +func healthz(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(200) +} diff --git a/server/control.go b/server/control.go index 197c94de968..8009d8aca0b 100644 --- a/server/control.go +++ b/server/control.go @@ -17,98 +17,387 @@ package server import ( "context" "fmt" - "io" + "math" "net" "runtime/debug" "sync" + "sync/atomic" "time" + "github.com/samber/lo" + "github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - frpErr "github.com/fatedier/frp/pkg/errors" + v1 "github.com/fatedier/frp/pkg/config/v1" + pkgerr "github.com/fatedier/frp/pkg/errors" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/util" - "github.com/fatedier/frp/pkg/util/version" + "github.com/fatedier/frp/pkg/util/wait" "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/metrics" "github.com/fatedier/frp/server/proxy" - - "github.com/fatedier/golib/control/shutdown" - "github.com/fatedier/golib/crypto" - "github.com/fatedier/golib/errors" + "github.com/fatedier/frp/server/registry" ) +type ControlID uint64 + +var nextControlID atomic.Uint64 + +const workConnPoolCapacityOffset = 10 + +type controlEntry struct { + ctl *Control + id ControlID + // runMu serializes lifecycle and routing decisions for one run ID. + // Replacements inherit it; removing the entry releases the manager's reference. + runMu *sync.Mutex + + registryOnline bool + registryControlID ControlID +} + type ControlManager struct { // controls indexed by run id - ctlsByRunID map[string]*Control + ctlsByRunID map[string]*controlEntry + registry *registry.ClientRegistry + closed bool mu sync.RWMutex } -func NewControlManager() *ControlManager { +func NewControlManager(clientRegistry *registry.ClientRegistry) *ControlManager { return &ControlManager{ - ctlsByRunID: make(map[string]*Control), + ctlsByRunID: make(map[string]*controlEntry), + registry: clientRegistry, } } -func (cm *ControlManager) Add(runID string, ctl *Control) (oldCtl *Control) { +// lockCurrentRun returns the current entry with its run gate held. It never +// waits for the gate while holding cm.mu and revalidates the gate after waiting. +// The global order is runMu, cm.mu, ctl.lifecycleMu, then registry locks. +func (cm *ControlManager) lockCurrentRun(runID string, allowClosed bool) (*controlEntry, bool) { + cm.mu.RLock() + entry, ok := cm.ctlsByRunID[runID] + if cm.closed && !allowClosed { + ok = false + } + cm.mu.RUnlock() + if !ok { + return nil, false + } + + runMu := entry.runMu + runMu.Lock() + cm.mu.RLock() + entry, ok = cm.ctlsByRunID[runID] + if (cm.closed && !allowClosed) || !ok || entry.runMu != runMu { + ok = false + } + cm.mu.RUnlock() + if !ok { + runMu.Unlock() + return nil, false + } + return entry, true +} + +// Add makes ctl the pending current generation and records the predecessor +// finalization barrier it must wait for before activation. +func (cm *ControlManager) Add(ctl *Control) error { + for { + // Never wait for a run gate while holding cm.mu. + cm.mu.RLock() + old := cm.ctlsByRunID[ctl.runID] + cm.mu.RUnlock() + if old != nil { + old.runMu.Lock() + } + + cm.mu.Lock() + if cm.closed { + cm.mu.Unlock() + if old != nil { + old.runMu.Unlock() + } + return fmt.Errorf("control manager is closed") + } + if cm.ctlsByRunID[ctl.runID] != old { + cm.mu.Unlock() + if old != nil { + old.runMu.Unlock() + } + continue + } + + id := ControlID(nextControlID.Add(1)) + if err := ctl.admit(cm, id); err != nil { + cm.mu.Unlock() + if old != nil { + old.runMu.Unlock() + } + return err + } + + runMu := &sync.Mutex{} + if old != nil { + runMu = old.runMu + } + entry := &controlEntry{ctl: ctl, id: id, runMu: runMu} + var ( + oldCtl *Control + barrier <-chan struct{} + ) + if old != nil { + oldCtl = old.ctl + barrier = oldCtl.markReplaced() + ctl.setHandoffBarrier(barrier) + entry.registryOnline = old.registryOnline + entry.registryControlID = old.registryControlID + } + cm.ctlsByRunID[ctl.runID] = entry + cm.mu.Unlock() + if old != nil { + old.runMu.Unlock() + } + + if oldCtl != nil { + oldCtl.Replaced(ctl) + } + return nil + } +} + +// Activate registers ctl as online only if it is still the pending current +// generation. +func (cm *ControlManager) Activate(ctl *Control) (bool, error) { + entry, ok := cm.lockCurrentRun(ctl.runID, false) + if !ok { + return false, nil + } + defer entry.runMu.Unlock() cm.mu.Lock() defer cm.mu.Unlock() - oldCtl, ok := cm.ctlsByRunID[runID] - if ok { - oldCtl.Replaced(ctl) + if cm.closed || cm.ctlsByRunID[ctl.runID] != entry || entry.ctl != ctl || entry.id != ctl.controlID { + return false, nil } - cm.ctlsByRunID[runID] = ctl - return + + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + if ctl.state != controlStatePending { + return false, nil + } + if ctl.activated { + return true, nil + } + + loginMsg := ctl.sessionCtx.LoginMsg + remoteAddr := ctl.sessionCtx.Conn.RemoteAddr().String() + if host, _, err := net.SplitHostPort(remoteAddr); err == nil { + remoteAddr = host + } + _, conflict := cm.registry.RegisterWithControlID( + loginMsg.User, + loginMsg.ClientID, + ctl.runID, + loginMsg.Hostname, + loginMsg.Version, + remoteAddr, + ctl.sessionCtx.WireProtocol, + uint64(entry.id), + ) + if conflict { + return true, fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User) + } + + entry.registryOnline = true + entry.registryControlID = entry.id + ctl.activated = true + return true, nil } -// we should make sure if it's the same control to prevent delete a new one -func (cm *ControlManager) Del(runID string, ctl *Control) { +// completeLogin reserves ctl's current ownership with its run gate while the +// bounded successful LoginResp write runs, then transitions it to running. +// The callback must only perform that bounded write; it must not call back into +// the control manager or the same control lifecycle. +func (cm *ControlManager) completeLogin(ctl *Control, writeSuccess func() error) (bool, error) { + entry, ok := cm.lockCurrentRun(ctl.runID, false) + if !ok { + return false, nil + } + defer entry.runMu.Unlock() + if entry.ctl != ctl || entry.id != ctl.controlID { + return false, nil + } + + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + if ctl.state != controlStatePending || !ctl.activated { + return false, nil + } + if err := writeSuccess(); err != nil { + return false, err + } + if !ctl.startLocked() { + return false, nil + } + return true, nil +} + +// Remove deletes and offlines ctl only if it is still the current generation. +func (cm *ControlManager) Remove(ctl *Control) bool { + entry, ok := cm.lockCurrentRun(ctl.runID, true) + if !ok { + return false + } + defer entry.runMu.Unlock() cm.mu.Lock() defer cm.mu.Unlock() - if c, ok := cm.ctlsByRunID[runID]; ok && c == ctl { - delete(cm.ctlsByRunID, runID) + + if cm.ctlsByRunID[ctl.runID] != entry || entry.ctl != ctl || entry.id != ctl.controlID { + return false } + delete(cm.ctlsByRunID, ctl.runID) + if entry.registryOnline { + cm.registry.MarkOfflineByRunIDAndControlID(ctl.runID, uint64(entry.registryControlID)) + } + return true } func (cm *ControlManager) GetByID(runID string) (ctl *Control, ok bool) { - cm.mu.RLock() - defer cm.mu.RUnlock() - ctl, ok = cm.ctlsByRunID[runID] - return + entry, ok := cm.lockCurrentRun(runID, false) + if !ok { + return nil, false + } + defer entry.runMu.Unlock() + ctl = entry.ctl + + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + if ctl.state != controlStateRunning { + return nil, false + } + return ctl, true } -type Control struct { - // all resource managers and controllers - rc *controller.ResourceController +// 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, wireProtocol, udpPacketCodec string) error) (bool, error) { + entry, ok := cm.lockCurrentRun(runID, false) + if !ok { + return false, nil + } + defer entry.runMu.Unlock() + ctl := entry.ctl - // proxy manager - pxyManager *proxy.Manager + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + if ctl.state != controlStateRunning { + return false, nil + } + return true, admit(ctl.sessionCtx.LoginMsg.User, ctl.sessionCtx.WireProtocol, ctl.sessionCtx.UDPPacketCodec) +} - // plugin manager - pluginManager *plugin.Manager +// RegisterWorkConn transfers conn to ctl only if ctl is still the current +// running generation. On error, ownership remains with the caller. +func (cm *ControlManager) RegisterWorkConn(ctl *Control, conn *proxy.WorkConn) error { + entry, ok := cm.lockCurrentRun(ctl.runID, false) + if !ok { + cm.mu.RLock() + closed := cm.closed + cm.mu.RUnlock() + if closed { + return fmt.Errorf("control manager is closed") + } + return fmt.Errorf("client control for run id [%s] is no longer current", ctl.runID) + } + defer entry.runMu.Unlock() + if entry.ctl != ctl || entry.id != ctl.controlID { + return fmt.Errorf("client control for run id [%s] is no longer current", ctl.runID) + } - // verifies authentication based on selected method - authVerifier auth.Verifier + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + if ctl.state != controlStateRunning { + return fmt.Errorf("client control for run id [%s] is not running", ctl.runID) + } - // login message - loginMsg *msg.Login + select { + case ctl.workConnCh <- conn: + ctl.xl.Debugf("new work connection registered") + return nil + default: + ctl.xl.Debugf("work connection pool is full, discarding") + return fmt.Errorf("work connection pool is full, discarding") + } +} +func (cm *ControlManager) Close() error { + cm.mu.Lock() + cm.closed = true + ctls := make([]*Control, 0, len(cm.ctlsByRunID)) + for _, entry := range cm.ctlsByRunID { + ctls = append(ctls, entry.ctl) + } + cm.mu.Unlock() + + for _, ctl := range ctls { + cm.Remove(ctl) + _ = ctl.Close() + } + return nil +} + +// SessionContext encapsulates the input parameters for creating a new Control. +type SessionContext struct { + // all resource managers and controllers + RC *controller.ResourceController + // proxy manager + PxyManager *proxy.Manager + // plugin manager + PluginManager *plugin.Manager + // verifies authentication based on selected method + AuthVerifier auth.Verifier + // key used for connection encryption + EncryptionKey []byte // control connection - conn net.Conn + Conn *msg.Conn + // login message + LoginMsg *msg.Login + // server configuration + ServerCfg *v1.ServerConfig + // negotiated wire protocol for this client session + WireProtocol string + UDPPacketCodec string +} + +type controlState uint8 + +const ( + controlStateCreated controlState = iota + controlStatePending + controlStateRunning + controlStateClosing + controlStateClosed +) + +type Control struct { + // session context + sessionCtx *SessionContext - // put a message in this channel to send it over control connection to client - sendCh chan (msg.Message) + // other components can use this to communicate with client + msgTransporter transport.MessageTransporter - // read from this channel to get the next message sent by client - readCh chan (msg.Message) + // msgDispatcher is a wrapper for control connection. + // It provides a channel for sending messages, and you can register handlers to process messages based on their respective types. + msgDispatcher *msg.Dispatcher // work connections - workConnCh chan net.Conn + workConnCh chan *proxy.WorkConn // proxies in one client proxies map[string]proxy.Proxy @@ -120,120 +409,195 @@ type Control struct { portsUsedNum int // last time got the Ping message - lastPing time.Time + lastPing atomic.Value - // A new run id will be generated when a new client login. - // If run id got from login message has same run id, it means it's the same client, so we can - // replace old controller instantly. - runID string + // runID never changes during the lifetime of a control. controlID is assigned + // once by ControlManager and distinguishes same-runID generations. + runID string + controlID ControlID + manager *ControlManager - // control status - status string + lifecycleMu sync.Mutex + state controlState + activated bool + handoffBarrier <-chan struct{} - readerShutdown *shutdown.Shutdown - writerShutdown *shutdown.Shutdown - managerShutdown *shutdown.Shutdown - allShutdown *shutdown.Shutdown + interruptOnce sync.Once + interruptErr error mu sync.RWMutex - // Server configuration information - serverCfg config.ServerCommonConf - - xl *xlog.Logger - ctx context.Context -} - -func NewControl( - ctx context.Context, - rc *controller.ResourceController, - pxyManager *proxy.Manager, - pluginManager *plugin.Manager, - authVerifier auth.Verifier, - ctlConn net.Conn, - loginMsg *msg.Login, - serverCfg config.ServerCommonConf, -) *Control { - - poolCount := loginMsg.PoolCount - if poolCount > int(serverCfg.MaxPoolCount) { - poolCount = int(serverCfg.MaxPoolCount) - } - return &Control{ - rc: rc, - pxyManager: pxyManager, - pluginManager: pluginManager, - authVerifier: authVerifier, - conn: ctlConn, - loginMsg: loginMsg, - sendCh: make(chan msg.Message, 10), - readCh: make(chan msg.Message, 10), - workConnCh: make(chan net.Conn, poolCount+10), - proxies: make(map[string]proxy.Proxy), - poolCount: poolCount, - portsUsedNum: 0, - lastPing: time.Now(), - runID: loginMsg.RunID, - status: consts.Working, - readerShutdown: shutdown.New(), - writerShutdown: shutdown.New(), - managerShutdown: shutdown.New(), - allShutdown: shutdown.New(), - serverCfg: serverCfg, - xl: xlog.FromContextSafe(ctx), - ctx: ctx, - } -} - -// Start send a login success message to client and start working. -func (ctl *Control) Start() { - loginRespMsg := &msg.LoginResp{ - Version: version.Full(), - RunID: ctl.runID, - ServerUDPPort: ctl.serverCfg.BindUDPPort, - Error: "", - } - msg.WriteMsg(ctl.conn, loginRespMsg) - - go ctl.writer() - for i := 0; i < ctl.poolCount; i++ { - ctl.sendCh <- &msg.ReqWorkConn{} - } - - go ctl.manager() - go ctl.reader() - go ctl.stoper() -} - -func (ctl *Control) RegisterWorkConn(conn net.Conn) error { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() + xl *xlog.Logger + ctx context.Context + doneCh chan struct{} + serverMetrics metrics.ServerMetrics +} - select { - case ctl.workConnCh <- conn: - xl.Debug("new work connection registered") +func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, error) { + 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+workConnPoolCapacityOffset), + proxies: make(map[string]proxy.Proxy), + poolCount: poolCount, + portsUsedNum: 0, + runID: sessionCtx.LoginMsg.RunID, + state: controlStateCreated, + xl: xlog.FromContextSafe(ctx), + ctx: ctx, + doneCh: make(chan struct{}), + serverMetrics: metrics.Server, + } + ctl.lastPing.Store(time.Now()) + + ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn) + ctl.registerMsgHandlers() + ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher) + return ctl, nil +} + +func (ctl *Control) RunID() string { + return ctl.runID +} + +func (ctl *Control) ID() ControlID { + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + return ctl.controlID +} + +func (ctl *Control) admit(manager *ControlManager, id ControlID) error { + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + if ctl.state != controlStateCreated { + return fmt.Errorf("control [%s] is not in created state", ctl.runID) + } + ctl.manager = manager + ctl.controlID = id + ctl.state = controlStatePending + return nil +} + +func (ctl *Control) setHandoffBarrier(barrier <-chan struct{}) { + ctl.lifecycleMu.Lock() + ctl.handoffBarrier = barrier + ctl.lifecycleMu.Unlock() +} + +func (ctl *Control) WaitForHandoff() { + ctl.lifecycleMu.Lock() + barrier := ctl.handoffBarrier + ctl.lifecycleMu.Unlock() + if barrier != nil { + <-barrier + } +} + +// Start starts the control session workers after login succeeds. +func (ctl *Control) Start() bool { + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + return ctl.startLocked() +} + +func (ctl *Control) startLocked() bool { + if ctl.state != controlStatePending || !ctl.activated { + return false + } + ctl.state = controlStateRunning + go ctl.worker() + return true +} + +func (ctl *Control) Close() error { + ctl.lifecycleMu.Lock() + switch ctl.state { + case controlStateCreated, controlStatePending: + ctl.state = controlStateClosing + ctl.finishLocked() + case controlStateRunning: + ctl.state = controlStateClosing + } + ctl.lifecycleMu.Unlock() + return ctl.interruptReadAndClose() +} + +func (ctl *Control) Replaced(newCtl *Control) { + ctl.markReplaced() + ctl.xl.Infof("replaced by client [%s] (control ID %d)", newCtl.runID, newCtl.ID()) + _ = ctl.interruptReadAndClose() +} + +// markReplaced returns the transitive predecessor barrier. A pending control +// has no worker, so it finishes immediately and passes its inherited barrier +// to the replacement. A running control is finished only by its worker. +func (ctl *Control) markReplaced() <-chan struct{} { + ctl.lifecycleMu.Lock() + defer ctl.lifecycleMu.Unlock() + + switch ctl.state { + case controlStateCreated: + ctl.state = controlStateClosing + ctl.finishLocked() return nil + case controlStatePending: + barrier := ctl.handoffBarrier + ctl.state = controlStateClosing + ctl.finishLocked() + return barrier + case controlStateRunning: + ctl.state = controlStateClosing + return ctl.doneCh + case controlStateClosing, controlStateClosed: + return ctl.doneCh default: - xl.Debug("work connection pool is full, discarding") - return fmt.Errorf("work connection pool is full, discarding") + return ctl.doneCh } } +func (ctl *Control) interruptReadAndClose() error { + ctl.interruptOnce.Do(func() { + _ = ctl.sessionCtx.Conn.SetReadDeadline(time.Now()) + ctl.interruptErr = ctl.sessionCtx.Conn.Close() + }) + return ctl.interruptErr +} + +func (ctl *Control) finishLocked() { + if ctl.state == controlStateClosed { + return + } + ctl.state = controlStateClosed + close(ctl.doneCh) +} + // When frps get one user connection, we get one work connection from the pool and return it. // If no workConn available in the pool, send message to frpc to get one or more // and wait until it is available. // return an error if wait timeout -func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) { +func (ctl *Control) GetWorkConn() (workConn *proxy.WorkConn, err error) { xl := ctl.xl defer func() { if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) + xl.Errorf("panic error: %v", err) + xl.Errorf(string(debug.Stack())) } }() @@ -242,304 +606,273 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) { select { case workConn, ok = <-ctl.workConnCh: if !ok { - err = frpErr.ErrCtlClosed + err = pkgerr.ErrCtlClosed return } - xl.Debug("get work connection from pool") + xl.Debugf("get work connection from pool") default: // no work connections available in the poll, send message to frpc to get more - if err = errors.PanicToError(func() { - ctl.sendCh <- &msg.ReqWorkConn{} - }); err != nil { + if err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil { return nil, fmt.Errorf("control is already closed") } select { case workConn, ok = <-ctl.workConnCh: if !ok { - err = frpErr.ErrCtlClosed - xl.Warn("no work connections available, %v", err) + err = pkgerr.ErrCtlClosed + xl.Warnf("no work connections available, %v", err) return } - case <-time.After(time.Duration(ctl.serverCfg.UserConnTimeout) * time.Second): + case <-time.After(time.Duration(ctl.sessionCtx.ServerCfg.UserConnTimeout) * time.Second): err = fmt.Errorf("timeout trying to get work connection") - xl.Warn("%v", err) + xl.Warnf("%v", err) return } } // When we get a work connection from pool, replace it with a new one. - errors.PanicToError(func() { - ctl.sendCh <- &msg.ReqWorkConn{} - }) + _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{}) return } -func (ctl *Control) Replaced(newCtl *Control) { - xl := ctl.xl - xl.Info("Replaced by client [%s]", newCtl.runID) - ctl.runID = "" - ctl.allShutdown.Start() -} - -func (ctl *Control) writer() { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() - - defer ctl.allShutdown.Start() - defer ctl.writerShutdown.Done() - - encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Token)) - if err != nil { - xl.Error("crypto new writer error: %v", err) - ctl.allShutdown.Start() +func (ctl *Control) heartbeatWorker() { + if ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout <= 0 { return } - for { - m, ok := <-ctl.sendCh - if !ok { - xl.Info("control writer is closing") - return - } - if err := msg.WriteMsg(encWriter, m); err != nil { - xl.Warn("write message to control connection error: %v", err) + xl := ctl.xl + wait.Until(func() { + if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout)*time.Second { + xl.Warnf("heartbeat timeout") + _ = ctl.Close() return } - } + }, time.Second, ctl.doneCh) } -func (ctl *Control) reader() { - xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() +// block until Control closed +func (ctl *Control) WaitClosed() { + <-ctl.doneCh +} - defer ctl.allShutdown.Start() - defer ctl.readerShutdown.Done() +func (ctl *Control) loginUserInfo() plugin.UserInfo { + return plugin.UserInfo{ + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.runID, + } +} - encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Token)) - for { - m, err := msg.ReadMsg(encReader) - if err != nil { - if err == io.EOF { - xl.Debug("control connection closed") - return - } - xl.Warn("read error: %v", err) - ctl.conn.Close() - return - } +func (ctl *Control) closeProxy(pxy proxy.Proxy) { + pxy.Close() + ctl.sessionCtx.PxyManager.Del(pxy.GetName()) + ctl.serverMetrics.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) - ctl.readCh <- m + notifyContent := &plugin.CloseProxyContent{ + User: ctl.loginUserInfo(), + CloseProxy: msg.CloseProxy{ + ProxyName: pxy.GetName(), + }, } + go func() { + _ = ctl.sessionCtx.PluginManager.CloseProxy(notifyContent) + }() } -func (ctl *Control) stoper() { +func (ctl *Control) worker() { xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) + ctl.serverMetrics.NewClient() + + go ctl.heartbeatWorker() + go ctl.msgDispatcher.Run() + go func() { + for i := 0; i < ctl.poolCount; i++ { + // Ignore the error: it means this control is already closing. + _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{}) } }() - ctl.allShutdown.WaitStart() - - ctl.conn.Close() - ctl.readerShutdown.WaitDone() - - close(ctl.readCh) - ctl.managerShutdown.WaitDone() - - close(ctl.sendCh) - ctl.writerShutdown.WaitDone() + <-ctl.msgDispatcher.Done() + ctl.lifecycleMu.Lock() + if ctl.state == controlStateRunning { + ctl.state = controlStateClosing + } + ctl.lifecycleMu.Unlock() + _ = ctl.interruptReadAndClose() ctl.mu.Lock() - defer ctl.mu.Unlock() - close(ctl.workConnCh) for workConn := range ctl.workConnCh { workConn.Close() } + proxies := ctl.proxies + ctl.proxies = make(map[string]proxy.Proxy) + ctl.mu.Unlock() - for _, pxy := range ctl.proxies { - pxy.Close() - ctl.pxyManager.Del(pxy.GetName()) - metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType) - - notifyContent := &plugin.CloseProxyContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - CloseProxy: msg.CloseProxy{ - ProxyName: pxy.GetName(), - }, - } - go func() { - ctl.pluginManager.CloseProxy(notifyContent) - }() + for _, pxy := range proxies { + ctl.closeProxy(pxy) } - ctl.allShutdown.Done() - xl.Info("client exit success") - metrics.Server.CloseClient() + ctl.serverMetrics.CloseClient() + if ctl.manager != nil { + ctl.manager.Remove(ctl) + } + xl.Infof("client exit success") + ctl.lifecycleMu.Lock() + ctl.finishLocked() + ctl.lifecycleMu.Unlock() } -// block until Control closed -func (ctl *Control) WaitClosed() { - ctl.allShutdown.WaitDone() +func (ctl *Control) registerMsgHandlers() { + ctl.msgDispatcher.RegisterHandler(&msg.NewProxy{}, ctl.handleNewProxy) + ctl.msgDispatcher.RegisterHandler(&msg.Ping{}, ctl.handlePing) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleVisitor{}, msg.AsyncHandler(ctl.handleNatHoleVisitor)) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleClient{}, msg.AsyncHandler(ctl.handleNatHoleClient)) + ctl.msgDispatcher.RegisterHandler(&msg.NatHoleReport{}, msg.AsyncHandler(ctl.handleNatHoleReport)) + ctl.msgDispatcher.RegisterHandler(&msg.CloseProxy{}, ctl.handleCloseProxy) } -func (ctl *Control) manager() { +func (ctl *Control) handleNewProxy(m msg.Message) { xl := ctl.xl - defer func() { - if err := recover(); err != nil { - xl.Error("panic error: %v", err) - xl.Error(string(debug.Stack())) - } - }() + inMsg := m.(*msg.NewProxy) - defer ctl.allShutdown.Start() - defer ctl.managerShutdown.Done() + content := &plugin.NewProxyContent{ + User: ctl.loginUserInfo(), + NewProxy: *inMsg, + } + var remoteAddr string + retContent, err := ctl.sessionCtx.PluginManager.NewProxy(content) + if err == nil { + inMsg = &retContent.NewProxy + remoteAddr, err = ctl.RegisterProxy(inMsg) + } - var heartbeatCh <-chan time.Time - if ctl.serverCfg.TCPMux || ctl.serverCfg.HeartbeatTimeout <= 0 { - // Don't need application heartbeat here. - // yamux will do same thing. + // register proxy in this control + resp := &msg.NewProxyResp{ + ProxyName: inMsg.ProxyName, + } + if err != nil { + xl.Warnf("new proxy [%s] type [%s] error: %v", inMsg.ProxyName, inMsg.ProxyType, err) + resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", inMsg.ProxyName), + err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient)) } else { - heartbeat := time.NewTicker(time.Second) - defer heartbeat.Stop() - heartbeatCh = heartbeat.C + resp.RemoteAddr = remoteAddr + xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType) + clientID := ctl.sessionCtx.LoginMsg.ClientID + if clientID == "" { + clientID = ctl.runID + } + ctl.serverMetrics.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.sessionCtx.LoginMsg.User, clientID) } + _ = ctl.msgDispatcher.Send(resp) +} - for { - select { - case <-heartbeatCh: - if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second { - xl.Warn("heartbeat timeout") - return - } - case rawMsg, ok := <-ctl.readCh: - if !ok { - return - } +func (ctl *Control) handlePing(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.Ping) - switch m := rawMsg.(type) { - case *msg.NewProxy: - content := &plugin.NewProxyContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - NewProxy: *m, - } - var remoteAddr string - retContent, err := ctl.pluginManager.NewProxy(content) - if err == nil { - m = &retContent.NewProxy - remoteAddr, err = ctl.RegisterProxy(m) - } - - // register proxy in this control - resp := &msg.NewProxyResp{ - ProxyName: m.ProxyName, - } - if err != nil { - xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err) - resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient) - } else { - resp.RemoteAddr = remoteAddr - xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType) - metrics.Server.NewProxy(m.ProxyName, m.ProxyType) - } - ctl.sendCh <- resp - case *msg.CloseProxy: - ctl.CloseProxy(m) - xl.Info("close proxy [%s] success", m.ProxyName) - case *msg.Ping: - content := &plugin.PingContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - Ping: *m, - } - retContent, err := ctl.pluginManager.Ping(content) - if err == nil { - m = &retContent.Ping - err = ctl.authVerifier.VerifyPing(m) - } - if err != nil { - xl.Warn("received invalid ping: %v", err) - ctl.sendCh <- &msg.Pong{ - Error: util.GenerateResponseErrorString("invalid ping", err, ctl.serverCfg.DetailedErrorsToClient), - } - return - } - ctl.lastPing = time.Now() - xl.Debug("receive heartbeat") - ctl.sendCh <- &msg.Pong{} - } - } + content := &plugin.PingContent{ + User: ctl.loginUserInfo(), + Ping: *inMsg, + } + retContent, err := ctl.sessionCtx.PluginManager.Ping(content) + if err == nil { + inMsg = &retContent.Ping + err = ctl.sessionCtx.AuthVerifier.VerifyPing(inMsg) + } + if err != nil { + xl.Warnf("received invalid ping: %v", err) + _ = ctl.msgDispatcher.Send(&msg.Pong{ + Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.sessionCtx.ServerCfg.DetailedErrorsToClient)), + }) + return } + ctl.lastPing.Store(time.Now()) + xl.Debugf("receive heartbeat") + _ = ctl.msgDispatcher.Send(&msg.Pong{}) +} + +func (ctl *Control) handleNatHoleVisitor(m msg.Message) { + inMsg := m.(*msg.NatHoleVisitor) + ctl.sessionCtx.RC.NatHoleController.HandleVisitor(inMsg, ctl.msgTransporter, ctl.sessionCtx.LoginMsg.User) +} + +func (ctl *Control) handleNatHoleClient(m msg.Message) { + inMsg := m.(*msg.NatHoleClient) + ctl.sessionCtx.RC.NatHoleController.HandleClient(inMsg, ctl.msgTransporter) +} + +func (ctl *Control) handleNatHoleReport(m msg.Message) { + inMsg := m.(*msg.NatHoleReport) + ctl.sessionCtx.RC.NatHoleController.HandleReport(inMsg) +} + +func (ctl *Control) handleCloseProxy(m msg.Message) { + xl := ctl.xl + inMsg := m.(*msg.CloseProxy) + _ = ctl.CloseProxy(inMsg) + xl.Infof("close proxy [%s] success", inMsg.ProxyName) } func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) { - var pxyConf config.ProxyConf - // Load configures from NewProxy message and check. - pxyConf, err = config.NewProxyConfFromMsg(pxyMsg, ctl.serverCfg) + var pxyConf v1.ProxyConfigurer + // Load configures from NewProxy message and validate. + pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.sessionCtx.ServerCfg) if err != nil { return } // User info userInfo := plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, RunID: ctl.runID, } - // NewProxy will return a interface Proxy. - // In fact it create different proxies by different proxy type, we just call run() here. - pxy, err := proxy.NewProxy(ctl.ctx, userInfo, ctl.rc, ctl.poolCount, ctl.GetWorkConn, pxyConf, ctl.serverCfg) + // NewProxy will return an interface Proxy. + // In fact, it creates different proxies based on the proxy type. We just call run() here. + pxy, err := proxy.NewProxy(ctl.ctx, &proxy.Options{ + UserInfo: userInfo, + LoginMsg: ctl.sessionCtx.LoginMsg, + PoolCount: ctl.poolCount, + ResourceController: ctl.sessionCtx.RC, + GetWorkConnFn: ctl.GetWorkConn, + Configurer: pxyConf, + ServerCfg: ctl.sessionCtx.ServerCfg, + EncryptionKey: ctl.sessionCtx.EncryptionKey, + WireProtocol: ctl.sessionCtx.WireProtocol, + UDPPacketCodec: ctl.sessionCtx.UDPPacketCodec, + }) if err != nil { return remoteAddr, err } // Check ports used number in each client - if ctl.serverCfg.MaxPortsPerClient > 0 { + if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 { ctl.mu.Lock() - if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.serverCfg.MaxPortsPerClient) { + if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(ctl.sessionCtx.ServerCfg.MaxPortsPerClient) { ctl.mu.Unlock() err = fmt.Errorf("exceed the max_ports_per_client") return } - ctl.portsUsedNum = ctl.portsUsedNum + pxy.GetUsedPortsNum() + ctl.portsUsedNum += pxy.GetUsedPortsNum() ctl.mu.Unlock() defer func() { if err != nil { ctl.mu.Lock() - ctl.portsUsedNum = ctl.portsUsedNum - pxy.GetUsedPortsNum() + ctl.portsUsedNum -= pxy.GetUsedPortsNum() ctl.mu.Unlock() } }() } + if ctl.sessionCtx.PxyManager.Exist(pxyMsg.ProxyName) { + err = fmt.Errorf("proxy [%s] already exists", pxyMsg.ProxyName) + return + } + remoteAddr, err = pxy.Run() if err != nil { return @@ -550,7 +883,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err } }() - err = ctl.pxyManager.Add(pxyMsg.ProxyName, pxy) + err = ctl.sessionCtx.PxyManager.Add(pxyMsg.ProxyName, pxy) if err != nil { return } @@ -569,29 +902,12 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { return } - if ctl.serverCfg.MaxPortsPerClient > 0 { - ctl.portsUsedNum = ctl.portsUsedNum - pxy.GetUsedPortsNum() + if ctl.sessionCtx.ServerCfg.MaxPortsPerClient > 0 { + ctl.portsUsedNum -= pxy.GetUsedPortsNum() } - pxy.Close() - ctl.pxyManager.Del(pxy.GetName()) delete(ctl.proxies, closeMsg.ProxyName) ctl.mu.Unlock() - metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType) - - notifyContent := &plugin.CloseProxyContent{ - User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, - }, - CloseProxy: msg.CloseProxy{ - ProxyName: pxy.GetName(), - }, - } - go func() { - ctl.pluginManager.CloseProxy(notifyContent) - }() - + ctl.closeProxy(pxy) return } diff --git a/server/control_test.go b/server/control_test.go new file mode 100644 index 00000000000..965ea5ea08b --- /dev/null +++ b/server/control_test.go @@ -0,0 +1,595 @@ +// 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 server + +import ( + "context" + "errors" + "math" + "net" + "os" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/auth" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/server/controller" + "github.com/fatedier/frp/server/proxy" + "github.com/fatedier/frp/server/registry" +) + +func TestControlPendingReplacementFinishesWithoutStarting(t *testing.T) { + clientRegistry := registry.NewClientRegistry() + manager := NewControlManager(clientRegistry) + metrics := newCountingServerMetrics() + oldCtl, oldConn := newLifecycleTestControl(t, "same-run", "client", metrics) + newCtl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + mustAddAndActivate(t, manager, oldCtl) + + err := manager.Add(newCtl) + require.NoError(t, err) + waitForControlDone(t, oldCtl) + require.False(t, oldCtl.Start()) + require.Equal(t, []string{"deadline", "close"}, oldConn.eventsSnapshot()) + require.Equal(t, int64(0), metrics.newClients()) + 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) + metrics := newCountingServerMetrics() + oldCtl, oldConn := newLifecycleTestControl(t, "same-run", "client", metrics) + newCtl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + mustAddAndActivate(t, manager, oldCtl) + require.True(t, oldCtl.Start()) + waitForSignal(t, oldConn.readStarted, "control reader to start") + + err := manager.Add(newCtl) + require.NoError(t, err) + waitForControlDone(t, oldCtl) + require.Equal(t, []string{"deadline", "close"}, oldConn.eventsSnapshot()) + require.Equal(t, int64(1), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) + + _, ok := manager.GetByID("same-run") + require.False(t, ok) + require.Same(t, newCtl, currentControlForTest(manager, "same-run")) + info, ok := clientRegistry.GetByKey("client") + require.True(t, ok) + require.True(t, info.Online) + require.Equal(t, uint64(oldCtl.ID()), info.ControlID) + + active, err := manager.Activate(newCtl) + require.NoError(t, err) + require.True(t, active) + _, ok = manager.GetByID("same-run") + require.False(t, ok) + info, ok = clientRegistry.GetByKey("client") + require.True(t, ok) + require.Equal(t, uint64(newCtl.ID()), info.ControlID) +} + +func TestControlClosePendingAndRunning(t *testing.T) { + t.Run("pending", func(t *testing.T) { + manager := NewControlManager(registry.NewClientRegistry()) + metrics := newCountingServerMetrics() + ctl, conn := newLifecycleTestControl(t, "pending", "pending", metrics) + err := manager.Add(ctl) + require.NoError(t, err) + + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) + require.Equal(t, []string{"deadline", "close"}, conn.eventsSnapshot()) + require.Equal(t, int64(0), metrics.newClients()) + require.Equal(t, int64(0), metrics.closedClients()) + }) + + t.Run("running", func(t *testing.T) { + manager := NewControlManager(registry.NewClientRegistry()) + metrics := newCountingServerMetrics() + ctl, conn := newLifecycleTestControl(t, "running", "running", metrics) + mustAddAndActivate(t, manager, ctl) + require.True(t, ctl.Start()) + waitForSignal(t, conn.readStarted, "control reader to start") + + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) + require.Equal(t, []string{"deadline", "close"}, conn.eventsSnapshot()) + require.Equal(t, int64(1), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) + }) +} + +func TestControlCloseAndReplacedAreIdempotent(t *testing.T) { + manager := NewControlManager(registry.NewClientRegistry()) + metrics := newCountingServerMetrics() + ctl, conn := newLifecycleTestControl(t, "same-run", "client", metrics) + replacement, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + err := manager.Add(ctl) + require.NoError(t, err) + err = manager.Add(replacement) + require.NoError(t, err) + require.NoError(t, ctl.Close()) + ctl.Replaced(replacement) + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) + + require.Equal(t, []string{"deadline", "close"}, conn.eventsSnapshot()) + require.Equal(t, int64(0), metrics.newClients()) + require.Equal(t, int64(0), metrics.closedClients()) +} + +func TestControlHeartbeatTimeoutInterruptsRead(t *testing.T) { + manager := NewControlManager(registry.NewClientRegistry()) + metrics := newCountingServerMetrics() + ctl, conn := newLifecycleTestControl(t, "heartbeat", "heartbeat", metrics) + ctl.sessionCtx.ServerCfg.Transport.HeartbeatTimeout = 1 + ctl.lastPing.Store(time.Now().Add(-2 * time.Second)) + + mustAddAndActivate(t, manager, ctl) + require.True(t, ctl.Start()) + waitForSignal(t, conn.readStarted, "control reader to start") + waitForControlDone(t, ctl) + + require.Equal(t, []string{"deadline", "close"}, conn.eventsSnapshot()) + require.Equal(t, int64(1), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) +} + +func TestControlStartReplacementRacePairsMetrics(t *testing.T) { + for range 100 { + clientRegistry := registry.NewClientRegistry() + manager := NewControlManager(clientRegistry) + metrics := newCountingServerMetrics() + ctl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + replacement, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + mustAddAndActivate(t, manager, ctl) + + startGate := make(chan struct{}) + startedCh := make(chan bool, 1) + addErrCh := make(chan error, 1) + go func() { + <-startGate + startedCh <- ctl.Start() + }() + go func() { + <-startGate + addErr := manager.Add(replacement) + addErrCh <- addErr + }() + close(startGate) + + started := <-startedCh + require.NoError(t, <-addErrCh) + waitForControlDone(t, ctl) + if started { + require.Equal(t, int64(1), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) + } else { + require.Equal(t, int64(0), metrics.newClients()) + require.Equal(t, int64(0), metrics.closedClients()) + } + } +} + +func TestControlManagerRejectsStaleActivateAndRemove(t *testing.T) { + clientRegistry := registry.NewClientRegistry() + manager := NewControlManager(clientRegistry) + metrics := newCountingServerMetrics() + oldCtl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + newCtl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + mustAddAndActivate(t, manager, oldCtl) + err := manager.Add(newCtl) + require.NoError(t, err) + require.Greater(t, uint64(newCtl.ID()), uint64(oldCtl.ID())) + + active, err := manager.Activate(oldCtl) + require.NoError(t, err) + require.False(t, active) + require.False(t, manager.Remove(oldCtl)) + + _, ok := manager.GetByID("same-run") + require.False(t, ok) + require.Same(t, newCtl, currentControlForTest(manager, "same-run")) + info, ok := clientRegistry.GetByKey("client") + require.True(t, ok) + require.True(t, info.Online) + require.Equal(t, uint64(oldCtl.ID()), info.ControlID) + + active, err = manager.Activate(newCtl) + require.NoError(t, err) + require.True(t, active) + info, ok = clientRegistry.GetByKey("client") + require.True(t, ok) + require.True(t, info.Online) + require.Equal(t, uint64(newCtl.ID()), info.ControlID) +} + +func TestControlManagerPreservesClientIDConflict(t *testing.T) { + clientRegistry := registry.NewClientRegistry() + manager := NewControlManager(clientRegistry) + metrics := newCountingServerMetrics() + first, _ := newLifecycleTestControl(t, "run-one", "shared-client", metrics) + conflicting, _ := newLifecycleTestControl(t, "run-two", "shared-client", metrics) + + mustAddAndActivate(t, manager, first) + err := manager.Add(conflicting) + require.NoError(t, err) + active, err := manager.Activate(conflicting) + require.True(t, active) + require.ErrorContains(t, err, "already online") + + require.True(t, manager.Remove(conflicting)) + info, ok := clientRegistry.GetByKey("shared-client") + require.True(t, ok) + require.True(t, info.Online) + require.Equal(t, "run-one", info.RunID) +} + +func TestControlManagerFailedLoginWriteReleasesRunWithoutStarting(t *testing.T) { + clientRegistry := registry.NewClientRegistry() + manager := NewControlManager(clientRegistry) + metrics := newCountingServerMetrics() + ctl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + replacement, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + mustAddAndActivate(t, manager, ctl) + + writeErr := errors.New("write failed") + committed, err := manager.completeLogin(ctl, func() error { return writeErr }) + require.ErrorIs(t, err, writeErr) + require.False(t, committed) + + err = manager.Add(replacement) + require.NoError(t, err) + waitForControlDone(t, ctl) + require.Same(t, replacement, currentControlForTest(manager, "same-run")) + require.Equal(t, int64(0), metrics.newClients()) + require.Equal(t, int64(0), metrics.closedClients()) + require.True(t, manager.Remove(replacement)) + info, ok := clientRegistry.GetByKey("client") + require.True(t, ok) + require.False(t, info.Online) + require.Empty(t, info.RunID) + require.Zero(t, info.ControlID) + require.False(t, info.DisconnectedAt.IsZero()) + require.NoError(t, replacement.Close()) +} + +func TestControlManagerCloseWaitsForInFlightLoginRun(t *testing.T) { + clientRegistry := registry.NewClientRegistry() + manager := NewControlManager(clientRegistry) + metrics := newCountingServerMetrics() + ctl, _ := newLifecycleTestControl(t, "same-run", "client", metrics) + + mustAddAndActivate(t, manager, ctl) + + writeEntered := make(chan struct{}) + resumeWrite := make(chan struct{}) + loginDone := make(chan struct { + committed bool + err error + }, 1) + go func() { + committed, loginErr := manager.completeLogin(ctl, func() error { + close(writeEntered) + <-resumeWrite + return nil + }) + loginDone <- struct { + committed bool + err error + }{committed: committed, err: loginErr} + }() + waitForSignal(t, writeEntered, "LoginResp write") + + closeDone := make(chan error, 1) + go func() { closeDone <- manager.Close() }() + waitForManagerClosed(t, manager) + select { + case err := <-closeDone: + t.Fatalf("manager close completed during LoginResp write: %v", err) + default: + } + + close(resumeWrite) + result := <-loginDone + require.NoError(t, result.err) + require.True(t, result.committed) + require.NoError(t, <-closeDone) + waitForControlDone(t, ctl) + require.Nil(t, currentControlForTest(manager, "same-run")) + require.Equal(t, int64(1), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) + info, ok := clientRegistry.GetByKey("client") + require.True(t, ok) + require.False(t, info.Online) +} + +func newLifecycleTestControl( + t *testing.T, + runID string, + clientID string, + serverMetrics *countingServerMetrics, +) (*Control, *deadlineReadConn) { + t.Helper() + conn := newDeadlineReadConn() + msgConn := msg.NewConn(conn, msg.NewV1ReadWriter(conn)) + ctl, err := NewControl(context.Background(), &SessionContext{ + RC: &controller.ResourceController{}, + PxyManager: proxy.NewManager(), + PluginManager: plugin.NewManager(), + AuthVerifier: auth.AlwaysPassVerifier, + Conn: msgConn, + LoginMsg: &msg.Login{ + RunID: runID, + ClientID: clientID, + }, + ServerCfg: &v1.ServerConfig{}, + }) + require.NoError(t, err) + ctl.serverMetrics = serverMetrics + t.Cleanup(func() { _ = ctl.Close() }) + return ctl, conn +} + +func mustAddAndActivate(t *testing.T, manager *ControlManager, ctl *Control) { + t.Helper() + require.NoError(t, manager.Add(ctl)) + active, err := manager.Activate(ctl) + require.NoError(t, err) + require.True(t, active) +} + +func waitForControlDone(t *testing.T, ctl *Control) { + t.Helper() + done := make(chan struct{}) + go func() { + ctl.WaitClosed() + close(done) + }() + waitForSignal(t, done, "control to finish") +} + +func currentControlForTest(manager *ControlManager, runID string) *Control { + manager.mu.RLock() + defer manager.mu.RUnlock() + entry := manager.ctlsByRunID[runID] + if entry == nil { + return nil + } + return entry.ctl +} + +func currentRunGateForTest(manager *ControlManager, runID string) *sync.Mutex { + manager.mu.RLock() + defer manager.mu.RUnlock() + entry := manager.ctlsByRunID[runID] + if entry == nil { + return nil + } + return entry.runMu +} + +func waitForManagerClosed(t *testing.T, manager *ControlManager) { + t.Helper() + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + manager.mu.RLock() + closed := manager.closed + manager.mu.RUnlock() + if closed { + return + } + } + t.Fatal("timed out waiting for control manager to close") +} + +func waitForSignal(t *testing.T, ch <-chan struct{}, description string) { + t.Helper() + select { + case <-ch: + case <-time.After(3 * time.Second): + t.Fatalf("timed out waiting for %s", description) + } +} + +type deadlineReadConn struct { + readStarted chan struct{} + unblockRead chan struct{} + + readOnce sync.Once + unblockOnce sync.Once + deadlineOnce sync.Once + closeOnce sync.Once + + eventsMu sync.Mutex + events []string +} + +func newDeadlineReadConn() *deadlineReadConn { + return &deadlineReadConn{ + readStarted: make(chan struct{}), + unblockRead: make(chan struct{}), + } +} + +func (c *deadlineReadConn) Read([]byte) (int, error) { + c.readOnce.Do(func() { close(c.readStarted) }) + <-c.unblockRead + return 0, os.ErrDeadlineExceeded +} + +func (*deadlineReadConn) Write(p []byte) (int, error) { return len(p), nil } + +func (c *deadlineReadConn) Close() error { + c.closeOnce.Do(func() { + c.recordEvent("close") + c.unblockOnce.Do(func() { close(c.unblockRead) }) + }) + return nil +} + +func (*deadlineReadConn) LocalAddr() net.Addr { return lifecycleTestAddr("local") } +func (*deadlineReadConn) RemoteAddr() net.Addr { return lifecycleTestAddr("remote") } + +func (c *deadlineReadConn) SetDeadline(deadline time.Time) error { + if err := c.SetReadDeadline(deadline); err != nil { + return err + } + return c.SetWriteDeadline(deadline) +} + +func (c *deadlineReadConn) SetReadDeadline(deadline time.Time) error { + if deadline.IsZero() { + return nil + } + c.deadlineOnce.Do(func() { + c.recordEvent("deadline") + c.unblockOnce.Do(func() { close(c.unblockRead) }) + }) + return nil +} + +func (*deadlineReadConn) SetWriteDeadline(time.Time) error { return nil } + +func (c *deadlineReadConn) recordEvent(event string) { + c.eventsMu.Lock() + c.events = append(c.events, event) + c.eventsMu.Unlock() +} + +func (c *deadlineReadConn) eventsSnapshot() []string { + c.eventsMu.Lock() + defer c.eventsMu.Unlock() + return append([]string(nil), c.events...) +} + +type lifecycleTestAddr string + +func (a lifecycleTestAddr) Network() string { return string(a) } +func (a lifecycleTestAddr) String() string { return string(a) } + +type countingServerMetrics struct { + mu sync.Mutex + newCount int64 + closeCount int64 + closeEnter chan struct{} + closeResume chan struct{} + closeOnce sync.Once +} + +func newCountingServerMetrics() *countingServerMetrics { + return &countingServerMetrics{} +} + +func (m *countingServerMetrics) NewClient() { + m.mu.Lock() + m.newCount++ + m.mu.Unlock() +} + +func (m *countingServerMetrics) CloseClient() { + m.mu.Lock() + m.closeCount++ + closeEnter := m.closeEnter + closeResume := m.closeResume + m.mu.Unlock() + if closeEnter != nil { + m.closeOnce.Do(func() { close(closeEnter) }) + <-closeResume + } +} + +func (*countingServerMetrics) NewProxy(string, string, string, string) {} +func (*countingServerMetrics) CloseProxy(string, string) {} +func (*countingServerMetrics) OpenConnection(string, string) {} +func (*countingServerMetrics) CloseConnection(string, string) {} +func (*countingServerMetrics) AddTrafficIn(string, string, int64) {} +func (*countingServerMetrics) AddTrafficOut(string, string, int64) {} + +func (m *countingServerMetrics) newClients() int64 { + m.mu.Lock() + defer m.mu.Unlock() + return m.newCount +} + +func (m *countingServerMetrics) closedClients() int64 { + m.mu.Lock() + defer m.mu.Unlock() + return m.closeCount +} diff --git a/server/controller/resource.go b/server/controller/resource.go index 47236c9e0d0..717c961676b 100644 --- a/server/controller/resource.go +++ b/server/controller/resource.go @@ -35,6 +35,9 @@ type ResourceController struct { // HTTP Group Controller HTTPGroupCtl *group.HTTPGroupController + // HTTPS Group Controller + HTTPSGroupCtl *group.HTTPSGroupController + // TCP Mux Group Controller TCPMuxGroupCtl *group.TCPMuxGroupCtl @@ -59,3 +62,13 @@ type ResourceController struct { // All server manager plugin PluginManager *plugin.Manager } + +func (rc *ResourceController) Close() error { + if rc.VhostHTTPSMuxer != nil { + rc.VhostHTTPSMuxer.Close() + } + if rc.TCPMuxHTTPConnectMuxer != nil { + rc.TCPMuxHTTPConnectMuxer.Close() + } + return nil +} diff --git a/server/dashboard.go b/server/dashboard.go deleted file mode 100644 index 68e99f8a2c1..00000000000 --- a/server/dashboard.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2017 fatedier, fatedier@gmail.com -// -// 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 server - -import ( - "crypto/tls" - "net" - "net/http" - "net/http/pprof" - "time" - - "github.com/fatedier/frp/assets" - frpNet "github.com/fatedier/frp/pkg/util/net" - - "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - httpServerReadTimeout = 60 * time.Second - httpServerWriteTimeout = 60 * time.Second -) - -func (svr *Service) RunDashboardServer(address string) (err error) { - // url router - router := mux.NewRouter() - router.HandleFunc("/healthz", svr.Healthz) - - // debug - if svr.cfg.PprofEnable { - router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - router.HandleFunc("/debug/pprof/profile", pprof.Profile) - router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - router.HandleFunc("/debug/pprof/trace", pprof.Trace) - router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) - } - - subRouter := router.NewRoute().Subrouter() - - user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd - subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware) - - // metrics - if svr.cfg.EnablePrometheus { - subRouter.Handle("/metrics", promhttp.Handler()) - } - - // api, see dashboard_api.go - subRouter.HandleFunc("/api/serverinfo", svr.APIServerInfo).Methods("GET") - subRouter.HandleFunc("/api/proxy/{type}", svr.APIProxyByType).Methods("GET") - subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.APIProxyByTypeAndName).Methods("GET") - subRouter.HandleFunc("/api/traffic/{name}", svr.APIProxyTraffic).Methods("GET") - - // view - subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET") - subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET") - - subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/static/", http.StatusMovedPermanently) - }) - - server := &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: httpServerReadTimeout, - WriteTimeout: httpServerWriteTimeout, - } - ln, err := net.Listen("tcp", address) - if err != nil { - return err - } - - if svr.cfg.DashboardTLSMode { - cert, err := tls.LoadX509KeyPair(svr.cfg.DashboardTLSCertFile, svr.cfg.DashboardTLSKeyFile) - if err != nil { - return err - } - tlsCfg := &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - ln = tls.NewListener(ln, tlsCfg) - } - go server.Serve(ln) - return -} diff --git a/server/dashboard_api.go b/server/dashboard_api.go deleted file mode 100644 index 7d159c7a89a..00000000000 --- a/server/dashboard_api.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright 2017 fatedier, fatedier@gmail.com -// -// 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 server - -import ( - "encoding/json" - "net/http" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" - "github.com/fatedier/frp/pkg/metrics/mem" - "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/pkg/util/version" - - "github.com/gorilla/mux" -) - -type GeneralResponse struct { - Code int - Msg string -} - -type serverInfoResp struct { - Version string `json:"version"` - BindPort int `json:"bind_port"` - BindUDPPort int `json:"bind_udp_port"` - VhostHTTPPort int `json:"vhost_http_port"` - VhostHTTPSPort int `json:"vhost_https_port"` - KCPBindPort int `json:"kcp_bind_port"` - SubdomainHost string `json:"subdomain_host"` - MaxPoolCount int64 `json:"max_pool_count"` - MaxPortsPerClient int64 `json:"max_ports_per_client"` - HeartBeatTimeout int64 `json:"heart_beat_timeout"` - - TotalTrafficIn int64 `json:"total_traffic_in"` - TotalTrafficOut int64 `json:"total_traffic_out"` - CurConns int64 `json:"cur_conns"` - ClientCounts int64 `json:"client_counts"` - ProxyTypeCounts map[string]int64 `json:"proxy_type_count"` -} - -// /healthz -func (svr *Service) Healthz(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) -} - -// api/serverinfo -func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) { - res := GeneralResponse{Code: 200} - defer func() { - log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) - w.WriteHeader(res.Code) - if len(res.Msg) > 0 { - w.Write([]byte(res.Msg)) - } - }() - - log.Info("Http request: [%s]", r.URL.Path) - serverStats := mem.StatsCollector.GetServer() - svrResp := serverInfoResp{ - Version: version.Full(), - BindPort: svr.cfg.BindPort, - BindUDPPort: svr.cfg.BindUDPPort, - VhostHTTPPort: svr.cfg.VhostHTTPPort, - VhostHTTPSPort: svr.cfg.VhostHTTPSPort, - KCPBindPort: svr.cfg.KCPBindPort, - SubdomainHost: svr.cfg.SubDomainHost, - MaxPoolCount: svr.cfg.MaxPoolCount, - MaxPortsPerClient: svr.cfg.MaxPortsPerClient, - HeartBeatTimeout: svr.cfg.HeartbeatTimeout, - - TotalTrafficIn: serverStats.TotalTrafficIn, - TotalTrafficOut: serverStats.TotalTrafficOut, - CurConns: serverStats.CurConns, - ClientCounts: serverStats.ClientCounts, - ProxyTypeCounts: serverStats.ProxyTypeCounts, - } - - buf, _ := json.Marshal(&svrResp) - res.Msg = string(buf) -} - -type BaseOutConf struct { - config.BaseProxyConf -} - -type TCPOutConf struct { - BaseOutConf - RemotePort int `json:"remote_port"` -} - -type TCPMuxOutConf struct { - BaseOutConf - config.DomainConf - Multiplexer string `json:"multiplexer"` -} - -type UDPOutConf struct { - BaseOutConf - RemotePort int `json:"remote_port"` -} - -type HTTPOutConf struct { - BaseOutConf - config.DomainConf - Locations []string `json:"locations"` - HostHeaderRewrite string `json:"host_header_rewrite"` -} - -type HTTPSOutConf struct { - BaseOutConf - config.DomainConf -} - -type STCPOutConf struct { - BaseOutConf -} - -type XTCPOutConf struct { - BaseOutConf -} - -func getConfByType(proxyType string) interface{} { - switch proxyType { - case consts.TCPProxy: - return &TCPOutConf{} - case consts.TCPMuxProxy: - return &TCPMuxOutConf{} - case consts.UDPProxy: - return &UDPOutConf{} - case consts.HTTPProxy: - return &HTTPOutConf{} - case consts.HTTPSProxy: - return &HTTPSOutConf{} - case consts.STCPProxy: - return &STCPOutConf{} - case consts.XTCPProxy: - return &XTCPOutConf{} - default: - return nil - } -} - -// Get proxy info. -type ProxyStatsInfo struct { - Name string `json:"name"` - Conf interface{} `json:"conf"` - TodayTrafficIn int64 `json:"today_traffic_in"` - TodayTrafficOut int64 `json:"today_traffic_out"` - CurConns int64 `json:"cur_conns"` - LastStartTime string `json:"last_start_time"` - LastCloseTime string `json:"last_close_time"` - Status string `json:"status"` -} - -type GetProxyInfoResp struct { - Proxies []*ProxyStatsInfo `json:"proxies"` -} - -// api/proxy/:type -func (svr *Service) APIProxyByType(w http.ResponseWriter, r *http.Request) { - res := GeneralResponse{Code: 200} - params := mux.Vars(r) - proxyType := params["type"] - - defer func() { - log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) - w.WriteHeader(res.Code) - if len(res.Msg) > 0 { - w.Write([]byte(res.Msg)) - } - }() - log.Info("Http request: [%s]", r.URL.Path) - - proxyInfoResp := GetProxyInfoResp{} - proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType) - - buf, _ := json.Marshal(&proxyInfoResp) - res.Msg = string(buf) -} - -func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) { - proxyStats := mem.StatsCollector.GetProxiesByType(proxyType) - proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats)) - for _, ps := range proxyStats { - proxyInfo := &ProxyStatsInfo{} - if pxy, ok := svr.pxyManager.GetByName(ps.Name); ok { - content, err := json.Marshal(pxy.GetConf()) - if err != nil { - log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err) - continue - } - proxyInfo.Conf = getConfByType(ps.Type) - if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil { - log.Warn("unmarshal proxy [%s] conf info error: %v", ps.Name, err) - continue - } - proxyInfo.Status = consts.Online - } else { - proxyInfo.Status = consts.Offline - } - proxyInfo.Name = ps.Name - proxyInfo.TodayTrafficIn = ps.TodayTrafficIn - proxyInfo.TodayTrafficOut = ps.TodayTrafficOut - proxyInfo.CurConns = ps.CurConns - proxyInfo.LastStartTime = ps.LastStartTime - proxyInfo.LastCloseTime = ps.LastCloseTime - proxyInfos = append(proxyInfos, proxyInfo) - } - return -} - -// Get proxy info by name. -type GetProxyStatsResp struct { - Name string `json:"name"` - Conf interface{} `json:"conf"` - TodayTrafficIn int64 `json:"today_traffic_in"` - TodayTrafficOut int64 `json:"today_traffic_out"` - CurConns int64 `json:"cur_conns"` - LastStartTime string `json:"last_start_time"` - LastCloseTime string `json:"last_close_time"` - Status string `json:"status"` -} - -// api/proxy/:type/:name -func (svr *Service) APIProxyByTypeAndName(w http.ResponseWriter, r *http.Request) { - res := GeneralResponse{Code: 200} - params := mux.Vars(r) - proxyType := params["type"] - name := params["name"] - - defer func() { - log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) - w.WriteHeader(res.Code) - if len(res.Msg) > 0 { - w.Write([]byte(res.Msg)) - } - }() - log.Info("Http request: [%s]", r.URL.Path) - - proxyStatsResp := GetProxyStatsResp{} - proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name) - if res.Code != 200 { - return - } - - buf, _ := json.Marshal(&proxyStatsResp) - res.Msg = string(buf) -} - -func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) { - proxyInfo.Name = proxyName - ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName) - if ps == nil { - code = 404 - msg = "no proxy info found" - } else { - if pxy, ok := svr.pxyManager.GetByName(proxyName); ok { - content, err := json.Marshal(pxy.GetConf()) - if err != nil { - log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err) - code = 400 - msg = "parse conf error" - return - } - proxyInfo.Conf = getConfByType(ps.Type) - if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil { - log.Warn("unmarshal proxy [%s] conf info error: %v", ps.Name, err) - code = 400 - msg = "parse conf error" - return - } - proxyInfo.Status = consts.Online - } else { - proxyInfo.Status = consts.Offline - } - proxyInfo.TodayTrafficIn = ps.TodayTrafficIn - proxyInfo.TodayTrafficOut = ps.TodayTrafficOut - proxyInfo.CurConns = ps.CurConns - proxyInfo.LastStartTime = ps.LastStartTime - proxyInfo.LastCloseTime = ps.LastCloseTime - code = 200 - } - - return -} - -// api/traffic/:name -type GetProxyTrafficResp struct { - Name string `json:"name"` - TrafficIn []int64 `json:"traffic_in"` - TrafficOut []int64 `json:"traffic_out"` -} - -func (svr *Service) APIProxyTraffic(w http.ResponseWriter, r *http.Request) { - res := GeneralResponse{Code: 200} - params := mux.Vars(r) - name := params["name"] - - defer func() { - log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) - w.WriteHeader(res.Code) - if len(res.Msg) > 0 { - w.Write([]byte(res.Msg)) - } - }() - log.Info("Http request: [%s]", r.URL.Path) - - trafficResp := GetProxyTrafficResp{} - trafficResp.Name = name - proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name) - - if proxyTrafficInfo == nil { - res.Code = 404 - res.Msg = "no proxy info found" - return - } - trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn - trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut - - buf, _ := json.Marshal(&trafficResp) - res.Msg = string(buf) -} diff --git a/server/group/base.go b/server/group/base.go new file mode 100644 index 00000000000..5684d8efc4d --- /dev/null +++ b/server/group/base.go @@ -0,0 +1,77 @@ +package group + +import ( + "net" + "sync" + + gerr "github.com/fatedier/golib/errors" +) + +// baseGroup contains the shared plumbing for listener-based groups +// (TCP, HTTPS, TCPMux). Each concrete group embeds this and provides +// its own Listen method with protocol-specific validation. +type baseGroup struct { + group string + groupKey string + + acceptCh chan net.Conn + realLn net.Listener + lns []*Listener + mu sync.Mutex + cleanupFn func() +} + +// initBase resets the baseGroup for a fresh listen cycle. +// Must be called under mu when len(lns) == 0. +func (bg *baseGroup) initBase(group, groupKey string, realLn net.Listener, cleanupFn func()) { + bg.group = group + bg.groupKey = groupKey + bg.realLn = realLn + bg.acceptCh = make(chan net.Conn) + bg.cleanupFn = cleanupFn +} + +// worker reads from the real listener and fans out to acceptCh. +// The parameters are captured at creation time so that the worker is +// bound to a specific listen cycle and cannot observe a later initBase. +func (bg *baseGroup) worker(realLn net.Listener, acceptCh chan<- net.Conn) { + for { + c, err := realLn.Accept() + if err != nil { + return + } + err = gerr.PanicToError(func() { + acceptCh <- c + }) + if err != nil { + c.Close() + return + } + } +} + +// newListener creates a new Listener wired to this baseGroup. +// Must be called under mu. +func (bg *baseGroup) newListener(addr net.Addr) *Listener { + ln := newListener(bg.acceptCh, addr, bg.closeListener) + bg.lns = append(bg.lns, ln) + return ln +} + +// closeListener removes ln from the list. When the last listener is removed, +// it closes acceptCh, closes the real listener, and calls cleanupFn. +func (bg *baseGroup) closeListener(ln *Listener) { + bg.mu.Lock() + defer bg.mu.Unlock() + for i, l := range bg.lns { + if l == ln { + bg.lns = append(bg.lns[:i], bg.lns[i+1:]...) + break + } + } + if len(bg.lns) == 0 { + close(bg.acceptCh) + bg.realLn.Close() + bg.cleanupFn() + } +} diff --git a/server/group/base_test.go b/server/group/base_test.go new file mode 100644 index 00000000000..1b470841e31 --- /dev/null +++ b/server/group/base_test.go @@ -0,0 +1,169 @@ +package group + +import ( + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeLn is a controllable net.Listener for tests. +type fakeLn struct { + connCh chan net.Conn + closed chan struct{} + once sync.Once +} + +func newFakeLn() *fakeLn { + return &fakeLn{ + connCh: make(chan net.Conn, 8), + closed: make(chan struct{}), + } +} + +func (f *fakeLn) Accept() (net.Conn, error) { + select { + case c := <-f.connCh: + return c, nil + case <-f.closed: + return nil, net.ErrClosed + } +} + +func (f *fakeLn) Close() error { + f.once.Do(func() { close(f.closed) }) + return nil +} + +func (f *fakeLn) Addr() net.Addr { return fakeAddr("127.0.0.1:9999") } + +func (f *fakeLn) inject(c net.Conn) { + select { + case f.connCh <- c: + case <-f.closed: + } +} + +func TestBaseGroup_WorkerFanOut(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + bg.initBase("g", "key", fl, func() {}) + + go bg.worker(fl, bg.acceptCh) + + c1, c2 := net.Pipe() + defer c2.Close() + fl.inject(c1) + + select { + case got := <-bg.acceptCh: + assert.Equal(t, c1, got) + got.Close() + case <-time.After(time.Second): + t.Fatal("timed out waiting for connection on acceptCh") + } + + fl.Close() +} + +func TestBaseGroup_WorkerStopsOnListenerClose(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + bg.initBase("g", "key", fl, func() {}) + + done := make(chan struct{}) + go func() { + bg.worker(fl, bg.acceptCh) + close(done) + }() + + fl.Close() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("worker did not stop after listener close") + } +} + +func TestBaseGroup_WorkerClosesConnOnClosedChannel(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + bg.initBase("g", "key", fl, func() {}) + + // Close acceptCh before worker sends. + close(bg.acceptCh) + + done := make(chan struct{}) + go func() { + bg.worker(fl, bg.acceptCh) + close(done) + }() + + c1, c2 := net.Pipe() + defer c2.Close() + fl.inject(c1) + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("worker did not stop after panic recovery") + } + + // c1 should have been closed by worker's panic recovery path. + buf := make([]byte, 1) + _, err := c1.Read(buf) + assert.Error(t, err, "connection should be closed by worker") +} + +func TestBaseGroup_CloseLastListenerTriggersCleanup(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + cleanupCalled := 0 + bg.initBase("g", "key", fl, func() { cleanupCalled++ }) + + bg.mu.Lock() + ln1 := bg.newListener(fl.Addr()) + ln2 := bg.newListener(fl.Addr()) + bg.mu.Unlock() + + go bg.worker(fl, bg.acceptCh) + + ln1.Close() + assert.Equal(t, 0, cleanupCalled, "cleanup should not run while listeners remain") + + ln2.Close() + assert.Equal(t, 1, cleanupCalled, "cleanup should run after last listener closed") +} + +func TestBaseGroup_CloseOneOfTwoListeners(t *testing.T) { + fl := newFakeLn() + var bg baseGroup + cleanupCalled := 0 + bg.initBase("g", "key", fl, func() { cleanupCalled++ }) + + bg.mu.Lock() + ln1 := bg.newListener(fl.Addr()) + ln2 := bg.newListener(fl.Addr()) + bg.mu.Unlock() + + go bg.worker(fl, bg.acceptCh) + + ln1.Close() + assert.Equal(t, 0, cleanupCalled) + + // ln2 should still receive connections. + c1, c2 := net.Pipe() + defer c2.Close() + fl.inject(c1) + + got, err := ln2.Accept() + require.NoError(t, err) + assert.Equal(t, c1, got) + got.Close() + + ln2.Close() + assert.Equal(t, 1, cleanupCalled) +} diff --git a/server/group/group.go b/server/group/group.go index ab38cf45708..1fbedf5cf3e 100644 --- a/server/group/group.go +++ b/server/group/group.go @@ -24,4 +24,6 @@ var ( ErrListenerClosed = errors.New("group listener closed") ErrGroupDifferentPort = errors.New("group should have same remote port") ErrProxyRepeated = errors.New("group proxy repeated") + + errGroupStale = errors.New("stale group reference") ) diff --git a/server/group/http.go b/server/group/http.go index 82c774c96dd..b5ed68bbc48 100644 --- a/server/group/http.go +++ b/server/group/http.go @@ -9,54 +9,42 @@ import ( "github.com/fatedier/frp/pkg/util/vhost" ) +// HTTPGroupController manages HTTP groups that use round-robin +// callback routing (fundamentally different from listener-based groups). type HTTPGroupController struct { - // groups by indexKey - groups map[string]*HTTPGroup - - // register createConn for each group to vhostRouter. - // createConn will get a connection from one proxy of the group + groupRegistry[*HTTPGroup] vhostRouter *vhost.Routers - - mu sync.Mutex } func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController { return &HTTPGroupController{ - groups: make(map[string]*HTTPGroup), - vhostRouter: vhostRouter, + groupRegistry: newGroupRegistry[*HTTPGroup](), + vhostRouter: vhostRouter, } } func (ctl *HTTPGroupController) Register( proxyName, group, groupKey string, routeConfig vhost.RouteConfig, -) (err error) { - - indexKey := group - ctl.mu.Lock() - g, ok := ctl.groups[indexKey] - if !ok { - g = NewHTTPGroup(ctl) - ctl.groups[indexKey] = g +) error { + for { + g := ctl.getOrCreate(group, func() *HTTPGroup { + return NewHTTPGroup(ctl) + }) + err := g.Register(proxyName, group, groupKey, routeConfig) + if err == errGroupStale { + continue + } + return err } - ctl.mu.Unlock() - - return g.Register(proxyName, group, groupKey, routeConfig) } -func (ctl *HTTPGroupController) UnRegister(proxyName, group string, routeConfig vhost.RouteConfig) { - indexKey := group - ctl.mu.Lock() - defer ctl.mu.Unlock() - g, ok := ctl.groups[indexKey] +func (ctl *HTTPGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) { + g, ok := ctl.get(group) if !ok { return } - - isEmpty := g.UnRegister(proxyName) - if isEmpty { - delete(ctl.groups, indexKey) - } + g.UnRegister(proxyName) } type HTTPGroup struct { @@ -66,10 +54,10 @@ type HTTPGroup struct { location string routeByHTTPUser string - // CreateConnFuncs indexed by echo proxy name + // CreateConnFuncs indexed by proxy name createFuncs map[string]vhost.CreateConnFunc pxyNames []string - index uint64 + index atomic.Uint64 ctl *HTTPGroupController mu sync.RWMutex } @@ -86,13 +74,17 @@ func (g *HTTPGroup) Register( proxyName, group, groupKey string, routeConfig vhost.RouteConfig, ) (err error) { - g.mu.Lock() defer g.mu.Unlock() + if !g.ctl.isCurrent(group, func(cur *HTTPGroup) bool { return cur == g }) { + return errGroupStale + } if len(g.createFuncs) == 0 { // the first proxy in this group tmp := routeConfig // copy object tmp.CreateConnFn = g.createConn + tmp.ChooseEndpointFn = g.chooseEndpoint + tmp.CreateConnByEndpointFn = g.createConnByEndpoint err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, routeConfig.IpsAllowList, &tmp) if err != nil { return @@ -123,7 +115,7 @@ func (g *HTTPGroup) Register( return nil } -func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) { +func (g *HTTPGroup) UnRegister(proxyName string) { g.mu.Lock() defer g.mu.Unlock() delete(g.createFuncs, proxyName) @@ -135,15 +127,16 @@ func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) { } if len(g.createFuncs) == 0 { - isEmpty = true g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser) + g.ctl.removeIf(g.group, func(cur *HTTPGroup) bool { + return cur == g + }) } - return } func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) { var f vhost.CreateConnFunc - newIndex := atomic.AddUint64(&g.index, 1) + newIndex := g.index.Add(1) g.mu.RLock() group := g.group @@ -151,8 +144,8 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) { location := g.location routeByHTTPUser := g.routeByHTTPUser if len(g.pxyNames) > 0 { - name := g.pxyNames[int(newIndex)%len(g.pxyNames)] - f, _ = g.createFuncs[name] + name := g.pxyNames[newIndex%uint64(len(g.pxyNames))] + f = g.createFuncs[name] } g.mu.RUnlock() @@ -163,3 +156,36 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) { return f(remoteAddr) } + +func (g *HTTPGroup) chooseEndpoint() (string, error) { + newIndex := g.index.Add(1) + name := "" + + g.mu.RLock() + group := g.group + domain := g.domain + location := g.location + routeByHTTPUser := g.routeByHTTPUser + if len(g.pxyNames) > 0 { + name = g.pxyNames[newIndex%uint64(len(g.pxyNames))] + } + g.mu.RUnlock() + + if name == "" { + return "", fmt.Errorf("no healthy endpoint for http group [%s], domain [%s], location [%s], routeByHTTPUser [%s]", + group, domain, location, routeByHTTPUser) + } + return name, nil +} + +func (g *HTTPGroup) createConnByEndpoint(endpoint, remoteAddr string) (net.Conn, error) { + var f vhost.CreateConnFunc + g.mu.RLock() + f = g.createFuncs[endpoint] + g.mu.RUnlock() + + if f == nil { + return nil, fmt.Errorf("no CreateConnFunc for endpoint [%s] in group [%s]", endpoint, g.group) + } + return f(remoteAddr) +} diff --git a/server/group/https.go b/server/group/https.go new file mode 100644 index 00000000000..1ab975788fc --- /dev/null +++ b/server/group/https.go @@ -0,0 +1,102 @@ +// Copyright 2025 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 group + +import ( + "context" + "net" + + "github.com/fatedier/frp/pkg/util/vhost" +) + +type HTTPSGroupController struct { + groupRegistry[*HTTPSGroup] + httpsMuxer *vhost.HTTPSMuxer +} + +func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController { + return &HTTPSGroupController{ + groupRegistry: newGroupRegistry[*HTTPSGroup](), + httpsMuxer: httpsMuxer, + } +} + +func (ctl *HTTPSGroupController) Listen( + ctx context.Context, + group, groupKey string, + routeConfig vhost.RouteConfig, +) (l net.Listener, err error) { + for { + g := ctl.getOrCreate(group, func() *HTTPSGroup { + return NewHTTPSGroup(ctl) + }) + l, err = g.Listen(ctx, group, groupKey, routeConfig) + if err == errGroupStale { + continue + } + return + } +} + +type HTTPSGroup struct { + baseGroup + + domain string + ctl *HTTPSGroupController +} + +func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup { + return &HTTPSGroup{ + ctl: ctl, + } +} + +func (g *HTTPSGroup) Listen( + ctx context.Context, + group, groupKey string, + routeConfig vhost.RouteConfig, +) (ln *Listener, err error) { + g.mu.Lock() + defer g.mu.Unlock() + if !g.ctl.isCurrent(group, func(cur *HTTPSGroup) bool { return cur == g }) { + return nil, errGroupStale + } + if len(g.lns) == 0 { + // the first listener, listen on the real address + httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig) + if errRet != nil { + return nil, errRet + } + + g.domain = routeConfig.Domain + g.initBase(group, groupKey, httpsLn, func() { + g.ctl.removeIf(g.group, func(cur *HTTPSGroup) bool { + return cur == g + }) + }) + ln = g.newListener(httpsLn.Addr()) + go g.worker(httpsLn, g.acceptCh) + } else { + // route config in the same group must be equal + if g.group != group || g.domain != routeConfig.Domain { + return nil, ErrGroupParamsInvalid + } + if g.groupKey != groupKey { + return nil, ErrGroupAuthFailed + } + ln = g.newListener(g.lns[0].Addr()) + } + return +} diff --git a/server/group/listener.go b/server/group/listener.go new file mode 100644 index 00000000000..33c5c0df550 --- /dev/null +++ b/server/group/listener.go @@ -0,0 +1,49 @@ +package group + +import ( + "net" + "sync" +) + +// Listener is a per-proxy virtual listener that receives connections +// from a shared group. It implements net.Listener. +type Listener struct { + acceptCh <-chan net.Conn + addr net.Addr + closeCh chan struct{} + onClose func(*Listener) + once sync.Once +} + +func newListener(acceptCh <-chan net.Conn, addr net.Addr, onClose func(*Listener)) *Listener { + return &Listener{ + acceptCh: acceptCh, + addr: addr, + closeCh: make(chan struct{}), + onClose: onClose, + } +} + +func (ln *Listener) Accept() (net.Conn, error) { + select { + case <-ln.closeCh: + return nil, ErrListenerClosed + case c, ok := <-ln.acceptCh: + if !ok { + return nil, ErrListenerClosed + } + return c, nil + } +} + +func (ln *Listener) Addr() net.Addr { + return ln.addr +} + +func (ln *Listener) Close() error { + ln.once.Do(func() { + close(ln.closeCh) + ln.onClose(ln) + }) + return nil +} diff --git a/server/group/listener_test.go b/server/group/listener_test.go new file mode 100644 index 00000000000..4e3e30e6044 --- /dev/null +++ b/server/group/listener_test.go @@ -0,0 +1,68 @@ +package group + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListener_Accept(t *testing.T) { + acceptCh := make(chan net.Conn, 1) + ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {}) + + c1, c2 := net.Pipe() + defer c1.Close() + defer c2.Close() + + acceptCh <- c1 + got, err := ln.Accept() + require.NoError(t, err) + assert.Equal(t, c1, got) +} + +func TestListener_AcceptAfterChannelClose(t *testing.T) { + acceptCh := make(chan net.Conn) + ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {}) + + close(acceptCh) + _, err := ln.Accept() + assert.ErrorIs(t, err, ErrListenerClosed) +} + +func TestListener_AcceptAfterListenerClose(t *testing.T) { + acceptCh := make(chan net.Conn) // open, not closed + ln := newListener(acceptCh, fakeAddr("127.0.0.1:1234"), func(*Listener) {}) + + ln.Close() + _, err := ln.Accept() + assert.ErrorIs(t, err, ErrListenerClosed) +} + +func TestListener_DoubleClose(t *testing.T) { + closeCalls := 0 + ln := newListener( + make(chan net.Conn), + fakeAddr("127.0.0.1:1234"), + func(*Listener) { closeCalls++ }, + ) + + assert.NotPanics(t, func() { + ln.Close() + ln.Close() + }) + assert.Equal(t, 1, closeCalls, "onClose should be called exactly once") +} + +func TestListener_Addr(t *testing.T) { + addr := fakeAddr("10.0.0.1:5555") + ln := newListener(make(chan net.Conn), addr, func(*Listener) {}) + assert.Equal(t, addr, ln.Addr()) +} + +// fakeAddr implements net.Addr for testing. +type fakeAddr string + +func (a fakeAddr) Network() string { return "tcp" } +func (a fakeAddr) String() string { return string(a) } diff --git a/server/group/registry.go b/server/group/registry.go new file mode 100644 index 00000000000..4064c535e8d --- /dev/null +++ b/server/group/registry.go @@ -0,0 +1,59 @@ +package group + +import ( + "sync" +) + +// groupRegistry is a concurrent map of named groups with +// automatic creation on first access. +type groupRegistry[G any] struct { + groups map[string]G + mu sync.Mutex +} + +func newGroupRegistry[G any]() groupRegistry[G] { + return groupRegistry[G]{ + groups: make(map[string]G), + } +} + +func (r *groupRegistry[G]) getOrCreate(key string, newFn func() G) G { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + if !ok { + g = newFn() + r.groups[key] = g + } + return g +} + +func (r *groupRegistry[G]) get(key string) (G, bool) { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + return g, ok +} + +// isCurrent returns true if key exists in the registry and matchFn +// returns true for the stored value. +func (r *groupRegistry[G]) isCurrent(key string, matchFn func(G) bool) bool { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + return ok && matchFn(g) +} + +// removeIf atomically looks up the group for key, calls fn on it, +// and removes the entry if fn returns true. +func (r *groupRegistry[G]) removeIf(key string, fn func(G) bool) { + r.mu.Lock() + defer r.mu.Unlock() + g, ok := r.groups[key] + if !ok { + return + } + if fn(g) { + delete(r.groups, key) + } +} diff --git a/server/group/registry_test.go b/server/group/registry_test.go new file mode 100644 index 00000000000..106d3998464 --- /dev/null +++ b/server/group/registry_test.go @@ -0,0 +1,102 @@ +package group + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetOrCreate_New(t *testing.T) { + r := newGroupRegistry[*int]() + called := 0 + v := 42 + got := r.getOrCreate("k", func() *int { called++; return &v }) + assert.Equal(t, 1, called) + assert.Equal(t, &v, got) +} + +func TestGetOrCreate_Existing(t *testing.T) { + r := newGroupRegistry[*int]() + v := 42 + r.getOrCreate("k", func() *int { return &v }) + + called := 0 + got := r.getOrCreate("k", func() *int { called++; return nil }) + assert.Equal(t, 0, called) + assert.Equal(t, &v, got) +} + +func TestGet_ExistingAndMissing(t *testing.T) { + r := newGroupRegistry[*int]() + v := 1 + r.getOrCreate("k", func() *int { return &v }) + + got, ok := r.get("k") + assert.True(t, ok) + assert.Equal(t, &v, got) + + _, ok = r.get("missing") + assert.False(t, ok) +} + +func TestIsCurrent(t *testing.T) { + r := newGroupRegistry[*int]() + v1 := 1 + v2 := 2 + r.getOrCreate("k", func() *int { return &v1 }) + + assert.True(t, r.isCurrent("k", func(g *int) bool { return g == &v1 })) + assert.False(t, r.isCurrent("k", func(g *int) bool { return g == &v2 })) + assert.False(t, r.isCurrent("missing", func(g *int) bool { return true })) +} + +func TestRemoveIf(t *testing.T) { + t.Run("removes when fn returns true", func(t *testing.T) { + r := newGroupRegistry[*int]() + v := 1 + r.getOrCreate("k", func() *int { return &v }) + r.removeIf("k", func(g *int) bool { return g == &v }) + _, ok := r.get("k") + assert.False(t, ok) + }) + + t.Run("keeps when fn returns false", func(t *testing.T) { + r := newGroupRegistry[*int]() + v := 1 + r.getOrCreate("k", func() *int { return &v }) + r.removeIf("k", func(g *int) bool { return false }) + _, ok := r.get("k") + assert.True(t, ok) + }) + + t.Run("noop on missing key", func(t *testing.T) { + r := newGroupRegistry[*int]() + r.removeIf("missing", func(g *int) bool { return true }) // should not panic + }) +} + +func TestConcurrentGetOrCreateAndRemoveIf(t *testing.T) { + r := newGroupRegistry[*int]() + const n = 100 + var wg sync.WaitGroup + wg.Add(n * 2) + for i := range n { + v := i + go func() { + defer wg.Done() + r.getOrCreate("k", func() *int { return &v }) + }() + go func() { + defer wg.Done() + r.removeIf("k", func(*int) bool { return true }) + }() + } + wg.Wait() + + // After all goroutines finish, accessing the key must not panic. + require.NotPanics(t, func() { + _, _ = r.get("k") + }) +} diff --git a/server/group/tcp.go b/server/group/tcp.go index c7fd2b2799d..d6bfbcff0f2 100644 --- a/server/group/tcp.go +++ b/server/group/tcp.go @@ -17,108 +17,91 @@ package group import ( "net" "strconv" - "sync" "github.com/fatedier/frp/server/ports" - - gerr "github.com/fatedier/golib/errors" ) -// TCPGroupCtl manage all TCPGroups +// TCPGroupCtl manages all TCPGroups. type TCPGroupCtl struct { - groups map[string]*TCPGroup - - // portManager is used to manage port + groupRegistry[*TCPGroup] portManager *ports.Manager - mu sync.Mutex } -// NewTCPGroupCtl return a new TcpGroupCtl +// NewTCPGroupCtl returns a new TCPGroupCtl. func NewTCPGroupCtl(portManager *ports.Manager) *TCPGroupCtl { return &TCPGroupCtl{ - groups: make(map[string]*TCPGroup), - portManager: portManager, + groupRegistry: newGroupRegistry[*TCPGroup](), + portManager: portManager, } } -// Listen is the wrapper for TCPGroup's Listen -// If there are no group, we will create one here +// Listen is the wrapper for TCPGroup's Listen. +// If there is no group, one will be created. func (tgc *TCPGroupCtl) Listen(proxyName string, group string, groupKey string, - addr string, port int) (l net.Listener, realPort int, err error) { - - tgc.mu.Lock() - tcpGroup, ok := tgc.groups[group] - if !ok { - tcpGroup = NewTCPGroup(tgc) - tgc.groups[group] = tcpGroup + addr string, port int, +) (l net.Listener, realPort int, err error) { + for { + tcpGroup := tgc.getOrCreate(group, func() *TCPGroup { + return NewTCPGroup(tgc) + }) + l, realPort, err = tcpGroup.Listen(proxyName, group, groupKey, addr, port) + if err == errGroupStale { + continue + } + return } - tgc.mu.Unlock() - - return tcpGroup.Listen(proxyName, group, groupKey, addr, port) } -// RemoveGroup remove TCPGroup from controller -func (tgc *TCPGroupCtl) RemoveGroup(group string) { - tgc.mu.Lock() - defer tgc.mu.Unlock() - delete(tgc.groups, group) -} - -// TCPGroup route connections to different proxies +// TCPGroup routes connections to different proxies. type TCPGroup struct { - group string - groupKey string + baseGroup + addr string port int realPort int - - acceptCh chan net.Conn - index uint64 - tcpLn net.Listener - lns []*TCPGroupListener ctl *TCPGroupCtl - mu sync.Mutex } -// NewTCPGroup return a new TCPGroup +// NewTCPGroup returns a new TCPGroup. func NewTCPGroup(ctl *TCPGroupCtl) *TCPGroup { return &TCPGroup{ - lns: make([]*TCPGroupListener, 0), - ctl: ctl, - acceptCh: make(chan net.Conn), + ctl: ctl, } } -// Listen will return a new TCPGroupListener -// if TCPGroup already has a listener, just add a new TCPGroupListener to the queues -// otherwise, listen on the real address -func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *TCPGroupListener, realPort int, err error) { +// Listen will return a new Listener. +// If TCPGroup already has a listener, just add a new Listener to the queues, +// otherwise listen on the real address. +func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr string, port int) (ln *Listener, realPort int, err error) { tg.mu.Lock() defer tg.mu.Unlock() + if !tg.ctl.isCurrent(group, func(cur *TCPGroup) bool { return cur == tg }) { + return nil, 0, errGroupStale + } if len(tg.lns) == 0 { // the first listener, listen on the real address realPort, err = tg.ctl.portManager.Acquire(proxyName, port) if err != nil { return } - tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(port))) + tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(realPort))) if errRet != nil { + tg.ctl.portManager.Release(realPort) err = errRet return } - ln = newTCPGroupListener(group, tg, tcpLn.Addr()) - tg.group = group - tg.groupKey = groupKey tg.addr = addr tg.port = port tg.realPort = realPort - tg.tcpLn = tcpLn - tg.lns = append(tg.lns, ln) - if tg.acceptCh == nil { - tg.acceptCh = make(chan net.Conn) - } - go tg.worker() + tg.initBase(group, groupKey, tcpLn, func() { + tg.ctl.portManager.Release(tg.realPort) + tg.ctl.removeIf(tg.group, func(cur *TCPGroup) bool { + return cur == tg + }) + }) + ln = tg.newListener(tcpLn.Addr()) + go tg.worker(tcpLn, tg.acceptCh) } else { // address and port in the same group must be equal if tg.group != group || tg.addr != addr { @@ -133,92 +116,8 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr err = ErrGroupAuthFailed return } - ln = newTCPGroupListener(group, tg, tg.lns[0].Addr()) + ln = tg.newListener(tg.lns[0].Addr()) realPort = tg.realPort - tg.lns = append(tg.lns, ln) - } - return -} - -// worker is called when the real tcp listener has been created -func (tg *TCPGroup) worker() { - for { - c, err := tg.tcpLn.Accept() - if err != nil { - return - } - err = gerr.PanicToError(func() { - tg.acceptCh <- c - }) - if err != nil { - return - } } -} - -func (tg *TCPGroup) Accept() <-chan net.Conn { - return tg.acceptCh -} - -// CloseListener remove the TCPGroupListener from the TCPGroup -func (tg *TCPGroup) CloseListener(ln *TCPGroupListener) { - tg.mu.Lock() - defer tg.mu.Unlock() - for i, tmpLn := range tg.lns { - if tmpLn == ln { - tg.lns = append(tg.lns[:i], tg.lns[i+1:]...) - break - } - } - if len(tg.lns) == 0 { - close(tg.acceptCh) - tg.tcpLn.Close() - tg.ctl.portManager.Release(tg.realPort) - tg.ctl.RemoveGroup(tg.group) - } -} - -// TCPGroupListener -type TCPGroupListener struct { - groupName string - group *TCPGroup - - addr net.Addr - closeCh chan struct{} -} - -func newTCPGroupListener(name string, group *TCPGroup, addr net.Addr) *TCPGroupListener { - return &TCPGroupListener{ - groupName: name, - group: group, - addr: addr, - closeCh: make(chan struct{}), - } -} - -// Accept will accept connections from TCPGroup -func (ln *TCPGroupListener) Accept() (c net.Conn, err error) { - var ok bool - select { - case <-ln.closeCh: - return nil, ErrListenerClosed - case c, ok = <-ln.group.Accept(): - if !ok { - return nil, ErrListenerClosed - } - return c, nil - } -} - -func (ln *TCPGroupListener) Addr() net.Addr { - return ln.addr -} - -// Close close the listener -func (ln *TCPGroupListener) Close() (err error) { - close(ln.closeCh) - - // remove self from TcpGroup - ln.group.CloseListener(ln) return } diff --git a/server/group/tcpmux.go b/server/group/tcpmux.go index fd805d91ebe..e17a152ac1d 100644 --- a/server/group/tcpmux.go +++ b/server/group/tcpmux.go @@ -18,209 +18,112 @@ import ( "context" "fmt" "net" - "sync" - "github.com/fatedier/frp/pkg/consts" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/tcpmux" "github.com/fatedier/frp/pkg/util/vhost" - - gerr "github.com/fatedier/golib/errors" ) -// TCPMuxGroupCtl manage all TCPMuxGroups +// TCPMuxGroupCtl manages all TCPMuxGroups. type TCPMuxGroupCtl struct { - groups map[string]*TCPMuxGroup - - // portManager is used to manage port + groupRegistry[*TCPMuxGroup] tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer - mu sync.Mutex } -// NewTCPMuxGroupCtl return a new TCPMuxGroupCtl +// NewTCPMuxGroupCtl returns a new TCPMuxGroupCtl. func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPMuxGroupCtl { return &TCPMuxGroupCtl{ - groups: make(map[string]*TCPMuxGroup), + groupRegistry: newGroupRegistry[*TCPMuxGroup](), tcpMuxHTTPConnectMuxer: tcpMuxHTTPConnectMuxer, } } -// Listen is the wrapper for TCPMuxGroup's Listen -// If there are no group, we will create one here +// Listen is the wrapper for TCPMuxGroup's Listen. +// If there is no group, one will be created. func (tmgc *TCPMuxGroupCtl) Listen( ctx context.Context, multiplexer, group, groupKey string, routeConfig vhost.RouteConfig, ) (l net.Listener, err error) { + for { + tcpMuxGroup := tmgc.getOrCreate(group, func() *TCPMuxGroup { + return NewTCPMuxGroup(tmgc) + }) - tmgc.mu.Lock() - tcpMuxGroup, ok := tmgc.groups[group] - if !ok { - tcpMuxGroup = NewTCPMuxGroup(tmgc) - tmgc.groups[group] = tcpMuxGroup - } - tmgc.mu.Unlock() - - switch multiplexer { - case consts.HTTPConnectTCPMultiplexer: - return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig) - default: - err = fmt.Errorf("unknown multiplexer [%s]", multiplexer) - return + switch v1.TCPMultiplexerType(multiplexer) { + case v1.TCPMultiplexerHTTPConnect: + l, err = tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig) + if err == errGroupStale { + continue + } + return + default: + return nil, fmt.Errorf("unknown multiplexer [%s]", multiplexer) + } } } -// RemoveGroup remove TCPMuxGroup from controller -func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) { - tmgc.mu.Lock() - defer tmgc.mu.Unlock() - delete(tmgc.groups, group) -} - -// TCPMuxGroup route connections to different proxies +// TCPMuxGroup routes connections to different proxies. type TCPMuxGroup struct { - group string - groupKey string + baseGroup + domain string routeByHTTPUser string - - acceptCh chan net.Conn - index uint64 - tcpMuxLn net.Listener - lns []*TCPMuxGroupListener - ctl *TCPMuxGroupCtl - mu sync.Mutex + username string + password string + ctl *TCPMuxGroupCtl } -// NewTCPMuxGroup return a new TCPMuxGroup +// NewTCPMuxGroup returns a new TCPMuxGroup. func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup { return &TCPMuxGroup{ - lns: make([]*TCPMuxGroupListener, 0), - ctl: ctl, - acceptCh: make(chan net.Conn), + ctl: ctl, } } -// Listen will return a new TCPMuxGroupListener -// if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues -// otherwise, listen on the real address +// HTTPConnectListen will return a new Listener. +// If TCPMuxGroup already has a listener, just add a new Listener to the queues, +// otherwise listen on the real address. func (tmg *TCPMuxGroup) HTTPConnectListen( ctx context.Context, group, groupKey string, routeConfig vhost.RouteConfig, -) (ln *TCPMuxGroupListener, err error) { - +) (ln *Listener, err error) { tmg.mu.Lock() defer tmg.mu.Unlock() + if !tmg.ctl.isCurrent(group, func(cur *TCPMuxGroup) bool { return cur == tmg }) { + return nil, errGroupStale + } if len(tmg.lns) == 0 { // the first listener, listen on the real address tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig) if errRet != nil { return nil, errRet } - ln = newTCPMuxGroupListener(group, tmg, tcpMuxLn.Addr()) - tmg.group = group - tmg.groupKey = groupKey tmg.domain = routeConfig.Domain tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser - tmg.tcpMuxLn = tcpMuxLn - tmg.lns = append(tmg.lns, ln) - if tmg.acceptCh == nil { - tmg.acceptCh = make(chan net.Conn) - } - go tmg.worker() + tmg.username = routeConfig.Username + tmg.password = routeConfig.Password + tmg.initBase(group, groupKey, tcpMuxLn, func() { + tmg.ctl.removeIf(tmg.group, func(cur *TCPMuxGroup) bool { + return cur == tmg + }) + }) + ln = tmg.newListener(tcpMuxLn.Addr()) + go tmg.worker(tcpMuxLn, tmg.acceptCh) } else { // route config in the same group must be equal - if tmg.group != group || tmg.domain != routeConfig.Domain || tmg.routeByHTTPUser != routeConfig.RouteByHTTPUser { + if tmg.group != group || tmg.domain != routeConfig.Domain || + tmg.routeByHTTPUser != routeConfig.RouteByHTTPUser || + tmg.username != routeConfig.Username || + tmg.password != routeConfig.Password { return nil, ErrGroupParamsInvalid } if tmg.groupKey != groupKey { return nil, ErrGroupAuthFailed } - ln = newTCPMuxGroupListener(group, tmg, tmg.lns[0].Addr()) - tmg.lns = append(tmg.lns, ln) - } - return -} - -// worker is called when the real TCP listener has been created -func (tmg *TCPMuxGroup) worker() { - for { - c, err := tmg.tcpMuxLn.Accept() - if err != nil { - return - } - err = gerr.PanicToError(func() { - tmg.acceptCh <- c - }) - if err != nil { - return - } + ln = tmg.newListener(tmg.lns[0].Addr()) } -} - -func (tmg *TCPMuxGroup) Accept() <-chan net.Conn { - return tmg.acceptCh -} - -// CloseListener remove the TCPMuxGroupListener from the TCPMuxGroup -func (tmg *TCPMuxGroup) CloseListener(ln *TCPMuxGroupListener) { - tmg.mu.Lock() - defer tmg.mu.Unlock() - for i, tmpLn := range tmg.lns { - if tmpLn == ln { - tmg.lns = append(tmg.lns[:i], tmg.lns[i+1:]...) - break - } - } - if len(tmg.lns) == 0 { - close(tmg.acceptCh) - tmg.tcpMuxLn.Close() - tmg.ctl.RemoveGroup(tmg.group) - } -} - -// TCPMuxGroupListener -type TCPMuxGroupListener struct { - groupName string - group *TCPMuxGroup - - addr net.Addr - closeCh chan struct{} -} - -func newTCPMuxGroupListener(name string, group *TCPMuxGroup, addr net.Addr) *TCPMuxGroupListener { - return &TCPMuxGroupListener{ - groupName: name, - group: group, - addr: addr, - closeCh: make(chan struct{}), - } -} - -// Accept will accept connections from TCPMuxGroup -func (ln *TCPMuxGroupListener) Accept() (c net.Conn, err error) { - var ok bool - select { - case <-ln.closeCh: - return nil, ErrListenerClosed - case c, ok = <-ln.group.Accept(): - if !ok { - return nil, ErrListenerClosed - } - return c, nil - } -} - -func (ln *TCPMuxGroupListener) Addr() net.Addr { - return ln.addr -} - -// Close close the listener -func (ln *TCPMuxGroupListener) Close() (err error) { - close(ln.closeCh) - - // remove self from TcpMuxGroup - ln.group.CloseListener(ln) return } diff --git a/server/http/controller.go b/server/http/controller.go new file mode 100644 index 00000000000..c1c257253f9 --- /dev/null +++ b/server/http/controller.go @@ -0,0 +1,358 @@ +// Copyright 2025 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 http + +import ( + "cmp" + "fmt" + "net/http" + "slices" + "strings" + "time" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/metrics/mem" + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/pkg/util/version" + "github.com/fatedier/frp/server/http/model" + "github.com/fatedier/frp/server/proxy" + "github.com/fatedier/frp/server/registry" +) + +type Controller struct { + // dependencies + serverCfg *v1.ServerConfig + clientRegistry *registry.ClientRegistry + pxyManager ProxyManager +} + +type ProxyManager interface { + GetByName(name string) (proxy.Proxy, bool) +} + +func NewController( + serverCfg *v1.ServerConfig, + clientRegistry *registry.ClientRegistry, + pxyManager ProxyManager, +) *Controller { + return &Controller{ + serverCfg: serverCfg, + clientRegistry: clientRegistry, + pxyManager: pxyManager, + } +} + +// /api/serverinfo +func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) { + return c.buildServerInfoResp(), nil +} + +func (c *Controller) buildServerInfoResp() model.ServerInfoResp { + serverStats := mem.StatsCollector.GetServer() + return model.ServerInfoResp{ + Version: version.Full(), + BindPort: c.serverCfg.BindPort, + VhostHTTPPort: c.serverCfg.VhostHTTPPort, + VhostHTTPSPort: c.serverCfg.VhostHTTPSPort, + TCPMuxHTTPConnectPort: c.serverCfg.TCPMuxHTTPConnectPort, + KCPBindPort: c.serverCfg.KCPBindPort, + QUICBindPort: c.serverCfg.QUICBindPort, + SubdomainHost: c.serverCfg.SubDomainHost, + MaxPoolCount: c.serverCfg.Transport.MaxPoolCount, + MaxPortsPerClient: c.serverCfg.MaxPortsPerClient, + HeartBeatTimeout: c.serverCfg.Transport.HeartbeatTimeout, + AllowPortsStr: types.PortsRangeSlice(c.serverCfg.AllowPorts).String(), + TLSForce: c.serverCfg.Transport.TLS.Force, + + TotalTrafficIn: serverStats.TotalTrafficIn, + TotalTrafficOut: serverStats.TotalTrafficOut, + CurConns: serverStats.CurConns, + ClientCounts: serverStats.ClientCounts, + ProxyTypeCounts: serverStats.ProxyTypeCounts, + } +} + +// /api/clients +func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) { + if c.clientRegistry == nil { + return nil, fmt.Errorf("client registry unavailable") + } + + userFilter := ctx.Query("user") + clientIDFilter := ctx.Query("clientId") + runIDFilter := ctx.Query("runId") + statusFilter := strings.ToLower(ctx.Query("status")) + + records := c.clientRegistry.List() + items := make([]model.ClientInfoResp, 0, len(records)) + for _, info := range records { + if userFilter != "" && info.User != userFilter { + continue + } + if clientIDFilter != "" && info.ClientID() != clientIDFilter { + continue + } + if runIDFilter != "" && info.RunID != runIDFilter { + continue + } + if !matchStatusFilter(info.Online, statusFilter) { + continue + } + items = append(items, buildClientInfoResp(info)) + } + + slices.SortFunc(items, func(a, b model.ClientInfoResp) int { + if v := cmp.Compare(a.User, b.User); v != 0 { + return v + } + if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 { + return v + } + return cmp.Compare(a.Key, b.Key) + }) + + return items, nil +} + +// /api/clients/{key} +func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) { + key := ctx.Param("key") + if key == "" { + return nil, fmt.Errorf("missing client key") + } + + if c.clientRegistry == nil { + return nil, fmt.Errorf("client registry unavailable") + } + + info, ok := c.clientRegistry.GetByKey(key) + if !ok { + return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("client %s not found", key)) + } + + return buildClientInfoResp(info), nil +} + +// /api/proxy/:type +func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) { + proxyType := ctx.Param("type") + + proxyInfoResp := model.GetProxyInfoResp{} + proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType) + slices.SortFunc(proxyInfoResp.Proxies, func(a, b *model.ProxyStatsInfo) int { + return cmp.Compare(a.Name, b.Name) + }) + + return proxyInfoResp, nil +} + +// /api/proxy/:type/:name +func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) { + proxyType := ctx.Param("type") + name := ctx.Param("name") + + proxyStatsResp, code, msg := c.getProxyStatsByTypeAndName(proxyType, name) + if code != 200 { + return nil, httppkg.NewError(code, msg) + } + + return proxyStatsResp, nil +} + +// /api/traffic/:name +func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + + trafficResp := model.GetProxyTrafficResp{} + trafficResp.Name = name + proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name) + + if proxyTrafficInfo == nil { + return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found") + } + trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn + trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut + + return trafficResp, nil +} + +// /api/proxies/:name +func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) { + name := ctx.Param("name") + + ps := mem.StatsCollector.GetProxyByName(name) + if ps == nil { + return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found") + } + + proxyInfo := model.GetProxyStatsResp{ + Name: ps.Name, + User: ps.User, + ClientID: ps.ClientID, + TodayTrafficIn: ps.TodayTrafficIn, + TodayTrafficOut: ps.TodayTrafficOut, + CurConns: ps.CurConns, + LastStartTime: ps.LastStartTime, + LastCloseTime: ps.LastCloseTime, + } + + if pxy, ok := c.pxyManager.GetByName(name); ok { + proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer()) + proxyInfo.Status = "online" + } else { + proxyInfo.Status = "offline" + } + + return proxyInfo, nil +} + +// DELETE /api/proxies?status=offline +func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) { + status := ctx.Query("status") + if status != "offline" { + return nil, httppkg.NewError(http.StatusBadRequest, "status only support offline") + } + cleared, total := mem.StatsCollector.ClearOfflineProxies() + log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total) + return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil +} + +func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*model.ProxyStatsInfo) { + proxyStats := mem.StatsCollector.GetProxiesByType(proxyType) + proxyInfos = make([]*model.ProxyStatsInfo, 0, len(proxyStats)) + for _, ps := range proxyStats { + proxyInfo := &model.ProxyStatsInfo{ + User: ps.User, + ClientID: ps.ClientID, + } + if pxy, ok := c.pxyManager.GetByName(ps.Name); ok { + proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer()) + proxyInfo.Status = "online" + } else { + proxyInfo.Status = "offline" + } + proxyInfo.Name = ps.Name + proxyInfo.TodayTrafficIn = ps.TodayTrafficIn + proxyInfo.TodayTrafficOut = ps.TodayTrafficOut + proxyInfo.CurConns = ps.CurConns + proxyInfo.LastStartTime = ps.LastStartTime + proxyInfo.LastCloseTime = ps.LastCloseTime + proxyInfos = append(proxyInfos, proxyInfo) + } + return +} + +func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo model.GetProxyStatsResp, code int, msg string) { + proxyInfo.Name = proxyName + ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName) + if ps == nil { + code = 404 + msg = "no proxy info found" + } else { + proxyInfo.User = ps.User + proxyInfo.ClientID = ps.ClientID + if pxy, ok := c.pxyManager.GetByName(proxyName); ok { + proxyInfo.Conf = getConfFromConfigurer(pxy.GetConfigurer()) + proxyInfo.Status = "online" + } else { + proxyInfo.Status = "offline" + } + proxyInfo.TodayTrafficIn = ps.TodayTrafficIn + proxyInfo.TodayTrafficOut = ps.TodayTrafficOut + proxyInfo.CurConns = ps.CurConns + proxyInfo.LastStartTime = ps.LastStartTime + proxyInfo.LastCloseTime = ps.LastCloseTime + code = 200 + } + + return +} + +func buildClientInfoResp(info registry.ClientInfo) model.ClientInfoResp { + resp := model.ClientInfoResp{ + Key: info.Key, + User: info.User, + ClientID: info.ClientID(), + RunID: info.RunID, + Version: info.Version, + WireProtocol: info.WireProtocol, + Hostname: info.Hostname, + ClientIP: info.IP, + FirstConnectedAt: toUnix(info.FirstConnectedAt), + LastConnectedAt: toUnix(info.LastConnectedAt), + Online: info.Online, + } + if !info.DisconnectedAt.IsZero() { + resp.DisconnectedAt = info.DisconnectedAt.Unix() + } + return resp +} + +func toUnix(t time.Time) int64 { + if t.IsZero() { + return 0 + } + return t.Unix() +} + +func matchStatusFilter(online bool, filter string) bool { + switch strings.ToLower(filter) { + case "", "all": + return true + case "online": + return online + case "offline": + return !online + default: + return true + } +} + +func getConfFromConfigurer(cfg v1.ProxyConfigurer) any { + outBase := model.BaseOutConf{ProxyBaseConfig: *cfg.GetBaseConfig()} + + switch c := cfg.(type) { + case *v1.TCPProxyConfig: + return &model.TCPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort} + case *v1.UDPProxyConfig: + return &model.UDPOutConf{BaseOutConf: outBase, RemotePort: c.RemotePort} + case *v1.HTTPProxyConfig: + return &model.HTTPOutConf{ + BaseOutConf: outBase, + DomainConfig: c.DomainConfig, + Locations: c.Locations, + HostHeaderRewrite: c.HostHeaderRewrite, + } + case *v1.HTTPSProxyConfig: + return &model.HTTPSOutConf{ + BaseOutConf: outBase, + DomainConfig: c.DomainConfig, + } + case *v1.TCPMuxProxyConfig: + return &model.TCPMuxOutConf{ + BaseOutConf: outBase, + DomainConfig: c.DomainConfig, + Multiplexer: c.Multiplexer, + RouteByHTTPUser: c.RouteByHTTPUser, + } + case *v1.STCPProxyConfig: + return &model.STCPOutConf{BaseOutConf: outBase} + case *v1.XTCPProxyConfig: + return &model.XTCPOutConf{BaseOutConf: outBase} + } + return outBase +} diff --git a/server/http/controller_test.go b/server/http/controller_test.go new file mode 100644 index 00000000000..44509371e69 --- /dev/null +++ b/server/http/controller_test.go @@ -0,0 +1,95 @@ +// 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 http + +import ( + "encoding/json" + "testing" + "time" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/proto/wire" + "github.com/fatedier/frp/server/registry" +) + +func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) { + cfg := &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{ + Name: "test-proxy", + Type: string(v1.ProxyTypeTCP), + ProxyBackend: v1.ProxyBackend{ + Plugin: v1.TypedClientPluginOptions{ + Type: v1.PluginHTTPProxy, + ClientPluginOptions: &v1.HTTPProxyPluginOptions{ + Type: v1.PluginHTTPProxy, + HTTPUser: "user", + HTTPPassword: "password", + }, + }, + }, + }, + RemotePort: 6000, + } + + content, err := json.Marshal(getConfFromConfigurer(cfg)) + if err != nil { + t.Fatalf("marshal conf failed: %v", err) + } + + var out map[string]any + if err := json.Unmarshal(content, &out); err != nil { + t.Fatalf("unmarshal conf failed: %v", err) + } + + pluginValue, ok := out["plugin"] + if !ok { + t.Fatalf("plugin field missing in output: %v", out) + } + plugin, ok := pluginValue.(map[string]any) + if !ok { + t.Fatalf("plugin field should be object, got: %#v", pluginValue) + } + + if got := plugin["type"]; got != v1.PluginHTTPProxy { + t.Fatalf("plugin type mismatch, want %q got %#v", v1.PluginHTTPProxy, got) + } + if got := plugin["httpUser"]; got != "user" { + t.Fatalf("plugin httpUser mismatch, want %q got %#v", "user", got) + } + if got := plugin["httpPassword"]; got != "password" { + t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got) + } +} + +func TestBuildClientInfoRespIncludesWireProtocol(t *testing.T) { + info := registry.ClientInfo{ + Key: "user.client", + User: "user", + RawClientID: "client", + RunID: "run-id", + Version: "1.0.0", + WireProtocol: wire.ProtocolV2, + Hostname: "host", + IP: "127.0.0.1", + FirstConnectedAt: time.Unix(1, 0), + LastConnectedAt: time.Unix(2, 0), + Online: true, + } + + resp := buildClientInfoResp(info) + if resp.WireProtocol != wire.ProtocolV2 { + t.Fatalf("wire protocol mismatch, want %q got %q", wire.ProtocolV2, resp.WireProtocol) + } +} diff --git a/server/http/controller_v2.go b/server/http/controller_v2.go new file mode 100644 index 00000000000..dfa7b495887 --- /dev/null +++ b/server/http/controller_v2.go @@ -0,0 +1,647 @@ +// 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 http + +import ( + "cmp" + "fmt" + "maps" + "math" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + "time" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/metrics/mem" + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/server/http/model" + "github.com/fatedier/frp/server/registry" +) + +const ( + defaultV2Page = 1 + defaultV2PageSize = 50 + maxV2PageSize = 200 + + v2SystemPruneTypeOfflineProxies = "offline_proxies" + v2ProxyTrafficDefaultDays = 7 + v2ProxyTrafficUnit = "bytes" + v2ProxyTrafficGranularity = "day" +) + +var apiV2ProxyTypes = []string{ + string(v1.ProxyTypeTCP), + string(v1.ProxyTypeUDP), + string(v1.ProxyTypeHTTP), + string(v1.ProxyTypeHTTPS), + string(v1.ProxyTypeTCPMUX), + string(v1.ProxyTypeSTCP), + string(v1.ProxyTypeXTCP), + string(v1.ProxyTypeSUDP), +} + +// /api/v2/system/info +func (c *Controller) APIV2SystemInfo(ctx *httppkg.Context) (any, error) { + info := c.buildServerInfoResp() + proxyTypeCounts := info.ProxyTypeCounts + if proxyTypeCounts == nil { + proxyTypeCounts = map[string]int64{} + } + + return model.V2SystemInfoResp{ + Version: info.Version, + Config: model.V2SystemInfoConfigResp{ + BindPort: info.BindPort, + VhostHTTPPort: info.VhostHTTPPort, + VhostHTTPSPort: info.VhostHTTPSPort, + TCPMuxHTTPConnectPort: info.TCPMuxHTTPConnectPort, + KCPBindPort: info.KCPBindPort, + QUICBindPort: info.QUICBindPort, + SubdomainHost: info.SubdomainHost, + MaxPoolCount: info.MaxPoolCount, + MaxPortsPerClient: info.MaxPortsPerClient, + HeartbeatTimeout: info.HeartBeatTimeout, + AllowPortsStr: info.AllowPortsStr, + TLSForce: info.TLSForce, + }, + Status: model.V2SystemInfoStatusResp{ + TotalTrafficIn: info.TotalTrafficIn, + TotalTrafficOut: info.TotalTrafficOut, + CurConns: info.CurConns, + ClientCounts: info.ClientCounts, + ProxyTypeCounts: proxyTypeCounts, + }, + }, nil +} + +// /api/v2/system/prune +func (c *Controller) APIV2SystemPrune(ctx *httppkg.Context) (any, error) { + pruneType, err := parseV2SystemPruneType(ctx.Query("type")) + if err != nil { + return nil, err + } + + cleared, total := mem.StatsCollector.PruneOfflineProxies() + return model.V2SystemPruneResp{ + Type: pruneType, + Cleared: cleared, + Total: total, + }, nil +} + +// /api/v2/users +func (c *Controller) APIV2UserList(ctx *httppkg.Context) (any, error) { + page, pageSize, err := parseV2PageParams(ctx) + if err != nil { + return nil, err + } + if c.clientRegistry == nil { + return nil, fmt.Errorf("client registry unavailable") + } + + userStats := make(map[string]*model.V2UserResp) + for _, info := range c.clientRegistry.List() { + item := getOrCreateV2User(userStats, info.User) + item.ClientCount++ + } + for _, proxyInfo := range c.listV2ProxyStats("") { + item := getOrCreateV2User(userStats, proxyInfo.User) + item.ProxyCount++ + } + + q := strings.ToLower(ctx.Query("q")) + items := make([]model.V2UserResp, 0, len(userStats)) + for _, item := range userStats { + if q != "" && !strings.Contains(strings.ToLower(item.User), q) { + continue + } + items = append(items, *item) + } + slices.SortFunc(items, func(a, b model.V2UserResp) int { + return cmp.Compare(a.User, b.User) + }) + + return buildV2PageResp(items, page, pageSize), nil +} + +// /api/v2/clients +func (c *Controller) APIV2ClientList(ctx *httppkg.Context) (any, error) { + page, pageSize, err := parseV2PageParams(ctx) + if err != nil { + return nil, err + } + if c.clientRegistry == nil { + return nil, fmt.Errorf("client registry unavailable") + } + statusFilter, err := parseV2StatusFilter(ctx.Query("status")) + if err != nil { + return nil, err + } + + userFilter, filterByUser := queryValue(ctx, "user") + clientIDFilter := ctx.Query("clientID") + runIDFilter := ctx.Query("runID") + q := strings.ToLower(ctx.Query("q")) + + records := c.clientRegistry.List() + items := make([]model.ClientInfoResp, 0, len(records)) + for _, info := range records { + if filterByUser && info.User != userFilter { + continue + } + if clientIDFilter != "" && info.ClientID() != clientIDFilter { + continue + } + if runIDFilter != "" && info.RunID != runIDFilter { + continue + } + if !matchV2StatusFilter(info.Online, statusFilter) { + continue + } + resp := buildClientInfoResp(info) + if q != "" && !matchV2ClientQuery(resp, q) { + continue + } + items = append(items, resp) + } + + slices.SortFunc(items, func(a, b model.ClientInfoResp) int { + if v := cmp.Compare(a.User, b.User); v != 0 { + return v + } + if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 { + return v + } + return cmp.Compare(a.Key, b.Key) + }) + + return buildV2PageResp(items, page, pageSize), nil +} + +// /api/v2/clients/{key} +func (c *Controller) APIV2ClientDetail(ctx *httppkg.Context) (any, error) { + key, err := decodeV2PathParam(ctx, "key", "client key") + if err != nil { + return nil, err + } + + if c.clientRegistry == nil { + return nil, fmt.Errorf("client registry unavailable") + } + + info, ok := c.clientRegistry.GetByKey(key) + if !ok { + return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("client %s not found", key)) + } + + resp := buildClientInfoResp(info) + status := c.buildV2ClientStatus(info) + return model.V2ClientDetailResp{ + ClientInfoResp: resp, + Status: status, + }, nil +} + +// /api/v2/proxies +func (c *Controller) APIV2ProxyList(ctx *httppkg.Context) (any, error) { + page, pageSize, err := parseV2PageParams(ctx) + if err != nil { + return nil, err + } + statusFilter, err := parseV2StatusFilter(ctx.Query("status")) + if err != nil { + return nil, err + } + + proxyType, err := parseV2ProxyTypeFilter(ctx.Query("type")) + if err != nil { + return nil, err + } + userFilter, filterByUser := queryValue(ctx, "user") + clientIDFilter := ctx.Query("clientID") + q := strings.ToLower(ctx.Query("q")) + + stats := c.listV2ProxyStats(proxyType) + items := make([]model.V2ProxyResp, 0, len(stats)) + for _, ps := range stats { + resp := c.buildV2ProxyResp(ps) + if filterByUser && resp.User != userFilter { + continue + } + if clientIDFilter != "" && resp.ClientID != clientIDFilter { + continue + } + if !matchV2StatusFilter(resp.Status.State == "online", statusFilter) { + continue + } + if q != "" && !matchV2ProxyQuery(resp, q) { + continue + } + items = append(items, resp) + } + + slices.SortFunc(items, func(a, b model.V2ProxyResp) int { + if v := cmp.Compare(a.Spec.Type, b.Spec.Type); v != 0 { + return v + } + return cmp.Compare(a.Name, b.Name) + }) + + return buildV2PageResp(items, page, pageSize), nil +} + +// /api/v2/proxies/{name} +func (c *Controller) APIV2ProxyDetail(ctx *httppkg.Context) (any, error) { + name, err := decodeV2PathParam(ctx, "name", "proxy name") + if err != nil { + return nil, err + } + + ps := mem.StatsCollector.GetProxyByName(name) + if ps == nil { + return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found") + } + return c.buildV2ProxyResp(ps), nil +} + +// /api/v2/proxies/{name}/traffic +func (c *Controller) APIV2ProxyTraffic(ctx *httppkg.Context) (any, error) { + name, err := decodeV2PathParam(ctx, "name", "proxy name") + if err != nil { + return nil, err + } + + proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name) + if proxyTrafficInfo == nil { + return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found") + } + + return buildV2ProxyTrafficResp(name, proxyTrafficInfo, time.Now()), nil +} + +func decodeV2PathParam(ctx *httppkg.Context, key string, label string) (string, error) { + raw := ctx.Param(key) + if raw == "" { + return "", fmt.Errorf("missing %s", label) + } + decoded, err := url.PathUnescape(raw) + if err != nil { + return "", httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("invalid %s", label)) + } + return decoded, nil +} + +func getOrCreateV2User(items map[string]*model.V2UserResp, user string) *model.V2UserResp { + item, ok := items[user] + if !ok { + item = &model.V2UserResp{User: user} + items[user] = item + } + return item +} + +func parseV2PageParams(ctx *httppkg.Context) (int, int, error) { + page, err := parseV2PositiveInt(ctx.Query("page"), defaultV2Page, "page") + if err != nil { + return 0, 0, err + } + pageSize, err := parseV2PositiveInt(ctx.Query("pageSize"), defaultV2PageSize, "pageSize") + if err != nil { + return 0, 0, err + } + if pageSize > maxV2PageSize { + return 0, 0, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("pageSize must be between 1 and %d", maxV2PageSize)) + } + if page > math.MaxInt/pageSize { + return 0, 0, httppkg.NewError(http.StatusBadRequest, "page is too large") + } + return page, pageSize, nil +} + +func parseV2PositiveInt(raw string, defaultValue int, name string) (int, error) { + if raw == "" { + return defaultValue, nil + } + value, err := strconv.Atoi(raw) + if err != nil || value < 1 { + return 0, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("%s must be a positive integer", name)) + } + return value, nil +} + +func parseV2StatusFilter(raw string) (string, error) { + status := strings.ToLower(raw) + switch status { + case "", "all", "online", "offline": + return status, nil + default: + return "", httppkg.NewError(http.StatusBadRequest, "status must be one of all, online, offline") + } +} + +func parseV2ProxyTypeFilter(raw string) (string, error) { + proxyType := strings.ToLower(raw) + if proxyType == "" { + return "", nil + } + if slices.Contains(apiV2ProxyTypes, proxyType) { + return proxyType, nil + } + return "", httppkg.NewError(http.StatusBadRequest, "type must be one of tcp, udp, http, https, tcpmux, stcp, xtcp, sudp") +} + +func parseV2SystemPruneType(raw string) (string, error) { + pruneType := strings.ToLower(raw) + switch pruneType { + case "": + return "", httppkg.NewError(http.StatusBadRequest, "type is required") + case v2SystemPruneTypeOfflineProxies: + return pruneType, nil + default: + return "", httppkg.NewError(http.StatusBadRequest, "type must be one of offline_proxies") + } +} + +func matchV2StatusFilter(online bool, filter string) bool { + switch filter { + case "", "all": + return true + case "online": + return online + case "offline": + return !online + default: + return true + } +} + +func buildV2PageResp[T any](items []T, page, pageSize int) model.V2PageResp[T] { + total := len(items) + return model.V2PageResp[T]{ + Total: total, + Page: page, + PageSize: pageSize, + Items: paginateV2Items(items, page, pageSize), + } +} + +func paginateV2Items[T any](items []T, page, pageSize int) []T { + start := (page - 1) * pageSize + if start >= len(items) { + return []T{} + } + end := min(start+pageSize, len(items)) + return items[start:end] +} + +func queryValue(ctx *httppkg.Context, key string) (string, bool) { + values, ok := ctx.Req.URL.Query()[key] + if !ok { + return "", false + } + if len(values) == 0 { + return "", true + } + return values[0], true +} + +func matchV2ClientQuery(item model.ClientInfoResp, q string) bool { + return containsV2Query(q, + item.Key, + item.User, + item.ClientID, + item.RunID, + item.Version, + item.WireProtocol, + item.Hostname, + item.ClientIP, + ) +} + +func matchV2ProxyQuery(item model.V2ProxyResp, q string) bool { + values := []string{ + item.Name, + item.Spec.Type, + item.User, + item.ClientID, + item.Status.State, + } + + switch item.Spec.Type { + case string(v1.ProxyTypeTCP): + if item.Spec.TCP != nil && item.Spec.TCP.RemotePort != nil { + values = append(values, strconv.Itoa(*item.Spec.TCP.RemotePort)) + } + case string(v1.ProxyTypeUDP): + if item.Spec.UDP != nil && item.Spec.UDP.RemotePort != nil { + values = append(values, strconv.Itoa(*item.Spec.UDP.RemotePort)) + } + case string(v1.ProxyTypeHTTP): + if item.Spec.HTTP != nil { + values = append(values, item.Spec.HTTP.CustomDomains...) + values = append(values, item.Spec.HTTP.Subdomain) + } + case string(v1.ProxyTypeHTTPS): + if item.Spec.HTTPS != nil { + values = append(values, item.Spec.HTTPS.CustomDomains...) + values = append(values, item.Spec.HTTPS.Subdomain) + } + case string(v1.ProxyTypeTCPMUX): + if item.Spec.TCPMux != nil { + values = append(values, item.Spec.TCPMux.CustomDomains...) + values = append(values, item.Spec.TCPMux.Subdomain) + } + } + + return containsV2Query(q, values...) +} + +func containsV2Query(q string, values ...string) bool { + for _, value := range values { + if strings.Contains(strings.ToLower(value), q) { + return true + } + } + return false +} + +func (c *Controller) listV2ProxyStats(proxyType string) []*mem.ProxyStats { + if proxyType != "" { + return mem.StatsCollector.GetProxiesByType(proxyType) + } + + items := make([]*mem.ProxyStats, 0) + for _, t := range apiV2ProxyTypes { + items = append(items, mem.StatsCollector.GetProxiesByType(t)...) + } + return items +} + +func buildV2ProxyTrafficResp(name string, traffic *mem.ProxyTrafficInfo, now time.Time) model.V2ProxyTrafficResp { + history := make([]model.V2ProxyTrafficPointResp, 0, v2ProxyTrafficDefaultDays) + for age := v2ProxyTrafficDefaultDays - 1; age >= 0; age-- { + history = append(history, model.V2ProxyTrafficPointResp{ + Date: now.AddDate(0, 0, -age).Format(time.DateOnly), + TrafficIn: v2TrafficValueAt(traffic.TrafficIn, age), + TrafficOut: v2TrafficValueAt(traffic.TrafficOut, age), + }) + } + + return model.V2ProxyTrafficResp{ + Name: name, + Unit: v2ProxyTrafficUnit, + Granularity: v2ProxyTrafficGranularity, + History: history, + } +} + +func v2TrafficValueAt(values []int64, todayFirstIndex int) int64 { + if todayFirstIndex >= len(values) { + return 0 + } + return values[todayFirstIndex] +} + +func (c *Controller) buildV2ClientStatus(info registry.ClientInfo) model.V2ClientStatusResp { + status := model.V2ClientStatusResp{State: "offline"} + if info.Online { + status.State = "online" + } + + user := info.User + clientID := info.ClientID() + for _, ps := range c.listV2ProxyStats("") { + if ps.User != user || ps.ClientID != clientID { + continue + } + status.CurConns += ps.CurConns + status.ProxyCount++ + } + return status +} + +func (c *Controller) buildV2ProxyResp(ps *mem.ProxyStats) model.V2ProxyResp { + state := "offline" + var cfg v1.ProxyConfigurer + if c.pxyManager != nil { + if pxy, ok := c.pxyManager.GetByName(ps.Name); ok { + state = "online" + cfg = pxy.GetConfigurer() + } + } + + return model.V2ProxyResp{ + Name: ps.Name, + User: ps.User, + ClientID: ps.ClientID, + Spec: buildV2ProxySpec(ps.Type, cfg), + Status: model.V2ProxyStatusResp{ + State: state, + TodayTrafficIn: ps.TodayTrafficIn, + TodayTrafficOut: ps.TodayTrafficOut, + CurConns: ps.CurConns, + LastStartAt: ps.LastStartAt, + LastCloseAt: ps.LastCloseAt, + }, + } +} + +func buildV2ProxySpec(proxyType string, cfg v1.ProxyConfigurer) model.V2ProxySpec { + spec := model.V2ProxySpec{Type: proxyType} + + switch proxyType { + case string(v1.ProxyTypeTCP): + block := &model.V2TCPProxySpec{} + if c, ok := cfg.(*v1.TCPProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + block.RemotePort = &c.RemotePort + } + spec.TCP = block + case string(v1.ProxyTypeUDP): + block := &model.V2UDPProxySpec{} + if c, ok := cfg.(*v1.UDPProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + block.RemotePort = &c.RemotePort + } + spec.UDP = block + case string(v1.ProxyTypeHTTP): + block := &model.V2HTTPProxySpec{} + if c, ok := cfg.(*v1.HTTPProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + block.CustomDomains = slices.Clone(c.CustomDomains) + block.Subdomain = c.SubDomain + block.Locations = slices.Clone(c.Locations) + block.HostHeaderRewrite = c.HostHeaderRewrite + } + spec.HTTP = block + case string(v1.ProxyTypeHTTPS): + block := &model.V2HTTPSProxySpec{} + if c, ok := cfg.(*v1.HTTPSProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + block.CustomDomains = slices.Clone(c.CustomDomains) + block.Subdomain = c.SubDomain + } + spec.HTTPS = block + case string(v1.ProxyTypeTCPMUX): + block := &model.V2TCPMuxProxySpec{} + if c, ok := cfg.(*v1.TCPMuxProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + block.CustomDomains = slices.Clone(c.CustomDomains) + block.Subdomain = c.SubDomain + block.Multiplexer = c.Multiplexer + block.RouteByHTTPUser = c.RouteByHTTPUser + } + spec.TCPMux = block + case string(v1.ProxyTypeSTCP): + block := &model.V2STCPProxySpec{} + if c, ok := cfg.(*v1.STCPProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + } + spec.STCP = block + case string(v1.ProxyTypeSUDP): + block := &model.V2SUDPProxySpec{} + if c, ok := cfg.(*v1.SUDPProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + } + spec.SUDP = block + case string(v1.ProxyTypeXTCP): + block := &model.V2XTCPProxySpec{} + if c, ok := cfg.(*v1.XTCPProxyConfig); ok { + block.V2ProxyBaseSpec = buildV2ProxyBaseSpec(c.GetBaseConfig()) + } + spec.XTCP = block + } + + return spec +} + +func buildV2ProxyBaseSpec(base *v1.ProxyBaseConfig) model.V2ProxyBaseSpec { + return model.V2ProxyBaseSpec{ + Annotations: maps.Clone(base.Annotations), + Metadatas: maps.Clone(base.Metadatas), + Transport: &model.V2ProxyTransportSpec{ + UseEncryption: base.Transport.UseEncryption, + UseCompression: base.Transport.UseCompression, + BandwidthLimit: base.Transport.BandwidthLimit.String(), + BandwidthLimitMode: base.Transport.BandwidthLimitMode, + }, + LoadBalancer: &model.V2ProxyLoadBalancerSpec{ + Group: base.LoadBalancer.Group, + }, + } +} diff --git a/server/http/controller_v2_proxy_spec_test.go b/server/http/controller_v2_proxy_spec_test.go new file mode 100644 index 00000000000..82229bd7b55 --- /dev/null +++ b/server/http/controller_v2_proxy_spec_test.go @@ -0,0 +1,393 @@ +// 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 http + +import ( + "encoding/json" + "strings" + "testing" + + configtypes "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/metrics/mem" + "github.com/fatedier/frp/server/http/model" +) + +func TestBuildV2ProxySpecAllTypesAndRedaction(t *testing.T) { + tests := []struct { + proxyType string + cfg v1.ProxyConfigurer + blockKeys []string + }{ + { + proxyType: "tcp", + cfg: &v1.TCPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "tcp"), + RemotePort: 6000, + }, + blockKeys: []string{"annotations", "loadBalancer", "metadatas", "remotePort", "transport"}, + }, + { + proxyType: "udp", + cfg: &v1.UDPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "udp"), + RemotePort: 7000, + }, + blockKeys: []string{"annotations", "loadBalancer", "metadatas", "remotePort", "transport"}, + }, + { + proxyType: "http", + cfg: &v1.HTTPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "http"), + DomainConfig: v1.DomainConfig{CustomDomains: []string{"app.example.com"}, SubDomain: "app"}, + Locations: []string{"/api"}, + HTTPUser: "secret-http-user", + HTTPPassword: "secret-http-password", + HostHeaderRewrite: "backend.example.com", + RequestHeaders: v1.HeaderOperations{Set: map[string]string{"X-Secret": "secret-request-header"}}, + ResponseHeaders: v1.HeaderOperations{Set: map[string]string{"X-Secret": "secret-response-header"}}, + RouteByHTTPUser: "secret-http-route-user", + }, + blockKeys: []string{"annotations", "customDomains", "hostHeaderRewrite", "loadBalancer", "locations", "metadatas", "subdomain", "transport"}, + }, + { + proxyType: "https", + cfg: &v1.HTTPSProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "https"), + DomainConfig: v1.DomainConfig{CustomDomains: []string{"secure.example.com"}, SubDomain: "secure"}, + }, + blockKeys: []string{"annotations", "customDomains", "loadBalancer", "metadatas", "subdomain", "transport"}, + }, + { + proxyType: "tcpmux", + cfg: &v1.TCPMuxProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "tcpmux"), + DomainConfig: v1.DomainConfig{CustomDomains: []string{"mux.example.com"}, SubDomain: "mux"}, + HTTPUser: strings.Join([]string{"secret", "mux-http-user"}, "-"), + HTTPPassword: strings.Join([]string{"secret", "mux-http-password"}, "-"), + RouteByHTTPUser: "displayed-mux-user", + Multiplexer: "httpconnect", + }, + blockKeys: []string{"annotations", "customDomains", "loadBalancer", "metadatas", "multiplexer", "routeByHTTPUser", "subdomain", "transport"}, + }, + { + proxyType: "stcp", + cfg: &v1.STCPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "stcp"), + Secretkey: strings.Join([]string{"secret", "stcp-key"}, "-"), + AllowUsers: []string{strings.Join([]string{"secret", "stcp-user"}, "-")}, + }, + blockKeys: []string{"annotations", "loadBalancer", "metadatas", "transport"}, + }, + { + proxyType: "sudp", + cfg: &v1.SUDPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "sudp"), + Secretkey: strings.Join([]string{"secret", "sudp-key"}, "-"), + AllowUsers: []string{strings.Join([]string{"secret", "sudp-user"}, "-")}, + }, + blockKeys: []string{"annotations", "loadBalancer", "metadatas", "transport"}, + }, + { + proxyType: "xtcp", + cfg: &v1.XTCPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "xtcp"), + Secretkey: strings.Join([]string{"secret", "xtcp-key"}, "-"), + AllowUsers: []string{strings.Join([]string{"secret", "xtcp-user"}, "-")}, + }, + blockKeys: []string{"annotations", "loadBalancer", "metadatas", "transport"}, + }, + } + + for _, tt := range tests { + t.Run(tt.proxyType, func(t *testing.T) { + spec := buildV2ProxySpec(tt.proxyType, tt.cfg) + raw := mustMarshalJSON(t, spec) + + var specObject map[string]json.RawMessage + if err := json.Unmarshal(raw, &specObject); err != nil { + t.Fatalf("unmarshal spec failed: %v", err) + } + assertRawJSONKeys(t, specObject, tt.proxyType, "type") + + var gotType string + if err := json.Unmarshal(specObject["type"], &gotType); err != nil { + t.Fatalf("unmarshal spec type failed: %v", err) + } + if gotType != tt.proxyType { + t.Fatalf("spec type mismatch, want %q got %q", tt.proxyType, gotType) + } + + var block map[string]json.RawMessage + if err := json.Unmarshal(specObject[tt.proxyType], &block); err != nil { + t.Fatalf("unmarshal active block failed: %v", err) + } + assertRawJSONKeys(t, block, tt.blockKeys...) + assertV2ProxyCommonSpec(t, block) + assertV2ProxyTypeFields(t, tt.proxyType, specObject[tt.proxyType]) + assertNoV2ProxySensitiveFields(t, block) + + content := string(raw) + for _, secret := range []string{ + "secret-proxy-name", + "secret-group-key", + "secret-local-host", + "secret-plugin-user", + "secret-plugin-password", + "secret-health-path", + "secret-http-user", + "secret-http-password", + "secret-request-header", + "secret-response-header", + "secret-http-route-user", + "secret-mux-http-user", + "secret-mux-http-password", + "secret-stcp-key", + "secret-stcp-user", + "secret-sudp-key", + "secret-sudp-user", + "secret-xtcp-key", + "secret-xtcp-user", + } { + if strings.Contains(content, secret) { + t.Fatalf("sensitive value %q leaked in spec: %s", secret, content) + } + } + }) + } +} + +func assertV2ProxyTypeFields(t *testing.T, proxyType string, raw json.RawMessage) { + t.Helper() + + switch proxyType { + case "tcp": + var block model.V2TCPProxySpec + if err := json.Unmarshal(raw, &block); err != nil { + t.Fatalf("unmarshal tcp block failed: %v", err) + } + if block.RemotePort == nil || *block.RemotePort != 6000 { + t.Fatalf("tcp remote port mismatch: %#v", block.RemotePort) + } + case "udp": + var block model.V2UDPProxySpec + if err := json.Unmarshal(raw, &block); err != nil { + t.Fatalf("unmarshal udp block failed: %v", err) + } + if block.RemotePort == nil || *block.RemotePort != 7000 { + t.Fatalf("udp remote port mismatch: %#v", block.RemotePort) + } + case "http": + var block model.V2HTTPProxySpec + if err := json.Unmarshal(raw, &block); err != nil { + t.Fatalf("unmarshal http block failed: %v", err) + } + if len(block.CustomDomains) != 1 || block.CustomDomains[0] != "app.example.com" || + block.Subdomain != "app" || len(block.Locations) != 1 || block.Locations[0] != "/api" || + block.HostHeaderRewrite != "backend.example.com" { + t.Fatalf("http fields mismatch: %#v", block) + } + case "https": + var block model.V2HTTPSProxySpec + if err := json.Unmarshal(raw, &block); err != nil { + t.Fatalf("unmarshal https block failed: %v", err) + } + if len(block.CustomDomains) != 1 || block.CustomDomains[0] != "secure.example.com" || block.Subdomain != "secure" { + t.Fatalf("https fields mismatch: %#v", block) + } + case "tcpmux": + var block model.V2TCPMuxProxySpec + if err := json.Unmarshal(raw, &block); err != nil { + t.Fatalf("unmarshal tcpmux block failed: %v", err) + } + if len(block.CustomDomains) != 1 || block.CustomDomains[0] != "mux.example.com" || + block.Subdomain != "mux" || block.Multiplexer != "httpconnect" || block.RouteByHTTPUser != "displayed-mux-user" { + t.Fatalf("tcpmux fields mismatch: %#v", block) + } + } +} + +func TestBuildV2ProxyRespOfflineTypedShells(t *testing.T) { + for _, proxyType := range apiV2ProxyTypes { + t.Run(proxyType, func(t *testing.T) { + resp := (&Controller{}).buildV2ProxyResp(&mem.ProxyStats{ + Name: "offline-" + proxyType, + Type: proxyType, + }) + if resp.Status.State != "offline" { + t.Fatalf("offline phase mismatch: %#v", resp.Status) + } + + var specObject map[string]json.RawMessage + if err := json.Unmarshal(mustMarshalJSON(t, resp.Spec), &specObject); err != nil { + t.Fatalf("unmarshal offline spec failed: %v", err) + } + assertRawJSONKeys(t, specObject, proxyType, "type") + assertRawJSONKeysFromMessage(t, specObject[proxyType]) + }) + } +} + +func TestBuildV2ProxySpecDoesNotPopulateMismatchedBlock(t *testing.T) { + spec := buildV2ProxySpec("tcp", &v1.UDPProxyConfig{ + ProxyBaseConfig: newV2ProxyTestBaseConfig(t, "udp"), + RemotePort: 7000, + }) + + var specObject map[string]json.RawMessage + if err := json.Unmarshal(mustMarshalJSON(t, spec), &specObject); err != nil { + t.Fatalf("unmarshal mismatched spec failed: %v", err) + } + assertRawJSONKeys(t, specObject, "tcp", "type") + assertRawJSONKeysFromMessage(t, specObject["tcp"]) +} + +func newV2ProxyTestBaseConfig(t *testing.T, proxyType string) v1.ProxyBaseConfig { + t.Helper() + + bandwidthLimit, err := configtypes.NewBandwidthQuantity("10MB") + if err != nil { + t.Fatalf("create bandwidth limit failed: %v", err) + } + enabled := false + return v1.ProxyBaseConfig{ + Name: "secret-proxy-name", + Type: proxyType, + Enabled: &enabled, + Annotations: map[string]string{"annotation-key": "annotation-value"}, + Metadatas: map[string]string{"metadata-key": "metadata-value"}, + Transport: v1.ProxyTransport{ + UseEncryption: true, + UseCompression: true, + BandwidthLimit: bandwidthLimit, + BandwidthLimitMode: configtypes.BandwidthLimitModeServer, + ProxyProtocolVersion: "v2", + }, + LoadBalancer: v1.LoadBalancerConfig{ + Group: "public-group", + GroupKey: "secret-group-key", + }, + HealthCheck: v1.HealthCheckConfig{ + Type: "http", + Path: "secret-health-path", + }, + ProxyBackend: v1.ProxyBackend{ + LocalIP: "secret-local-host", + LocalPort: 8080, + Plugin: v1.TypedClientPluginOptions{ + Type: v1.PluginHTTPProxy, + ClientPluginOptions: &v1.HTTPProxyPluginOptions{ + Type: v1.PluginHTTPProxy, + HTTPUser: "secret-plugin-user", + HTTPPassword: "secret-plugin-password", + }, + }, + }, + } +} + +func assertV2ProxyCommonSpec(t *testing.T, block map[string]json.RawMessage) { + t.Helper() + + var annotations map[string]string + if err := json.Unmarshal(block["annotations"], &annotations); err != nil { + t.Fatalf("unmarshal annotations failed: %v", err) + } + if annotations["annotation-key"] != "annotation-value" { + t.Fatalf("annotations mismatch: %#v", annotations) + } + + var metadatas map[string]string + if err := json.Unmarshal(block["metadatas"], &metadatas); err != nil { + t.Fatalf("unmarshal metadatas failed: %v", err) + } + if metadatas["metadata-key"] != "metadata-value" { + t.Fatalf("metadatas mismatch: %#v", metadatas) + } + + assertRawJSONKeysFromMessage(t, block["transport"], + "bandwidthLimit", + "bandwidthLimitMode", + "useCompression", + "useEncryption", + ) + var transport model.V2ProxyTransportSpec + if err := json.Unmarshal(block["transport"], &transport); err != nil { + t.Fatalf("unmarshal transport failed: %v", err) + } + if !transport.UseEncryption || !transport.UseCompression || + transport.BandwidthLimit != "10MB" || transport.BandwidthLimitMode != "server" { + t.Fatalf("transport mismatch: %#v", transport) + } + + assertRawJSONKeysFromMessage(t, block["loadBalancer"], "group") + var loadBalancer model.V2ProxyLoadBalancerSpec + if err := json.Unmarshal(block["loadBalancer"], &loadBalancer); err != nil { + t.Fatalf("unmarshal load balancer failed: %v", err) + } + if loadBalancer.Group != "public-group" { + t.Fatalf("load balancer mismatch: %#v", loadBalancer) + } +} + +func assertNoV2ProxySensitiveFields(t *testing.T, value any) { + t.Helper() + + forbidden := map[string]struct{}{ + "allowUsers": {}, + "enabled": {}, + "groupKey": {}, + "healthCheck": {}, + "httpPassword": {}, + "httpUser": {}, + "localIP": {}, + "localPort": {}, + "name": {}, + "natTraversal": {}, + "plugin": {}, + "proxyProtocolVersion": {}, + "requestHeaders": {}, + "responseHeaders": {}, + "secretKey": {}, + "type": {}, + } + + var walk func(any) + walk = func(current any) { + switch current := current.(type) { + case map[string]any: + for key, nested := range current { + if _, ok := forbidden[key]; ok { + t.Fatalf("sensitive field %q leaked in active block", key) + } + walk(nested) + } + case []any: + for _, nested := range current { + walk(nested) + } + } + } + + raw, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal active block failed: %v", err) + } + var decoded any + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatalf("decode active block failed: %v", err) + } + walk(decoded) +} diff --git a/server/http/controller_v2_test.go b/server/http/controller_v2_test.go new file mode 100644 index 00000000000..042cedbc5dd --- /dev/null +++ b/server/http/controller_v2_test.go @@ -0,0 +1,908 @@ +// 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 http + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/metrics/mem" + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/server/http/model" + serverproxy "github.com/fatedier/frp/server/proxy" + "github.com/fatedier/frp/server/registry" +) + +type v2EnvelopeForTest[T any] struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data T `json:"data"` +} + +type fakeStatsCollector struct { + server *mem.ServerStats + proxies map[string]*mem.ProxyStats + traffic map[string]*mem.ProxyTrafficInfo + pruneable map[string]bool +} + +func (f *fakeStatsCollector) GetServer() *mem.ServerStats { + if f.server != nil { + return f.server + } + return &mem.ServerStats{ProxyTypeCounts: map[string]int64{}} +} + +func (f *fakeStatsCollector) GetProxiesByType(proxyType string) []*mem.ProxyStats { + items := make([]*mem.ProxyStats, 0) + for _, ps := range f.proxies { + if ps.Type == proxyType { + items = append(items, ps) + } + } + return items +} + +func (f *fakeStatsCollector) GetProxiesByTypeAndName(proxyType string, proxyName string) *mem.ProxyStats { + ps := f.proxies[proxyName] + if ps != nil && ps.Type == proxyType { + return ps + } + return nil +} + +func (f *fakeStatsCollector) GetProxyByName(proxyName string) *mem.ProxyStats { + return f.proxies[proxyName] +} + +func (f *fakeStatsCollector) GetProxyTraffic(name string) *mem.ProxyTrafficInfo { + return f.traffic[name] +} + +func (f *fakeStatsCollector) ClearOfflineProxies() (int, int) { + return 0, len(f.proxies) +} + +func (f *fakeStatsCollector) PruneOfflineProxies() (int, int) { + total := len(f.proxies) + cleared := 0 + for name := range f.pruneable { + if _, ok := f.proxies[name]; ok { + delete(f.proxies, name) + cleared++ + } + } + f.pruneable = map[string]bool{} + return cleared, total +} + +func TestAPIV2SystemInfoEnvelope(t *testing.T) { + oldStatsCollector := mem.StatsCollector + mem.StatsCollector = &fakeStatsCollector{ + server: &mem.ServerStats{ + TotalTrafficIn: 1024, + TotalTrafficOut: 2048, + CurConns: 3, + ClientCounts: 4, + ProxyTypeCounts: map[string]int64{ + "tcp": 2, + "http": 1, + }, + }, + proxies: map[string]*mem.ProxyStats{}, + } + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + controller := NewController(&v1.ServerConfig{ + BindPort: 7000, + VhostHTTPPort: 8080, + VhostHTTPSPort: 8443, + TCPMuxHTTPConnectPort: 9000, + KCPBindPort: 7001, + QUICBindPort: 7002, + SubDomainHost: "example.com", + MaxPortsPerClient: 8, + AllowPorts: []types.PortsRange{ + {Start: 1000, End: 1002}, + {Single: 2000}, + }, + Transport: v1.ServerTransportConfig{ + MaxPoolCount: 5, + HeartbeatTimeout: 90, + TLS: v1.TLSServerConfig{ + Force: true, + }, + }, + }, registry.NewClientRegistry(), serverproxy.NewManager()) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/system/info") + if resp.Code != http.StatusOK { + t.Fatalf("status mismatch, want %d got %d", http.StatusOK, resp.Code) + } + + rawResp := decodeResponse[v2EnvelopeForTest[map[string]json.RawMessage]](t, resp) + if rawResp.Code != http.StatusOK || rawResp.Msg != "success" { + t.Fatalf("envelope mismatch: %#v", rawResp) + } + assertRawJSONKeys(t, rawResp.Data, "config", "status", "version") + assertRawJSONKeysFromMessage(t, rawResp.Data["config"], + "allowPortsStr", + "bindPort", + "heartbeatTimeout", + "kcpBindPort", + "maxPoolCount", + "maxPortsPerClient", + "quicBindPort", + "subdomainHost", + "tcpmuxHTTPConnectPort", + "tlsForce", + "vhostHTTPPort", + "vhostHTTPSPort", + ) + assertRawJSONKeysFromMessage(t, rawResp.Data["status"], + "clientCounts", + "curConns", + "proxyTypeCount", + "totalTrafficIn", + "totalTrafficOut", + ) + + systemResp := decodeResponse[v2EnvelopeForTest[model.V2SystemInfoResp]](t, resp) + if systemResp.Data.Version == "" { + t.Fatal("version should be set at top level") + } + if systemResp.Data.Config.BindPort != 7000 || + systemResp.Data.Config.VhostHTTPPort != 8080 || + systemResp.Data.Config.VhostHTTPSPort != 8443 || + systemResp.Data.Config.TCPMuxHTTPConnectPort != 9000 || + systemResp.Data.Config.KCPBindPort != 7001 || + systemResp.Data.Config.QUICBindPort != 7002 || + systemResp.Data.Config.SubdomainHost != "example.com" || + systemResp.Data.Config.MaxPoolCount != 5 || + systemResp.Data.Config.MaxPortsPerClient != 8 || + systemResp.Data.Config.HeartbeatTimeout != 90 || + systemResp.Data.Config.AllowPortsStr != "1000-1002,2000" || + !systemResp.Data.Config.TLSForce { + t.Fatalf("config mismatch: %#v", systemResp.Data.Config) + } + if systemResp.Data.Status.TotalTrafficIn != 1024 || + systemResp.Data.Status.TotalTrafficOut != 2048 || + systemResp.Data.Status.CurConns != 3 || + systemResp.Data.Status.ClientCounts != 4 || + systemResp.Data.Status.ProxyTypeCounts["tcp"] != 2 || + systemResp.Data.Status.ProxyTypeCounts["http"] != 1 { + t.Fatalf("status mismatch: %#v", systemResp.Data.Status) + } +} + +func TestAPIV2SystemPruneOfflineProxies(t *testing.T) { + oldStatsCollector := mem.StatsCollector + collector := &fakeStatsCollector{ + proxies: map[string]*mem.ProxyStats{ + "tcp-offline": {Name: "tcp-offline", Type: "tcp"}, + "http-offline": {Name: "http-offline", Type: "http"}, + "udp-offline": {Name: "udp-offline", Type: "udp"}, + "tcp-online": {Name: "tcp-online", Type: "tcp"}, + "http-online": {Name: "http-online", Type: "http"}, + "udp-online": {Name: "udp-online", Type: "udp"}, + "stcp-restarted": {Name: "stcp-restarted", Type: "stcp"}, + "xtcp-restarted": {Name: "xtcp-restarted", Type: "xtcp"}, + "sudp-same-time": {Name: "sudp-same-time", Type: "sudp"}, + "tcpmux-running": {Name: "tcpmux-running", Type: "tcpmux"}, + }, + pruneable: map[string]bool{ + "tcp-offline": true, + "http-offline": true, + "udp-offline": true, + }, + } + mem.StatsCollector = collector + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + controller := NewController(&v1.ServerConfig{}, registry.NewClientRegistry(), serverproxy.NewManager()) + router := newV2TestRouter(controller) + + resp := performRequestWithMethod(router, http.MethodPost, "/api/v2/system/prune?type=offline_proxies") + if resp.Code != http.StatusOK { + t.Fatalf("status mismatch, want %d got %d, body: %s", http.StatusOK, resp.Code, resp.Body.String()) + } + rawResp := decodeResponse[v2EnvelopeForTest[map[string]json.RawMessage]](t, resp) + if rawResp.Code != http.StatusOK || rawResp.Msg != "success" { + t.Fatalf("envelope mismatch: %#v", rawResp) + } + assertRawJSONKeys(t, rawResp.Data, "cleared", "total", "type") + pruneResp := decodeResponse[v2EnvelopeForTest[model.V2SystemPruneResp]](t, resp) + if pruneResp.Data.Type != "offline_proxies" || pruneResp.Data.Cleared != 3 || pruneResp.Data.Total != 10 { + t.Fatalf("prune response mismatch: %#v", pruneResp.Data) + } + if _, ok := collector.proxies["tcp-offline"]; ok { + t.Fatal("pruned proxy statistics should be removed") + } + if _, ok := collector.proxies["tcp-online"]; !ok { + t.Fatal("online proxy statistics should remain") + } + + resp = performRequestWithMethod(router, http.MethodPost, "/api/v2/system/prune?type=offline_proxies") + if resp.Code != http.StatusOK { + t.Fatalf("second prune status mismatch, want %d got %d", http.StatusOK, resp.Code) + } + pruneResp = decodeResponse[v2EnvelopeForTest[model.V2SystemPruneResp]](t, resp) + if pruneResp.Data.Cleared != 0 || pruneResp.Data.Total != 7 { + t.Fatalf("second prune response mismatch: %#v", pruneResp.Data) + } +} + +func TestAPIV2SystemPruneTypeErrorsUseEnvelope(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequestWithMethod(router, http.MethodPost, "/api/v2/system/prune") + if resp.Code != http.StatusBadRequest { + t.Fatalf("missing type status mismatch, want %d got %d", http.StatusBadRequest, resp.Code) + } + errResp := decodeResponse[httppkg.V2Response](t, resp) + if errResp.Code != http.StatusBadRequest || errResp.Msg != "type is required" || errResp.Data != nil { + t.Fatalf("missing type error envelope mismatch: %#v", errResp) + } + + resp = performRequestWithMethod(router, http.MethodPost, "/api/v2/system/prune?type=clients") + if resp.Code != http.StatusBadRequest { + t.Fatalf("invalid type status mismatch, want %d got %d", http.StatusBadRequest, resp.Code) + } + errResp = decodeResponse[httppkg.V2Response](t, resp) + if errResp.Code != http.StatusBadRequest || errResp.Msg != "type must be one of offline_proxies" || errResp.Data != nil { + t.Fatalf("invalid type error envelope mismatch: %#v", errResp) + } +} + +func TestAPIV2ClientListEnvelopePaginationAndFilters(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/clients?page=1&pageSize=1") + if resp.Code != http.StatusOK { + t.Fatalf("status mismatch, want %d got %d", http.StatusOK, resp.Code) + } + pageResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.ClientInfoResp]]](t, resp) + if pageResp.Code != http.StatusOK || pageResp.Msg != "success" { + t.Fatalf("envelope mismatch: %#v", pageResp) + } + if pageResp.Data.Total != 3 || pageResp.Data.Page != 1 || pageResp.Data.PageSize != 1 || len(pageResp.Data.Items) != 1 { + t.Fatalf("page data mismatch: %#v", pageResp.Data) + } + if got := pageResp.Data.Items[0].User; got != "" { + t.Fatalf("first sorted user mismatch, want empty got %q", got) + } + + resp = performRequest(router, "/api/v2/clients?user=&page=1&pageSize=50") + emptyUserResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.ClientInfoResp]]](t, resp) + if emptyUserResp.Data.Total != 1 || emptyUserResp.Data.Items[0].User != "" { + t.Fatalf("empty user filter mismatch: %#v", emptyUserResp.Data) + } + + resp = performRequest(router, "/api/v2/clients?user=alice&status=online&q=alice-host") + aliceResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.ClientInfoResp]]](t, resp) + if aliceResp.Data.Total != 1 || aliceResp.Data.Items[0].User != "alice" { + t.Fatalf("alice filter mismatch: %#v", aliceResp.Data) + } + + resp = performRequest(router, "/api/v2/clients?status=offline") + offlineResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.ClientInfoResp]]](t, resp) + if offlineResp.Data.Total != 1 || offlineResp.Data.Items[0].User != "bob" { + t.Fatalf("offline filter mismatch: %#v", offlineResp.Data) + } +} + +func TestAPIV2PageParamErrorsUseEnvelope(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/clients?page=0") + if resp.Code != http.StatusBadRequest { + t.Fatalf("status mismatch, want %d got %d", http.StatusBadRequest, resp.Code) + } + errResp := decodeResponse[httppkg.V2Response](t, resp) + if errResp.Code != http.StatusBadRequest || errResp.Data != nil { + t.Fatalf("error envelope mismatch: %#v", errResp) + } + + resp = performRequest(router, "/api/v2/clients?pageSize=201") + if resp.Code != http.StatusBadRequest { + t.Fatalf("status mismatch, want %d got %d", http.StatusBadRequest, resp.Code) + } + + resp = performRequest(router, fmt.Sprintf("/api/v2/clients?page=%d&pageSize=2", math.MaxInt)) + if resp.Code != http.StatusBadRequest { + t.Fatalf("status mismatch for overflowing page offset, want %d got %d", http.StatusBadRequest, resp.Code) + } +} + +func TestAPIV2ClientDetailEnvelope(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/clients/alice.client-a") + if resp.Code != http.StatusOK { + t.Fatalf("status mismatch, want %d got %d", http.StatusOK, resp.Code) + } + detailResp := decodeResponse[v2EnvelopeForTest[model.V2ClientDetailResp]](t, resp) + if detailResp.Data.User != "alice" || detailResp.Data.ClientID != "client-a" { + t.Fatalf("client detail mismatch: %#v", detailResp.Data) + } + if detailResp.Data.Status.State != "online" || detailResp.Data.Status.CurConns != 5 || detailResp.Data.Status.ProxyCount != 2 { + t.Fatalf("client detail status mismatch: %#v", detailResp.Data.Status) + } +} + +func TestAPIV2ClientDetailEncodedKey(t *testing.T) { + oldStatsCollector := mem.StatsCollector + mem.StatsCollector = &fakeStatsCollector{ + proxies: map[string]*mem.ProxyStats{ + "tcp-url": { + Name: "tcp-url", + Type: "tcp", + User: "url", + ClientID: "client/a?b#c", + CurConns: 7, + }, + }, + } + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + clientRegistry := registry.NewClientRegistry() + clientRegistry.Register("url", "client/a?b#c", "run-url", "url-host", "1.0.0", "127.0.0.4", "v2") + controller := NewController(&v1.ServerConfig{}, clientRegistry, serverproxy.NewManager()) + router := newV2TestRouter(controller) + + encodedKey := url.PathEscape("url.client/a?b#c") + resp := performRequest(router, "/api/v2/clients/"+encodedKey) + if resp.Code != http.StatusOK { + t.Fatalf("encoded client key status mismatch, want %d got %d, body: %s", http.StatusOK, resp.Code, resp.Body.String()) + } + encodedResp := decodeResponse[v2EnvelopeForTest[model.V2ClientDetailResp]](t, resp) + if encodedResp.Data.User != "url" || encodedResp.Data.ClientID != "client/a?b#c" { + t.Fatalf("encoded client detail mismatch: %#v", encodedResp.Data) + } + if encodedResp.Data.Status.CurConns != 7 || encodedResp.Data.Status.ProxyCount != 1 { + t.Fatalf("encoded client detail status mismatch: %#v", encodedResp.Data.Status) + } +} + +func TestAPIV2ProxyListDetailAndUsers(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/proxies?type=invalid") + if resp.Code != http.StatusBadRequest { + t.Fatalf("invalid proxy type status mismatch, want %d got %d", http.StatusBadRequest, resp.Code) + } + errResp := decodeResponse[httppkg.V2Response](t, resp) + if errResp.Code != http.StatusBadRequest || errResp.Data != nil { + t.Fatalf("invalid proxy type error envelope mismatch: %#v", errResp) + } + + resp = performRequest(router, "/api/v2/proxies?type=tcp&user=&page=1&pageSize=50") + proxyResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.V2ProxyResp]]](t, resp) + if proxyResp.Data.Total != 1 { + t.Fatalf("proxy filter total mismatch: %#v", proxyResp.Data) + } + proxyItem := proxyResp.Data.Items[0] + if proxyItem.Name != "tcp-empty" || proxyItem.Spec.Type != "tcp" || proxyItem.User != "" || proxyItem.Status.State != "offline" { + t.Fatalf("proxy item mismatch: %#v", proxyItem) + } + rawProxyResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[map[string]json.RawMessage]]](t, resp) + assertRawJSONKeys(t, rawProxyResp.Data.Items[0], "clientID", "name", "spec", "status", "user") + var rawListSpec map[string]json.RawMessage + if err := json.Unmarshal(rawProxyResp.Data.Items[0]["spec"], &rawListSpec); err != nil { + t.Fatalf("unmarshal list proxy spec failed: %v", err) + } + assertRawJSONKeys(t, rawListSpec, "tcp", "type") + assertRawJSONKeysFromMessage(t, rawListSpec["tcp"]) + + resp = performRequest(router, "/api/v2/proxies/tcp-alice") + rawProxyDetailResp := decodeResponse[v2EnvelopeForTest[map[string]json.RawMessage]](t, resp) + assertRawJSONKeysFromMessage(t, rawProxyDetailResp.Data["status"], + "curConns", + "lastCloseAt", + "lastStartAt", + "phase", + "todayTrafficIn", + "todayTrafficOut", + ) + proxyDetailResp := decodeResponse[v2EnvelopeForTest[model.V2ProxyResp]](t, resp) + if proxyDetailResp.Data.Name != "tcp-alice" || proxyDetailResp.Data.User != "alice" { + t.Fatalf("proxy detail mismatch: %#v", proxyDetailResp.Data) + } + assertRawJSONKeys(t, rawProxyDetailResp.Data, "clientID", "name", "spec", "status", "user") + var rawDetailSpec map[string]json.RawMessage + if err := json.Unmarshal(rawProxyDetailResp.Data["spec"], &rawDetailSpec); err != nil { + t.Fatalf("unmarshal detail proxy spec failed: %v", err) + } + assertRawJSONKeys(t, rawDetailSpec, "tcp", "type") + assertRawJSONKeysFromMessage(t, rawDetailSpec["tcp"]) + if proxyDetailResp.Data.Status.LastStartAt != 1783504200 || proxyDetailResp.Data.Status.LastCloseAt != 1783504300 { + t.Fatalf("proxy detail timestamp mismatch: %#v", proxyDetailResp.Data.Status) + } + + resp = performRequest(router, "/api/v2/users?page=1&pageSize=50") + userResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.V2UserResp]]](t, resp) + if userResp.Data.Total != 3 { + t.Fatalf("user total mismatch: %#v", userResp.Data) + } + expectedProxyCounts := map[string]int{ + "": 1, + "alice": 2, + "bob": 1, + } + for _, item := range userResp.Data.Items { + if item.ClientCount != 1 || item.ProxyCount != expectedProxyCounts[item.User] { + t.Fatalf("user counts mismatch: %#v", item) + } + } +} + +func TestAPIV2ProxyTrafficEnvelopeSchemaAndHistory(t *testing.T) { + oldStatsCollector := mem.StatsCollector + mem.StatsCollector = &fakeStatsCollector{ + proxies: map[string]*mem.ProxyStats{ + "ssh": {Name: "ssh", Type: "tcp"}, + }, + traffic: map[string]*mem.ProxyTrafficInfo{ + "ssh": { + Name: "ssh", + TrafficIn: []int64{70, 60, 50, 40, 30, 20, 10}, + TrafficOut: []int64{700, 600, 500, 400, 300, 200, 100}, + }, + }, + } + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + controller := NewController(&v1.ServerConfig{}, registry.NewClientRegistry(), serverproxy.NewManager()) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/proxies/ssh/traffic") + if resp.Code != http.StatusOK { + t.Fatalf("status mismatch, want %d got %d, body: %s", http.StatusOK, resp.Code, resp.Body.String()) + } + rawResp := decodeResponse[v2EnvelopeForTest[map[string]json.RawMessage]](t, resp) + if rawResp.Code != http.StatusOK || rawResp.Msg != "success" { + t.Fatalf("envelope mismatch: %#v", rawResp) + } + assertRawJSONKeys(t, rawResp.Data, "granularity", "history", "name", "unit") + + trafficResp := decodeResponse[v2EnvelopeForTest[model.V2ProxyTrafficResp]](t, resp) + if trafficResp.Data.Name != "ssh" || trafficResp.Data.Unit != "bytes" || trafficResp.Data.Granularity != "day" { + t.Fatalf("traffic metadata mismatch: %#v", trafficResp.Data) + } + if len(trafficResp.Data.History) != 7 { + t.Fatalf("history length mismatch, want 7 got %d: %#v", len(trafficResp.Data.History), trafficResp.Data.History) + } + + wantIn := []int64{10, 20, 30, 40, 50, 60, 70} + wantOut := []int64{100, 200, 300, 400, 500, 600, 700} + var prevDate time.Time + for i, point := range trafficResp.Data.History { + assertRawJSONKeysFromMessage(t, mustMarshalJSON(t, point), "date", "trafficIn", "trafficOut") + if point.TrafficIn != wantIn[i] || point.TrafficOut != wantOut[i] { + t.Fatalf("history[%d] traffic mismatch: %#v", i, point) + } + parsedDate, err := time.Parse(time.DateOnly, point.Date) + if err != nil { + t.Fatalf("history[%d] date should be yyyy-mm-dd, got %q: %v", i, point.Date, err) + } + if i > 0 && !parsedDate.Equal(prevDate.AddDate(0, 0, 1)) { + t.Fatalf("history dates should be oldest to newest, got %s after %s", point.Date, prevDate.Format(time.DateOnly)) + } + prevDate = parsedDate + } +} + +func TestAPIV2ProxyTrafficNotFoundEnvelope(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/proxies/missing/traffic") + if resp.Code != http.StatusNotFound { + t.Fatalf("status mismatch, want %d got %d, body: %s", http.StatusNotFound, resp.Code, resp.Body.String()) + } + errResp := decodeResponse[httppkg.V2Response](t, resp) + if errResp.Code != http.StatusNotFound || errResp.Msg != "no proxy info found" || errResp.Data != nil { + t.Fatalf("not found envelope mismatch: %#v", errResp) + } +} + +func TestAPIV2ProxyDetailAndTrafficEncodedName(t *testing.T) { + name := "folder/ssh?x#y" + oldStatsCollector := mem.StatsCollector + mem.StatsCollector = &fakeStatsCollector{ + proxies: map[string]*mem.ProxyStats{ + name: {Name: name, Type: "tcp", User: "encoded"}, + }, + traffic: map[string]*mem.ProxyTrafficInfo{ + name: { + Name: name, + TrafficIn: []int64{1}, + TrafficOut: []int64{2}, + }, + }, + } + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + controller := NewController(&v1.ServerConfig{}, registry.NewClientRegistry(), serverproxy.NewManager()) + router := newV2TestRouter(controller) + encodedName := url.PathEscape(name) + + resp := performRequest(router, "/api/v2/proxies/"+encodedName) + if resp.Code != http.StatusOK { + t.Fatalf("encoded proxy detail status mismatch, want %d got %d, body: %s", http.StatusOK, resp.Code, resp.Body.String()) + } + detailResp := decodeResponse[v2EnvelopeForTest[model.V2ProxyResp]](t, resp) + if detailResp.Data.Name != name || detailResp.Data.User != "encoded" { + t.Fatalf("encoded proxy detail mismatch: %#v", detailResp.Data) + } + + resp = performRequest(router, "/api/v2/proxies/"+encodedName+"/traffic") + if resp.Code != http.StatusOK { + t.Fatalf("encoded traffic status mismatch, want %d got %d, body: %s", http.StatusOK, resp.Code, resp.Body.String()) + } + trafficResp := decodeResponse[v2EnvelopeForTest[model.V2ProxyTrafficResp]](t, resp) + if trafficResp.Data.Name != name { + t.Fatalf("encoded traffic name mismatch: %#v", trafficResp.Data) + } + if got := trafficResp.Data.History[len(trafficResp.Data.History)-1]; got.TrafficIn != 1 || got.TrafficOut != 2 { + t.Fatalf("encoded traffic latest point mismatch: %#v", got) + } +} + +func TestAPIV2ProxyTrafficInvalidEncodedNameUses400Envelope(t *testing.T) { + controller := newV2TestController(t) + handler := httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ProxyTraffic) + req := httptest.NewRequest(http.MethodGet, "/api/v2/proxies/%25ZZ/traffic", nil) + req = mux.SetURLVars(req, map[string]string{"name": "%ZZ"}) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + + if resp.Code != http.StatusBadRequest { + t.Fatalf("status mismatch, want %d got %d, body: %s", http.StatusBadRequest, resp.Code, resp.Body.String()) + } + errResp := decodeResponse[httppkg.V2Response](t, resp) + if errResp.Code != http.StatusBadRequest || errResp.Msg != "invalid proxy name" || errResp.Data != nil { + t.Fatalf("invalid encoded name envelope mismatch: %#v", errResp) + } +} + +func TestMatchV2ProxyQueryMatchesSpecFields(t *testing.T) { + tests := []struct { + name string + item model.V2ProxyResp + q string + want bool + }{ + { + name: "tcp remote port", + item: model.V2ProxyResp{Name: "tcp-proxy", Spec: model.V2ProxySpec{ + Type: "tcp", + TCP: &model.V2TCPProxySpec{RemotePort: v2TestIntPtr(6000)}, + }}, + q: "6000", + want: true, + }, + { + name: "udp remote port", + item: model.V2ProxyResp{Name: "udp-proxy", Spec: model.V2ProxySpec{ + Type: "udp", + UDP: &model.V2UDPProxySpec{RemotePort: v2TestIntPtr(7000)}, + }}, + q: "7000", + want: true, + }, + { + name: "remote port does not match colon form", + item: model.V2ProxyResp{Name: "tcp-proxy", Spec: model.V2ProxySpec{ + Type: "tcp", + TCP: &model.V2TCPProxySpec{RemotePort: v2TestIntPtr(6000)}, + }}, + q: ":6000", + want: false, + }, + { + name: "http custom domain", + item: model.V2ProxyResp{Name: "http-proxy", Spec: model.V2ProxySpec{ + Type: "http", + HTTP: &model.V2HTTPProxySpec{CustomDomains: []string{"app.example.com"}}, + }}, + q: "app.example.com", + want: true, + }, + { + name: "https subdomain", + item: model.V2ProxyResp{Name: "https-proxy", Spec: model.V2ProxySpec{ + Type: "https", + HTTPS: &model.V2HTTPSProxySpec{Subdomain: "portal"}, + }}, + q: "portal", + want: true, + }, + { + name: "subdomain does not match expanded host", + item: model.V2ProxyResp{Name: "https-proxy", Spec: model.V2ProxySpec{ + Type: "https", + HTTPS: &model.V2HTTPSProxySpec{Subdomain: "portal"}, + }}, + q: "portal.example.com", + want: false, + }, + { + name: "tcpmux custom domain", + item: model.V2ProxyResp{Name: "tcpmux-proxy", Spec: model.V2ProxySpec{ + Type: "tcpmux", + TCPMux: &model.V2TCPMuxProxySpec{CustomDomains: []string{"mux.example.com"}}, + }}, + q: "mux.example.com", + want: true, + }, + { + name: "offline shell does not match online spec fields", + item: model.V2ProxyResp{Name: "offline-proxy", Spec: model.V2ProxySpec{ + Type: "tcp", + TCP: &model.V2TCPProxySpec{}, + }}, + q: "6000", + want: false, + }, + { + name: "offline shell does not contribute zero remote port", + item: model.V2ProxyResp{Name: "offline-proxy", Spec: model.V2ProxySpec{ + Type: "tcp", + TCP: &model.V2TCPProxySpec{}, + }}, + q: "0", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := matchV2ProxyQuery(tt.item, tt.q); got != tt.want { + t.Fatalf("matchV2ProxyQuery() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLegacyAPIResponsesRemainBare(t *testing.T) { + controller := newV2TestController(t) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/serverinfo") + var serverInfo model.ServerInfoResp + if err := json.Unmarshal(resp.Body.Bytes(), &serverInfo); err != nil { + t.Fatalf("legacy serverinfo should be a bare object: %v, body: %s", err, resp.Body.String()) + } + if serverInfo.Version == "" { + t.Fatal("legacy serverinfo version should be set") + } + var serverInfoRaw map[string]json.RawMessage + if err := json.Unmarshal(resp.Body.Bytes(), &serverInfoRaw); err != nil { + t.Fatalf("unmarshal legacy serverinfo object failed: %v", err) + } + if _, ok := serverInfoRaw["data"]; ok { + t.Fatalf("legacy serverinfo should not use v2 envelope: %s", resp.Body.String()) + } + if _, ok := serverInfoRaw["config"]; ok { + t.Fatalf("legacy serverinfo should stay flat, got config in: %s", resp.Body.String()) + } + + resp = performRequest(router, "/api/clients") + var clients []model.ClientInfoResp + if err := json.Unmarshal(resp.Body.Bytes(), &clients); err != nil { + t.Fatalf("legacy clients should be a bare array: %v, body: %s", err, resp.Body.String()) + } + if len(clients) != 3 { + t.Fatalf("legacy clients total mismatch, want 3 got %d", len(clients)) + } + + resp = performRequest(router, "/api/proxy/tcp") + var proxies model.GetProxyInfoResp + if err := json.Unmarshal(resp.Body.Bytes(), &proxies); err != nil { + t.Fatalf("legacy proxy response should be {proxies}: %v, body: %s", err, resp.Body.String()) + } + if len(proxies.Proxies) != 2 { + t.Fatalf("legacy tcp proxy total mismatch, want 2 got %d", len(proxies.Proxies)) + } + var envelope httppkg.V2Response + if err := json.Unmarshal(resp.Body.Bytes(), &envelope); err == nil && envelope.Code != 0 { + t.Fatalf("legacy proxy response should not use v2 envelope: %#v", envelope) + } + + resp = performRequest(router, "/api/traffic/tcp-alice") + var traffic model.GetProxyTrafficResp + if err := json.Unmarshal(resp.Body.Bytes(), &traffic); err != nil { + t.Fatalf("legacy traffic should be a bare object: %v, body: %s", err, resp.Body.String()) + } + if traffic.Name != "tcp-alice" || + len(traffic.TrafficIn) != 2 || traffic.TrafficIn[0] != 7 || traffic.TrafficIn[1] != 6 || + len(traffic.TrafficOut) != 2 || traffic.TrafficOut[0] != 70 || traffic.TrafficOut[1] != 60 { + t.Fatalf("legacy traffic should preserve today-first arrays, got: %#v", traffic) + } + var trafficRaw map[string]json.RawMessage + if err := json.Unmarshal(resp.Body.Bytes(), &trafficRaw); err != nil { + t.Fatalf("unmarshal legacy traffic object failed: %v", err) + } + if _, ok := trafficRaw["data"]; ok { + t.Fatalf("legacy traffic should not use v2 envelope: %s", resp.Body.String()) + } +} + +func v2TestIntPtr(value int) *int { + return &value +} + +func newV2TestController(t *testing.T) *Controller { + t.Helper() + + oldStatsCollector := mem.StatsCollector + mem.StatsCollector = &fakeStatsCollector{ + proxies: map[string]*mem.ProxyStats{ + "tcp-empty": { + Name: "tcp-empty", + Type: "tcp", + User: "", + ClientID: "legacy-client", + TodayTrafficIn: 10, + TodayTrafficOut: 20, + CurConns: 1, + }, + "tcp-alice": { + Name: "tcp-alice", + Type: "tcp", + User: "alice", + ClientID: "client-a", + TodayTrafficIn: 30, + TodayTrafficOut: 40, + CurConns: 2, + LastStartTime: "07-08 12:30:00", + LastCloseTime: "07-08 12:31:40", + LastStartAt: 1783504200, + LastCloseAt: 1783504300, + }, + "http-alice": { + Name: "http-alice", + Type: "http", + User: "alice", + ClientID: "client-a", + CurConns: 3, + }, + "udp-bob": { + Name: "udp-bob", + Type: "udp", + User: "bob", + ClientID: "client-b", + }, + }, + traffic: map[string]*mem.ProxyTrafficInfo{ + "tcp-alice": { + Name: "tcp-alice", + TrafficIn: []int64{7, 6}, + TrafficOut: []int64{70, 60}, + }, + }, + } + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + clientRegistry := registry.NewClientRegistry() + clientRegistry.Register("", "legacy-client", "run-empty", "empty-host", "1.0.0", "127.0.0.1", "v1") + clientRegistry.Register("alice", "client-a", "run-a", "alice-host", "1.0.0", "127.0.0.2", "v2") + clientRegistry.Register("bob", "client-b", "run-b", "bob-host", "1.0.0", "127.0.0.3", "v1") + clientRegistry.MarkOfflineByRunID("run-b") + + return NewController(&v1.ServerConfig{}, clientRegistry, serverproxy.NewManager()) +} + +func newV2TestRouter(controller *Controller) *mux.Router { + router := mux.NewRouter() + router.HandleFunc("/api/v2/users", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2UserList)).Methods(http.MethodGet) + router.HandleFunc("/api/v2/system/info", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2SystemInfo)).Methods(http.MethodGet) + router.HandleFunc("/api/v2/system/prune", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2SystemPrune)).Methods(http.MethodPost) + router.HandleFunc("/api/v2/clients", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ClientList)).Methods(http.MethodGet) + encodedPathRouter := router.NewRoute().Subrouter() + encodedPathRouter.UseEncodedPath() + encodedPathRouter.HandleFunc("/api/v2/clients/{key}", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ClientDetail)).Methods(http.MethodGet) + router.HandleFunc("/api/v2/proxies", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ProxyList)).Methods(http.MethodGet) + encodedPathRouter.HandleFunc("/api/v2/proxies/{name}/traffic", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ProxyTraffic)).Methods(http.MethodGet) + encodedPathRouter.HandleFunc("/api/v2/proxies/{name}", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ProxyDetail)).Methods(http.MethodGet) + router.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(controller.APIServerInfo)).Methods(http.MethodGet) + router.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(controller.APIClientList)).Methods(http.MethodGet) + router.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(controller.APIProxyByType)).Methods(http.MethodGet) + router.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(controller.APIProxyTraffic)).Methods(http.MethodGet) + return router +} + +func performRequest(handler http.Handler, target string) *httptest.ResponseRecorder { + return performRequestWithMethod(handler, http.MethodGet, target) +} + +func performRequestWithMethod(handler http.Handler, method, target string) *httptest.ResponseRecorder { + req := httptest.NewRequest(method, target, nil) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + return resp +} + +func decodeResponse[T any](t *testing.T, resp *httptest.ResponseRecorder) T { + t.Helper() + + var out T + if err := json.Unmarshal(resp.Body.Bytes(), &out); err != nil { + t.Fatalf("unmarshal response failed: %v, body: %s", err, resp.Body.String()) + } + return out +} + +func assertRawJSONKeys(t *testing.T, raw map[string]json.RawMessage, want ...string) { + t.Helper() + + if len(raw) != len(want) { + t.Fatalf("json keys mismatch, want %v got %v", want, raw) + } + for _, key := range want { + if _, ok := raw[key]; !ok { + t.Fatalf("json key %q missing from %v", key, raw) + } + } +} + +func assertRawJSONKeysFromMessage(t *testing.T, raw json.RawMessage, want ...string) { + t.Helper() + + var out map[string]json.RawMessage + if err := json.Unmarshal(raw, &out); err != nil { + t.Fatalf("unmarshal raw json object failed: %v, body: %s", err, string(raw)) + } + assertRawJSONKeys(t, out, want...) +} + +func mustMarshalJSON(t *testing.T, value any) json.RawMessage { + t.Helper() + + out, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal json failed: %v", err) + } + return out +} diff --git a/server/http/model/types.go b/server/http/model/types.go new file mode 100644 index 00000000000..010c5079244 --- /dev/null +++ b/server/http/model/types.go @@ -0,0 +1,136 @@ +// Copyright 2025 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 model + +import ( + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type ServerInfoResp struct { + Version string `json:"version"` + BindPort int `json:"bindPort"` + VhostHTTPPort int `json:"vhostHTTPPort"` + VhostHTTPSPort int `json:"vhostHTTPSPort"` + TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"` + KCPBindPort int `json:"kcpBindPort"` + QUICBindPort int `json:"quicBindPort"` + SubdomainHost string `json:"subdomainHost"` + MaxPoolCount int64 `json:"maxPoolCount"` + MaxPortsPerClient int64 `json:"maxPortsPerClient"` + HeartBeatTimeout int64 `json:"heartbeatTimeout"` + AllowPortsStr string `json:"allowPortsStr,omitempty"` + TLSForce bool `json:"tlsForce,omitempty"` + + TotalTrafficIn int64 `json:"totalTrafficIn"` + TotalTrafficOut int64 `json:"totalTrafficOut"` + CurConns int64 `json:"curConns"` + ClientCounts int64 `json:"clientCounts"` + ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"` +} + +type ClientInfoResp struct { + Key string `json:"key"` + User string `json:"user"` + ClientID string `json:"clientID"` + RunID string `json:"runID"` + Version string `json:"version,omitempty"` + WireProtocol string `json:"wireProtocol,omitempty"` + Hostname string `json:"hostname"` + ClientIP string `json:"clientIP,omitempty"` + FirstConnectedAt int64 `json:"firstConnectedAt"` + LastConnectedAt int64 `json:"lastConnectedAt"` + DisconnectedAt int64 `json:"disconnectedAt,omitempty"` + Online bool `json:"online"` +} + +type BaseOutConf struct { + v1.ProxyBaseConfig +} + +type TCPOutConf struct { + BaseOutConf + RemotePort int `json:"remotePort"` +} + +type TCPMuxOutConf struct { + BaseOutConf + v1.DomainConfig + Multiplexer string `json:"multiplexer"` + RouteByHTTPUser string `json:"routeByHTTPUser"` +} + +type UDPOutConf struct { + BaseOutConf + RemotePort int `json:"remotePort"` +} + +type HTTPOutConf struct { + BaseOutConf + v1.DomainConfig + Locations []string `json:"locations"` + HostHeaderRewrite string `json:"hostHeaderRewrite"` +} + +type HTTPSOutConf struct { + BaseOutConf + v1.DomainConfig +} + +type STCPOutConf struct { + BaseOutConf +} + +type XTCPOutConf struct { + BaseOutConf +} + +// Get proxy info. +type ProxyStatsInfo struct { + Name string `json:"name"` + Conf any `json:"conf"` + User string `json:"user,omitempty"` + ClientID string `json:"clientID,omitempty"` + TodayTrafficIn int64 `json:"todayTrafficIn"` + TodayTrafficOut int64 `json:"todayTrafficOut"` + CurConns int64 `json:"curConns"` + LastStartTime string `json:"lastStartTime"` + LastCloseTime string `json:"lastCloseTime"` + Status string `json:"status"` +} + +type GetProxyInfoResp struct { + Proxies []*ProxyStatsInfo `json:"proxies"` +} + +// Get proxy info by name. +type GetProxyStatsResp struct { + Name string `json:"name"` + Conf any `json:"conf"` + User string `json:"user,omitempty"` + ClientID string `json:"clientID,omitempty"` + TodayTrafficIn int64 `json:"todayTrafficIn"` + TodayTrafficOut int64 `json:"todayTrafficOut"` + CurConns int64 `json:"curConns"` + LastStartTime string `json:"lastStartTime"` + LastCloseTime string `json:"lastCloseTime"` + Status string `json:"status"` +} + +// /api/traffic/:name +type GetProxyTrafficResp struct { + Name string `json:"name"` + TrafficIn []int64 `json:"trafficIn"` + TrafficOut []int64 `json:"trafficOut"` +} diff --git a/server/http/model/v2.go b/server/http/model/v2.go new file mode 100644 index 00000000000..383041b1f95 --- /dev/null +++ b/server/http/model/v2.go @@ -0,0 +1,179 @@ +// 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 model + +type V2PageResp[T any] struct { + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + Items []T `json:"items"` +} + +type V2SystemInfoResp struct { + Version string `json:"version"` + Config V2SystemInfoConfigResp `json:"config"` + Status V2SystemInfoStatusResp `json:"status"` +} + +type V2SystemInfoConfigResp struct { + BindPort int `json:"bindPort"` + VhostHTTPPort int `json:"vhostHTTPPort"` + VhostHTTPSPort int `json:"vhostHTTPSPort"` + TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"` + KCPBindPort int `json:"kcpBindPort"` + QUICBindPort int `json:"quicBindPort"` + SubdomainHost string `json:"subdomainHost"` + MaxPoolCount int64 `json:"maxPoolCount"` + MaxPortsPerClient int64 `json:"maxPortsPerClient"` + HeartbeatTimeout int64 `json:"heartbeatTimeout"` + AllowPortsStr string `json:"allowPortsStr"` + TLSForce bool `json:"tlsForce"` +} + +type V2SystemInfoStatusResp struct { + TotalTrafficIn int64 `json:"totalTrafficIn"` + TotalTrafficOut int64 `json:"totalTrafficOut"` + CurConns int64 `json:"curConns"` + ClientCounts int64 `json:"clientCounts"` + ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"` +} + +type V2SystemPruneResp struct { + Type string `json:"type"` + Cleared int `json:"cleared"` + Total int `json:"total"` +} + +type V2UserResp struct { + User string `json:"user"` + ClientCount int `json:"clientCount"` + ProxyCount int `json:"proxyCount"` +} + +type V2ClientDetailResp struct { + ClientInfoResp + Status V2ClientStatusResp `json:"status"` +} + +type V2ClientStatusResp struct { + State string `json:"phase"` + CurConns int64 `json:"curConns"` + ProxyCount int64 `json:"proxyCount"` +} + +type V2ProxyResp struct { + Name string `json:"name"` + User string `json:"user"` + ClientID string `json:"clientID"` + Spec V2ProxySpec `json:"spec"` + Status V2ProxyStatusResp `json:"status"` +} + +type V2ProxySpec struct { + Type string `json:"type"` + + TCP *V2TCPProxySpec `json:"tcp,omitempty"` + UDP *V2UDPProxySpec `json:"udp,omitempty"` + HTTP *V2HTTPProxySpec `json:"http,omitempty"` + HTTPS *V2HTTPSProxySpec `json:"https,omitempty"` + TCPMux *V2TCPMuxProxySpec `json:"tcpmux,omitempty"` + STCP *V2STCPProxySpec `json:"stcp,omitempty"` + SUDP *V2SUDPProxySpec `json:"sudp,omitempty"` + XTCP *V2XTCPProxySpec `json:"xtcp,omitempty"` +} + +type V2ProxyBaseSpec struct { + Annotations map[string]string `json:"annotations,omitempty"` + Metadatas map[string]string `json:"metadatas,omitempty"` + Transport *V2ProxyTransportSpec `json:"transport,omitempty"` + LoadBalancer *V2ProxyLoadBalancerSpec `json:"loadBalancer,omitempty"` +} + +type V2ProxyTransportSpec struct { + UseEncryption bool `json:"useEncryption"` + UseCompression bool `json:"useCompression"` + BandwidthLimit string `json:"bandwidthLimit"` + BandwidthLimitMode string `json:"bandwidthLimitMode"` +} + +type V2ProxyLoadBalancerSpec struct { + Group string `json:"group"` +} + +type V2TCPProxySpec struct { + V2ProxyBaseSpec + RemotePort *int `json:"remotePort,omitempty"` +} + +type V2UDPProxySpec struct { + V2ProxyBaseSpec + RemotePort *int `json:"remotePort,omitempty"` +} + +type V2HTTPProxySpec struct { + V2ProxyBaseSpec + CustomDomains []string `json:"customDomains,omitempty"` + Subdomain string `json:"subdomain,omitempty"` + Locations []string `json:"locations,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` +} + +type V2HTTPSProxySpec struct { + V2ProxyBaseSpec + CustomDomains []string `json:"customDomains,omitempty"` + Subdomain string `json:"subdomain,omitempty"` +} + +type V2TCPMuxProxySpec struct { + V2ProxyBaseSpec + CustomDomains []string `json:"customDomains,omitempty"` + Subdomain string `json:"subdomain,omitempty"` + Multiplexer string `json:"multiplexer,omitempty"` + RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` +} + +type V2STCPProxySpec struct { + V2ProxyBaseSpec +} + +type V2SUDPProxySpec struct { + V2ProxyBaseSpec +} + +type V2XTCPProxySpec struct { + V2ProxyBaseSpec +} + +type V2ProxyStatusResp struct { + State string `json:"phase"` + TodayTrafficIn int64 `json:"todayTrafficIn"` + TodayTrafficOut int64 `json:"todayTrafficOut"` + CurConns int64 `json:"curConns"` + LastStartAt int64 `json:"lastStartAt,omitempty"` + LastCloseAt int64 `json:"lastCloseAt,omitempty"` +} + +type V2ProxyTrafficResp struct { + Name string `json:"name"` + Unit string `json:"unit"` + Granularity string `json:"granularity"` + History []V2ProxyTrafficPointResp `json:"history"` +} + +type V2ProxyTrafficPointResp struct { + Date string `json:"date"` + TrafficIn int64 `json:"trafficIn"` + TrafficOut int64 `json:"trafficOut"` +} diff --git a/server/metrics/metrics.go b/server/metrics/metrics.go index faf00d1495d..68be2677d35 100644 --- a/server/metrics/metrics.go +++ b/server/metrics/metrics.go @@ -7,7 +7,7 @@ import ( type ServerMetrics interface { NewClient() CloseClient() - NewProxy(name string, proxyType string) + NewProxy(name string, proxyType string, user string, clientID string) CloseProxy(name string, proxyType string) OpenConnection(name string, proxyType string) CloseConnection(name string, proxyType string) @@ -27,11 +27,11 @@ func Register(m ServerMetrics) { type noopServerMetrics struct{} -func (noopServerMetrics) NewClient() {} -func (noopServerMetrics) CloseClient() {} -func (noopServerMetrics) NewProxy(name string, proxyType string) {} -func (noopServerMetrics) CloseProxy(name string, proxyType string) {} -func (noopServerMetrics) OpenConnection(name string, proxyType string) {} -func (noopServerMetrics) CloseConnection(name string, proxyType string) {} -func (noopServerMetrics) AddTrafficIn(name string, proxyType string, trafficBytes int64) {} -func (noopServerMetrics) AddTrafficOut(name string, proxyType string, trafficBytes int64) {} +func (noopServerMetrics) NewClient() {} +func (noopServerMetrics) CloseClient() {} +func (noopServerMetrics) NewProxy(string, string, string, string) {} +func (noopServerMetrics) CloseProxy(string, string) {} +func (noopServerMetrics) OpenConnection(string, string) {} +func (noopServerMetrics) CloseConnection(string, string) {} +func (noopServerMetrics) AddTrafficIn(string, string, int64) {} +func (noopServerMetrics) AddTrafficOut(string, string, int64) {} diff --git a/server/ports/ports.go b/server/ports/ports.go index f852f843596..653715eace1 100644 --- a/server/ports/ports.go +++ b/server/ports/ports.go @@ -6,6 +6,10 @@ import ( "strconv" "sync" "time" + + "k8s.io/utils/clock" + + "github.com/fatedier/frp/pkg/config/types" ) const ( @@ -36,20 +40,35 @@ type Manager struct { bindAddr string netType string + clock clock.WithTicker mu sync.Mutex } -func NewManager(netType string, bindAddr string, allowPorts map[int]struct{}) *Manager { +func NewManager(netType string, bindAddr string, allowPorts []types.PortsRange) *Manager { + return newManagerWithClock(netType, bindAddr, allowPorts, clock.RealClock{}) +} + +func newManagerWithClock(netType string, bindAddr string, allowPorts []types.PortsRange, clk clock.WithTicker) *Manager { + if clk == nil { + clk = clock.RealClock{} + } pm := &Manager{ reservedPorts: make(map[string]*PortCtx), usedPorts: make(map[int]*PortCtx), freePorts: make(map[int]struct{}), bindAddr: bindAddr, netType: netType, + clock: clk, } if len(allowPorts) > 0 { - for port := range allowPorts { - pm.freePorts[port] = struct{}{} + for _, pair := range allowPorts { + if pair.Single > 0 { + pm.freePorts[pair.Single] = struct{}{} + } else { + for i := pair.Start; i <= pair.End; i++ { + pm.freePorts[i] = struct{}{} + } + } } } else { for i := MinPort; i <= MaxPort; i++ { @@ -64,7 +83,7 @@ func (pm *Manager) Acquire(name string, port int) (realPort int, err error) { portCtx := &PortCtx{ ProxyName: name, Closed: false, - UpdateTime: time.Now(), + UpdateTime: pm.clock.Now(), } var ok bool @@ -82,9 +101,7 @@ func (pm *Manager) Acquire(name string, port int) (realPort int, err error) { if ctx, ok := pm.reservedPorts[name]; ok { if pm.isPortAvailable(ctx.Port) { realPort = ctx.Port - pm.usedPorts[realPort] = portCtx - pm.reservedPorts[name] = portCtx - delete(pm.freePorts, realPort) + pm.markPortAcquiredLocked(name, realPort, portCtx) return } } @@ -101,9 +118,7 @@ func (pm *Manager) Acquire(name string, port int) (realPort int, err error) { } if pm.isPortAvailable(k) { realPort = k - pm.usedPorts[realPort] = portCtx - pm.reservedPorts[name] = portCtx - delete(pm.freePorts, realPort) + pm.markPortAcquiredLocked(name, realPort, portCtx) break } } @@ -115,9 +130,7 @@ func (pm *Manager) Acquire(name string, port int) (realPort int, err error) { if _, ok = pm.freePorts[port]; ok { if pm.isPortAvailable(port) { realPort = port - pm.usedPorts[realPort] = portCtx - pm.reservedPorts[name] = portCtx - delete(pm.freePorts, realPort) + pm.markPortAcquiredLocked(name, realPort, portCtx) } else { err = ErrPortUnAvailable } @@ -132,6 +145,13 @@ func (pm *Manager) Acquire(name string, port int) (realPort int, err error) { return } +// markPortAcquiredLocked records a successful acquisition. pm.mu must be held. +func (pm *Manager) markPortAcquiredLocked(name string, port int, portCtx *PortCtx) { + pm.usedPorts[port] = portCtx + pm.reservedPorts[name] = portCtx + delete(pm.freePorts, port) +} + func (pm *Manager) isPortAvailable(port int) bool { if pm.netType == "udp" { addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(pm.bindAddr, strconv.Itoa(port))) @@ -161,20 +181,36 @@ func (pm *Manager) Release(port int) { pm.freePorts[port] = struct{}{} delete(pm.usedPorts, port) ctx.Closed = true - ctx.UpdateTime = time.Now() + ctx.UpdateTime = pm.clock.Now() } } // Release reserved port if it isn't used in last 24 hours. func (pm *Manager) cleanReservedPortsWorker() { + pm.cleanReservedPortsWorkerUntil(nil) +} + +func (pm *Manager) cleanReservedPortsWorkerUntil(stopCh <-chan struct{}) { + ticker := pm.clock.NewTicker(CleanReservedPortsInterval) + defer ticker.Stop() + for { - time.Sleep(CleanReservedPortsInterval) - pm.mu.Lock() - for name, ctx := range pm.reservedPorts { - if ctx.Closed && time.Since(ctx.UpdateTime) > MaxPortReservedDuration { - delete(pm.reservedPorts, name) - } + select { + case <-ticker.C(): + pm.cleanReservedPortsOnce() + case <-stopCh: + return + } + } +} + +func (pm *Manager) cleanReservedPortsOnce() { + pm.mu.Lock() + defer pm.mu.Unlock() + + for name, ctx := range pm.reservedPorts { + if ctx.Closed && pm.clock.Since(ctx.UpdateTime) > MaxPortReservedDuration { + delete(pm.reservedPorts, name) } - pm.mu.Unlock() } } diff --git a/server/ports/ports_test.go b/server/ports/ports_test.go new file mode 100644 index 00000000000..cd560b25da9 --- /dev/null +++ b/server/ports/ports_test.go @@ -0,0 +1,72 @@ +package ports + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + clocktesting "k8s.io/utils/clock/testing" + + "github.com/fatedier/frp/pkg/config/types" +) + +func TestManagerUsesClockForPortTimestamps(t *testing.T) { + require := require.New(t) + + port := freeTCPPort(t) + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + pm := newManagerWithClock("tcp", "127.0.0.1", []types.PortsRange{{Single: port}}, clk) + + realPort, err := pm.Acquire("proxy", port) + require.NoError(err) + require.Equal(port, realPort) + require.Equal(start, pm.usedPorts[port].UpdateTime) + + releasedAt := start.Add(time.Minute) + clk.SetTime(releasedAt) + pm.Release(port) + + require.Equal(releasedAt, pm.reservedPorts["proxy"].UpdateTime) +} + +func TestManagerCleanReservedPortsWorkerUsesClockTicker(t *testing.T) { + require := require.New(t) + + port := freeTCPPort(t) + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + pm := newManagerWithClock("tcp", "127.0.0.1", []types.PortsRange{{Single: port}}, clk) + + realPort, err := pm.Acquire("proxy", port) + require.NoError(err) + require.Equal(port, realPort) + pm.Release(port) + require.True(pm.hasReservedPort("proxy")) + + require.Eventually(clk.HasWaiters, time.Second, time.Millisecond) + clk.Step(MaxPortReservedDuration + CleanReservedPortsInterval + time.Minute) + + require.Eventually(func() bool { + return !pm.hasReservedPort("proxy") + }, time.Second, time.Millisecond) +} + +func (pm *Manager) hasReservedPort(name string) bool { + pm.mu.Lock() + defer pm.mu.Unlock() + + _, ok := pm.reservedPorts[name] + return ok +} + +func freeTCPPort(t *testing.T) int { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + + return listener.Addr().(*net.TCPAddr).Port +} diff --git a/server/proxy/http.go b/server/proxy/http.go index 2e054349eae..303ab37835e 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -17,32 +17,50 @@ package proxy import ( "io" "net" + "reflect" "strings" - "github.com/fatedier/frp/pkg/config" - frpNet "github.com/fatedier/frp/pkg/util/net" + libio "github.com/fatedier/golib/io" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/util/limit" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/vhost" "github.com/fatedier/frp/server/metrics" - - frpIo "github.com/fatedier/golib/io" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.HTTPProxyConfig](), NewHTTPProxy) +} + type HTTPProxy struct { *BaseProxy - cfg *config.HTTPProxyConf + cfg *v1.HTTPProxyConfig closeFuncs []func() } +func NewHTTPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.HTTPProxyConfig) + if !ok { + return nil + } + return &HTTPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } +} + func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { xl := pxy.xl routeConfig := vhost.RouteConfig{ RewriteHost: pxy.cfg.HostHeaderRewrite, RouteByHTTPUser: pxy.cfg.RouteByHTTPUser, - Headers: pxy.cfg.Headers, + Headers: pxy.cfg.RequestHeaders.Set, + ResponseHeaders: pxy.cfg.ResponseHeaders.Set, Username: pxy.cfg.HTTPUser, - Password: pxy.cfg.HTTPPwd, + Password: pxy.cfg.HTTPPassword, IpsAllowList: pxy.cfg.IpsAllowList, CreateConnFn: pxy.GetRealConn, } @@ -58,30 +76,25 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { } }() - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) + addrs := make([]string, 0) + for _, domain := range domains { routeConfig.Domain = domain for _, location := range locations { routeConfig.Location = location - tmpRouteConfig := routeConfig // handle group - if pxy.cfg.Group != "" { - err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, routeConfig) + if pxy.cfg.LoadBalancer.Group != "" { + err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig) if err != nil { return } - pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig) + pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig) }) } else { - // no group err = pxy.rc.HTTPReverseProxy.Register(routeConfig) if err != nil { return @@ -90,57 +103,20 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig) }) } - addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, int(pxy.serverCfg.VhostHTTPPort))) - xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]", - routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser) - } - } - - if pxy.cfg.SubDomain != "" { - routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost - for _, location := range locations { - routeConfig.Location = location - - tmpRouteConfig := routeConfig - - // handle group - if pxy.cfg.Group != "" { - err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, routeConfig) - if err != nil { - return - } - - pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig) - }) - } else { - err = pxy.rc.HTTPReverseProxy.Register(routeConfig) - if err != nil { - return - } - pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig) - }) - } - addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort)) - - xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]", - routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser) + addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPPort)) + xl.Infof("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]", + routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) } } remoteAddr = strings.Join(addrs, ",") return } -func (pxy *HTTPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err error) { xl := pxy.xl rAddr, errRet := net.ResolveTCPAddr("tcp", remoteAddr) if errRet != nil { - xl.Warn("resolve TCP addr [%s] error: %v", remoteAddr, errRet) + xl.Warnf("resolve TCP addr [%s] error: %v", remoteAddr, errRet) // we do not return error here since remoteAddr is not necessary for proxies without proxy protocol enabled } @@ -151,25 +127,33 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err } var rwc io.ReadWriteCloser = tmpConn - if pxy.cfg.UseEncryption { - rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.serverCfg.Token)) + if pxy.cfg.Transport.UseEncryption { + rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { - xl.Error("create encryption stream error: %v", err) + xl.Errorf("create encryption stream error: %v", err) + tmpConn.Close() return } } - if pxy.cfg.UseCompression { - rwc = frpIo.WithCompression(rwc) + if pxy.cfg.Transport.UseCompression { + rwc = libio.WithCompression(rwc) } - workConn = frpNet.WrapReadWriteCloserToConn(rwc, tmpConn) - workConn = frpNet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) - metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType) + + if pxy.GetLimiter() != nil { + rwc = libio.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error { + return rwc.Close() + }) + } + + workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn) + workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) + metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) return } func (pxy *HTTPProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) { name := pxy.GetName() - proxyType := pxy.GetConf().GetBaseInfo().ProxyType + proxyType := pxy.GetConfigurer().GetBaseConfig().Type metrics.Server.CloseConnection(name, proxyType) metrics.Server.AddTrafficIn(name, proxyType, totalWrite) metrics.Server.AddTrafficOut(name, proxyType, totalRead) diff --git a/server/proxy/https.go b/server/proxy/https.go index cd566462201..e2efc736f0c 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -15,16 +15,33 @@ package proxy import ( + "net" + "reflect" "strings" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/vhost" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.HTTPSProxyConfig](), NewHTTPSProxy) +} + type HTTPSProxy struct { *BaseProxy - cfg *config.HTTPSProxyConf + cfg *v1.HTTPSProxyConfig +} + +func NewHTTPSProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.HTTPSProxyConfig) + if !ok { + return nil + } + return &HTTPSProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } } func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { @@ -36,44 +53,39 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { pxy.Close() } }() - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } - - routeConfig.Domain = domain - l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig) - if errRet != nil { - err = errRet - return - } - xl.Info("https proxy listen for host [%s]", routeConfig.Domain) - pxy.listeners = append(pxy.listeners, l) - addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort)) - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) - if pxy.cfg.SubDomain != "" { - routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost - l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig) - if errRet != nil { - err = errRet - return + addrs := make([]string, 0) + for _, domain := range domains { + l, err := pxy.listenForDomain(routeConfig, domain) + if err != nil { + return "", err } - xl.Info("https proxy listen for host [%s]", routeConfig.Domain) pxy.listeners = append(pxy.listeners, l) - addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, int(pxy.serverCfg.VhostHTTPSPort))) + addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort)) + xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group) } - pxy.startListenHandler(pxy, HandleUserTCPConnection) + pxy.startCommonTCPListenersHandler() remoteAddr = strings.Join(addrs, ",") return } -func (pxy *HTTPSProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *HTTPSProxy) Close() { pxy.BaseProxy.Close() } + +func (pxy *HTTPSProxy) listenForDomain(routeConfig *vhost.RouteConfig, domain string) (net.Listener, error) { + tmpRouteConfig := *routeConfig + tmpRouteConfig.Domain = domain + + if pxy.cfg.LoadBalancer.Group != "" { + return pxy.rc.HTTPSGroupCtl.Listen( + pxy.ctx, + pxy.cfg.LoadBalancer.Group, + pxy.cfg.LoadBalancer.GroupKey, + tmpRouteConfig, + ) + } + return pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, &tmpRouteConfig) +} diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index 43c2c74e9b1..f4e75f0afc4 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -16,47 +16,86 @@ package proxy import ( "context" + "errors" "fmt" "io" "net" + "reflect" "strconv" "sync" "time" - "github.com/fatedier/frp/pkg/config" + libio "github.com/fatedier/golib/io" + "golang.org/x/time/rate" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" - frpNet "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/proto/wire" + "github.com/fatedier/frp/pkg/util/limit" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/metrics" - - frpIo "github.com/fatedier/golib/io" ) -type GetWorkConnFn func() (net.Conn, error) +var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy) Proxy{} + +func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy) Proxy) { + proxyFactoryRegistry[proxyConfType] = factory +} + +type WorkConn struct { + conn *msg.Conn +} + +func NewWorkConn(conn *msg.Conn) *WorkConn { + return &WorkConn{conn: conn} +} + +func (c *WorkConn) Start(m *msg.StartWorkConn) (net.Conn, error) { + if err := c.conn.WriteMsg(m); err != nil { + return nil, err + } + return c.conn, nil +} + +func (c *WorkConn) Close() error { + return c.conn.Close() +} + +type GetWorkConnFn func() (*WorkConn, error) type Proxy interface { Context() context.Context Run() (remoteAddr string, err error) GetName() string - GetConf() config.ProxyConf + GetConfigurer() v1.ProxyConfigurer GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, err error) GetUsedPortsNum() int GetResourceController() *controller.ResourceController GetUserInfo() plugin.UserInfo + GetLimiter() *rate.Limiter + GetLoginMsg() *msg.Login Close() } type BaseProxy struct { - name string - rc *controller.ResourceController - listeners []net.Listener - usedPortsNum int - poolCount int - getWorkConnFn GetWorkConnFn - serverCfg config.ServerCommonConf - userInfo plugin.UserInfo + 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 @@ -83,9 +122,21 @@ func (pxy *BaseProxy) GetUserInfo() plugin.UserInfo { return pxy.userInfo } +func (pxy *BaseProxy) GetLoginMsg() *msg.Login { + return pxy.loginMsg +} + +func (pxy *BaseProxy) GetLimiter() *rate.Limiter { + return pxy.limiter +} + +func (pxy *BaseProxy) GetConfigurer() v1.ProxyConfigurer { + return pxy.configurer +} + func (pxy *BaseProxy) Close() { xl := xlog.FromContextSafe(pxy.ctx) - xl.Info("proxy closing") + xl.Infof("proxy closing") for _, l := range pxy.listeners { l.Close() } @@ -97,32 +148,32 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, xl := xlog.FromContextSafe(pxy.ctx) // try all connections from the pool for i := 0; i < pxy.poolCount+1; i++ { - if workConn, err = pxy.getWorkConnFn(); err != nil { - xl.Warn("failed to get work connection: %v", err) + var pxyWorkConn *WorkConn + if pxyWorkConn, err = pxy.getWorkConnFn(); err != nil { + xl.Warnf("failed to get work connection: %v", err) return } - xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String()) + xl.Debugf("get a new work connection: [%s]", pxyWorkConn.conn.RemoteAddr().String()) xl.Spawn().AppendPrefix(pxy.GetName()) - workConn = frpNet.NewContextConn(pxy.ctx, workConn) var ( srcAddr string dstAddr string srcPortStr string dstPortStr string - srcPort int - dstPort int + srcPort uint64 + dstPort uint64 ) if src != nil { srcAddr, srcPortStr, _ = net.SplitHostPort(src.String()) - srcPort, _ = strconv.Atoi(srcPortStr) + srcPort, _ = strconv.ParseUint(srcPortStr, 10, 16) } if dst != nil { dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String()) - dstPort, _ = strconv.Atoi(dstPortStr) + dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16) } - err := msg.WriteMsg(workConn, &msg.StartWorkConn{ + workConn, err = pxyWorkConn.Start(&msg.StartWorkConn{ ProxyName: pxy.GetName(), SrcAddr: srcAddr, SrcPort: uint16(srcPort), @@ -131,24 +182,54 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, Error: "", }) if err != nil { - xl.Warn("failed to send message to work connection from pool: %v, times: %d", err, i) - workConn.Close() + xl.Warnf("failed to send message to work connection from pool: %v, times: %d", err, i) + pxyWorkConn.Close() + workConn = nil } else { + workConn = netpkg.NewContextConn(pxy.ctx, workConn) break } } if err != nil { - xl.Error("try to get work connection failed in the end") + xl.Errorf("try to get work connection failed in the end") return } return } -// startListenHandler start a goroutine handler for each listener. -// p: p will just be passed to handler(Proxy, frpNet.Conn). -// handler: each proxy type can set different handler function to deal with connections accepted from listeners. -func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn, config.ServerCommonConf)) { +// startVisitorListener sets up a VisitorManager listener for visitor-based proxies (STCP, SUDP). +func (pxy *BaseProxy) startVisitorListener(secretKey string, allowUsers []string, proxyType string) error { + // if allowUsers is empty, only allow same user from proxy + if len(allowUsers) == 0 { + allowUsers = []string{pxy.GetUserInfo().User} + } + listener, err := pxy.rc.VisitorManager.Listen(pxy.GetName(), secretKey, allowUsers) + if err != nil { + return err + } + pxy.listeners = append(pxy.listeners, listener) + pxy.xl.Infof("%s proxy custom listen success", proxyType) + pxy.startCommonTCPListenersHandler() + return nil +} + +// buildDomains constructs a list of domains from custom domains and subdomain configuration. +func (pxy *BaseProxy) buildDomains(customDomains []string, subDomain string) []string { + domains := make([]string, 0, len(customDomains)+1) + for _, d := range customDomains { + if d != "" { + domains = append(domains, d) + } + } + if subDomain != "" { + domains = append(domains, subDomain+"."+pxy.serverCfg.SubDomainHost) + } + return domains +} + +// startCommonTCPListenersHandler start a goroutine handler for each listener. +func (pxy *BaseProxy) startCommonTCPListenersHandler() { xl := xlog.FromContextSafe(pxy.ctx) for _, listener := range pxy.listeners { go func(l net.Listener) { @@ -165,105 +246,41 @@ func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn, } else { tempDelay *= 2 } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max + if maxTime := 1 * time.Second; tempDelay > maxTime { + tempDelay = maxTime } - xl.Info("met temporary error: %s, sleep for %s ...", err, tempDelay) + xl.Infof("met temporary error: %s, sleep for %s ...", err, tempDelay) time.Sleep(tempDelay) continue } - xl.Warn("listener is closed: %s", err) + xl.Warnf("listener is closed: %s", err) return } - xl.Info("get a user connection [%s]", c.RemoteAddr().String()) - go handler(p, c, pxy.serverCfg) + xl.Infof("get a user connection [%s]", c.RemoteAddr().String()) + go pxy.handleUserTCPConnection(c) } }(listener) } } -func NewProxy(ctx context.Context, userInfo plugin.UserInfo, rc *controller.ResourceController, poolCount int, - getWorkConnFn GetWorkConnFn, pxyConf config.ProxyConf, serverCfg config.ServerCommonConf) (pxy Proxy, err error) { - - xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(pxyConf.GetBaseInfo().ProxyName) - basePxy := BaseProxy{ - name: pxyConf.GetBaseInfo().ProxyName, - rc: rc, - listeners: make([]net.Listener, 0), - poolCount: poolCount, - getWorkConnFn: getWorkConnFn, - serverCfg: serverCfg, - xl: xl, - ctx: xlog.NewContext(ctx, xl), - userInfo: userInfo, - } - switch cfg := pxyConf.(type) { - case *config.TCPProxyConf: - basePxy.usedPortsNum = 1 - pxy = &TCPProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.TCPMuxProxyConf: - pxy = &TCPMuxProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.HTTPProxyConf: - pxy = &HTTPProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.HTTPSProxyConf: - pxy = &HTTPSProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.UDPProxyConf: - basePxy.usedPortsNum = 1 - pxy = &UDPProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.STCPProxyConf: - pxy = &STCPProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.XTCPProxyConf: - pxy = &XTCPProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - case *config.SUDPProxyConf: - pxy = &SUDPProxy{ - BaseProxy: &basePxy, - cfg: cfg, - } - default: - return pxy, fmt.Errorf("proxy type not support") - } - return -} - // HandleUserTCPConnection is used for incoming user TCP connections. -// It can be used for tcp, http, https type. -func HandleUserTCPConnection(pxy Proxy, userConn net.Conn, serverCfg config.ServerCommonConf) { +func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { xl := xlog.FromContextSafe(pxy.Context()) defer userConn.Close() + cfg := pxy.configurer.GetBaseConfig() // server plugin hook rc := pxy.GetResourceController() content := &plugin.NewUserConnContent{ User: pxy.GetUserInfo(), ProxyName: pxy.GetName(), - ProxyType: pxy.GetConf().GetBaseInfo().ProxyType, + ProxyType: cfg.Type, RemoteAddr: userConn.RemoteAddr().String(), } _, err := rc.PluginManager.NewUserConn(content) if err != nil { - xl.Warn("the user conn [%s] was rejected, err:%v", content.RemoteAddr, err) + xl.Warnf("the user conn [%s] was rejected, err:%v", content.RemoteAddr, err) return } @@ -275,29 +292,280 @@ func HandleUserTCPConnection(pxy Proxy, userConn net.Conn, serverCfg config.Serv defer workConn.Close() var local io.ReadWriteCloser = workConn - cfg := pxy.GetConf().GetBaseInfo() - xl.Trace("handler user tcp connection, use_encryption: %t, use_compression: %t", cfg.UseEncryption, cfg.UseCompression) - if cfg.UseEncryption { - local, err = frpIo.WithEncryption(local, []byte(serverCfg.Token)) + xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t", + cfg.Transport.UseEncryption, cfg.Transport.UseCompression) + if cfg.Transport.UseEncryption { + local, err = libio.WithEncryption(local, pxy.encryptionKey) if err != nil { - xl.Error("create encryption stream error: %v", err) + xl.Errorf("create encryption stream error: %v", err) return } } - if cfg.UseCompression { - local = frpIo.WithCompression(local) + if cfg.Transport.UseCompression { + var recycleFn func() + local, recycleFn = libio.WithCompressionFromPool(local) + defer recycleFn() + } + + if pxy.GetLimiter() != nil { + local = libio.WrapReadWriteCloser(limit.NewReader(local, pxy.GetLimiter()), limit.NewWriter(local, pxy.GetLimiter()), func() error { + return local.Close() + }) } - xl.Debug("join connections, workConn(l[%s] r[%s]) userConn(l[%s] r[%s])", workConn.LocalAddr().String(), + + xl.Debugf("join connections, workConn(l[%s] r[%s]) userConn(l[%s] r[%s])", workConn.LocalAddr().String(), workConn.RemoteAddr().String(), userConn.LocalAddr().String(), userConn.RemoteAddr().String()) name := pxy.GetName() - proxyType := pxy.GetConf().GetBaseInfo().ProxyType + proxyType := cfg.Type metrics.Server.OpenConnection(name, proxyType) - inCount, outCount := frpIo.Join(local, userConn) + inCount, outCount, _ := pxy.joinUserConnection(local, userConn, proxyType, xl) metrics.Server.CloseConnection(name, proxyType) metrics.Server.AddTrafficIn(name, proxyType, inCount) metrics.Server.AddTrafficOut(name, proxyType, outCount) - xl.Debug("join connections closed") + xl.Debugf("join connections closed") +} + +func (pxy *BaseProxy) joinUserConnection(local io.ReadWriteCloser, userConn net.Conn, proxyType string, xl *xlog.Logger) (int64, int64, []error) { + visitorWireProtocol := wireProtocolFromConn(userConn) + 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) +} + +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() + } + 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 + } + return wire.ProtocolV1 +} + +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, 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 + wait sync.WaitGroup + recordErrs = make([]error, 2) + ) + closeBoth := func() { + _ = proxyConn.Close() + _ = visitorConn.Close() + } + + wait.Add(2) + go func() { + defer wait.Done() + defer once.Do(closeBoth) + recordErrs[0] = bridgeSUDPProxyToVisitor(proxyRW, visitorRW, &outCount, xl) + }() + go func() { + defer wait.Done() + defer once.Do(closeBoth) + recordErrs[1] = bridgeSUDPVisitorToProxy(visitorRW, proxyRW, &inCount, xl) + }() + wait.Wait() + + for _, err := range recordErrs { + if err != nil { + errs = append(errs, err) + } + } + return +} + +func bridgeSUDPProxyToVisitor(from msg.ReadWriter, to msg.ReadWriter, count *int64, xl *xlog.Logger) error { + for { + rawMsg, err := from.ReadMsg() + if err != nil { + return normalizeSUDPBridgeError(err) + } + + switch m := rawMsg.(type) { + case *msg.UDPPacket: + if err := to.WriteMsg(m); err != nil { + return normalizeSUDPBridgeError(err) + } + *count += int64(len(m.Content)) + case *msg.Ping: + traceSUDPBridge(xl, "bridge SUDP ping from proxy to visitor") + if err := to.WriteMsg(m); err != nil { + return normalizeSUDPBridgeError(err) + } + default: + return fmt.Errorf("unexpected SUDP proxy message %T", rawMsg) + } + } +} + +func bridgeSUDPVisitorToProxy(from msg.ReadWriter, to msg.ReadWriter, count *int64, xl *xlog.Logger) error { + for { + rawMsg, err := from.ReadMsg() + if err != nil { + return normalizeSUDPBridgeError(err) + } + + switch m := rawMsg.(type) { + case *msg.UDPPacket: + if err := to.WriteMsg(m); err != nil { + return normalizeSUDPBridgeError(err) + } + *count += int64(len(m.Content)) + case *msg.Ping: + traceSUDPBridge(xl, "drop SUDP ping from visitor to proxy") + continue + default: + return fmt.Errorf("unexpected SUDP visitor message %T", rawMsg) + } + } +} + +func normalizeSUDPBridgeError(err error) error { + if err == nil || errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { + return nil + } + return err +} + +func traceSUDPBridge(xl *xlog.Logger, format string, args ...any) { + if xl != nil { + xl.Tracef(format, args...) + } +} + +type Options struct { + UserInfo plugin.UserInfo + LoginMsg *msg.Login + PoolCount int + ResourceController *controller.ResourceController + GetWorkConnFn GetWorkConnFn + Configurer v1.ProxyConfigurer + ServerCfg *v1.ServerConfig + EncryptionKey []byte + WireProtocol string + UDPPacketCodec string +} + +func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { + configurer := options.Configurer + xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(configurer.GetBaseConfig().Name) + + var limiter *rate.Limiter + limitBytes := configurer.GetBaseConfig().Transport.BandwidthLimit.Bytes() + if limitBytes > 0 && configurer.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeServer { + limiter = limit.NewBandwidthLimiter(limitBytes) + } + + 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, + udpPacketCodec: options.UDPPacketCodec, + } + + factory := proxyFactoryRegistry[reflect.TypeOf(configurer)] + if factory == nil { + return pxy, fmt.Errorf("proxy type not support") + } + pxy = factory(&basePxy) + if pxy == nil { + return nil, fmt.Errorf("proxy not created") + } + return pxy, nil } type Manager struct { @@ -324,6 +592,13 @@ func (pm *Manager) Add(name string, pxy Proxy) error { return nil } +func (pm *Manager) Exist(name string) bool { + pm.mu.RLock() + defer pm.mu.RUnlock() + _, ok := pm.pxys[name] + return ok +} + func (pm *Manager) Del(name string) { pm.mu.Lock() defer pm.mu.Unlock() diff --git a/server/proxy/proxy_test.go b/server/proxy/proxy_test.go new file mode 100644 index 00000000000..381c743e453 --- /dev/null +++ b/server/proxy/proxy_test.go @@ -0,0 +1,109 @@ +// 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 ( + "context" + "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/proto/wire" +) + +func TestWorkConnStartWritesStartWorkConn(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + serverMsgConn := msg.NewConn(server, msg.NewV2ReadWriter(server)) + clientMsgConn := msg.NewConn(client, msg.NewV2ReadWriter(client)) + workConn := NewWorkConn(serverMsgConn) + + in := &msg.StartWorkConn{ProxyName: "tcp", SrcAddr: "127.0.0.1", SrcPort: 1234} + type startResult struct { + conn net.Conn + err error + } + resultCh := make(chan startResult, 1) + go func() { + conn, err := workConn.Start(in) + resultCh <- startResult{conn: conn, err: err} + }() + + out, err := clientMsgConn.ReadMsg() + require.NoError(t, err) + require.Equal(t, in, out) + + result := <-resultCh + require.NoError(t, result.err) + require.Same(t, serverMsgConn, result.conn) +} + +func TestGetWorkConnFromPoolStartWorkConnUnchangedForUDPWireV2(t *testing.T) { + startMsg := getStartWorkConnFromPool(t, &v1.UDPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{Name: "udp", Type: string(v1.ProxyTypeUDP)}, + }, wire.ProtocolV2) + + require.Equal(t, msg.StartWorkConn{ProxyName: "udp"}, startMsg) +} + +func TestGetWorkConnFromPoolLeavesRawTCPPayloadUnframed(t *testing.T) { + startMsg := getStartWorkConnFromPool(t, &v1.TCPProxyConfig{ + ProxyBaseConfig: v1.ProxyBaseConfig{Name: "tcp", Type: string(v1.ProxyTypeTCP)}, + }, wire.ProtocolV2) + + require.Equal(t, msg.StartWorkConn{ProxyName: "tcp"}, startMsg) +} + +func getStartWorkConnFromPool(t *testing.T, cfg v1.ProxyConfigurer, wireProtocol string) msg.StartWorkConn { + t.Helper() + + client, server := net.Pipe() + t.Cleanup(func() { + client.Close() + server.Close() + }) + + serverMsgConn := msg.NewConn(server, msg.NewV2ReadWriter(server)) + clientMsgConn := msg.NewConn(client, msg.NewV2ReadWriter(client)) + pxy := &BaseProxy{ + name: cfg.GetBaseConfig().Name, + configurer: cfg, + poolCount: 0, + ctx: context.Background(), + wireProtocol: wireProtocol, + getWorkConnFn: func() (*WorkConn, error) { + return NewWorkConn(serverMsgConn), nil + }, + } + + errCh := make(chan error, 1) + go func() { + conn, err := pxy.GetWorkConnFromPool(nil, nil) + if conn != nil { + conn.Close() + } + errCh <- err + }() + + var startMsg msg.StartWorkConn + require.NoError(t, clientMsgConn.ReadMsgInto(&startMsg)) + require.NoError(t, <-errCh) + return startMsg +} diff --git a/server/proxy/stcp.go b/server/proxy/stcp.go index 5ac47eaaca5..f8c17a46fda 100644 --- a/server/proxy/stcp.go +++ b/server/proxy/stcp.go @@ -15,30 +15,34 @@ package proxy import ( - "github.com/fatedier/frp/pkg/config" + "reflect" + + v1 "github.com/fatedier/frp/pkg/config/v1" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.STCPProxyConfig](), NewSTCPProxy) +} + type STCPProxy struct { *BaseProxy - cfg *config.STCPProxyConf + cfg *v1.STCPProxyConfig } -func (pxy *STCPProxy) Run() (remoteAddr string, err error) { - xl := pxy.xl - listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk) - if errRet != nil { - err = errRet - return +func NewSTCPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.STCPProxyConfig) + if !ok { + return nil + } + return &STCPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, } - pxy.listeners = append(pxy.listeners, listener) - xl.Info("stcp proxy custom listen success") - - pxy.startListenHandler(pxy, HandleUserTCPConnection) - return } -func (pxy *STCPProxy) GetConf() config.ProxyConf { - return pxy.cfg +func (pxy *STCPProxy) Run() (remoteAddr string, err error) { + err = pxy.startVisitorListener(pxy.cfg.Secretkey, pxy.cfg.AllowUsers, "stcp") + return } func (pxy *STCPProxy) Close() { diff --git a/server/proxy/sudp.go b/server/proxy/sudp.go index c4dba6d6634..d409fed63b6 100644 --- a/server/proxy/sudp.go +++ b/server/proxy/sudp.go @@ -15,31 +15,34 @@ package proxy import ( - "github.com/fatedier/frp/pkg/config" + "reflect" + + v1 "github.com/fatedier/frp/pkg/config/v1" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy) +} + type SUDPProxy struct { *BaseProxy - cfg *config.SUDPProxyConf + cfg *v1.SUDPProxyConfig } -func (pxy *SUDPProxy) Run() (remoteAddr string, err error) { - xl := pxy.xl - - listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk) - if errRet != nil { - err = errRet - return +func NewSUDPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.SUDPProxyConfig) + if !ok { + return nil + } + return &SUDPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, } - pxy.listeners = append(pxy.listeners, listener) - xl.Info("sudp proxy custom listen success") - - pxy.startListenHandler(pxy, HandleUserTCPConnection) - return } -func (pxy *SUDPProxy) GetConf() config.ProxyConf { - return pxy.cfg +func (pxy *SUDPProxy) Run() (remoteAddr string, err error) { + err = pxy.startVisitorListener(pxy.cfg.Secretkey, pxy.cfg.AllowUsers, "sudp") + return } func (pxy *SUDPProxy) Close() { diff --git a/server/proxy/sudp_benchmark_test.go b/server/proxy/sudp_benchmark_test.go new file mode 100644 index 00000000000..a2e337d9f63 --- /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 new file mode 100644 index 00000000000..b7ea9bc6e8a --- /dev/null +++ b/server/proxy/sudp_test.go @@ -0,0 +1,358 @@ +// 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 ( + "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")}) + + var count int64 + err := bridgeSUDPProxyToVisitor( + newSUDPBridgeRW(t, &in, wire.ProtocolV1, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV2, ""), + &count, + nil, + ) + require.NoError(t, err) + require.Equal(t, int64(len("proxy-to-visitor")), count) + + frame, err := wire.NewConn(&out).ReadFrame() + require.NoError(t, err) + require.Equal(t, wire.FrameTypeMessage, frame.Type) + require.GreaterOrEqual(t, len(frame.Payload), 2) + require.Equal(t, msg.V2TypeUDPPacket, binary.BigEndian.Uint16(frame.Payload[:2])) + + var got msg.UDPPacket + require.NoError(t, msg.DecodeV2MessageFrameInto(frame, &got)) + require.Equal(t, []byte("proxy-to-visitor"), got.Content) +} + +func TestSUDPBridgeTranscodesVisitorV2ToProxyV1(t *testing.T) { + var in, out bytes.Buffer + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", &msg.UDPPacket{Content: []byte("visitor-to-proxy")}) + + var count int64 + err := bridgeSUDPVisitorToProxy( + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV1, ""), + &count, + nil, + ) + require.NoError(t, err) + require.Equal(t, int64(len("visitor-to-proxy")), count) + + reader := bufio.NewReader(&out) + typeByte, err := reader.ReadByte() + require.NoError(t, err) + require.Equal(t, msg.TypeUDPPacket, typeByte) + require.NoError(t, reader.UnreadByte()) + + var got msg.UDPPacket + require.NoError(t, msg.ReadMsgInto(reader, &got)) + require.Equal(t, []byte("visitor-to-proxy"), got.Content) +} + +func TestSUDPBridgeTranscodesProxyV2BinaryToVisitorV2JSON(t *testing.T) { + var in, out bytes.Buffer + packet := newSUDPBridgeUDPPacket("proxy-binary-to-json") + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, wire.UDPPacketCodecBinary, packet) + + var count int64 + err := bridgeSUDPProxyToVisitor( + 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 := 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{}) + + var count int64 + err := bridgeSUDPVisitorToProxy( + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV1, ""), + &count, + nil, + ) + require.NoError(t, err) + require.Zero(t, count) + require.Empty(t, out.Bytes()) +} + +func TestSUDPBridgeRejectsUnknownVisitorMessage(t *testing.T) { + var in, out bytes.Buffer + writeSUDPBridgeMsg(t, &in, wire.ProtocolV2, "", &msg.Pong{}) + + var count int64 + err := bridgeSUDPVisitorToProxy( + newSUDPBridgeRW(t, &in, wire.ProtocolV2, ""), + newSUDPBridgeRW(t, &out, wire.ProtocolV1, ""), + &count, + nil, + ) + require.ErrorContains(t, err, "unexpected SUDP visitor message *msg.Pong") + require.Zero(t, count) + 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)) + require.True(t, isMixedWireProtocol("", wire.ProtocolV2)) + require.True(t, isMixedWireProtocol(wire.ProtocolV2, wire.ProtocolV1)) +} + +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/proxy/tcp.go b/server/proxy/tcp.go index 0cf9c5f95d2..c305e779a84 100644 --- a/server/proxy/tcp.go +++ b/server/proxy/tcp.go @@ -17,22 +17,40 @@ package proxy import ( "fmt" "net" + "reflect" "strconv" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.TCPProxyConfig](), NewTCPProxy) +} + type TCPProxy struct { *BaseProxy - cfg *config.TCPProxyConf + cfg *v1.TCPProxyConfig + + realBindPort int +} - realPort int +func NewTCPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.TCPProxyConfig) + if !ok { + return nil + } + baseProxy.usedPortsNum = 1 + return &TCPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } } func (pxy *TCPProxy) Run() (remoteAddr string, err error) { xl := pxy.xl - if pxy.cfg.Group != "" { - l, realPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort) + if pxy.cfg.LoadBalancer.Group != "" { + l, realBindPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, + pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort) if errRet != nil { err = errRet return @@ -42,41 +60,37 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) { l.Close() } }() - pxy.realPort = realPort + pxy.realBindPort = realBindPort pxy.listeners = append(pxy.listeners, l) - xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.Group) + xl.Infof("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.LoadBalancer.Group) } else { - pxy.realPort, err = pxy.rc.TCPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort) + pxy.realBindPort, err = pxy.rc.TCPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort) if err != nil { return } defer func() { if err != nil { - pxy.rc.TCPPortManager.Release(pxy.realPort) + pxy.rc.TCPPortManager.Release(pxy.realBindPort) } }() - listener, errRet := net.Listen("tcp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realPort))) + listener, errRet := net.Listen("tcp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realBindPort))) if errRet != nil { err = errRet return } pxy.listeners = append(pxy.listeners, listener) - xl.Info("tcp proxy listen port [%d]", pxy.cfg.RemotePort) + xl.Infof("tcp proxy listen port [%d]", pxy.cfg.RemotePort) } - pxy.cfg.RemotePort = pxy.realPort - remoteAddr = fmt.Sprintf(":%d", pxy.realPort) - pxy.startListenHandler(pxy, HandleUserTCPConnection) + pxy.cfg.RemotePort = pxy.realBindPort + remoteAddr = fmt.Sprintf(":%d", pxy.realBindPort) + pxy.startCommonTCPListenersHandler() return } -func (pxy *TCPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *TCPProxy) Close() { pxy.BaseProxy.Close() - if pxy.cfg.Group == "" { - pxy.rc.TCPPortManager.Release(pxy.realPort) + if pxy.cfg.LoadBalancer.Group == "" { + pxy.rc.TCPPortManager.Release(pxy.realBindPort) } } diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go index b812e601fd3..0c421c8eb32 100644 --- a/server/proxy/tcpmux.go +++ b/server/proxy/tcpmux.go @@ -17,68 +17,79 @@ package proxy import ( "fmt" "net" + "reflect" "strings" - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/vhost" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.TCPMuxProxyConfig](), NewTCPMuxProxy) +} + type TCPMuxProxy struct { *BaseProxy - cfg *config.TCPMuxProxyConf + cfg *v1.TCPMuxProxyConfig +} + +func NewTCPMuxProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.TCPMuxProxyConfig) + if !ok { + return nil + } + return &TCPMuxProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } } -func (pxy *TCPMuxProxy) httpConnectListen(domain, routeByHTTPUser string, addrs []string) ([]string, error) { +func (pxy *TCPMuxProxy) httpConnectListen( + domain, routeByHTTPUser, httpUser, httpPwd string, addrs []string) ([]string, error, +) { var l net.Listener var err error routeConfig := &vhost.RouteConfig{ Domain: domain, RouteByHTTPUser: routeByHTTPUser, + Username: httpUser, + Password: httpPwd, } - if pxy.cfg.Group != "" { - l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, *routeConfig) + if pxy.cfg.LoadBalancer.Group != "" { + l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, + pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, *routeConfig) } else { l, err = pxy.rc.TCPMuxHTTPConnectMuxer.Listen(pxy.ctx, routeConfig) } if err != nil { return nil, err } - pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s], group [%s] routeByHTTPUser [%s]", - domain, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser) + pxy.xl.Infof("tcpmux httpconnect multiplexer listens for host [%s], group [%s] routeByHTTPUser [%s]", + domain, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) pxy.listeners = append(pxy.listeners, l) return append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.TCPMuxHTTPConnectPort)), nil } func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { - addrs := make([]string, 0) - for _, domain := range pxy.cfg.CustomDomains { - if domain == "" { - continue - } + domains := pxy.buildDomains(pxy.cfg.CustomDomains, pxy.cfg.SubDomain) - addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, addrs) - if err != nil { - return "", err - } - } - - if pxy.cfg.SubDomain != "" { - addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, pxy.cfg.RouteByHTTPUser, addrs) + addrs := make([]string, 0) + for _, domain := range domains { + addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) if err != nil { return "", err } } - pxy.startListenHandler(pxy, HandleUserTCPConnection) + pxy.startCommonTCPListenersHandler() remoteAddr = strings.Join(addrs, ",") return remoteAddr, err } func (pxy *TCPMuxProxy) Run() (remoteAddr string, err error) { - switch pxy.cfg.Multiplexer { - case consts.HTTPConnectTCPMultiplexer: + switch v1.TCPMultiplexerType(pxy.cfg.Multiplexer) { + case v1.TCPMultiplexerHTTPConnect: remoteAddr, err = pxy.httpConnectRun() default: err = fmt.Errorf("unknown multiplexer [%s]", pxy.cfg.Multiplexer) @@ -90,10 +101,6 @@ func (pxy *TCPMuxProxy) Run() (remoteAddr string, err error) { return remoteAddr, err } -func (pxy *TCPMuxProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *TCPMuxProxy) Close() { pxy.BaseProxy.Close() } diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 9e3c067509d..47bdfc8cbd3 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -19,24 +19,30 @@ import ( "fmt" "io" "net" + "reflect" "strconv" "time" - "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/golib/errors" + libio "github.com/fatedier/golib/io" + + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" - frpNet "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/limit" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/server/metrics" - - "github.com/fatedier/golib/errors" - frpIo "github.com/fatedier/golib/io" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy) +} + type UDPProxy struct { *BaseProxy - cfg *config.UDPProxyConf + cfg *v1.UDPProxyConfig - realPort int + realBindPort int // udpConn is the listener of udp packages udpConn *net.UDPConn @@ -57,21 +63,33 @@ type UDPProxy struct { isClosed bool } +func NewUDPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.UDPProxyConfig) + if !ok { + return nil + } + baseProxy.usedPortsNum = 1 + return &UDPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + } +} + func (pxy *UDPProxy) Run() (remoteAddr string, err error) { xl := pxy.xl - pxy.realPort, err = pxy.rc.UDPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort) + pxy.realBindPort, err = pxy.rc.UDPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort) if err != nil { return "", fmt.Errorf("acquire port %d error: %v", pxy.cfg.RemotePort, err) } defer func() { if err != nil { - pxy.rc.UDPPortManager.Release(pxy.realPort) + pxy.rc.UDPPortManager.Release(pxy.realBindPort) } }() - remoteAddr = fmt.Sprintf(":%d", pxy.realPort) - pxy.cfg.RemotePort = pxy.realPort - addr, errRet := net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realPort))) + remoteAddr = fmt.Sprintf(":%d", pxy.realBindPort) + pxy.cfg.RemotePort = pxy.realBindPort + addr, errRet := net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realBindPort))) if errRet != nil { err = errRet return @@ -79,10 +97,10 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { udpConn, errRet := net.ListenUDP("udp", addr) if errRet != nil { err = errRet - xl.Warn("listen udp port error: %v", err) + xl.Warnf("listen udp port error: %v", err) return } - xl.Info("udp proxy listen port [%d]", pxy.cfg.RemotePort) + xl.Infof("udp proxy listen port [%d]", pxy.cfg.RemotePort) pxy.udpConn = udpConn pxy.sendCh = make(chan *msg.UDPPacket, 1024) @@ -90,42 +108,44 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { pxy.checkCloseCh = make(chan int) // read message from workConn, if it returns any error, notify proxy to start a new workConn - workConnReaderFn := func(conn net.Conn) { + workConnReaderFn := func(payloadConn *msg.Conn) { for { var ( rawMsg msg.Message errRet error ) - xl.Trace("loop waiting message from udp workConn") + xl.Tracef("loop waiting message from udp workConn") // client will send heartbeat in workConn for keeping alive - conn.SetReadDeadline(time.Now().Add(time.Duration(60) * time.Second)) - if rawMsg, errRet = msg.ReadMsg(conn); errRet != nil { - xl.Warn("read from workConn for udp error: %v", errRet) - conn.Close() + _ = payloadConn.SetReadDeadline(time.Now().Add(time.Duration(60) * time.Second)) + if rawMsg, errRet = payloadConn.ReadMsg(); errRet != nil { + xl.Warnf("read from workConn for udp error: %v", errRet) + _ = payloadConn.Close() // notify proxy to start a new work connection // ignore error here, it means the proxy is closed - errors.PanicToError(func() { + _ = errors.PanicToError(func() { pxy.checkCloseCh <- 1 }) return } - conn.SetReadDeadline(time.Time{}) + if err := payloadConn.SetReadDeadline(time.Time{}); err != nil { + xl.Warnf("set read deadline error: %v", err) + } switch m := rawMsg.(type) { case *msg.Ping: - xl.Trace("udp work conn get ping message") + xl.Tracef("udp work conn get ping message") continue case *msg.UDPPacket: if errRet := errors.PanicToError(func() { - xl.Trace("get udp message from workConn: %s", m.Content) + xl.Tracef("get udp message from workConn, len: %d", len(m.Content)) pxy.readCh <- m metrics.Server.AddTrafficOut( pxy.GetName(), - pxy.GetConf().GetBaseInfo().ProxyType, + pxy.GetConfigurer().GetBaseConfig().Type, int64(len(m.Content)), ) }); errRet != nil { - conn.Close() - xl.Info("reader goroutine for udp work connection closed") + _ = payloadConn.Close() + xl.Infof("reader goroutine for udp work connection closed") return } } @@ -133,29 +153,29 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { } // send message to workConn - workConnSenderFn := func(conn net.Conn, ctx context.Context) { + workConnSenderFn := func(payloadConn *msg.Conn, ctx context.Context) { var errRet error for { select { case udpMsg, ok := <-pxy.sendCh: if !ok { - xl.Info("sender goroutine for udp work connection closed") + xl.Infof("sender goroutine for udp work connection closed") return } - if errRet = msg.WriteMsg(conn, udpMsg); errRet != nil { - xl.Info("sender goroutine for udp work connection closed: %v", errRet) - conn.Close() + if errRet = payloadConn.WriteMsg(udpMsg); errRet != nil { + xl.Infof("sender goroutine for udp work connection closed: %v", errRet) + _ = payloadConn.Close() return } - xl.Trace("send message to udp workConn: %s", udpMsg.Content) + xl.Tracef("send message to udp workConn, len: %d", len(udpMsg.Content)) metrics.Server.AddTrafficIn( pxy.GetName(), - pxy.GetConf().GetBaseInfo().ProxyType, + pxy.GetConfigurer().GetBaseConfig().Type, int64(len(udpMsg.Content)), ) continue case <-ctx.Done(): - xl.Info("sender goroutine for udp work connection closed") + xl.Infof("sender goroutine for udp work connection closed") return } } @@ -184,22 +204,36 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { } var rwc io.ReadWriteCloser = workConn - if pxy.cfg.UseEncryption { - rwc, err = frpIo.WithEncryption(rwc, []byte(pxy.serverCfg.Token)) + if pxy.cfg.Transport.UseEncryption { + rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) if err != nil { - xl.Error("create encryption stream error: %v", err) + xl.Errorf("create encryption stream error: %v", err) workConn.Close() continue } } - if pxy.cfg.UseCompression { - rwc = frpIo.WithCompression(rwc) + if pxy.cfg.Transport.UseCompression { + rwc = libio.WithCompression(rwc) } - pxy.workConn = frpNet.WrapReadWriteCloserToConn(rwc, workConn) + if pxy.GetLimiter() != nil { + rwc = libio.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error { + return rwc.Close() + }) + } + + pxy.workConn = netpkg.WrapReadWriteCloserToConn(rwc, workConn) + // Plain UDP payload follows the negotiated wire protocol for message framing. + 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(pxy.workConn) - go workConnSenderFn(pxy.workConn, ctx) + go workConnReaderFn(payloadConn) + go workConnSenderFn(payloadConn, ctx) _, ok := <-pxy.checkCloseCh cancel() if !ok { @@ -219,10 +253,6 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { return remoteAddr, nil } -func (pxy *UDPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *UDPProxy) Close() { pxy.mu.Lock() defer pxy.mu.Unlock() @@ -240,5 +270,5 @@ func (pxy *UDPProxy) Close() { close(pxy.readCh) close(pxy.sendCh) } - pxy.rc.UDPPortManager.Release(pxy.realPort) + pxy.rc.UDPPortManager.Release(pxy.realBindPort) } diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go index 317a3d484c9..a6488d42770 100644 --- a/server/proxy/xtcp.go +++ b/server/proxy/xtcp.go @@ -16,82 +16,86 @@ package proxy import ( "fmt" + "net" + "reflect" + "sync" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" - - "github.com/fatedier/golib/errors" ) +func init() { + RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy) +} + type XTCPProxy struct { *BaseProxy - cfg *config.XTCPProxyConf + cfg *v1.XTCPProxyConfig - closeCh chan struct{} + closeCh chan struct{} + closeOnce sync.Once +} + +func NewXTCPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.XTCPProxyConfig) + if !ok { + return nil + } + return &XTCPProxy{ + BaseProxy: baseProxy, + cfg: unwrapped, + closeCh: make(chan struct{}), + } } func (pxy *XTCPProxy) Run() (remoteAddr string, err error) { xl := pxy.xl if pxy.rc.NatHoleController == nil { - xl.Error("udp port for xtcp is not specified.") err = fmt.Errorf("xtcp is not supported in frps") return } - sidCh := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Sk) + allowUsers := pxy.cfg.AllowUsers + // if allowUsers is empty, only allow same user from proxy + if len(allowUsers) == 0 { + allowUsers = []string{pxy.GetUserInfo().User} + } + sidCh, err := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Secretkey, allowUsers) + if err != nil { + return "", err + } go func() { for { select { case <-pxy.closeCh: - break - case sidRequest := <-sidCh: - sr := sidRequest + return + case sid := <-sidCh: workConn, errRet := pxy.GetWorkConnFromPool(nil, nil) if errRet != nil { continue } - m := &msg.NatHoleSid{ - Sid: sr.Sid, - } - errRet = msg.WriteMsg(workConn, m) + errRet = writeNatHoleSid(workConn, pxy.wireProtocol, sid) if errRet != nil { - xl.Warn("write nat hole sid package error, %v", errRet) - workConn.Close() - break + xl.Warnf("write nat hole sid package error, %v", errRet) } - - go func() { - raw, errRet := msg.ReadMsg(workConn) - if errRet != nil { - xl.Warn("read nat hole client ok package error: %v", errRet) - workConn.Close() - return - } - if _, ok := raw.(*msg.NatHoleClientDetectOK); !ok { - xl.Warn("read nat hole client ok package format error") - workConn.Close() - return - } - - select { - case sr.NotifyCh <- struct{}{}: - default: - } - }() + workConn.Close() } } }() return } -func (pxy *XTCPProxy) GetConf() config.ProxyConf { - return pxy.cfg +func writeNatHoleSid(workConn net.Conn, wireProtocol string, sid string) error { + workMsgConn := msg.NewConn(workConn, msg.NewReadWriter(workConn, wireProtocol)) + return workMsgConn.WriteMsg(&msg.NatHoleSid{ + Sid: sid, + }) } func (pxy *XTCPProxy) Close() { - pxy.BaseProxy.Close() - pxy.rc.NatHoleController.CloseClient(pxy.GetName()) - errors.PanicToError(func() { + pxy.closeOnce.Do(func() { + pxy.BaseProxy.Close() + pxy.rc.NatHoleController.CloseClient(pxy.GetName()) close(pxy.closeCh) }) } diff --git a/server/proxy/xtcp_test.go b/server/proxy/xtcp_test.go new file mode 100644 index 00000000000..b2e6c1ad4f8 --- /dev/null +++ b/server/proxy/xtcp_test.go @@ -0,0 +1,93 @@ +// 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 ( + "bufio" + "encoding/binary" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestWriteNatHoleSidUsesWireV2MessageFrame(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + setPipeDeadline(t, client, server) + + errCh := make(chan error, 1) + go func() { + errCh <- writeNatHoleSid(server, wire.ProtocolV2, "sid-v2") + }() + + frame, err := wire.NewConn(client).ReadFrame() + require.NoError(t, err) + require.Equal(t, wire.FrameTypeMessage, frame.Type) + require.GreaterOrEqual(t, len(frame.Payload), 2) + require.Equal(t, msg.V2TypeNatHoleSid, binary.BigEndian.Uint16(frame.Payload[:2])) + + var out msg.NatHoleSid + require.NoError(t, msg.DecodeV2MessageFrameInto(frame, &out)) + require.Equal(t, "sid-v2", out.Sid) + require.NoError(t, <-errCh) +} + +func TestWriteNatHoleSidUsesLegacyCodecForWireV1AndDefault(t *testing.T) { + for _, tc := range []struct { + name string + wireProtocol string + }{ + {name: "default", wireProtocol: ""}, + {name: "v1", wireProtocol: wire.ProtocolV1}, + } { + t.Run(tc.name, func(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + setPipeDeadline(t, client, server) + + errCh := make(chan error, 1) + go func() { + errCh <- writeNatHoleSid(server, tc.wireProtocol, "sid-legacy") + }() + + reader := bufio.NewReader(client) + typeByte, err := reader.ReadByte() + require.NoError(t, err) + require.Equal(t, msg.TypeNatHoleSid, typeByte) + require.NoError(t, reader.UnreadByte()) + + var out msg.NatHoleSid + require.NoError(t, msg.ReadMsgInto(reader, &out)) + require.Equal(t, "sid-legacy", out.Sid) + require.NoError(t, <-errCh) + }) + } +} + +func setPipeDeadline(t *testing.T, conns ...net.Conn) { + t.Helper() + + deadline := time.Now().Add(time.Second) + for _, conn := range conns { + require.NoError(t, conn.SetDeadline(deadline)) + } +} diff --git a/server/registry/registry.go b/server/registry/registry.go new file mode 100644 index 00000000000..a3632f5d7a9 --- /dev/null +++ b/server/registry/registry.go @@ -0,0 +1,216 @@ +// Copyright 2025 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 registry + +import ( + "fmt" + "sync" + "time" + + "k8s.io/utils/clock" +) + +// ClientInfo captures metadata about a connected frpc instance. +type ClientInfo struct { + Key string + User string + RawClientID string + RunID string + ControlID uint64 + Hostname string + IP string + Version string + WireProtocol string + FirstConnectedAt time.Time + LastConnectedAt time.Time + DisconnectedAt time.Time + Online bool +} + +// ClientRegistry keeps track of active clients keyed by "{user}.{clientID}" (runID fallback when raw clientID is empty). +// Entries without an explicit raw clientID are removed on disconnect to avoid stale offline records. +type ClientRegistry struct { + mu sync.RWMutex + clients map[string]*ClientInfo + runIndex map[string]string + clock clock.PassiveClock +} + +func NewClientRegistry() *ClientRegistry { + return newClientRegistryWithClock(clock.RealClock{}) +} + +func newClientRegistryWithClock(clk clock.PassiveClock) *ClientRegistry { + if clk == nil { + clk = clock.RealClock{} + } + return &ClientRegistry{ + clients: make(map[string]*ClientInfo), + runIndex: make(map[string]string), + clock: clk, + } +} + +// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client. +func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version, remoteAddr, wireProtocol string) (key string, conflict bool) { + return cr.RegisterWithControlID(user, rawClientID, runID, hostname, version, remoteAddr, wireProtocol, 0) +} + +// RegisterWithControlID is the generation-aware form used by ControlManager. +// A control ID is process-local and prevents an older control generation from +// changing the registry entry now owned by a newer generation with the same run ID. +func (cr *ClientRegistry) RegisterWithControlID( + user, rawClientID, runID, hostname, version, remoteAddr, wireProtocol string, + controlID uint64, +) (key string, conflict bool) { + if runID == "" { + return "", false + } + + effectiveID := rawClientID + if effectiveID == "" { + effectiveID = runID + } + key = cr.composeClientKey(user, effectiveID) + enforceUnique := rawClientID != "" + + now := cr.clock.Now() + cr.mu.Lock() + defer cr.mu.Unlock() + + info, exists := cr.clients[key] + if enforceUnique && exists && info.Online && info.RunID != "" && info.RunID != runID { + return key, true + } + if previousKey, ok := cr.runIndex[runID]; ok && previousKey != key { + if previous, ok := cr.clients[previousKey]; ok && previous.RunID == runID { + if previous.RawClientID == "" { + delete(cr.clients, previousKey) + } else { + setClientOffline(previous, now) + } + } + delete(cr.runIndex, runID) + } + + if !exists { + info = &ClientInfo{ + Key: key, + User: user, + FirstConnectedAt: now, + } + cr.clients[key] = info + } else if info.RunID != "" { + delete(cr.runIndex, info.RunID) + } + + info.RawClientID = rawClientID + info.RunID = runID + info.ControlID = controlID + info.Hostname = hostname + info.IP = remoteAddr + info.Version = version + info.WireProtocol = wireProtocol + if info.FirstConnectedAt.IsZero() { + info.FirstConnectedAt = now + } + info.LastConnectedAt = now + info.DisconnectedAt = time.Time{} + info.Online = true + + cr.runIndex[runID] = key + return key, false +} + +// MarkOfflineByRunID marks the client as offline when the corresponding control disconnects. +func (cr *ClientRegistry) MarkOfflineByRunID(runID string) { + cr.markOfflineByRunID(runID, 0, false) +} + +// MarkOfflineByRunIDAndControlID marks a client offline only when the registry +// entry still belongs to the supplied control generation. +func (cr *ClientRegistry) MarkOfflineByRunIDAndControlID(runID string, controlID uint64) { + cr.markOfflineByRunID(runID, controlID, true) +} + +func (cr *ClientRegistry) markOfflineByRunID(runID string, controlID uint64, matchControlID bool) { + cr.mu.Lock() + defer cr.mu.Unlock() + + key, ok := cr.runIndex[runID] + if !ok { + return + } + if info, ok := cr.clients[key]; ok && info.RunID == runID && (!matchControlID || info.ControlID == controlID) { + if info.RawClientID == "" { + delete(cr.clients, key) + } else { + setClientOffline(info, cr.clock.Now()) + } + } + if info, ok := cr.clients[key]; !ok || info.RunID != runID { + delete(cr.runIndex, runID) + } +} + +func setClientOffline(info *ClientInfo, now time.Time) { + info.RunID = "" + info.ControlID = 0 + info.Online = false + info.DisconnectedAt = now +} + +// List returns a snapshot of all known clients. +func (cr *ClientRegistry) List() []ClientInfo { + cr.mu.RLock() + defer cr.mu.RUnlock() + + result := make([]ClientInfo, 0, len(cr.clients)) + for _, info := range cr.clients { + result = append(result, *info) + } + return result +} + +// GetByKey retrieves a client by its composite key ({user}.{clientID} with runID fallback). +func (cr *ClientRegistry) GetByKey(key string) (ClientInfo, bool) { + cr.mu.RLock() + defer cr.mu.RUnlock() + + info, ok := cr.clients[key] + if !ok { + return ClientInfo{}, false + } + return *info, true +} + +// ClientID returns the resolved client identifier for external use. +func (info ClientInfo) ClientID() string { + if info.RawClientID != "" { + return info.RawClientID + } + return info.RunID +} + +func (cr *ClientRegistry) composeClientKey(user, id string) string { + switch { + case user == "": + return id + case id == "": + return user + default: + return fmt.Sprintf("%s.%s", user, id) + } +} diff --git a/server/registry/registry_test.go b/server/registry/registry_test.go new file mode 100644 index 00000000000..bacac949c30 --- /dev/null +++ b/server/registry/registry_test.go @@ -0,0 +1,160 @@ +// 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 registry + +import ( + "testing" + "time" + + clocktesting "k8s.io/utils/clock/testing" + + "github.com/fatedier/frp/pkg/proto/wire" +) + +func TestClientRegistryRegisterStoresWireProtocol(t *testing.T) { + registry := NewClientRegistry() + key, conflict := registry.Register("user", "client-id", "run-id", "host", "1.0.0", "127.0.0.1", wire.ProtocolV2) + if conflict { + t.Fatal("unexpected client conflict") + } + + info, ok := registry.GetByKey(key) + if !ok { + t.Fatalf("client %q not found", key) + } + if info.WireProtocol != wire.ProtocolV2 { + t.Fatalf("wire protocol mismatch, want %q got %q", wire.ProtocolV2, info.WireProtocol) + } +} + +func TestClientRegistryUsesClockForTimestamps(t *testing.T) { + start := time.Date(2026, time.May, 8, 12, 30, 0, 0, time.UTC) + clk := clocktesting.NewFakeClock(start) + registry := newClientRegistryWithClock(clk) + + key, conflict := registry.Register("user", "client-id", "run-id", "host", "1.0.0", "127.0.0.1", wire.ProtocolV2) + if conflict { + t.Fatal("unexpected client conflict") + } + + info, ok := registry.GetByKey(key) + if !ok { + t.Fatalf("client %q not found", key) + } + if !info.FirstConnectedAt.Equal(start) { + t.Fatalf("first connected time mismatch, want %s got %s", start, info.FirstConnectedAt) + } + if !info.LastConnectedAt.Equal(start) { + t.Fatalf("last connected time mismatch, want %s got %s", start, info.LastConnectedAt) + } + + disconnectedAt := start.Add(time.Minute) + clk.SetTime(disconnectedAt) + registry.MarkOfflineByRunID("run-id") + + info, ok = registry.GetByKey(key) + if !ok { + t.Fatalf("client %q not found after disconnect", key) + } + if !info.DisconnectedAt.Equal(disconnectedAt) { + t.Fatalf("disconnected time mismatch, want %s got %s", disconnectedAt, info.DisconnectedAt) + } +} + +func TestClientRegistryControlIDPreventsStaleOffline(t *testing.T) { + registry := NewClientRegistry() + key, conflict := registry.RegisterWithControlID( + "user", "client-id", "run-id", "old-host", "1.0.0", "127.0.0.1", wire.ProtocolV1, 1, + ) + if conflict { + t.Fatal("unexpected client conflict") + } + _, conflict = registry.RegisterWithControlID( + "user", "client-id", "run-id", "new-host", "1.0.1", "127.0.0.2", wire.ProtocolV2, 2, + ) + if conflict { + t.Fatal("same run ID replacement should not conflict") + } + + registry.MarkOfflineByRunIDAndControlID("run-id", 1) + info, ok := registry.GetByKey(key) + if !ok { + t.Fatalf("client %q not found", key) + } + if !info.Online || info.ControlID != 2 || info.Hostname != "new-host" { + t.Fatalf("stale offline changed current generation: %+v", info) + } + + registry.MarkOfflineByRunIDAndControlID("run-id", 2) + info, ok = registry.GetByKey(key) + if !ok { + t.Fatalf("client %q not found after disconnect", key) + } + if info.Online || info.ControlID != 0 || info.RunID != "" { + t.Fatalf("current generation was not marked offline: %+v", info) + } +} + +func TestClientRegistryClientIDConflictSemantics(t *testing.T) { + registry := NewClientRegistry() + _, conflict := registry.RegisterWithControlID( + "user", "client-id", "run-one", "host", "1.0.0", "127.0.0.1", wire.ProtocolV1, 1, + ) + if conflict { + t.Fatal("unexpected initial client conflict") + } + _, conflict = registry.RegisterWithControlID( + "user", "client-id", "run-two", "host", "1.0.0", "127.0.0.2", wire.ProtocolV1, 2, + ) + if !conflict { + t.Fatal("different online run IDs with the same explicit client ID must conflict") + } + + registry.MarkOfflineByRunIDAndControlID("run-one", 1) + _, conflict = registry.RegisterWithControlID( + "user", "client-id", "run-two", "host", "1.0.0", "127.0.0.2", wire.ProtocolV1, 2, + ) + if conflict { + t.Fatal("offline explicit client ID should be reusable") + } +} + +func TestClientRegistrySameRunIDMovesBetweenClientKeys(t *testing.T) { + registry := NewClientRegistry() + oldKey, conflict := registry.RegisterWithControlID( + "user", "old-client", "run-id", "old-host", "1.0.0", "127.0.0.1", wire.ProtocolV1, 1, + ) + if conflict { + t.Fatal("unexpected initial client conflict") + } + newKey, conflict := registry.RegisterWithControlID( + "user", "new-client", "run-id", "new-host", "1.0.1", "127.0.0.2", wire.ProtocolV2, 2, + ) + if conflict { + t.Fatal("same run ID moving to a new client key should not conflict") + } + + oldInfo, ok := registry.GetByKey(oldKey) + if !ok { + t.Fatalf("old explicit client %q should remain as offline history", oldKey) + } + if oldInfo.Online || oldInfo.RunID != "" || oldInfo.ControlID != 0 { + t.Fatalf("old client key remained online: %+v", oldInfo) + } + newInfo, ok := registry.GetByKey(newKey) + if !ok || !newInfo.Online || newInfo.RunID != "run-id" || newInfo.ControlID != 2 { + t.Fatalf("new client key was not registered: %+v", newInfo) + } +} diff --git a/server/service.go b/server/service.go index c3d398b2749..82883bb274c 100644 --- a/server/service.go +++ b/server/service.go @@ -18,24 +18,34 @@ import ( "bytes" "context" "crypto/tls" + "errors" "fmt" "io" "net" "net/http" - "sort" + "os" "strconv" "time" - "github.com/fatedier/frp/assets" + "github.com/fatedier/golib/crypto" + "github.com/fatedier/golib/net/mux" + fmux "github.com/hashicorp/yamux" + quic "github.com/quic-go/quic-go" + "github.com/samber/lo" + "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/config" + 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" plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/pkg/proto/wire" + "github.com/fatedier/frp/pkg/ssh" "github.com/fatedier/frp/pkg/transport" + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/pkg/util/log" - frpNet "github.com/fatedier/frp/pkg/util/net" + netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/tcpmux" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/version" @@ -43,20 +53,30 @@ import ( "github.com/fatedier/frp/pkg/util/xlog" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/group" - "github.com/fatedier/frp/server/metrics" "github.com/fatedier/frp/server/ports" "github.com/fatedier/frp/server/proxy" + "github.com/fatedier/frp/server/registry" "github.com/fatedier/frp/server/visitor" - - "github.com/fatedier/golib/net/mux" - fmux "github.com/hashicorp/yamux" ) const ( connReadTimeout time.Duration = 10 * time.Second + connWriteTimeout time.Duration = 5 * time.Second vhostReadWriteTimeout time.Duration = 30 * time.Second ) +var errControlReplaced = errors.New("control was replaced during login") + +func init() { + crypto.DefaultSalt = "frp" + // Disable quic-go's receive buffer warning. + os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true") + // Disable quic-go's ECN support by default. It may cause issues on certain operating systems. + if os.Getenv("QUIC_GO_DISABLE_ECN") == "" { + os.Setenv("QUIC_GO_DISABLE_ECN", "true") + } +} + // Server service type Service struct { // Dispatch connections to different handlers listen on same port @@ -68,15 +88,24 @@ type Service struct { // Accept connections using kcp kcpListener net.Listener + // Accept connections using quic + quicListener *quic.Listener + // Accept connections using websocket websocketListener net.Listener // Accept frp tls connections tlsListener net.Listener + // Accept pipe connections from ssh tunnel gateway + sshTunnelListener *netpkg.InternalListener + // Manage all controllers ctlManager *ControlManager + // Track logical clients keyed by user.clientID (runID fallback when raw clientID is empty). + clientRegistry *registry.ClientRegistry + // Manage all proxies pxyManager *proxy.Manager @@ -89,36 +118,73 @@ type Service struct { // All resource managers and controllers rc *controller.ResourceController - // Verifies authentication based on selected method - authVerifier auth.Verifier + // web server for dashboard UI and apis + webServer *httppkg.Server + + sshTunnelGateway *ssh.Gateway + + // Auth runtime and encryption materials + auth *auth.ServerAuth tlsConfig *tls.Config - cfg config.ServerCommonConf + cfg *v1.ServerConfig + + // service context + ctx context.Context + // call cancel to stop service + cancel context.CancelFunc } -func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { +func NewService(cfg *v1.ServerConfig) (*Service, error) { tlsConfig, err := transport.NewServerTLSConfig( - cfg.TLSCertFile, - cfg.TLSKeyFile, - cfg.TLSTrustedCaFile) + cfg.Transport.TLS.CertFile, + cfg.Transport.TLS.KeyFile, + cfg.Transport.TLS.TrustedCaFile) if err != nil { - return + return nil, err + } + + var webServer *httppkg.Server + if cfg.WebServer.Port > 0 { + ws, err := httppkg.NewServer(cfg.WebServer) + if err != nil { + return nil, err + } + webServer = ws + + modelmetrics.EnableMem() + if cfg.EnablePrometheus { + modelmetrics.EnablePrometheus() + } + } + + authRuntime, err := auth.BuildServerAuth(&cfg.Auth) + if err != nil { + return nil, err } - svr = &Service{ - ctlManager: NewControlManager(), - pxyManager: proxy.NewManager(), - pluginManager: plugin.NewManager(), + clientRegistry := registry.NewClientRegistry() + svr := &Service{ + ctlManager: NewControlManager(clientRegistry), + clientRegistry: clientRegistry, + pxyManager: proxy.NewManager(), + pluginManager: plugin.NewManager(), rc: &controller.ResourceController{ VisitorManager: visitor.NewManager(), TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts), UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts), }, - httpVhostRouter: vhost.NewRouters(), - authVerifier: auth.NewAuthVerifier(cfg.ServerConfig), - tlsConfig: tlsConfig, - cfg: cfg, + sshTunnelListener: netpkg.NewInternalListener(), + httpVhostRouter: vhost.NewRouters(), + auth: authRuntime, + webServer: webServer, + tlsConfig: tlsConfig, + cfg: cfg, + ctx: context.Background(), + } + if webServer != nil { + webServer.RouteRegister(svr.registerRouteHandlers) } // Create tcpmux httpconnect multiplexer. @@ -127,28 +193,20 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.TCPMuxHTTPConnectPort)) l, err = net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("Create server listener error, %v", err) - return + return nil, fmt.Errorf("create server listener error, %v", err) } svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout) if err != nil { - err = fmt.Errorf("Create vhost tcpMuxer error, %v", err) - return + return nil, fmt.Errorf("create vhost tcpMuxer error, %v", err) } - log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough) + log.Infof("tcpmux httpconnect multiplexer listen on %s, passthrough: %v", address, cfg.TCPMuxPassthrough) } // Init all plugins - plugin_names := make([]string, 0, len(cfg.HTTPPlugins)) - for n := range cfg.HTTPPlugins { - plugin_names = append(plugin_names, n) - } - sort.Strings(plugin_names) - - for _, name := range plugin_names { - svr.pluginManager.Register(plugin.NewHTTPPluginOptions(cfg.HTTPPlugins[name])) - log.Info("plugin [%s] has been registered", name) + for _, p := range cfg.HTTPPlugins { + svr.pluginManager.Register(plugin.NewHTTPPluginOptions(p)) + log.Infof("plugin [%s] has been registered", p.Name) } svr.rc.PluginManager = svr.pluginManager @@ -181,35 +239,59 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort)) ln, err := net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("Create server listener error, %v", err) - return + return nil, fmt.Errorf("create server listener error, %v", err) } svr.muxer = mux.NewMux(ln) - svr.muxer.SetKeepAlive(time.Duration(cfg.TCPKeepAlive) * time.Second) - go svr.muxer.Serve() + svr.muxer.SetKeepAlive(time.Duration(cfg.Transport.TCPKeepAlive) * time.Second) + go func() { + _ = svr.muxer.Serve() + }() ln = svr.muxer.DefaultListener() svr.listener = ln - log.Info("frps tcp listen on %s", address) + log.Infof("frps tcp listen on %s", address) // Listen for accepting connections from client using kcp protocol. if cfg.KCPBindPort > 0 { address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort)) - svr.kcpListener, err = frpNet.ListenKcp(address) + svr.kcpListener, err = netpkg.ListenKcp(address) if err != nil { - err = fmt.Errorf("Listen on kcp address udp %s error: %v", address, err) - return + return nil, fmt.Errorf("listen on kcp udp address %s error: %v", address, err) + } + log.Infof("frps kcp listen on udp %s", address) + } + + if cfg.QUICBindPort > 0 { + address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.QUICBindPort)) + quicTLSCfg := tlsConfig.Clone() + quicTLSCfg.NextProtos = []string{"frp"} + svr.quicListener, err = quic.ListenAddr(address, quicTLSCfg, &quic.Config{ + MaxIdleTimeout: time.Duration(cfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(cfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second, + }) + if err != nil { + return nil, fmt.Errorf("listen on quic udp address %s error: %v", address, err) + } + log.Infof("frps quic listen on %s", address) + } + + if cfg.SSHTunnelGateway.BindPort > 0 { + sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway, cfg.BindAddr, svr.sshTunnelListener) + if err != nil { + return nil, fmt.Errorf("create ssh gateway error: %v", err) } - log.Info("frps kcp listen on udp %s", address) + svr.sshTunnelGateway = sshGateway + log.Infof("frps sshTunnelGateway listen on port %d", cfg.SSHTunnelGateway.BindPort) } // Listen for accepting connections from client using websocket protocol. - websocketPrefix := []byte("GET " + frpNet.FrpWebsocketPath) + websocketPrefix := []byte("GET " + netpkg.FrpWebsocketPath) websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool { return bytes.Equal(data, websocketPrefix) }) - svr.websocketListener = frpNet.NewWebsocketListener(websocketLn) + svr.websocketListener = netpkg.NewWebsocketListener(websocketLn) // Create http vhost muxer. if cfg.VhostHTTPPort > 0 { @@ -219,120 +301,157 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { svr.rc.HTTPReverseProxy = rp address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort)) + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetUnencryptedHTTP2(true) server := &http.Server{ - Addr: address, - Handler: rp, + Addr: address, + Handler: rp, + ReadHeaderTimeout: 60 * time.Second, + Protocols: protocols, } var l net.Listener if httpMuxOn { - l = svr.muxer.ListenHttp(1) + l = svr.muxer.ListenHTTP(1) } else { l, err = net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("Create vhost http listener error, %v", err) - return + return nil, fmt.Errorf("create vhost http listener error, %v", err) } } - go server.Serve(l) - log.Info("http service listen on %s", address) + go func() { + _ = server.Serve(l) + }() + log.Infof("http service listen on %s", address) } // Create https vhost muxer. if cfg.VhostHTTPSPort > 0 { var l net.Listener if httpsMuxOn { - l = svr.muxer.ListenHttps(1) + l = svr.muxer.ListenHTTPS(1) } else { address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort)) l, err = net.Listen("tcp", address) if err != nil { - err = fmt.Errorf("Create server listener error, %v", err) - return + return nil, fmt.Errorf("create server listener error, %v", err) } - log.Info("https service listen on %s", address) + log.Infof("https service listen on %s", address) } svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout) if err != nil { - err = fmt.Errorf("Create vhost httpsMuxer error, %v", err) - return + return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err) } + + // Init HTTPS group controller after HTTPSMuxer is created + svr.rc.HTTPSGroupCtl = group.NewHTTPSGroupController(svr.rc.VhostHTTPSMuxer) } // frp tls listener svr.tlsListener = svr.muxer.Listen(2, 1, func(data []byte) bool { // tls first byte can be 0x16 only when vhost https port is not same with bind port - return int(data[0]) == frpNet.FRPTLSHeadByte || int(data[0]) == 0x16 + return int(data[0]) == netpkg.FRPTLSHeadByte || int(data[0]) == 0x16 }) // Create nat hole controller. - if cfg.BindUDPPort > 0 { - var nc *nathole.Controller - address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindUDPPort)) - nc, err = nathole.NewController(address) - if err != nil { - err = fmt.Errorf("Create nat hole controller error, %v", err) - return - } - svr.rc.NatHoleController = nc - log.Info("nat hole udp service listen on %s", address) + nc, err := nathole.NewController(time.Duration(cfg.NatHoleAnalysisDataReserveHours) * time.Hour) + if err != nil { + return nil, fmt.Errorf("create nat hole controller error, %v", err) + } + svr.rc.NatHoleController = nc + return svr, nil +} + +func (svr *Service) Run(ctx context.Context) { + ctx, cancel := context.WithCancel(ctx) + svr.ctx = ctx + svr.cancel = cancel + + // run dashboard web server. + if svr.webServer != nil { + go func() { + log.Infof("dashboard listen on %s", svr.webServer.Address()) + if err := svr.webServer.Run(); err != nil { + log.Warnf("dashboard server exit with error: %v", err) + } + }() } - var statsEnable bool - // Create dashboard web server. - if cfg.DashboardPort > 0 { - // Init dashboard assets - assets.Load(cfg.AssetsDir) + go svr.HandleListener(svr.sshTunnelListener, true) - address := net.JoinHostPort(cfg.DashboardAddr, strconv.Itoa(cfg.DashboardPort)) - err = svr.RunDashboardServer(address) - if err != nil { - err = fmt.Errorf("Create dashboard web server error, %v", err) - return - } - log.Info("Dashboard listen on %s", address) - statsEnable = true + if svr.kcpListener != nil { + go svr.HandleListener(svr.kcpListener, false) } - if statsEnable { - modelmetrics.EnableMem() - if cfg.EnablePrometheus { - modelmetrics.EnablePrometheus() - } + if svr.quicListener != nil { + go svr.HandleQUICListener(svr.quicListener) } - return -} + go svr.HandleListener(svr.websocketListener, false) + go svr.HandleListener(svr.tlsListener, false) -func (svr *Service) Run() { if svr.rc.NatHoleController != nil { - go svr.rc.NatHoleController.Run() + go svr.rc.NatHoleController.CleanWorker(svr.ctx) } - if svr.cfg.KCPBindPort > 0 { - go svr.HandleListener(svr.kcpListener) + + if svr.sshTunnelGateway != nil { + go svr.sshTunnelGateway.Run() } - go svr.HandleListener(svr.websocketListener) - go svr.HandleListener(svr.tlsListener) + svr.HandleListener(svr.listener, false) - svr.HandleListener(svr.listener) + <-svr.ctx.Done() + // service context may not be canceled by svr.Close(), we should call it here to release resources + if svr.listener != nil { + svr.Close() + } } -func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { - xl := xlog.FromContextSafe(ctx) +func (svr *Service) Close() error { + if svr.kcpListener != nil { + svr.kcpListener.Close() + } + if svr.quicListener != nil { + svr.quicListener.Close() + } + if svr.websocketListener != nil { + svr.websocketListener.Close() + } + if svr.tlsListener != nil { + svr.tlsListener.Close() + } + if svr.sshTunnelListener != nil { + svr.sshTunnelListener.Close() + } + if svr.listener != nil { + svr.listener.Close() + } + if svr.webServer != nil { + svr.webServer.Close() + } + if svr.sshTunnelGateway != nil { + svr.sshTunnelGateway.Close() + } + svr.rc.Close() + svr.muxer.Close() + svr.ctlManager.Close() + if svr.cancel != nil { + svr.cancel() + } + return nil +} - var ( - rawMsg msg.Message - err error - ) +func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) { + xl := xlog.FromContextSafe(ctx) - conn.SetReadDeadline(time.Now().Add(connReadTimeout)) - if rawMsg, err = msg.ReadMsg(conn); err != nil { - log.Trace("Failed to read message: %v", err) + acceptedConn, err := svr.acceptConnection(ctx, conn) + if err != nil { + log.Tracef("failed to accept frp connection: %v", err) conn.Close() return } - conn.SetReadDeadline(time.Time{}) + conn = acceptedConn.conn - switch m := rawMsg.(type) { + switch m := acceptedConn.firstMsg.(type) { case *msg.Login: // server plugin hook content := &plugin.LoginContent{ @@ -340,79 +459,268 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { ClientAddress: conn.RemoteAddr().String(), } retContent, err := svr.pluginManager.Login(content) + var ctl *Control if err == nil { m = &retContent.Login - err = svr.RegisterControl(conn, m) + controlConn := acceptedConn.conn + if !internal { + var controlRW io.ReadWriter + controlRW, err = acceptedConn.newControlReadWriter(conn, svr.auth.EncryptionKey()) + if err == nil { + controlConn = acceptedConn.messageConnFor(controlRW) + } + } + if err == nil { + ctl, err = svr.RegisterControl(controlConn, m, internal, acceptedConn.wireProtocol, acceptedConn.udpPacketCodec) + } } - // If login failed, send error message there. - // Otherwise send success message in control's work goroutine. if err != nil { - xl.Warn("register control error: %v", err) - msg.WriteMsg(conn, &msg.LoginResp{ - Version: version.Full(), - Error: util.GenerateResponseErrorString("register control error", err, svr.cfg.DetailedErrorsToClient), + xl.Warnf("register control error: %v", err) + if ctl != nil { + svr.ctlManager.Remove(ctl) + } + if writeErr := writeWithDeadline(conn, connWriteTimeout, func() error { + return acceptedConn.conn.WriteMsg(&msg.LoginResp{ + Version: version.Full(), + Error: util.GenerateResponseErrorString("register control error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)), + }) + }); writeErr != nil { + xl.Warnf("write login error response error: %v", writeErr) + } + if ctl != nil { + _ = ctl.Close() + } else { + conn.Close() + } + return + } + if err = svr.completeControlLogin(ctl, func() error { + return writeWithDeadline(conn, connWriteTimeout, func() error { + return acceptedConn.conn.WriteMsg(&msg.LoginResp{ + Version: version.Full(), + RunID: ctl.runID, + Error: "", + }) }) - conn.Close() + }); err != nil { + xl.Warnf("complete control login error: %v", err) + svr.ctlManager.Remove(ctl) + _ = ctl.Close() + return } case *msg.NewWorkConn: - if err := svr.RegisterWorkConn(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)), + }) conn.Close() } case *msg.NewVisitorConn: - if err = svr.RegisterVisitorConn(conn, m); err != nil { - xl.Warn("register visitor conn error: %v", err) - msg.WriteMsg(conn, &msg.NewVisitorConnResp{ + if err = svr.RegisterVisitorConn(conn, m, acceptedConn.wireProtocol); err != nil { + xl.Warnf("register visitor conn error: %v", err) + _ = acceptedConn.conn.WriteMsg(&msg.NewVisitorConnResp{ ProxyName: m.ProxyName, - Error: util.GenerateResponseErrorString("register visitor conn error", err, svr.cfg.DetailedErrorsToClient), + Error: util.GenerateResponseErrorString("register visitor conn error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)), }) conn.Close() } else { - msg.WriteMsg(conn, &msg.NewVisitorConnResp{ + _ = acceptedConn.conn.WriteMsg(&msg.NewVisitorConnResp{ ProxyName: m.ProxyName, Error: "", }) } default: - log.Warn("Error message type for the new connection [%s]", conn.RemoteAddr().String()) + log.Warnf("error message type for the new connection [%s]", conn.RemoteAddr().String()) conn.Close() } } -func (svr *Service) HandleListener(l net.Listener) { +func (svr *Service) completeControlLogin(ctl *Control, writeSuccess func() error) error { + committed, err := svr.ctlManager.completeLogin(ctl, writeSuccess) + if err != nil { + return err + } + if !committed { + return errControlReplaced + } + return nil +} + +type acceptedConnection struct { + 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) { + _ = conn.SetReadDeadline(time.Now().Add(connReadTimeout)) + checkedConn, isV2, err := wire.CheckMagic(conn) + if err != nil { + return nil, fmt.Errorf("read wire protocol magic: %w", err) + } + + wireProtocol := wire.ProtocolV1 + if isV2 { + wireProtocol = wire.ProtocolV2 + } + + conn = netpkg.NewContextConn(ctx, checkedConn) + acceptedConn := &acceptedConnection{wireProtocol: wireProtocol} + if isV2 { + wireConn := wire.NewConn(conn) + rw := msg.NewV2ReadWriterWithConn(wireConn) + acceptedConn.conn = msg.NewConn(conn, rw) + acceptedConn.firstMsg, err = acceptedConn.readFirstV2Msg(conn, wireConn) + } else { + rw := msg.NewV1ReadWriter(conn) + acceptedConn.conn = msg.NewConn(conn, rw) + acceptedConn.firstMsg, err = acceptedConn.conn.ReadMsg() + } + if err != nil { + return nil, err + } + _ = conn.SetReadDeadline(time.Time{}) + return acceptedConn, nil +} + +func writeWithDeadline(conn net.Conn, timeout time.Duration, writeFn func() error) error { + _ = conn.SetWriteDeadline(time.Now().Add(timeout)) + defer func() { + _ = conn.SetWriteDeadline(time.Time{}) + }() + return writeFn() +} + +func (ac *acceptedConnection) messageConnFor(rw io.ReadWriter) *msg.Conn { + return msg.NewConn(ac.conn, msg.NewReadWriter(rw, ac.wireProtocol)) +} + +func (ac *acceptedConnection) newControlReadWriter(rw io.ReadWriter, key []byte) (io.ReadWriter, error) { + if ac.wireProtocol == wire.ProtocolV2 { + if ac.cryptoContext == nil { + return nil, fmt.Errorf("missing v2 crypto negotiation") + } + return netpkg.NewAEADCryptoReadWriter( + rw, + key, + netpkg.AEADCryptoRoleServer, + ac.cryptoContext.Algorithm, + ac.cryptoContext.TranscriptHash, + ) + } + return netpkg.NewCryptoReadWriter(rw, key) +} + +func (ac *acceptedConnection) readFirstV2Msg(conn net.Conn, wireConn *wire.Conn) (msg.Message, error) { + frame, err := wireConn.ReadFrame() + if err != nil { + 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 + } + frame, err = wireConn.ReadFrame() + if err != nil { + return nil, fmt.Errorf("read first v2 message frame: %w", err) + } + } + + m, err := msg.DecodeV2MessageFrame(frame) + if err != nil { + return nil, fmt.Errorf("decode v2 message: %w", err) + } + return m, nil +} + +func (ac *acceptedConnection) handleClientHello(conn net.Conn, wireConn *wire.Conn, frame *wire.Frame) error { + var hello wire.ClientHello + if err := wireConn.UnmarshalFrame(frame, &hello); err != nil { + return fmt.Errorf("decode ClientHello: %w", err) + } + + serverHello, err := wire.NewServerHello(hello) + if err != nil { + serverHello = wire.DefaultServerHello() + serverHello.Error = err.Error() + if writeErr := writeWithDeadline(conn, connWriteTimeout, func() error { + return wireConn.WriteJSONFrame(wire.FrameTypeServerHello, serverHello) + }); writeErr != nil { + return fmt.Errorf("%w; write ServerHello error: %v", err, writeErr) + } + return err + } + serverHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeServerHello, serverHello) + if err != nil { + return fmt.Errorf("encode ServerHello: %w", err) + } + cryptoContext := wire.NewCryptoContext( + serverHello.Selected.Crypto.Algorithm, + frame.Payload, + serverHelloFrame.Payload, + ) + if err := writeWithDeadline(conn, connWriteTimeout, func() error { + return wireConn.WriteFrame(serverHelloFrame) + }); err != nil { + return fmt.Errorf("write ServerHello: %w", err) + } + ac.cryptoContext = cryptoContext + ac.udpPacketCodec = serverHello.Selected.Message.UDPPacketCodec + return nil +} + +// HandleListener accepts connections from client and call handleConnection to handle them. +// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway. +// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters. +func (svr *Service) HandleListener(l net.Listener, internal bool) { // Listen for incoming connections from client. for { c, err := l.Accept() if err != nil { - log.Warn("Listener for incoming connections from client closed") + log.Warnf("listener for incoming connections from client closed") return } // inject xlog object into net.Conn context xl := xlog.New() ctx := context.Background() - c = frpNet.NewContextConn(xlog.NewContext(ctx, xl), c) + c = netpkg.NewContextConn(xlog.NewContext(ctx, xl), c) - log.Trace("start check TLS connection...") - originConn := c - var isTLS, custom bool - c, isTLS, custom, err = frpNet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLSOnly, connReadTimeout) - if err != nil { - log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err) - originConn.Close() - continue + if !internal { + log.Tracef("start check TLS connection...") + originConn := c + forceTLS := svr.cfg.Transport.TLS.Force + var isTLS, custom bool + c, isTLS, custom, err = netpkg.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, forceTLS, connReadTimeout) + if err != nil { + log.Warnf("checkAndEnableTLSServerConnWithTimeout error: %v", err) + originConn.Close() + continue + } + log.Tracef("check TLS connection success, isTLS: %v custom: %v internal: %v", isTLS, custom, internal) } - log.Trace("check TLS connection success, isTLS: %v custom: %v", isTLS, custom) // Start a new goroutine to handle connection. go func(ctx context.Context, frpConn net.Conn) { - if svr.cfg.TCPMux { + if lo.FromPtr(svr.cfg.Transport.TCPMux) && !internal { fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second - fmuxCfg.LogOutput = io.Discard + fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second + // Use trace level for yamux logs + fmuxCfg.LogOutput = xlog.NewTraceWriter(xlog.FromContextSafe(ctx)) + fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Server(frpConn, fmuxCfg) if err != nil { - log.Warn("Failed to create mux connection: %v", err) + log.Warnf("failed to create mux connection: %v", err) frpConn.Close() return } @@ -420,79 +728,150 @@ func (svr *Service) HandleListener(l net.Listener) { for { stream, err := session.AcceptStream() if err != nil { - log.Debug("Accept new mux stream error: %v", err) + log.Debugf("accept new mux stream error: %v", err) session.Close() return } - go svr.handleConnection(ctx, stream) + go svr.handleConnection(ctx, stream, internal) } } else { - svr.handleConnection(ctx, frpConn) + svr.handleConnection(ctx, frpConn, internal) } }(ctx, c) } } -func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err error) { +func (svr *Service) HandleQUICListener(l *quic.Listener) { + // Listen for incoming connections from client. + for { + c, err := l.Accept(context.Background()) + if err != nil { + log.Warnf("quic listener for incoming connections from client closed") + return + } + // Start a new goroutine to handle connection. + go func(ctx context.Context, frpConn *quic.Conn) { + for { + stream, err := frpConn.AcceptStream(context.Background()) + if err != nil { + log.Debugf("accept new quic mux stream error: %v", err) + _ = frpConn.CloseWithError(0, "") + return + } + go svr.handleConnection(ctx, netpkg.QuicStreamToNetConn(stream, frpConn), false) + } + }(context.Background(), c) + } +} + +func (svr *Service) RegisterControl( + ctlConn *msg.Conn, + 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 if loginMsg.RunID == "" { loginMsg.RunID, err = util.RandID() if err != nil { - return + return nil, err } } + if err := validation.ValidateRunID(loginMsg.RunID); err != nil { + return nil, fmt.Errorf("invalid run id: %w", err) + } - ctx := frpNet.NewContextFromConn(ctlConn) + ctx := netpkg.NewContextFromConn(ctlConn) xl := xlog.FromContextSafe(ctx) xl.AppendPrefix(loginMsg.RunID) ctx = xlog.NewContext(ctx, xl) - xl.Info("client login info: ip [%s] version [%s] hostname [%s] os [%s] arch [%s]", + xl.Infof("client login info: ip [%s] version [%s] hostname [%s] os [%s] arch [%s]", ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch) - // Check client version. - if ok, msg := version.Compat(loginMsg.Version); !ok { - err = fmt.Errorf("%s", msg) - return - } - // Check auth. - if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil { - return + authVerifier := svr.auth.Verifier + if internal && loginMsg.ClientSpec.AlwaysAuthPass { + authVerifier = auth.AlwaysPassVerifier + } + if err := authVerifier.VerifyLogin(loginMsg); err != nil { + return nil, err } - ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg) - if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil { - oldCtl.allShutdown.WaitDone() + 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, + UDPPacketCodec: udpPacketCodec, + }) + if err != nil { + xl.Warnf("create new controller error: %v", err) + // don't return detailed errors to client + return nil, fmt.Errorf("unexpected error when creating new controller") } - ctl.Start() + if err := svr.ctlManager.Add(ctl); err != nil { + return ctl, err + } + ctl.WaitForHandoff() - // for statistics - metrics.Server.NewClient() + active, err := svr.ctlManager.Activate(ctl) + if err != nil { + return ctl, err + } + if !active { + return ctl, errControlReplaced + } - go func() { - // block until control closed - ctl.WaitClosed() - svr.ctlManager.Del(loginMsg.RunID, ctl) - }() - return + return ctl, nil } // RegisterWorkConn register a new work connection to control and proxies need it. -func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error { - xl := frpNet.NewLogFromConn(workConn) +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.Warn("No client control found for run id [%s]", newMsg.RunID) + 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{ User: plugin.UserInfo{ - User: ctl.loginMsg.User, - Metas: ctl.loginMsg.Metas, - RunID: ctl.loginMsg.RunID, + User: ctl.sessionCtx.LoginMsg.User, + Metas: ctl.sessionCtx.LoginMsg.Metas, + RunID: ctl.sessionCtx.LoginMsg.RunID, }, NewWorkConn: *newMsg, } @@ -500,19 +879,39 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) if err == nil { newMsg = &retContent.NewWorkConn // Check auth. - err = svr.authVerifier.VerifyNewWorkConn(newMsg) + err = ctl.sessionCtx.AuthVerifier.VerifyNewWorkConn(newMsg) } if err != nil { - xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID) - msg.WriteMsg(workConn, &msg.StartWorkConn{ - Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, ctl.serverCfg.DetailedErrorsToClient), - }) - return fmt.Errorf("invalid NewWorkConn with run id [%s]", newMsg.RunID) + xl.Warnf("invalid NewWorkConn with run id [%s]", newMsg.RunID) + return err } - return ctl.RegisterWorkConn(workConn) + return svr.ctlManager.RegisterWorkConn(ctl, proxy.NewWorkConn(workConn)) } -func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error { - return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey, - newMsg.UseEncryption, newMsg.UseCompression) +func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn, wireProtocol 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, 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, 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 + } + if !admitted { + return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID) + } + return nil + } + return admit("", wireProtocol, "") } diff --git a/server/service_test.go b/server/service_test.go new file mode 100644 index 00000000000..d4fcbe77fe7 --- /dev/null +++ b/server/service_test.go @@ -0,0 +1,976 @@ +// 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 server + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "net/http" + "runtime" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/fatedier/golib/net/mux" + "github.com/stretchr/testify/require" + + "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" + "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/server/controller" + "github.com/fatedier/frp/server/proxy" + "github.com/fatedier/frp/server/registry" + "github.com/fatedier/frp/server/visitor" +) + +func TestWriteWithDeadlineTimesOutAndClearsDeadline(t *testing.T) { + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + err := writeWithDeadline(serverConn, 50*time.Millisecond, func() error { + _, writeErr := serverConn.Write([]byte("x")) + return writeErr + }) + require.Error(t, err) + + var netErr net.Error + require.True(t, errors.As(err, &netErr)) + require.True(t, netErr.Timeout()) + + readCh := make(chan byte, 1) + errCh := make(chan error, 1) + go func() { + buf := make([]byte, 1) + if _, readErr := clientConn.Read(buf); readErr != nil { + errCh <- readErr + return + } + readCh <- buf[0] + }() + + _, err = serverConn.Write([]byte("y")) + require.NoError(t, err) + + select { + case b := <-readCh: + require.Equal(t, byte('y'), b) + case err := <-errCh: + require.NoError(t, err) + case <-time.After(time.Second): + t.Fatal("timed out waiting for write after deadline reset") + } +} + +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) + + sharedMux := mux.NewMux(listener) + httpListener := sharedMux.ListenHTTP(1) + muxServeErr := make(chan error, 1) + go func() { + muxServeErr <- sharedMux.Serve() + }() + + newProtocols := func(http1, unencryptedHTTP2 bool) *http.Protocols { + protocols := new(http.Protocols) + protocols.SetHTTP1(http1) + protocols.SetUnencryptedHTTP2(unencryptedHTTP2) + return protocols + } + + const handlerProtocolHeader = "X-Test-Handler-Protocol" + httpServer := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(handlerProtocolHeader, r.Proto) + w.WriteHeader(http.StatusNoContent) + }), + ReadHeaderTimeout: time.Second, + Protocols: newProtocols(true, true), + } + httpServeErr := make(chan error, 1) + go func() { + httpServeErr <- httpServer.Serve(httpListener) + }() + t.Cleanup(func() { + require.NoError(t, httpServer.Close()) + require.ErrorIs(t, waitForResult(t, httpServeErr, "shared HTTP server to stop"), http.ErrServerClosed) + require.NoError(t, sharedMux.Close()) + require.ErrorIs(t, waitForResult(t, muxServeErr, "shared mux to stop"), net.ErrClosed) + }) + + for _, tc := range []struct { + name string + http1 bool + unencryptedHTTP2 bool + expectedProtocol string + }{ + {name: "HTTP/1.1", http1: true, expectedProtocol: "HTTP/1.1"}, + {name: "HTTP/2 prior knowledge", unencryptedHTTP2: true, expectedProtocol: "HTTP/2.0"}, + } { + t.Run(tc.name, func(t *testing.T) { + transport := &http.Transport{ + Protocols: newProtocols(tc.http1, tc.unencryptedHTTP2), + } + defer transport.CloseIdleConnections() + client := &http.Client{Transport: transport, Timeout: 3 * time.Second} + request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://"+listener.Addr().String()+"/", nil) + require.NoError(t, err) + response, err := client.Do(request) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, response.StatusCode) + require.Equal(t, tc.expectedProtocol, response.Proto) + require.Equal(t, tc.expectedProtocol, response.Header.Get(handlerProtocolHeader)) + require.NoError(t, response.Body.Close()) + }) + } +} + +func TestServiceControlHandoffSkipsStalePendingGeneration(t *testing.T) { + svr := newControlTestService(t) + metrics := newCountingServerMetrics() + metrics.closeEnter = make(chan struct{}) + metrics.closeResume = make(chan struct{}) + + ctlA, connA, err := registerLifecycleTestControl(svr) + require.NoError(t, err) + ctlA.serverMetrics = metrics + require.NoError(t, svr.completeControlLogin(ctlA, func() error { return nil })) + waitForSignal(t, connA.readStarted, "A reader to start") + + require.NoError(t, ctlA.Close()) + waitForSignal(t, metrics.closeEnter, "A finalization barrier") + + type registerResult struct { + ctl *Control + conn *deadlineReadConn + err error + } + resultB := make(chan registerResult, 1) + go func() { + ctl, conn, registerErr := registerLifecycleTestControl(svr) + resultB <- registerResult{ctl: ctl, conn: conn, err: registerErr} + }() + ctlB := waitForDifferentCurrentControl(t, svr.ctlManager, "shared-run", ctlA) + ctlB.serverMetrics = metrics + + resultC := make(chan registerResult, 1) + go func() { + ctl, conn, registerErr := registerLifecycleTestControl(svr) + resultC <- registerResult{ctl: ctl, conn: conn, err: registerErr} + }() + ctlC := waitForDifferentCurrentControl(t, svr.ctlManager, "shared-run", ctlB) + ctlC.serverMetrics = metrics + waitForControlDone(t, ctlB) + + select { + case result := <-resultB: + t.Fatalf("B returned before A finalized: %v", result.err) + default: + } + select { + case result := <-resultC: + t.Fatalf("C returned before A finalized: %v", result.err) + default: + } + + close(metrics.closeResume) + waitForControlDone(t, ctlA) + + b := <-resultB + require.Same(t, ctlB, b.ctl) + require.ErrorIs(t, b.err, errControlReplaced) + require.False(t, svr.ctlManager.Remove(ctlB)) + require.NoError(t, ctlB.Close()) + + c := <-resultC + require.NoError(t, c.err) + require.Same(t, ctlC, c.ctl) + _, ok := svr.ctlManager.GetByID("shared-run") + require.False(t, ok) + require.Same(t, ctlC, currentControlForTest(svr.ctlManager, "shared-run")) + + info, ok := svr.clientRegistry.GetByKey("client") + require.True(t, ok) + require.True(t, info.Online) + require.Equal(t, uint64(ctlC.ID()), info.ControlID) + + var staleWrites atomic.Int64 + err = svr.completeControlLogin(ctlB, func() error { + staleWrites.Add(1) + return nil + }) + require.ErrorIs(t, err, errControlReplaced) + require.Equal(t, int64(0), staleWrites.Load()) + + require.NoError(t, svr.completeControlLogin(ctlC, func() error { return nil })) + waitForSignal(t, c.conn.readStarted, "C reader to start") + current, ok := svr.ctlManager.GetByID("shared-run") + require.True(t, ok) + require.Same(t, ctlC, current) + require.Equal(t, int64(2), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) + + require.NoError(t, ctlC.Close()) + waitForControlDone(t, ctlC) + require.Equal(t, int64(2), metrics.newClients()) + require.Equal(t, int64(2), metrics.closedClients()) + _, ok = svr.ctlManager.GetByID("shared-run") + require.False(t, ok) +} + +func TestServiceLoginResponseSynchronizationIsScopedToRun(t *testing.T) { + svr := newControlTestService(t) + metrics := newCountingServerMetrics() + ctlA, connA, err := registerLifecycleTestControl(svr) + require.NoError(t, err) + ctlA.serverMetrics = metrics + + writeEntered := make(chan struct{}) + resumeWrite := make(chan struct{}) + var resumeWriteOnce sync.Once + resume := func() { + resumeWriteOnce.Do(func() { close(resumeWrite) }) + } + t.Cleanup(resume) + writeCount := atomic.Int64{} + loginDone := make(chan error, 1) + go func() { + loginDone <- svr.completeControlLogin(ctlA, func() error { + close(writeEntered) + <-resumeWrite + writeCount.Add(1) + return nil + }) + }() + waitForSignal(t, writeEntered, "A LoginResp write") + + runMu := currentRunGateForTest(svr.ctlManager, "shared-run") + require.NotNil(t, runMu) + if !svr.ctlManager.mu.TryLock() { + t.Fatal("ControlManager mutex was held while LoginResp write was in progress") + } + svr.ctlManager.mu.Unlock() + + ctlB, connB := newLifecycleTestControl(t, "shared-run", "client", metrics) + gateAvailable := make(chan bool) + addDone := make(chan error, 1) + go func() { + if runMu.TryLock() { + runMu.Unlock() + gateAvailable <- true + } else { + gateAvailable <- false + } + addErr := svr.ctlManager.Add(ctlB) + addDone <- addErr + }() + available := waitForResult(t, gateAvailable, "same-run replacement gate probe") + require.False(t, available, "same-run gate was available to replacement during LoginResp write") + select { + case addErr := <-addDone: + t.Fatalf("same-run replacement completed during LoginResp write: %v", addErr) + case <-time.After(20 * time.Millisecond): + } + require.Same(t, ctlA, currentControlForTest(svr.ctlManager, "shared-run")) + + otherMetrics := newCountingServerMetrics() + otherCtl, otherConn := newLifecycleTestControl(t, "other-run", "other-client", otherMetrics) + type unrelatedResult struct { + addErr error + active bool + activateErr error + loginErr error + current *Control + found bool + } + unrelatedDone := make(chan unrelatedResult, 1) + go func() { + result := unrelatedResult{} + result.addErr = svr.ctlManager.Add(otherCtl) + if result.addErr == nil { + result.active, result.activateErr = svr.ctlManager.Activate(otherCtl) + } + if result.activateErr == nil && result.active { + result.loginErr = svr.completeControlLogin(otherCtl, func() error { return nil }) + } + result.current, result.found = svr.ctlManager.GetByID("other-run") + unrelatedDone <- result + }() + result := waitForResult(t, unrelatedDone, "unrelated run lifecycle") + require.NoError(t, result.addErr) + require.NoError(t, result.activateErr) + require.True(t, result.active) + require.NoError(t, result.loginErr) + require.True(t, result.found) + require.Same(t, otherCtl, result.current) + waitForSignal(t, otherConn.readStarted, "unrelated control reader to start") + require.Equal(t, int64(1), otherMetrics.newClients()) + + resume() + require.NoError(t, waitForResult(t, loginDone, "LoginResp completion")) + require.NoError(t, waitForResult(t, addDone, "replacement")) + waitForControlDone(t, ctlA) + require.Same(t, ctlB, currentControlForTest(svr.ctlManager, "shared-run")) + require.Equal(t, int64(1), writeCount.Load()) + require.Equal(t, int64(1), metrics.newClients()) + require.Equal(t, int64(1), metrics.closedClients()) + require.Equal(t, []string{"deadline", "close"}, connA.eventsSnapshot()) + + require.False(t, svr.ctlManager.Remove(ctlA)) + require.NoError(t, ctlA.Close()) + require.True(t, svr.ctlManager.Remove(ctlB)) + require.NoError(t, ctlB.Close()) + require.Equal(t, []string{"deadline", "close"}, connB.eventsSnapshot()) + + require.NoError(t, otherCtl.Close()) + waitForControlDone(t, otherCtl) + require.Equal(t, int64(1), otherMetrics.newClients()) + require.Equal(t, int64(1), otherMetrics.closedClients()) +} + +func TestServiceVisitorAdmissionSerializesReplacement(t *testing.T) { + svr := newControlTestService(t) + ctlA, controlConn, err := registerLifecycleTestControl(svr) + require.NoError(t, err) + ctlA.sessionCtx.LoginMsg.User = "old-user" + require.NoError(t, svr.completeControlLogin(ctlA, func() error { return nil })) + waitForSignal(t, controlConn.readStarted, "A reader to start") + + admissionEntered := make(chan struct{}) + resumeAdmission := make(chan struct{}) + var resumeOnce sync.Once + resume := func() { + resumeOnce.Do(func() { close(resumeAdmission) }) + } + t.Cleanup(resume) + type admissionResult struct { + admitted bool + user string + wireProtocol string + udpPacketCodec string + err error + } + admissionDone := make(chan admissionResult, 1) + go func() { + 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 <- result + }() + waitForSignal(t, admissionEntered, "visitor admission callback") + runMu := currentRunGateForTest(svr.ctlManager, "shared-run") + require.NotNil(t, runMu) + + type registerResult struct { + ctl *Control + err error + } + gateAvailable := make(chan bool) + replacementDone := make(chan registerResult, 1) + go func() { + if runMu.TryLock() { + runMu.Unlock() + gateAvailable <- true + } else { + gateAvailable <- false + } + ctl, _, registerErr := registerLifecycleTestControl(svr) + replacementDone <- registerResult{ctl: ctl, err: registerErr} + }() + available := waitForResult(t, gateAvailable, "visitor replacement gate probe") + require.False(t, available, "same-run gate was available during visitor admission") + select { + case result := <-replacementDone: + t.Fatalf("replacement completed during visitor admission: %v", result.err) + case <-time.After(20 * time.Millisecond): + } + require.Same(t, ctlA, currentControlForTest(svr.ctlManager, "shared-run")) + + resume() + admission := waitForResult(t, admissionDone, "visitor admission") + 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 + require.Same(t, ctlB, currentControlForTest(svr.ctlManager, "shared-run")) + waitForControlDone(t, ctlA) + require.True(t, svr.ctlManager.Remove(ctlB)) + require.NoError(t, ctlB.Close()) +} + +func TestServiceWorkConnRoutingRequiresCurrentRunningControl(t *testing.T) { + svr := newControlTestService(t) + ctl, controlConn, err := registerLifecycleTestControl(svr) + require.NoError(t, err) + + pendingConn := newCountingCloseConn() + pendingMsgConn := msg.NewConn(pendingConn, msg.NewV1ReadWriter(pendingConn)) + 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) + + require.NoError(t, svr.completeControlLogin(ctl, func() error { return nil })) + waitForSignal(t, controlConn.readStarted, "control reader to start") + current, ok := svr.ctlManager.GetByID("shared-run") + require.True(t, ok) + require.Same(t, ctl, current) + require.Len(t, ctl.workConnCh, 0) + + runningConn := newCountingCloseConn() + runningMsgConn := msg.NewConn(runningConn, msg.NewV1ReadWriter(runningConn)) + 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()) + waitForControlDone(t, ctl) + 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 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 + 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) { + 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") + + barrier := newWorkConnBarrierPlugin() + svr.pluginManager.Register(barrier) + workConn := newCountingCloseConn() + workMsgConn := msg.NewConn(workConn, msg.NewV1ReadWriter(workConn)) + routeDone := make(chan error, 1) + go func() { + routeDone <- registerWorkConnAsCaller(svr, workMsgConn, &msg.NewWorkConn{RunID: "shared-run"}, wire.ProtocolV1, false) + }() + waitForSignal(t, barrier.entered, "work connection plugin barrier") + + var replacement *Control + switch action { + case "replace": + replacement, _, err = registerLifecycleTestControl(svr) + require.NoError(t, err) + case "close": + require.NoError(t, ctl.Close()) + waitForControlDone(t, ctl) + } + + close(barrier.resume) + require.Error(t, waitForResult(t, routeDone, "work connection route to finish")) + require.Equal(t, int64(1), workConn.closeCount.Load()) + require.Len(t, ctl.workConnCh, 0) + + if replacement != nil { + require.Len(t, replacement.workConnCh, 0) + require.True(t, svr.ctlManager.Remove(replacement)) + require.NoError(t, replacement.Close()) + waitForControlDone(t, replacement) + } + }) + } +} + +func TestServiceVisitorRoutingExcludesPendingUser(t *testing.T) { + svr := newControlTestService(t) + listener, err := svr.rc.VisitorManager.Listen("visitor", "secret", []string{"pending-user"}) + require.NoError(t, err) + t.Cleanup(func() { _ = listener.Close() }) + + controlConn := newDeadlineReadConn() + controlMsgConn := msg.NewConn(controlConn, msg.NewV1ReadWriter(controlConn)) + ctl, err := svr.RegisterControl(controlMsgConn, &msg.Login{ + RunID: "visitor-run", + User: "pending-user", + ClientID: "visitor-client", + ClientSpec: msg.ClientSpec{ + AlwaysAuthPass: true, + }, + }, true, wire.ProtocolV1, "") + require.NoError(t, err) + + timestamp := time.Now().Unix() + visitorMsg := &msg.NewVisitorConn{ + RunID: "visitor-run", + ProxyName: "visitor", + Timestamp: timestamp, + SignKey: util.GetAuthKey("secret", timestamp), + } + pendingConn := newCountingCloseConn() + err = svr.RegisterVisitorConn(pendingConn, visitorMsg, wire.ProtocolV1) + require.ErrorContains(t, err, "no client control found") + require.NoError(t, pendingConn.Close()) + require.Equal(t, int64(1), pendingConn.closeCount.Load()) + + require.NoError(t, svr.completeControlLogin(ctl, func() error { return nil })) + waitForSignal(t, controlConn.readStarted, "control reader to start") + runningConn := newCountingCloseConn() + require.NoError(t, svr.RegisterVisitorConn(runningConn, visitorMsg, wire.ProtocolV1)) + accepted, err := listener.Accept() + require.NoError(t, err) + require.NoError(t, accepted.Close()) + require.Equal(t, int64(1), runningConn.closeCount.Load()) + + require.NoError(t, ctl.Close()) + 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{} + cfg.Auth.Method = v1.AuthMethodToken + authRuntime, err := auth.BuildServerAuth(&cfg.Auth) + require.NoError(t, err) + clientRegistry := registry.NewClientRegistry() + return &Service{ + ctlManager: NewControlManager(clientRegistry), + clientRegistry: clientRegistry, + pxyManager: proxy.NewManager(), + pluginManager: plugin.NewManager(), + rc: &controller.ResourceController{ + VisitorManager: visitor.NewManager(), + }, + auth: authRuntime, + cfg: cfg, + } +} + +func registerLifecycleTestControl(svr *Service) (*Control, *deadlineReadConn, error) { + conn := newDeadlineReadConn() + msgConn := msg.NewConn(conn, msg.NewReadWriter(conn, wire.ProtocolV1)) + ctl, err := svr.RegisterControl(msgConn, &msg.Login{ + RunID: "shared-run", + ClientID: "client", + ClientSpec: msg.ClientSpec{ + AlwaysAuthPass: true, + }, + }, true, wire.ProtocolV1, "") + return ctl, conn, err +} + +func waitForDifferentCurrentControl(t *testing.T, manager *ControlManager, runID string, old *Control) *Control { + t.Helper() + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + if ctl := currentControlForTest(manager, runID); ctl != nil && ctl != old { + return ctl + } + runtime.Gosched() + } + t.Fatalf("timed out waiting for a new current control after ID %d", old.ID()) + return nil +} + +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() + } + return err +} + +func waitForResult[T any](t *testing.T, ch <-chan T, description string) T { + t.Helper() + select { + case result := <-ch: + return result + case <-time.After(3 * time.Second): + t.Fatalf("timed out waiting for %s", description) + var zero T + return zero + } +} + +type workConnBarrierPlugin struct { + entered chan struct{} + resume chan struct{} +} + +func newWorkConnBarrierPlugin() *workConnBarrierPlugin { + return &workConnBarrierPlugin{ + entered: make(chan struct{}), + resume: make(chan struct{}), + } +} + +func (*workConnBarrierPlugin) Name() string { return "work-conn-barrier" } + +func (*workConnBarrierPlugin) IsSupport(op string) bool { return op == plugin.OpNewWorkConn } + +func (p *workConnBarrierPlugin) Handle( + context.Context, + string, + any, +) (*plugin.Response, any, error) { + close(p.entered) + <-p.resume + return &plugin.Response{Unchange: true}, nil, nil +} + +type countingCloseConn struct { + closeCount atomic.Int64 +} + +func newCountingCloseConn() *countingCloseConn { return &countingCloseConn{} } + +func (*countingCloseConn) Read([]byte) (int, error) { return 0, net.ErrClosed } +func (*countingCloseConn) Write(p []byte) (int, error) { return len(p), nil } +func (c *countingCloseConn) Close() error { c.closeCount.Add(1); return nil } +func (*countingCloseConn) LocalAddr() net.Addr { return lifecycleTestAddr("local") } +func (*countingCloseConn) RemoteAddr() net.Addr { return lifecycleTestAddr("remote") } +func (*countingCloseConn) SetDeadline(time.Time) error { return nil } +func (*countingCloseConn) SetReadDeadline(time.Time) error { return nil } +func (*countingCloseConn) SetWriteDeadline(time.Time) error { return nil } diff --git a/server/visitor/visitor.go b/server/visitor/visitor.go index 4796b416fbb..7776aad87e1 100644 --- a/server/visitor/visitor.go +++ b/server/visitor/visitor.go @@ -18,68 +18,89 @@ import ( "fmt" "io" "net" + "slices" "sync" - frpNet "github.com/fatedier/frp/pkg/util/net" - "github.com/fatedier/frp/pkg/util/util" + libio "github.com/fatedier/golib/io" - frpIo "github.com/fatedier/golib/io" + netpkg "github.com/fatedier/frp/pkg/util/net" + "github.com/fatedier/frp/pkg/util/util" ) +type listenerBundle struct { + l *netpkg.InternalListener + sk string + allowUsers []string +} + // Manager for visitor listeners. type Manager struct { - visitorListeners map[string]*frpNet.CustomListener - skMap map[string]string + listeners map[string]*listenerBundle mu sync.RWMutex } func NewManager() *Manager { return &Manager{ - visitorListeners: make(map[string]*frpNet.CustomListener), - skMap: make(map[string]string), + listeners: make(map[string]*listenerBundle), } } -func (vm *Manager) Listen(name string, sk string) (l *frpNet.CustomListener, err error) { +func (vm *Manager) Listen(name string, sk string, allowUsers []string) (*netpkg.InternalListener, error) { vm.mu.Lock() defer vm.mu.Unlock() - if _, ok := vm.visitorListeners[name]; ok { - err = fmt.Errorf("custom listener for [%s] is repeated", name) - return + if _, ok := vm.listeners[name]; ok { + return nil, fmt.Errorf("custom listener for [%s] is repeated", name) } - l = frpNet.NewCustomListener() - vm.visitorListeners[name] = l - vm.skMap[name] = sk - return + l := netpkg.NewInternalListener() + vm.listeners[name] = &listenerBundle{ + l: l, + sk: sk, + allowUsers: allowUsers, + } + return l, nil } func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey string, - useEncryption bool, useCompression bool) (err error) { - + useEncryption bool, useCompression bool, visitorUser string, + wireProtocol string, udpPacketCodecs ...string, +) (err error) { + udpPacketCodec := "" + if len(udpPacketCodecs) > 0 { + udpPacketCodec = udpPacketCodecs[0] + } vm.mu.RLock() defer vm.mu.RUnlock() - if l, ok := vm.visitorListeners[name]; ok { - var sk string - if sk = vm.skMap[name]; util.GetAuthKey(sk, timestamp) != signKey { + if l, ok := vm.listeners[name]; ok { + if util.GetAuthKey(l.sk, timestamp) != signKey { err = fmt.Errorf("visitor connection of [%s] auth failed", name) return } + if !slices.Contains(l.allowUsers, visitorUser) && !slices.Contains(l.allowUsers, "*") { + err = fmt.Errorf("visitor connection of [%s] user [%s] not allowed", name, visitorUser) + return + } + var rwc io.ReadWriteCloser = conn if useEncryption { - if rwc, err = frpIo.WithEncryption(rwc, []byte(sk)); err != nil { + if rwc, err = libio.WithEncryption(rwc, []byte(l.sk)); err != nil { err = fmt.Errorf("create encryption connection failed: %v", err) return } } if useCompression { - rwc = frpIo.WithCompression(rwc) + rwc = libio.WithCompression(rwc) } - err = l.PutConn(frpNet.WrapReadWriteCloserToConn(rwc, conn)) + visitorConn := netpkg.WrapReadWriteCloserToConn(rwc, conn) + err = l.l.PutConn(&wireProtocolConn{ + Conn: visitorConn, + wireProtocol: wireProtocol, + udpPacketCodec: udpPacketCodec, + }) } else { err = fmt.Errorf("custom listener for [%s] doesn't exist", name) return @@ -87,10 +108,23 @@ func (vm *Manager) NewConn(name string, conn net.Conn, timestamp int64, signKey return } +type wireProtocolConn struct { + net.Conn + 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() - delete(vm.visitorListeners, name) - delete(vm.skMap, name) + delete(vm.listeners, name) } diff --git a/server/visitor/visitor_test.go b/server/visitor/visitor_test.go new file mode 100644 index 00000000000..04c50ff01e9 --- /dev/null +++ b/server/visitor/visitor_test.go @@ -0,0 +1,66 @@ +// 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 visitor + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/proto/wire" + "github.com/fatedier/frp/pkg/util/util" +) + +func TestManagerNewConnCarriesWireProtocolAndUDPPacketCodec(t *testing.T) { + vm := NewManager() + listener, err := vm.Listen("sudp", "secret", []string{"*"}) + require.NoError(t, err) + defer listener.Close() + + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + now := time.Now().Unix() + errCh := make(chan error, 1) + go func() { + errCh <- vm.NewConn( + "sudp", + server, + now, + util.GetAuthKey("secret", now), + false, + false, + "user", + wire.ProtocolV2, + wire.UDPPacketCodecBinary, + ) + }() + + acceptedConn, err := listener.Accept() + require.NoError(t, err) + defer acceptedConn.Close() + + metadata, ok := acceptedConn.(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, <-errCh) +} diff --git a/test/e2e/compatibility/compatibility_test.go b/test/e2e/compatibility/compatibility_test.go new file mode 100644 index 00000000000..ad5055102f7 --- /dev/null +++ b/test/e2e/compatibility/compatibility_test.go @@ -0,0 +1,337 @@ +package compatibility + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/process" +) + +type compatTestContext struct { + CurrentFRPSPath string + CurrentFRPCPath string + BaselineFRPSPath string + BaselineFRPCPath string + BaselineVersion string + LogLevel string + Debug bool + RunCurrentCurrent bool +} + +var compatCtx compatTestContext + +func registerFlags(flags *flag.FlagSet) { + flags.StringVar(&compatCtx.CurrentFRPSPath, "current-frps-path", "../../bin/frps", "The current frps binary to use.") + flags.StringVar(&compatCtx.CurrentFRPCPath, "current-frpc-path", "../../bin/frpc", "The current frpc binary to use.") + flags.StringVar(&compatCtx.BaselineFRPSPath, "baseline-frps-path", "", "The baseline frps binary to use.") + flags.StringVar(&compatCtx.BaselineFRPCPath, "baseline-frpc-path", "", "The baseline frpc binary to use.") + flags.StringVar(&compatCtx.BaselineVersion, "baseline-version", "custom", "The baseline version label for reporting.") + flags.StringVar(&compatCtx.LogLevel, "log-level", "debug", "Log level.") + flags.BoolVar(&compatCtx.Debug, "debug", false, "Enable debug mode to print detailed info.") + flags.BoolVar(&compatCtx.RunCurrentCurrent, "run-current-current", true, "Run current frps/current frpc sanity checks.") +} + +func validateCompatContext(t *compatTestContext) error { + paths := map[string]string{ + "current-frps-path": t.CurrentFRPSPath, + "current-frpc-path": t.CurrentFRPCPath, + "baseline-frps-path": t.BaselineFRPSPath, + "baseline-frpc-path": t.BaselineFRPCPath, + } + for name, path := range paths { + if path == "" { + return fmt.Errorf("%s can't be empty", name) + } + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("load %s error: %v", name, err) + } + } + return nil +} + +func TestMain(m *testing.M) { + registerFlags(flag.CommandLine) + flag.Parse() + + if err := validateCompatContext(&compatCtx); err != nil { + fmt.Println(err) + os.Exit(1) + } + + framework.TestContext.Debug = compatCtx.Debug + framework.TestContext.LogLevel = compatCtx.LogLevel + log.InitLogger("console", compatCtx.LogLevel, 0, true) + os.Exit(m.Run()) +} + +func TestCompatibilityE2E(t *testing.T) { + gomega.RegisterFailHandler(framework.Fail) + + suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration() + suiteConfig.EmitSpecProgress = true + suiteConfig.RandomizeAllSpecs = true + ginkgo.RunSpecs(t, "frp compatibility e2e suite", suiteConfig, reporterConfig) +} + +var _ = ginkgo.Describe("[Compatibility: WireProtocol]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("current frps and current frpc support v1 and v2", func() { + if !compatCtx.RunCurrentCurrent { + ginkgo.Skip("current/current sanity checks already ran") + } + + webPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +webServer.port = %d + `, webPort) + + v1PortName := port.GenName("CompatCurrentV1") + v1ClientConf := tcpClientConfig("compat-current-v1", v1PortName, ` +clientID = "compat-current-v1" +transport.wireProtocol = "v1" +`) + + v2PortName := port.GenName("CompatCurrentV2") + v2ClientConf := tcpClientConfig("compat-current-v2", v2PortName, ` +clientID = "compat-current-v2" +transport.wireProtocol = "v2" +`) + + f.RunProcessesWithBinaries( + compatCtx.CurrentFRPSPath, + compatCtx.CurrentFRPCPath, + serverConf, + []string{v1ClientConf, v2ClientConf}, + ) + + framework.NewRequestExpect(f).PortName(v1PortName).Ensure() + framework.NewRequestExpect(f).PortName(v2PortName).Ensure() + expectClientWireProtocol(webPort, "compat-current-v1", "v1") + expectClientWireProtocol(webPort, "compat-current-v2", "v2") + }) + + ginkgo.It("current frps accepts baseline frpc using v1", func() { + webPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +webServer.port = %d +`, webPort) + + portName := port.GenName("CompatBaselineFRPC") + clientConf := tcpClientConfig("tcp", portName, "") + + f.RunProcessesWithBinaries( + compatCtx.CurrentFRPSPath, + compatCtx.BaselineFRPCPath, + serverConf, + []string{clientConf}, + ) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + expectSingleClientWireProtocol(webPort, "v1") + }) + + ginkgo.It("baseline frps accepts current frpc defaulting to v1", func() { + portName := port.GenName("CompatBaselineFRPS") + clientConf := tcpClientConfig("tcp", portName, "") + + f.RunProcessesWithBinaries( + compatCtx.BaselineFRPSPath, + compatCtx.CurrentFRPCPath, + consts.DefaultServerConfig, + []string{clientConf}, + ) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("baseline frps handles current frpc forced to v2 according to baseline support", func() { + portName := port.GenName("CompatBaselineFRPSForcedV2") + clientConf := tcpClientConfig("tcp", portName, ` +transport.wireProtocol = "v2" +`) + + _, clientProcesses := f.RunProcessesWithBinaries( + compatCtx.BaselineFRPSPath, + compatCtx.CurrentFRPCPath, + consts.DefaultServerConfig, + []string{clientConf}, + ) + + // frp v0.69.0 added control connection wireProtocol v2 support, so + // baseline frps v0.69.0 and newer should accept a current frpc forced + // to v2. Older known baselines still must reject the unsupported protocol. + // For custom baselines, the version is unknown and the binary may be either + // side of the support boundary, so this versioned expectation is skipped. + supportsV2, knownVersion := baselineSupportsControlWireProtocolV2(compatCtx.BaselineVersion) + if !knownVersion { + ginkgo.Skip(fmt.Sprintf("baseline version %q is not semver; skip versioned forced-v2 expectation", compatCtx.BaselineVersion)) + } + if supportsV2 { + framework.NewRequestExpect(f).PortName(portName).Ensure() + return + } + + expectProcessExit(clientProcesses[0], 5*time.Second) + framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure() + }) +}) + +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" +serverPort = {{ .%s }} +loginFailExit = true +log.level = "trace" +%s + +[[proxies]] +name = "%s" +type = "tcp" +localPort = {{ .%s }} +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(): + case <-time.After(timeout): + framework.Failf("process did not exit within %s; output:\n%s", timeout, p.Output()) + } +} + +func baselineSupportsControlWireProtocolV2(version string) (supports bool, known bool) { + version = strings.TrimPrefix(version, "v") + parts := strings.Split(version, ".") + if len(parts) != 3 { + return false, false + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return false, false + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return false, false + } + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return false, false + } + + return compareSemanticVersion(major, minor, patch, 0, 69, 0) >= 0, true +} + +func compareSemanticVersion(major, minor, patch int, baseMajor, baseMinor, basePatch int) int { + if major != baseMajor { + return major - baseMajor + } + if minor != baseMinor { + return minor - baseMinor + } + return patch - basePatch +} + +type wireClientInfo struct { + ClientID string `json:"clientID"` + WireProtocol string `json:"wireProtocol"` +} + +func expectSingleClientWireProtocol(webPort int, wireProtocol string) { + clients := getWireClientInfos(webPort) + framework.ExpectEqual(len(clients), 1) + framework.ExpectEqual(clients[0].WireProtocol, wireProtocol) +} + +func expectClientWireProtocol(webPort int, clientID string, wireProtocol string) { + for _, client := range getWireClientInfos(webPort) { + if client.ClientID == clientID { + framework.ExpectEqual(client.WireProtocol, wireProtocol) + return + } + } + framework.Failf("client %q not found in /api/clients response", clientID) +} + +func getWireClientInfos(webPort int) []wireClientInfo { + client := http.Client{Timeout: consts.DefaultTimeout} + resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/api/clients", webPort)) + framework.ExpectNoError(err) + defer resp.Body.Close() + framework.ExpectEqual(resp.StatusCode, http.StatusOK) + + content, err := io.ReadAll(resp.Body) + framework.ExpectNoError(err) + + var clients []wireClientInfo + framework.ExpectNoError(json.Unmarshal(content, &clients)) + return clients +} diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index b392954bf09..ee00e1031e2 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -3,12 +3,11 @@ package e2e import ( "testing" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/test/e2e/framework" - - "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/config" - "github.com/onsi/gomega" ) var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { @@ -33,9 +32,15 @@ var _ = ginkgo.SynchronizedAfterSuite(func() { func RunE2ETests(t *testing.T) { gomega.RegisterFailHandler(framework.Fail) - log.Info("Starting e2e run %q on Ginkgo node %d of total %d", - framework.RunID, config.GinkgoConfig.ParallelNode, config.GinkgoConfig.ParallelTotal) - ginkgo.RunSpecs(t, "frp e2e suite") + suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration() + // Turn on EmitSpecProgress to get spec progress (especially on interrupt) + suiteConfig.EmitSpecProgress = true + // Randomize specs as well as suites + suiteConfig.RandomizeAllSpecs = true + + log.Infof("starting e2e run %q on Ginkgo node %d of total %d", + framework.RunID, suiteConfig.ParallelProcess, suiteConfig.ParallelTotal) + ginkgo.RunSpecs(t, "frp e2e suite", suiteConfig, reporterConfig) } // setupSuite is the boilerplate that can be used to setup ginkgo test suites, on the SynchronizedBeforeSuite step. diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2dddb95234c..e32f89f083f 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -6,15 +6,17 @@ import ( "os" "testing" - "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/test/e2e/framework" + _ "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/pkg/util/log" // test source - _ "github.com/fatedier/frp/test/e2e/basic" - _ "github.com/fatedier/frp/test/e2e/features" - _ "github.com/fatedier/frp/test/e2e/plugin" - - _ "github.com/onsi/ginkgo" + "github.com/fatedier/frp/test/e2e/framework" + _ "github.com/fatedier/frp/test/e2e/legacy/basic" + _ "github.com/fatedier/frp/test/e2e/legacy/features" + _ "github.com/fatedier/frp/test/e2e/legacy/plugin" + _ "github.com/fatedier/frp/test/e2e/v1/basic" + _ "github.com/fatedier/frp/test/e2e/v1/features" + _ "github.com/fatedier/frp/test/e2e/v1/plugin" ) // handleFlags sets up all flags and parses the command line. @@ -32,7 +34,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - log.InitLog("console", "", framework.TestContext.LogLevel, 0, true) + log.InitLogger("console", framework.TestContext.LogLevel, 0, true) os.Exit(m.Run()) } diff --git a/test/e2e/examples.go b/test/e2e/examples.go index 6423aec1ede..9f0bc335852 100644 --- a/test/e2e/examples.go +++ b/test/e2e/examples.go @@ -3,29 +3,30 @@ package e2e import ( "fmt" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Example]", func() { +var _ = ginkgo.Describe("[Feature: Example]", func() { f := framework.NewDefaultFramework() - Describe("TCP", func() { - It("Expose a TCP echo server", func() { + ginkgo.Describe("TCP", func() { + ginkgo.It("Expose a TCP echo server", func() { serverConf := consts.DefaultServerConfig clientConf := consts.DefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` - [tcp] - type = tcp - local_port = {{ .%s }} - remote_port = %d + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) diff --git a/test/e2e/features/bandwidth_limit.go b/test/e2e/features/bandwidth_limit.go deleted file mode 100644 index f984fdb9fe3..00000000000 --- a/test/e2e/features/bandwidth_limit.go +++ /dev/null @@ -1,47 +0,0 @@ -package features - -import ( - "fmt" - "strings" - "time" - - "github.com/fatedier/frp/test/e2e/framework" - "github.com/fatedier/frp/test/e2e/framework/consts" - "github.com/fatedier/frp/test/e2e/mock/server/streamserver" - "github.com/fatedier/frp/test/e2e/pkg/request" - - . "github.com/onsi/ginkgo" -) - -var _ = Describe("[Feature: Bandwidth Limit]", func() { - f := framework.NewDefaultFramework() - - It("Proxy Bandwidth Limit", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig - - localPort := f.AllocPort() - localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) - f.RunServer("", localServer) - - remotePort := f.AllocPort() - clientConf += fmt.Sprintf(` - [tcp] - type = tcp - local_port = %d - remote_port = %d - bandwidth_limit = 10KB - `, localPort, remotePort) - - f.RunProcesses([]string{serverConf}, []string{clientConf}) - - content := strings.Repeat("a", 50*1024) // 5KB - start := time.Now() - framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { - r.Body([]byte(content)).Timeout(30 * time.Second) - }).ExpectResp([]byte(content)).Ensure() - duration := time.Now().Sub(start) - - framework.ExpectTrue(duration.Seconds() > 7, "100Kb with 10KB limit, want > 7 seconds, but got %d seconds", duration.Seconds()) - }) -}) diff --git a/test/e2e/framework/cleanup.go b/test/e2e/framework/cleanup.go deleted file mode 100644 index d2d1c5a6ce3..00000000000 --- a/test/e2e/framework/cleanup.go +++ /dev/null @@ -1,59 +0,0 @@ -package framework - -import ( - "sync" -) - -// CleanupActionHandle is an integer pointer type for handling cleanup action -type CleanupActionHandle *int -type cleanupFuncHandle struct { - actionHandle CleanupActionHandle - actionHook func() -} - -var cleanupActionsLock sync.Mutex -var cleanupHookList = []cleanupFuncHandle{} - -// AddCleanupAction installs a function that will be called in the event of the -// whole test being terminated. This allows arbitrary pieces of the overall -// test to hook into SynchronizedAfterSuite(). -// The hooks are called in last-in-first-out order. -func AddCleanupAction(fn func()) CleanupActionHandle { - p := CleanupActionHandle(new(int)) - cleanupActionsLock.Lock() - defer cleanupActionsLock.Unlock() - c := cleanupFuncHandle{actionHandle: p, actionHook: fn} - cleanupHookList = append([]cleanupFuncHandle{c}, cleanupHookList...) - return p -} - -// RemoveCleanupAction removes a function that was installed by -// AddCleanupAction. -func RemoveCleanupAction(p CleanupActionHandle) { - cleanupActionsLock.Lock() - defer cleanupActionsLock.Unlock() - for i, item := range cleanupHookList { - if item.actionHandle == p { - cleanupHookList = append(cleanupHookList[:i], cleanupHookList[i+1:]...) - break - } - } -} - -// RunCleanupActions runs all functions installed by AddCleanupAction. It does -// not remove them (see RemoveCleanupAction) but it does run unlocked, so they -// may remove themselves. -func RunCleanupActions() { - list := []func(){} - func() { - cleanupActionsLock.Lock() - defer cleanupActionsLock.Unlock() - for _, p := range cleanupHookList { - list = append(list, p.actionHook) - } - }() - // Run unlocked. - for _, fn := range list { - fn() - } -} diff --git a/test/e2e/framework/client.go b/test/e2e/framework/client.go index 76614354a48..c13cee10a46 100644 --- a/test/e2e/framework/client.go +++ b/test/e2e/framework/client.go @@ -1,11 +1,9 @@ package framework -type FRPClient struct { - port int -} +import ( + clientsdk "github.com/fatedier/frp/pkg/sdk/client" +) -func (f *Framework) FRPClient(port int) *FRPClient { - return &FRPClient{ - port: port, - } +func (f *Framework) APIClientForFrpc(port int) *clientsdk.Client { + return clientsdk.New("127.0.0.1", port) } diff --git a/test/e2e/framework/consts/consts.go b/test/e2e/framework/consts/consts.go index d38e183daed..4c7c2f13cc1 100644 --- a/test/e2e/framework/consts/consts.go +++ b/test/e2e/framework/consts/consts.go @@ -18,14 +18,28 @@ var ( PortClientAdmin string DefaultServerConfig = ` +bindPort = {{ .%s }} +log.level = "trace" +` + + DefaultClientConfig = ` +serverAddr = "127.0.0.1" +serverPort = {{ .%s }} +loginFailExit = false +log.level = "trace" +` + + LegacyDefaultServerConfig = ` [common] bind_port = {{ .%s }} log_level = trace ` - DefaultClientConfig = ` + LegacyDefaultClientConfig = ` [common] + server_addr = 127.0.0.1 server_port = {{ .%s }} + login_fail_exit = false log_level = trace ` ) @@ -33,6 +47,9 @@ var ( func init() { PortServerName = port.GenName("Server") PortClientAdmin = port.GenName("ClientAdmin") + LegacyDefaultServerConfig = fmt.Sprintf(LegacyDefaultServerConfig, port.GenName("Server")) + LegacyDefaultClientConfig = fmt.Sprintf(LegacyDefaultClientConfig, port.GenName("Server")) + DefaultServerConfig = fmt.Sprintf(DefaultServerConfig, port.GenName("Server")) DefaultClientConfig = fmt.Sprintf(DefaultClientConfig, port.GenName("Server")) } diff --git a/test/e2e/framework/expect.go b/test/e2e/framework/expect.go index dab6da70ef0..ea80ecb8bb7 100644 --- a/test/e2e/framework/expect.go +++ b/test/e2e/framework/expect.go @@ -5,71 +5,55 @@ import ( ) // ExpectEqual expects the specified two are the same, otherwise an exception raises -func ExpectEqual(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectEqual(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.Equal(extra), explain...) } // ExpectEqualValues expects the specified two are the same, it not strict about type -func ExpectEqualValues(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectEqualValues(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.BeEquivalentTo(extra), explain...) } -func ExpectEqualValuesWithOffset(offset int, actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectEqualValuesWithOffset(offset int, actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1+offset, actual).To(gomega.BeEquivalentTo(extra), explain...) } // ExpectNotEqual expects the specified two are not the same, otherwise an exception raises -func ExpectNotEqual(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectNotEqual(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).NotTo(gomega.Equal(extra), explain...) } -// ExpectError expects an error happens, otherwise an exception raises -func ExpectError(err error, explain ...interface{}) { - gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred(), explain...) -} - -func ExpectErrorWithOffset(offset int, err error, explain ...interface{}) { +func ExpectErrorWithOffset(offset int, err error, explain ...any) { gomega.ExpectWithOffset(1+offset, err).To(gomega.HaveOccurred(), explain...) } // ExpectNoError checks if "err" is set, and if so, fails assertion while logging the error. -func ExpectNoError(err error, explain ...interface{}) { +func ExpectNoError(err error, explain ...any) { ExpectNoErrorWithOffset(1, err, explain...) } // ExpectNoErrorWithOffset checks if "err" is set, and if so, fails assertion while logging the error at "offset" levels above its caller // (for example, for call chain f -> g -> ExpectNoErrorWithOffset(1, ...) error would be logged for "f"). -func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) { +func ExpectNoErrorWithOffset(offset int, err error, explain ...any) { gomega.ExpectWithOffset(1+offset, err).NotTo(gomega.HaveOccurred(), explain...) } -// ExpectConsistOf expects actual contains precisely the extra elements. The ordering of the elements does not matter. -func ExpectConsistOf(actual interface{}, extra interface{}, explain ...interface{}) { - gomega.ExpectWithOffset(1, actual).To(gomega.ConsistOf(extra), explain...) +func ExpectContainSubstring(actual, substr string, explain ...any) { + gomega.ExpectWithOffset(1, actual).To(gomega.ContainSubstring(substr), explain...) } -func ExpectContainElements(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectContainElements(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).To(gomega.ContainElements(extra), explain...) } -func ExpectNotContainElements(actual interface{}, extra interface{}, explain ...interface{}) { +func ExpectNotContainElements(actual any, extra any, explain ...any) { gomega.ExpectWithOffset(1, actual).NotTo(gomega.ContainElements(extra), explain...) } -// ExpectHaveKey expects the actual map has the key in the keyset -func ExpectHaveKey(actual interface{}, key interface{}, explain ...interface{}) { - gomega.ExpectWithOffset(1, actual).To(gomega.HaveKey(key), explain...) -} - -// ExpectEmpty expects actual is empty -func ExpectEmpty(actual interface{}, explain ...interface{}) { - gomega.ExpectWithOffset(1, actual).To(gomega.BeEmpty(), explain...) -} - -func ExpectTrue(actual interface{}, explain ...interface{}) { +func ExpectTrue(actual any, explain ...any) { gomega.ExpectWithOffset(1, actual).Should(gomega.BeTrue(), explain...) } -func ExpectTrueWithOffset(offset int, actual interface{}, explain ...interface{}) { +func ExpectTrueWithOffset(offset int, actual any, explain ...any) { gomega.ExpectWithOffset(1+offset, actual).Should(gomega.BeTrue(), explain...) } diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 344f64c3ab9..34c0eb63a1c 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -9,12 +9,11 @@ import ( "strings" "text/template" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/mock/server" "github.com/fatedier/frp/test/e2e/pkg/port" "github.com/fatedier/frp/test/e2e/pkg/process" - - "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/config" ) type Options struct { @@ -30,8 +29,8 @@ type Framework struct { // ports used in this framework indexed by port name. usedPorts map[string]int - // record ports alloced by this framework and release them after each test - allocedPorts []int + // record ports allocated by this framework and release them after each test + allocatedPorts []int // portAllocator to alloc port for this test case. portAllocator *port.Allocator @@ -39,11 +38,6 @@ type Framework struct { // Multiple default mock servers used for e2e testing. mockServers *MockServers - // To make sure that this framework cleans up after itself, no matter what, - // we install a Cleanup action before each test and clear it after. If we - // should abort, the AfterSuite hook should run all Cleanup actions. - cleanupHandle CleanupActionHandle - // beforeEachStarted indicates that BeforeEach has started beforeEachStarted bool @@ -63,11 +57,12 @@ type Framework struct { } func NewDefaultFramework() *Framework { + suiteConfig, _ := ginkgo.GinkgoConfiguration() options := Options{ - TotalParallelNode: config.GinkgoConfig.ParallelTotal, - CurrentNodeIndex: config.GinkgoConfig.ParallelNode, - FromPortIndex: 20000, - ToPortIndex: 50000, + TotalParallelNode: suiteConfig.ParallelTotal, + CurrentNodeIndex: suiteConfig.ParallelProcess, + FromPortIndex: 10000, + ToPortIndex: 30000, } return NewFramework(options) } @@ -87,8 +82,6 @@ func NewFramework(opt Options) *Framework { func (f *Framework) BeforeEach() { f.beforeEachStarted = true - f.cleanupHandle = AddCleanupAction(f.AfterEach) - dir, err := os.MkdirTemp(os.TempDir(), "frp-e2e-test-*") ExpectNoError(err) f.TempDirectory = dir @@ -102,7 +95,8 @@ func (f *Framework) BeforeEach() { for k, v := range params { switch t := v.(type) { case int: - f.usedPorts[k] = int(t) + f.usedPorts[k] = t + default: } } } @@ -112,19 +106,17 @@ func (f *Framework) AfterEach() { return } - RemoveCleanupAction(f.cleanupHandle) - // stop processor for _, p := range f.serverProcesses { - p.Stop() - if TestContext.Debug { + _ = p.Stop() + if TestContext.Debug || ginkgo.CurrentSpecReport().Failed() { fmt.Println(p.ErrorOutput()) fmt.Println(p.StdOutput()) } } for _, p := range f.clientProcesses { - p.Stop() - if TestContext.Debug { + _ = p.Stop() + if TestContext.Debug || ginkgo.CurrentSpecReport().Failed() { fmt.Println(p.ErrorOutput()) fmt.Println(p.StdOutput()) } @@ -152,11 +144,11 @@ func (f *Framework) AfterEach() { } f.usedPorts = make(map[string]int) - // release alloced ports - for _, port := range f.allocedPorts { + // release allocated ports + for _, port := range f.allocatedPorts { f.portAllocator.Release(port) } - f.allocedPorts = make([]int, 0) + f.allocatedPorts = make([]int, 0) // clear os envs f.osEnvs = make([]string, 0) @@ -216,7 +208,7 @@ func (f *Framework) RenderTemplates(templates []string) (outs []string, ports ma } for _, t := range templates { - tmpl, err := template.New("").Parse(t) + tmpl, err := template.New("frp-e2e").Parse(t) if err != nil { return nil, nil, err } @@ -236,12 +228,33 @@ func (f *Framework) PortByName(name string) int { func (f *Framework) AllocPort() int { port := f.portAllocator.Get() ExpectTrue(port > 0, "alloc port failed") - f.allocedPorts = append(f.allocedPorts, port) + f.allocatedPorts = append(f.allocatedPorts, port) return port } -func (f *Framework) ReleasePort(port int) { - f.portAllocator.Release(port) +func (f *Framework) AllocPortExcludingRanges(ranges ...[2]int) int { + for range 1000 { + port := f.portAllocator.Get() + ExpectTrue(port > 0, "alloc port failed") + + inExcludedRange := false + for _, portRange := range ranges { + if port >= portRange[0] && port <= portRange[1] { + inExcludedRange = true + break + } + } + if inExcludedRange { + f.portAllocator.Release(port) + continue + } + + f.allocatedPorts = append(f.allocatedPorts, port) + return port + } + + Failf("alloc port outside excluded ranges failed") + return 0 } func (f *Framework) RunServer(portName string, s server.Server) { @@ -259,7 +272,7 @@ func (f *Framework) SetEnvs(envs []string) { func (f *Framework) WriteTempFile(name string, content string) string { filePath := filepath.Join(f.TempDirectory, name) - err := os.WriteFile(filePath, []byte(content), 0766) + err := os.WriteFile(filePath, []byte(content), 0o600) ExpectNoError(err) return filePath } diff --git a/test/e2e/framework/ginkgowrapper/wrapper.go b/test/e2e/framework/ginkgowrapper/wrapper.go deleted file mode 100644 index 01ccf4d9914..00000000000 --- a/test/e2e/framework/ginkgowrapper/wrapper.go +++ /dev/null @@ -1,80 +0,0 @@ -// Package ginkgowrapper wraps Ginkgo Fail and Skip functions to panic -// with structured data instead of a constant string. -package ginkgowrapper - -import ( - "bufio" - "bytes" - "regexp" - "runtime" - "runtime/debug" - "strings" - - "github.com/onsi/ginkgo" -) - -// FailurePanic is the value that will be panicked from Fail. -type FailurePanic struct { - Message string // The failure message passed to Fail - Filename string // The filename that is the source of the failure - Line int // The line number of the filename that is the source of the failure - FullStackTrace string // A full stack trace starting at the source of the failure -} - -// String makes FailurePanic look like the old Ginkgo panic when printed. -func (FailurePanic) String() string { return ginkgo.GINKGO_PANIC } - -// Fail wraps ginkgo.Fail so that it panics with more useful -// information about the failure. This function will panic with a -// FailurePanic. -func Fail(message string, callerSkip ...int) { - skip := 1 - if len(callerSkip) > 0 { - skip += callerSkip[0] - } - - _, file, line, _ := runtime.Caller(skip) - fp := FailurePanic{ - Message: message, - Filename: file, - Line: line, - FullStackTrace: pruneStack(skip), - } - - defer func() { - e := recover() - if e != nil { - panic(fp) - } - }() - - ginkgo.Fail(message, skip) -} - -// ginkgo adds a lot of test running infrastructure to the stack, so -// we filter those out -var stackSkipPattern = regexp.MustCompile(`onsi/ginkgo`) - -func pruneStack(skip int) string { - skip += 2 // one for pruneStack and one for debug.Stack - stack := debug.Stack() - scanner := bufio.NewScanner(bytes.NewBuffer(stack)) - var prunedStack []string - - // skip the top of the stack - for i := 0; i < 2*skip+1; i++ { - scanner.Scan() - } - - for scanner.Scan() { - if stackSkipPattern.Match(scanner.Bytes()) { - scanner.Scan() // these come in pairs - } else { - prunedStack = append(prunedStack, scanner.Text()) - scanner.Scan() // these come in pairs - prunedStack = append(prunedStack, scanner.Text()) - } - } - - return strings.Join(prunedStack, "\n") -} diff --git a/test/e2e/framework/log.go b/test/e2e/framework/log.go index ff33f41dbe0..a466f68bf1f 100644 --- a/test/e2e/framework/log.go +++ b/test/e2e/framework/log.go @@ -1,94 +1,33 @@ package framework import ( - "bytes" "fmt" - "regexp" - "runtime/debug" "time" - "github.com/onsi/ginkgo" - - e2eginkgowrapper "github.com/fatedier/frp/test/e2e/framework/ginkgowrapper" + "github.com/onsi/ginkgo/v2" ) func nowStamp() string { return time.Now().Format(time.StampMilli) } -func log(level string, format string, args ...interface{}) { +func log(level string, format string, args ...any) { fmt.Fprintf(ginkgo.GinkgoWriter, nowStamp()+": "+level+": "+format+"\n", args...) } // Logf logs the info. -func Logf(format string, args ...interface{}) { +func Logf(format string, args ...any) { log("INFO", format, args...) } -// Failf logs the fail info, including a stack trace. -func Failf(format string, args ...interface{}) { - FailfWithOffset(1, format, args...) -} - -// FailfWithOffset calls "Fail" and logs the error with a stack trace that starts at "offset" levels above its caller -// (for example, for call chain f -> g -> FailfWithOffset(1, ...) error would be logged for "f"). -func FailfWithOffset(offset int, format string, args ...interface{}) { +// Failf logs the fail info, including a stack trace starts with its direct caller +// (for example, for call chain f -> g -> Failf("foo", ...) error would be logged for "g"). +func Failf(format string, args ...any) { msg := fmt.Sprintf(format, args...) - skip := offset + 1 - log("FAIL", "%s\n\nFull Stack Trace\n%s", msg, PrunedStack(skip)) - e2eginkgowrapper.Fail(nowStamp()+": "+msg, skip) -} - -// Fail is a replacement for ginkgo.Fail which logs the problem as it occurs -// together with a stack trace and then calls ginkgowrapper.Fail. -func Fail(msg string, callerSkip ...int) { skip := 1 - if len(callerSkip) > 0 { - skip += callerSkip[0] - } - log("FAIL", "%s\n\nFull Stack Trace\n%s", msg, PrunedStack(skip)) - e2eginkgowrapper.Fail(nowStamp()+": "+msg, skip) + ginkgo.Fail(msg, skip) + panic("unreachable") } -var codeFilterRE = regexp.MustCompile(`/github.com/onsi/ginkgo/`) - -// PrunedStack is a wrapper around debug.Stack() that removes information -// about the current goroutine and optionally skips some of the initial stack entries. -// With skip == 0, the returned stack will start with the caller of PruneStack. -// From the remaining entries it automatically filters out useless ones like -// entries coming from Ginkgo. -// -// This is a modified copy of PruneStack in https://github.com/onsi/ginkgo/blob/f90f37d87fa6b1dd9625e2b1e83c23ffae3de228/internal/codelocation/code_location.go#L25: -// - simplified API and thus renamed (calls debug.Stack() instead of taking a parameter) -// - source code filtering updated to be specific to Kubernetes -// - optimized to use bytes and in-place slice filtering from -// https://github.com/golang/go/wiki/SliceTricks#filter-in-place -func PrunedStack(skip int) []byte { - fullStackTrace := debug.Stack() - stack := bytes.Split(fullStackTrace, []byte("\n")) - // Ensure that the even entries are the method names and the - // the odd entries the source code information. - if len(stack) > 0 && bytes.HasPrefix(stack[0], []byte("goroutine ")) { - // Ignore "goroutine 29 [running]:" line. - stack = stack[1:] - } - // The "+2" is for skipping over: - // - runtime/debug.Stack() - // - PrunedStack() - skip += 2 - if len(stack) > 2*skip { - stack = stack[2*skip:] - } - n := 0 - for i := 0; i < len(stack)/2; i++ { - // We filter out based on the source code file name. - if !codeFilterRE.Match([]byte(stack[i*2+1])) { - stack[n] = stack[i*2] - stack[n+1] = stack[i*2+1] - n += 2 - } - } - stack = stack[:n] - - return bytes.Join(stack, []byte("\n")) -} +// Fail is an alias for ginkgo.Fail. +var Fail = ginkgo.Fail diff --git a/test/e2e/framework/mockservers.go b/test/e2e/framework/mockservers.go index af41ab90baf..65d9f6210e1 100644 --- a/test/e2e/framework/mockservers.go +++ b/test/e2e/framework/mockservers.go @@ -33,9 +33,11 @@ func NewMockServers(portAllocator *port.Allocator) *MockServers { httpPort := portAllocator.Get() s.tcpEchoServer = streamserver.New(streamserver.TCP, streamserver.WithBindPort(tcpPort)) s.udpEchoServer = streamserver.New(streamserver.UDP, streamserver.WithBindPort(udpPort)) - s.httpSimpleServer = httpserver.New(httpserver.WithBindPort(httpPort), httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte(consts.TestString)) - }))) + s.httpSimpleServer = httpserver.New(httpserver.WithBindPort(httpPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(consts.TestString)) + })), + ) udsIndex := portAllocator.Get() udsAddr := fmt.Sprintf("%s/frp_echo_server_%d.sock", os.TempDir(), udsIndex) @@ -54,10 +56,7 @@ func (m *MockServers) Run() error { if err := m.udsEchoServer.Run(); err != nil { return err } - if err := m.httpSimpleServer.Run(); err != nil { - return err - } - return nil + return m.httpSimpleServer.Run() } func (m *MockServers) Close() { @@ -68,19 +67,11 @@ func (m *MockServers) Close() { os.Remove(m.udsEchoServer.BindAddr()) } -func (m *MockServers) GetTemplateParams() map[string]interface{} { - ret := make(map[string]interface{}) +func (m *MockServers) GetTemplateParams() map[string]any { + ret := make(map[string]any) ret[TCPEchoServerPort] = m.tcpEchoServer.BindPort() ret[UDPEchoServerPort] = m.udpEchoServer.BindPort() ret[UDSEchoServerAddr] = m.udsEchoServer.BindAddr() ret[HTTPSimpleServerPort] = m.httpSimpleServer.BindPort() return ret } - -func (m *MockServers) GetParam(key string) interface{} { - params := m.GetTemplateParams() - if v, ok := params[key]; ok { - return v - } - return nil -} diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index 32ef2f8424c..ffbec33bf69 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -2,96 +2,220 @@ package framework import ( "fmt" + "maps" + "net" "os" "path/filepath" + "strconv" "time" + "github.com/fatedier/frp/pkg/config" flog "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/process" ) -// RunProcesses run multiple processes from templates. -// The first template should always be frps. -func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) ([]*process.Process, []*process.Process) { - templates := make([]string, 0, len(serverTemplates)+len(clientTemplates)) - for _, t := range serverTemplates { - templates = append(templates, t) - } - for _, t := range clientTemplates { - templates = append(templates, t) - } +// RunProcesses starts one frps and zero or more frpc processes from templates. +func (f *Framework) RunProcesses(serverTemplate string, clientTemplates []string) (*process.Process, []*process.Process) { + return f.RunProcessesWithBinaries(TestContext.FRPServerPath, TestContext.FRPClientPath, serverTemplate, clientTemplates) +} + +// RunProcessesWithBinaries starts one frps and zero or more frpc processes with explicit binary paths. +func (f *Framework) RunProcessesWithBinaries( + serverBinaryPath string, + clientBinaryPath string, + serverTemplate string, + clientTemplates []string, +) (*process.Process, []*process.Process) { + templates := append([]string{serverTemplate}, clientTemplates...) outs, ports, err := f.RenderTemplates(templates) ExpectNoError(err) - ExpectTrue(len(templates) > 0) - for name, port := range ports { - f.usedPorts[name] = port + maps.Copy(f.usedPorts, ports) + + // Start frps. + serverPath := filepath.Join(f.TempDirectory, "frp-e2e-server-0") + err = os.WriteFile(serverPath, []byte(outs[0]), 0o600) + ExpectNoError(err) + + if TestContext.Debug { + flog.Debugf("[%s] %s", serverPath, outs[0]) } - currentServerProcesses := make([]*process.Process, 0, len(serverTemplates)) - for i := range serverTemplates { - path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i)) - err = os.WriteFile(path, []byte(outs[i]), 0666) - ExpectNoError(err) - flog.Trace("[%s] %s", path, outs[i]) + serverProcess := process.NewWithEnvs(serverBinaryPath, []string{"-c", serverPath}, f.osEnvs) + f.serverConfPaths = append(f.serverConfPaths, serverPath) + f.serverProcesses = append(f.serverProcesses, serverProcess) + err = serverProcess.Start() + ExpectNoError(err) - p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs) - f.serverConfPaths = append(f.serverConfPaths, path) - f.serverProcesses = append(f.serverProcesses, p) - currentServerProcesses = append(currentServerProcesses, p) - err = p.Start() - ExpectNoError(err) + if port, ok := ports[consts.PortServerName]; ok { + ExpectNoError(WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), 5*time.Second)) + } else { + time.Sleep(2 * time.Second) } - time.Sleep(time.Second) - currentClientProcesses := make([]*process.Process, 0, len(clientTemplates)) + // Start frpc(s). + clientProcesses := make([]*process.Process, 0, len(clientTemplates)) for i := range clientTemplates { - index := i + len(serverTemplates) path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i)) - err = os.WriteFile(path, []byte(outs[index]), 0666) + err = os.WriteFile(path, []byte(outs[1+i]), 0o600) ExpectNoError(err) - flog.Trace("[%s] %s", path, outs[index]) - p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs) + if TestContext.Debug { + flog.Debugf("[%s] %s", path, outs[1+i]) + } + + p := process.NewWithEnvs(clientBinaryPath, []string{"-c", path}, f.osEnvs) f.clientConfPaths = append(f.clientConfPaths, path) f.clientProcesses = append(f.clientProcesses, p) - currentClientProcesses = append(currentClientProcesses, p) + clientProcesses = append(clientProcesses, p) err = p.Start() ExpectNoError(err) - time.Sleep(500 * time.Millisecond) } - time.Sleep(time.Second) + // Wait for each client's proxies to register with frps. + // If any client has no proxies (e.g. visitor-only), fall back to sleep + // for the remaining time since visitors have no deterministic readiness signal. + allConfirmed := len(clientProcesses) > 0 + start := time.Now() + for i, p := range clientProcesses { + configPath := f.clientConfPaths[len(f.clientConfPaths)-len(clientProcesses)+i] + if !waitForClientProxyReady(configPath, p, 5*time.Second) { + allConfirmed = false + } + } + if len(clientProcesses) > 0 && !allConfirmed { + remaining := 1500*time.Millisecond - time.Since(start) + if remaining > 0 { + time.Sleep(remaining) + } + } - return currentServerProcesses, currentClientProcesses + return serverProcess, clientProcesses } func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) { + p, output, err := f.StartFrps(args...) + if err != nil { + return p, output, err + } + select { + case <-p.Done(): + case <-time.After(2 * time.Second): + } + return p, p.Output(), nil +} + +// StartFrps starts frps without an implicit sleep so tests can wait on an +// explicit readiness event. +func (f *Framework) StartFrps(args ...string) (*process.Process, string, error) { p := process.NewWithEnvs(TestContext.FRPServerPath, args, f.osEnvs) f.serverProcesses = append(f.serverProcesses, p) err := p.Start() if err != nil { - return p, p.StdOutput(), err + return p, p.Output(), err } - // sleep for a while to get std output - time.Sleep(500 * time.Millisecond) - return p, p.StdOutput(), nil + return p, p.Output(), nil } func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) { + p, output, err := f.StartFrpc(args...) + if err != nil { + return p, output, err + } + select { + case <-p.Done(): + case <-time.After(1500 * time.Millisecond): + } + return p, p.Output(), nil +} + +// StartFrpc starts frpc without an implicit sleep so tests can wait on an +// explicit login or proxy-readiness event. +func (f *Framework) StartFrpc(args ...string) (*process.Process, string, error) { p := process.NewWithEnvs(TestContext.FRPClientPath, args, f.osEnvs) f.clientProcesses = append(f.clientProcesses, p) err := p.Start() if err != nil { - return p, p.StdOutput(), err + return p, p.Output(), err } - time.Sleep(500 * time.Millisecond) - return p, p.StdOutput(), nil + return p, p.Output(), nil } func (f *Framework) GenerateConfigFile(content string) string { f.configFileIndex++ path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-config-%d", f.configFileIndex)) - err := os.WriteFile(path, []byte(content), 0666) + err := os.WriteFile(path, []byte(content), 0o600) ExpectNoError(err) return path } + +// waitForClientProxyReady parses the client config to extract proxy names, +// then waits for each proxy's "start proxy success" log in the process output. +// Returns true only if proxies were expected and all registered successfully. +func waitForClientProxyReady(configPath string, p *process.Process, timeout time.Duration) bool { + _, proxyCfgs, _, _, err := config.LoadClientConfig(configPath, false) + if err != nil || len(proxyCfgs) == 0 { + return false + } + + // Use a single deadline so the total wait across all proxies does not exceed timeout. + deadline := time.Now().Add(timeout) + for _, cfg := range proxyCfgs { + remaining := time.Until(deadline) + if remaining <= 0 { + return false + } + name := cfg.GetBaseConfig().Name + pattern := fmt.Sprintf("[%s] start proxy success", name) + if err := p.WaitForOutput(pattern, 1, remaining); err != nil { + return false + } + } + return true +} + +// WaitForTCPUnreachable polls a TCP address until a connection fails or timeout. +func WaitForTCPUnreachable(addr string, interval, timeout time.Duration) error { + if interval <= 0 { + return fmt.Errorf("invalid interval for TCP unreachable on %s: interval must be positive", addr) + } + if timeout <= 0 { + return fmt.Errorf("invalid timeout for TCP unreachable on %s: timeout must be positive", addr) + } + deadline := time.Now().Add(timeout) + for { + remaining := time.Until(deadline) + if remaining <= 0 { + return fmt.Errorf("timeout waiting for TCP unreachable on %s", addr) + } + dialTimeout := min(interval, remaining) + conn, err := net.DialTimeout("tcp", addr, dialTimeout) + if err != nil { + return nil + } + conn.Close() + time.Sleep(min(interval, time.Until(deadline))) + } +} + +// WaitForTCPReady polls a TCP address until a connection succeeds or timeout. +func WaitForTCPReady(addr string, timeout time.Duration) error { + if timeout <= 0 { + return fmt.Errorf("invalid timeout for TCP readiness on %s: timeout must be positive", addr) + } + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", addr, 100*time.Millisecond) + if err == nil { + conn.Close() + return nil + } + lastErr = err + time.Sleep(50 * time.Millisecond) + } + if lastErr == nil { + return fmt.Errorf("timeout waiting for TCP readiness on %s before any dial attempt", addr) + } + return fmt.Errorf("timeout waiting for TCP readiness on %s: %w", addr, lastErr) +} diff --git a/test/e2e/framework/request.go b/test/e2e/framework/request.go index 5dccd661e4c..a6ac565a7c2 100644 --- a/test/e2e/framework/request.go +++ b/test/e2e/framework/request.go @@ -11,7 +11,7 @@ import ( func SpecifiedHTTPBodyHandler(body []byte) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - w.Write(body) + _, _ = w.Write(body) } } @@ -20,7 +20,7 @@ func ExpectResponseCode(code int) EnsureFunc { if resp.Code == code { return true } - flog.Warn("Expect code %d, but got %d", code, resp.Code) + flog.Warnf("expect code %d, but got %d", code, resp.Code) return false } } @@ -42,7 +42,7 @@ type RequestExpect struct { f *Framework expectResp []byte expectError bool - explain []interface{} + explain []any } func NewRequestExpect(f *Framework) *RequestExpect { @@ -51,7 +51,7 @@ func NewRequestExpect(f *Framework) *RequestExpect { f: f, expectResp: []byte(consts.TestString), expectError: false, - explain: make([]interface{}, 0), + explain: make([]any, 0), } } @@ -94,7 +94,7 @@ func (e *RequestExpect) ExpectError(expectErr bool) *RequestExpect { return e } -func (e *RequestExpect) Explain(explain ...interface{}) *RequestExpect { +func (e *RequestExpect) Explain(explain ...any) *RequestExpect { e.explain = explain return e } @@ -111,20 +111,16 @@ func (e *RequestExpect) Ensure(fns ...EnsureFunc) { if len(fns) == 0 { if !bytes.Equal(e.expectResp, ret.Content) { - flog.Trace("Response info: %+v", ret) + flog.Tracef("response info: %+v", ret) } - ExpectEqualValuesWithOffset(1, ret.Content, e.expectResp, e.explain...) + ExpectEqualValuesWithOffset(1, string(ret.Content), string(e.expectResp), e.explain...) } else { for _, fn := range fns { ok := fn(ret) if !ok { - flog.Trace("Response info: %+v", ret) + flog.Tracef("response info: %+v", ret) } ExpectTrueWithOffset(1, ok, e.explain...) } } } - -func (e *RequestExpect) Do() (*request.Response, error) { - return e.req.Do() -} diff --git a/test/e2e/framework/test_context.go b/test/e2e/framework/test_context.go index 958c78f8d7b..38f0fb25782 100644 --- a/test/e2e/framework/test_context.go +++ b/test/e2e/framework/test_context.go @@ -4,8 +4,6 @@ import ( "flag" "fmt" "os" - - "github.com/onsi/ginkgo/config" ) type TestContextType struct { @@ -24,14 +22,7 @@ var TestContext TestContextType // The other Register*Flags methods below can be used to add more // test-specific flags. However, those settings then get added // regardless whether the test is actually in the test suite. -// func RegisterCommonFlags(flags *flag.FlagSet) { - // Turn on EmitSpecProgress to get spec progress (especially on interrupt) - config.GinkgoConfig.EmitSpecProgress = true - - // Randomize specs as well as suites - config.GinkgoConfig.RandomizeAllSpecs = true - flags.StringVar(&TestContext.FRPClientPath, "frpc-path", "../../bin/frpc", "The frp client binary to use.") flags.StringVar(&TestContext.FRPServerPath, "frps-path", "../../bin/frps", "The frp server binary to use.") flags.StringVar(&TestContext.LogLevel, "log-level", "debug", "Log level.") diff --git a/test/e2e/basic/basic.go b/test/e2e/legacy/basic/basic.go similarity index 66% rename from test/e2e/basic/basic.go rename to test/e2e/legacy/basic/basic.go index 13f17168477..0115c6cb926 100644 --- a/test/e2e/basic/basic.go +++ b/test/e2e/legacy/basic/basic.go @@ -4,6 +4,9 @@ import ( "crypto/tls" "fmt" "strings" + "time" + + "github.com/onsi/ginkgo/v2" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/test/e2e/framework" @@ -12,20 +15,19 @@ import ( "github.com/fatedier/frp/test/e2e/mock/server/streamserver" "github.com/fatedier/frp/test/e2e/pkg/port" "github.com/fatedier/frp/test/e2e/pkg/request" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Basic]", func() { +var _ = ginkgo.Describe("[Feature: Basic]", func() { f := framework.NewDefaultFramework() - Describe("TCP && UDP", func() { + ginkgo.Describe("TCP && UDP", func() { types := []string{"tcp", "udp"} for _, t := range types { proxyType := t - It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { + serverConf := consts.LegacyDefaultServerConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) localPortName := "" protocol := "tcp" @@ -77,10 +79,10 @@ var _ = Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f). @@ -93,15 +95,16 @@ var _ = Describe("[Feature: Basic]", func() { } }) - Describe("HTTP", func() { - It("proxy to HTTP server", func() { - serverConf := consts.DefaultServerConfig + ginkgo.Describe("HTTP", func() { + ginkgo.It("proxy to HTTP server", func() { + serverConf := consts.LegacyDefaultServerConfig vhostHTTPPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` @@ -146,13 +149,13 @@ var _ = Describe("[Feature: Basic]", func() { if tests[i].customDomains == "" { tests[i].customDomains = test.proxyName + ".example.com" } - clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { - for _, domain := range strings.Split(test.customDomains, ",") { + for domain := range strings.SplitSeq(test.customDomains, ",") { domain = strings.TrimSpace(domain) framework.NewRequestExpect(f). Explain(test.proxyName + "-" + domain). @@ -175,16 +178,17 @@ var _ = Describe("[Feature: Basic]", func() { }) }) - Describe("HTTPS", func() { - It("proxy to HTTPS server", func() { - serverConf := consts.DefaultServerConfig + ginkgo.Describe("HTTPS", func() { + ginkgo.It("proxy to HTTPS server", func() { + serverConf := consts.LegacyDefaultServerConfig vhostHTTPSPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_https_port = %d `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` [%s] @@ -228,22 +232,22 @@ var _ = Describe("[Feature: Basic]", func() { if tests[i].customDomains == "" { tests[i].customDomains = test.proxyName + ".example.com" } - clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf.String()}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) localServer := httpserver.New( httpserver.WithBindPort(localPort), - httpserver.WithTlsConfig(tlsConfig), + httpserver.WithTLSConfig(tlsConfig), httpserver.WithResponse([]byte("test")), ) f.RunServer("", localServer) for _, test := range tests { - for _, domain := range strings.Split(test.customDomains, ",") { + for domain := range strings.SplitSeq(test.customDomains, ",") { domain = strings.TrimSpace(domain) framework.NewRequestExpect(f). Explain(test.proxyName + "-" + domain). @@ -275,14 +279,18 @@ var _ = Describe("[Feature: Basic]", func() { }) }) - Describe("STCP && SUDP", func() { - types := []string{"stcp", "sudp"} + ginkgo.Describe("STCP && SUDP && XTCP", func() { + types := []string{"stcp", "sudp", "xtcp"} for _, t := range types { proxyType := t - It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { - serverConf := consts.DefaultServerConfig - clientServerConf := consts.DefaultClientConfig - clientVisitorConf := consts.DefaultClientConfig + ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { + serverConf := consts.LegacyDefaultServerConfig + var clientServerConf strings.Builder + clientServerConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user1") + var clientVisitorConf strings.Builder + clientVisitorConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user1") + var clientUser2VisitorConf strings.Builder + clientUser2VisitorConf.WriteString(consts.LegacyDefaultClientConfig + "\nuser = user2") localPortName := "" protocol := "tcp" @@ -293,6 +301,10 @@ var _ = Describe("[Feature: Basic]", func() { case "sudp": localPortName = framework.UDPEchoServerPort protocol = "udp" + case "xtcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + ginkgo.Skip("stun server is not stable") } correctSK := "abc" @@ -319,37 +331,46 @@ var _ = Describe("[Feature: Basic]", func() { } tests := []struct { - proxyName string - bindPortName string - visitorSK string - extraConfig string - expectError bool + proxyName string + bindPortName string + visitorSK string + commonExtraConfig string + proxyExtraConfig string + visitorExtraConfig string + expectError bool + deployUser2Client bool + // skipXTCP is used to skip xtcp test case + skipXTCP bool }{ { proxyName: "normal", bindPortName: port.GenName("Normal"), visitorSK: correctSK, + skipXTCP: true, }, { - proxyName: "with-encryption", - bindPortName: port.GenName("WithEncryption"), - visitorSK: correctSK, - extraConfig: "use_encryption = true", + proxyName: "with-encryption", + bindPortName: port.GenName("WithEncryption"), + visitorSK: correctSK, + commonExtraConfig: "use_encryption = true", + skipXTCP: true, }, { - proxyName: "with-compression", - bindPortName: port.GenName("WithCompression"), - visitorSK: correctSK, - extraConfig: "use_compression = true", + proxyName: "with-compression", + bindPortName: port.GenName("WithCompression"), + visitorSK: correctSK, + commonExtraConfig: "use_compression = true", + skipXTCP: true, }, { proxyName: "with-encryption-and-compression", bindPortName: port.GenName("WithEncryptionAndCompression"), visitorSK: correctSK, - extraConfig: ` + commonExtraConfig: ` use_encryption = true use_compression = true `, + skipXTCP: true, }, { proxyName: "with-error-sk", @@ -357,20 +378,61 @@ var _ = Describe("[Feature: Basic]", func() { visitorSK: wrongSK, expectError: true, }, + { + proxyName: "allowed-user", + bindPortName: port.GenName("AllowedUser"), + visitorSK: correctSK, + proxyExtraConfig: "allow_users = another, user2", + visitorExtraConfig: "server_user = user1", + deployUser2Client: true, + }, + { + proxyName: "not-allowed-user", + bindPortName: port.GenName("NotAllowedUser"), + visitorSK: correctSK, + proxyExtraConfig: "allow_users = invalid", + visitorExtraConfig: "server_user = user1", + expectError: true, + }, + { + proxyName: "allow-all", + bindPortName: port.GenName("AllowAll"), + visitorSK: correctSK, + proxyExtraConfig: "allow_users = *", + visitorExtraConfig: "server_user = user1", + deployUser2Client: true, + }, } // build all client config for _, test := range tests { - clientServerConf += getProxyServerConf(test.proxyName, test.extraConfig) + "\n" + clientServerConf.WriteString(getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n") } for _, test := range tests { - clientVisitorConf += getProxyVisitorConf(test.proxyName, test.bindPortName, test.visitorSK, test.extraConfig) + "\n" + config := getProxyVisitorConf( + test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig, + ) + "\n" + if test.deployUser2Client { + clientUser2VisitorConf.WriteString(config) + } else { + clientVisitorConf.WriteString(config) + } } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf}) + f.RunProcesses(serverConf, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) for _, test := range tests { + timeout := time.Second + if t == "xtcp" { + if test.skipXTCP { + continue + } + timeout = 10 * time.Second + } framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Timeout(timeout) + }). Protocol(protocol). PortName(test.bindPortName). Explain(test.proxyName). @@ -381,10 +443,11 @@ var _ = Describe("[Feature: Basic]", func() { } }) - Describe("TCPMUX", func() { - It("Type tcpmux", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.Describe("TCPMUX", func() { + ginkgo.It("Type tcpmux", func() { + serverConf := consts.LegacyDefaultServerConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") serverConf += fmt.Sprintf(` @@ -427,14 +490,14 @@ var _ = Describe("[Feature: Basic]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.extraConfig) + "\n") localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName))) f.RunServer(port.GenName(test.proxyName), localServer) } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf.String()}) // Request without HTTP connect should get error framework.NewRequestExpect(f). diff --git a/test/e2e/basic/client.go b/test/e2e/legacy/basic/client.go similarity index 57% rename from test/e2e/basic/client.go rename to test/e2e/legacy/basic/client.go index 8d370928c51..d9b9b6dadfc 100644 --- a/test/e2e/basic/client.go +++ b/test/e2e/legacy/basic/client.go @@ -1,24 +1,24 @@ package basic import ( + "context" "fmt" "strconv" "strings" "time" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/request" - clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: ClientManage]", func() { +var _ = ginkgo.Describe("[Feature: ClientManage]", func() { f := framework.NewDefaultFramework() - It("Update && Reload API", func() { - serverConf := consts.DefaultServerConfig + ginkgo.It("Update && Reload API", func() { + serverConf := consts.LegacyDefaultServerConfig adminPort := f.AllocPort() @@ -26,7 +26,7 @@ var _ = Describe("[Feature: ClientManage]", func() { p2Port := f.AllocPort() p3Port := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` admin_port = %d [p1] @@ -48,26 +48,28 @@ var _ = Describe("[Feature: ClientManage]", func() { framework.TCPEchoServerPort, p2Port, framework.TCPEchoServerPort, p3Port) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(p1Port).Ensure() framework.NewRequestExpect(f).Port(p2Port).Ensure() framework.NewRequestExpect(f).Port(p3Port).Ensure() - client := clientsdk.New("127.0.0.1", adminPort) - conf, err := client.GetConfig() + client := f.APIClientForFrpc(adminPort) + conf, err := client.GetConfig(context.Background()) framework.ExpectNoError(err) newP2Port := f.AllocPort() // change p2 port and remove p3 proxy newClientConf := strings.ReplaceAll(conf, strconv.Itoa(p2Port), strconv.Itoa(newP2Port)) p3Index := strings.Index(newClientConf, "[p3]") - newClientConf = newClientConf[:p3Index] + if p3Index >= 0 { + newClientConf = newClientConf[:p3Index] + } - err = client.UpdateConfig(newClientConf) + err = client.UpdateConfig(context.Background(), newClientConf) framework.ExpectNoError(err) - err = client.Reload() + err = client.Reload(context.Background(), true) framework.ExpectNoError(err) time.Sleep(time.Second) @@ -77,18 +79,18 @@ var _ = Describe("[Feature: ClientManage]", func() { framework.NewRequestExpect(f).Port(p3Port).Explain("p3 port").ExpectError(true).Ensure() }) - It("healthz", func() { - serverConf := consts.DefaultServerConfig + ginkgo.It("healthz", func() { + serverConf := consts.LegacyDefaultServerConfig dashboardPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` admin_addr = 0.0.0.0 admin_port = %d admin_user = admin admin_pwd = admin `, dashboardPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPPath("/healthz") @@ -100,4 +102,31 @@ var _ = Describe("[Feature: ClientManage]", func() { Ensure(framework.ExpectResponseCode(401)) }) + ginkgo.It("stop", func() { + serverConf := consts.LegacyDefaultServerConfig + + adminPort := f.AllocPort() + testPort := f.AllocPort() + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` + admin_port = %d + + [test] + type = tcp + local_port = {{ .%s }} + remote_port = %d + `, adminPort, framework.TCPEchoServerPort, testPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(testPort).Ensure() + + client := f.APIClientForFrpc(adminPort) + err := client.Stop(context.Background()) + framework.ExpectNoError(err) + + time.Sleep(3 * time.Second) + + // frpc stopped so the port is not listened, expect error + framework.NewRequestExpect(f).Port(testPort).ExpectError(true).Ensure() + }) }) diff --git a/test/e2e/basic/client_server.go b/test/e2e/legacy/basic/client_server.go similarity index 59% rename from test/e2e/basic/client_server.go rename to test/e2e/legacy/basic/client_server.go index c19077c4825..330b9cfc2c3 100644 --- a/test/e2e/basic/client_server.go +++ b/test/e2e/legacy/basic/client_server.go @@ -3,24 +3,43 @@ package basic import ( "fmt" "strings" + "time" + + "github.com/onsi/ginkgo/v2" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/cert" "github.com/fatedier/frp/test/e2e/pkg/port" - - . "github.com/onsi/ginkgo" ) type generalTestConfigures struct { - server string - client string - expectError bool + server string + client string + clientPrefix string + client2 string + client2Prefix string + testDelay time.Duration + expectError bool +} + +func renderBindPortConfig(protocol string) string { + switch protocol { + case "kcp": + return fmt.Sprintf(`kcp_bind_port = {{ .%s }}`, consts.PortServerName) + case "quic": + return fmt.Sprintf(`quic_bind_port = {{ .%s }}`, consts.PortServerName) + default: + return "" + } } func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig + if configures.clientPrefix != "" { + clientConf = configures.clientPrefix + } serverConf += fmt.Sprintf(` %s @@ -45,7 +64,23 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur framework.UDPEchoServerPort, udpPortName, ) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + clientConfs := []string{clientConf} + if configures.client2 != "" { + client2Conf := consts.LegacyDefaultClientConfig + if configures.client2Prefix != "" { + client2Conf = configures.client2Prefix + } + client2Conf += fmt.Sprintf(` + %s + `, configures.client2) + clientConfs = append(clientConfs, client2Conf) + } + + f.RunProcesses(serverConf, clientConfs) + + if configures.testDelay > 0 { + time.Sleep(configures.testDelay) + } framework.NewRequestExpect(f).PortName(tcpPortName).ExpectError(configures.expectError).Explain("tcp proxy").Ensure() framework.NewRequestExpect(f).Protocol("udp"). @@ -54,29 +89,55 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur // defineClientServerTest test a normal tcp and udp proxy with specified TestConfigures. func defineClientServerTest(desc string, f *framework.Framework, configures *generalTestConfigures) { - It(desc, func() { + ginkgo.It(desc, func() { runClientServerTest(f, configures) }) } -var _ = Describe("[Feature: Client-Server]", func() { +var _ = ginkgo.Describe("[Feature: Client-Server]", func() { f := framework.NewDefaultFramework() - Describe("Protocol", func() { - supportProtocols := []string{"tcp", "kcp", "websocket"} + ginkgo.Describe("Protocol", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} for _, protocol := range supportProtocols { configures := &generalTestConfigures{ server: fmt.Sprintf(` - kcp_bind_port = {{ .%s }} - protocol = %s" - `, consts.PortServerName, protocol), + %s + `, renderBindPortConfig(protocol)), client: "protocol = " + protocol, } defineClientServerTest(protocol, f, configures) } }) - Describe("Authentication", func() { + // wss is special, it needs to be tested separately. + // frps only supports ws, so there should be a proxy to terminate TLS before frps. + ginkgo.Describe("Protocol wss", func() { + wssPort := f.AllocPort() + configures := &generalTestConfigures{ + clientPrefix: fmt.Sprintf(` + [common] + server_addr = 127.0.0.1 + server_port = %d + protocol = wss + log_level = trace + login_fail_exit = false + `, wssPort), + // Due to the fact that frps cannot directly accept wss connections, we use the https2http plugin of another frpc to terminate TLS. + client2: fmt.Sprintf(` + [wss2ws] + type = tcp + remote_port = %d + plugin = https2http + plugin_local_addr = 127.0.0.1:{{ .%s }} + `, wssPort, consts.PortServerName), + testDelay: 10 * time.Second, + } + + defineClientServerTest("wss", f, configures) + }) + + ginkgo.Describe("Authentication", func() { defineClientServerTest("Token Correct", f, &generalTestConfigures{ server: "token = 123456", client: "token = 123456", @@ -89,16 +150,17 @@ var _ = Describe("[Feature: Client-Server]", func() { }) }) - Describe("TLS", func() { - supportProtocols := []string{"tcp", "kcp", "websocket"} + ginkgo.Describe("TLS", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} for _, protocol := range supportProtocols { tmp := protocol - defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ + // Since v0.50.0, the default value of tls_enable has been changed to true. + // Therefore, here it needs to be set as false to test the scenario of turning it off. + defineClientServerTest("Disable TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ server: fmt.Sprintf(` - kcp_bind_port = {{ .%s }} - protocol = %s - `, consts.PortServerName, protocol), - client: fmt.Sprintf(`tls_enable = true + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(`tls_enable = false protocol = %s `, protocol), }) @@ -106,32 +168,33 @@ var _ = Describe("[Feature: Client-Server]", func() { defineClientServerTest("enable tls_only, client with TLS", f, &generalTestConfigures{ server: "tls_only = true", - client: "tls_enable = true", }) defineClientServerTest("enable tls_only, client without TLS", f, &generalTestConfigures{ server: "tls_only = true", + client: "tls_enable = false", expectError: true, }) }) - Describe("TLS with custom certificate", func() { - supportProtocols := []string{"tcp", "kcp", "websocket"} + ginkgo.Describe("TLS with custom certificate", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} var ( caCrtPath string serverCrtPath, serverKeyPath string clientCrtPath, clientKeyPath string ) - JustBeforeEach(func() { + ginkgo.JustBeforeEach(func() { generator := &cert.SelfSignedCertGenerator{} - artifacts, err := generator.Generate("0.0.0.0") + artifacts, err := generator.Generate("127.0.0.1") framework.ExpectNoError(err) caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert)) serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert)) serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key)) generator.SetCA(artifacts.CACert, artifacts.CAKey) - generator.Generate("0.0.0.0") + _, err = generator.Generate("127.0.0.1") + framework.ExpectNoError(err) clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert)) clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key)) }) @@ -139,34 +202,30 @@ var _ = Describe("[Feature: Client-Server]", func() { for _, protocol := range supportProtocols { tmp := protocol - It("one-way authentication: "+tmp, func() { + ginkgo.It("one-way authentication: "+tmp, func() { runClientServerTest(f, &generalTestConfigures{ server: fmt.Sprintf(` - protocol = %s - kcp_bind_port = {{ .%s }} + %s tls_trusted_ca_file = %s - `, tmp, consts.PortServerName, caCrtPath), + `, renderBindPortConfig(tmp), caCrtPath), client: fmt.Sprintf(` protocol = %s - tls_enable = true tls_cert_file = %s tls_key_file = %s `, tmp, clientCrtPath, clientKeyPath), }) }) - It("mutual authentication: "+tmp, func() { + ginkgo.It("mutual authentication: "+tmp, func() { runClientServerTest(f, &generalTestConfigures{ server: fmt.Sprintf(` - protocol = %s - kcp_bind_port = {{ .%s }} + %s tls_cert_file = %s tls_key_file = %s tls_trusted_ca_file = %s - `, tmp, consts.PortServerName, serverCrtPath, serverKeyPath, caCrtPath), + `, renderBindPortConfig(tmp), serverCrtPath, serverKeyPath, caCrtPath), client: fmt.Sprintf(` protocol = %s - tls_enable = true tls_cert_file = %s tls_key_file = %s tls_trusted_ca_file = %s @@ -176,13 +235,13 @@ var _ = Describe("[Feature: Client-Server]", func() { } }) - Describe("TLS with custom certificate and specified server name", func() { + ginkgo.Describe("TLS with custom certificate and specified server name", func() { var ( caCrtPath string serverCrtPath, serverKeyPath string clientCrtPath, clientKeyPath string ) - JustBeforeEach(func() { + ginkgo.JustBeforeEach(func() { generator := &cert.SelfSignedCertGenerator{} artifacts, err := generator.Generate("example.com") framework.ExpectNoError(err) @@ -191,12 +250,13 @@ var _ = Describe("[Feature: Client-Server]", func() { serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert)) serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key)) generator.SetCA(artifacts.CACert, artifacts.CAKey) - generator.Generate("example.com") + _, err = generator.Generate("example.com") + framework.ExpectNoError(err) clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert)) clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key)) }) - It("mutual authentication", func() { + ginkgo.It("mutual authentication", func() { runClientServerTest(f, &generalTestConfigures{ server: fmt.Sprintf(` tls_cert_file = %s @@ -204,7 +264,6 @@ var _ = Describe("[Feature: Client-Server]", func() { tls_trusted_ca_file = %s `, serverCrtPath, serverKeyPath, caCrtPath), client: fmt.Sprintf(` - tls_enable = true tls_server_name = example.com tls_cert_file = %s tls_key_file = %s @@ -213,7 +272,7 @@ var _ = Describe("[Feature: Client-Server]", func() { }) }) - It("mutual authentication with incorrect server name", func() { + ginkgo.It("mutual authentication with incorrect server name", func() { runClientServerTest(f, &generalTestConfigures{ server: fmt.Sprintf(` tls_cert_file = %s @@ -221,7 +280,6 @@ var _ = Describe("[Feature: Client-Server]", func() { tls_trusted_ca_file = %s `, serverCrtPath, serverKeyPath, caCrtPath), client: fmt.Sprintf(` - tls_enable = true tls_server_name = invalid.com tls_cert_file = %s tls_key_file = %s @@ -232,38 +290,33 @@ var _ = Describe("[Feature: Client-Server]", func() { }) }) - Describe("TLS with disable_custom_tls_first_byte", func() { - supportProtocols := []string{"tcp", "kcp", "websocket"} + ginkgo.Describe("TLS with disable_custom_tls_first_byte set to false", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} for _, protocol := range supportProtocols { tmp := protocol defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ server: fmt.Sprintf(` - kcp_bind_port = {{ .%s }} - protocol = %s - `, consts.PortServerName, protocol), + %s + `, renderBindPortConfig(protocol)), client: fmt.Sprintf(` - tls_enable = true protocol = %s - disable_custom_tls_first_byte = true + disable_custom_tls_first_byte = false `, protocol), }) } }) - Describe("IPv6 bind address", func() { - supportProtocols := []string{"tcp", "kcp", "websocket"} + ginkgo.Describe("IPv6 bind address", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} for _, protocol := range supportProtocols { tmp := protocol defineClientServerTest("IPv6 bind address: "+strings.ToUpper(tmp), f, &generalTestConfigures{ server: fmt.Sprintf(` bind_addr = :: - kcp_bind_port = {{ .%s }} - protocol = %s - `, consts.PortServerName, protocol), + %s + `, renderBindPortConfig(protocol)), client: fmt.Sprintf(` - tls_enable = true protocol = %s - disable_custom_tls_first_byte = true `, protocol), }) } diff --git a/test/e2e/basic/cmd.go b/test/e2e/legacy/basic/cmd.go similarity index 79% rename from test/e2e/basic/cmd.go rename to test/e2e/legacy/basic/cmd.go index 562310fdf0e..7d86233a0ad 100644 --- a/test/e2e/basic/cmd.go +++ b/test/e2e/legacy/basic/cmd.go @@ -1,25 +1,24 @@ package basic import ( - "fmt" "strconv" "strings" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/pkg/request" - - . "github.com/onsi/ginkgo" ) const ( ConfigValidStr = "syntax is ok" ) -var _ = Describe("[Feature: Cmd]", func() { +var _ = ginkgo.Describe("[Feature: Cmd]", func() { f := framework.NewDefaultFramework() - Describe("Verify", func() { - It("frps valid", func() { + ginkgo.Describe("Verify", func() { + ginkgo.It("frps valid", func() { path := f.GenerateConfigFile(` [common] bind_addr = 0.0.0.0 @@ -29,7 +28,7 @@ var _ = Describe("[Feature: Cmd]", func() { framework.ExpectNoError(err) framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output) }) - It("frps invalid", func() { + ginkgo.It("frps invalid", func() { path := f.GenerateConfigFile(` [common] bind_addr = 0.0.0.0 @@ -39,7 +38,7 @@ var _ = Describe("[Feature: Cmd]", func() { framework.ExpectNoError(err) framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output) }) - It("frpc valid", func() { + ginkgo.It("frpc valid", func() { path := f.GenerateConfigFile(` [common] server_addr = 0.0.0.0 @@ -49,7 +48,7 @@ var _ = Describe("[Feature: Cmd]", func() { framework.ExpectNoError(err) framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output) }) - It("frpc invalid", func() { + ginkgo.It("frpc invalid", func() { path := f.GenerateConfigFile(` [common] server_addr = 0.0.0.0 @@ -62,29 +61,29 @@ var _ = Describe("[Feature: Cmd]", func() { }) }) - Describe("Single proxy", func() { - It("TCP", func() { + ginkgo.Describe("Single proxy", func() { + ginkgo.It("TCP", func() { serverPort := f.AllocPort() _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort)) framework.ExpectNoError(err) localPort := f.PortByName(framework.TCPEchoServerPort) remotePort := f.AllocPort() - _, _, err = f.RunFrpc("tcp", "-s", fmt.Sprintf("127.0.0.1:%d", serverPort), "-t", "123", "-u", "test", + _, _, err = f.RunFrpc("tcp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "tcp_test") framework.ExpectNoError(err) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) - It("UDP", func() { + ginkgo.It("UDP", func() { serverPort := f.AllocPort() _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort)) framework.ExpectNoError(err) localPort := f.PortByName(framework.UDPEchoServerPort) remotePort := f.AllocPort() - _, _, err = f.RunFrpc("udp", "-s", fmt.Sprintf("127.0.0.1:%d", serverPort), "-t", "123", "-u", "test", + _, _, err = f.RunFrpc("udp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "udp_test") framework.ExpectNoError(err) @@ -92,13 +91,13 @@ var _ = Describe("[Feature: Cmd]", func() { Port(remotePort).Ensure() }) - It("HTTP", func() { + ginkgo.It("HTTP", func() { serverPort := f.AllocPort() vhostHTTPPort := f.AllocPort() _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost_http_port", strconv.Itoa(vhostHTTPPort)) framework.ExpectNoError(err) - _, _, err = f.RunFrpc("http", "-s", "127.0.0.1:"+strconv.Itoa(serverPort), "-t", "123", "-u", "test", + _, _, err = f.RunFrpc("http", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", "-n", "udp_test", "-l", strconv.Itoa(f.PortByName(framework.HTTPSimpleServerPort)), "--custom_domain", "test.example.com") framework.ExpectNoError(err) diff --git a/test/e2e/basic/config.go b/test/e2e/legacy/basic/config.go similarity index 80% rename from test/e2e/basic/config.go rename to test/e2e/legacy/basic/config.go index d21bc57c9e2..334cc062e5b 100644 --- a/test/e2e/basic/config.go +++ b/test/e2e/legacy/basic/config.go @@ -3,20 +3,20 @@ package basic import ( "fmt" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/port" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Config]", func() { +var _ = ginkgo.Describe("[Feature: Config]", func() { f := framework.NewDefaultFramework() - Describe("Template", func() { - It("render by env", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.Describe("Template", func() { + ginkgo.It("render by env", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig portName := port.GenName("TCP") serverConf += fmt.Sprintf(` @@ -33,14 +33,14 @@ var _ = Describe("[Feature: Config]", func() { `, "`", "`", framework.TCPEchoServerPort, portName) f.SetEnvs([]string{"FRP_TOKEN=123"}) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).PortName(portName).Ensure() }) }) - Describe("Includes", func() { - It("split tcp proxies into different files", func() { + ginkgo.Describe("Includes", func() { + ginkgo.It("split tcp proxies into different files", func() { serverPort := f.AllocPort() serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(` [common] diff --git a/test/e2e/basic/http.go b/test/e2e/legacy/basic/http.go similarity index 86% rename from test/e2e/basic/http.go rename to test/e2e/legacy/basic/http.go index d3b9bce2f85..790521dbab8 100644 --- a/test/e2e/basic/http.go +++ b/test/e2e/legacy/basic/http.go @@ -6,20 +6,20 @@ import ( "net/url" "strconv" + "github.com/gorilla/websocket" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/mock/server/httpserver" "github.com/fatedier/frp/test/e2e/pkg/request" - - "github.com/gorilla/websocket" - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: HTTP]", func() { +var _ = ginkgo.Describe("[Feature: HTTP]", func() { f := framework.NewDefaultFramework() getDefaultServerConf := func(vhostHTTPPort int) string { - conf := consts.DefaultServerConfig + ` + conf := consts.LegacyDefaultServerConfig + ` vhost_http_port = %d ` return fmt.Sprintf(conf, vhostHTTPPort) @@ -31,7 +31,7 @@ var _ = Describe("[Feature: HTTP]", func() { ) } - It("HTTP route by locations", func() { + ginkgo.It("HTTP route by locations", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) @@ -41,7 +41,7 @@ var _ = Describe("[Feature: HTTP]", func() { barPort := f.AllocPort() f.RunServer("", newHTTPServer(barPort, "bar")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http @@ -56,7 +56,7 @@ var _ = Describe("[Feature: HTTP]", func() { locations = /bar `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tests := []struct { path string @@ -78,7 +78,7 @@ var _ = Describe("[Feature: HTTP]", func() { } }) - It("HTTP route by HTTP user", func() { + ginkgo.It("HTTP route by HTTP user", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) @@ -91,7 +91,7 @@ var _ = Describe("[Feature: HTTP]", func() { otherPort := f.AllocPort() f.RunServer("", newHTTPServer(otherPort, "other")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http @@ -111,7 +111,7 @@ var _ = Describe("[Feature: HTTP]", func() { custom_domains = normal.example.com `, fooPort, barPort, otherPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // user1 framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort). @@ -138,11 +138,11 @@ var _ = Describe("[Feature: HTTP]", func() { Ensure() }) - It("HTTP Basic Auth", func() { + ginkgo.It("HTTP Basic Auth", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -152,7 +152,7 @@ var _ = Describe("[Feature: HTTP]", func() { http_pwd = test `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -176,11 +176,11 @@ var _ = Describe("[Feature: HTTP]", func() { Ensure() }) - It("Wildcard domain", func() { + ginkgo.It("Wildcard domain", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -188,7 +188,7 @@ var _ = Describe("[Feature: HTTP]", func() { custom_domains = *.example.com `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not match host framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -212,7 +212,7 @@ var _ = Describe("[Feature: HTTP]", func() { Ensure() }) - It("Subdomain", func() { + ginkgo.It("Subdomain", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) serverConf += ` @@ -225,7 +225,7 @@ var _ = Describe("[Feature: HTTP]", func() { barPort := f.AllocPort() f.RunServer("", newHTTPServer(barPort, "bar")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http @@ -238,7 +238,7 @@ var _ = Describe("[Feature: HTTP]", func() { subdomain = bar `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // foo framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). @@ -257,7 +257,7 @@ var _ = Describe("[Feature: HTTP]", func() { Ensure() }) - It("Modify headers", func() { + ginkgo.It("Modify headers", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) @@ -265,12 +265,12 @@ var _ = Describe("[Feature: HTTP]", func() { localServer := httpserver.New( httpserver.WithBindPort(localPort), httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte(req.Header.Get("X-From-Where"))) + _, _ = w.Write([]byte(req.Header.Get("X-From-Where"))) })), ) f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -279,7 +279,7 @@ var _ = Describe("[Feature: HTTP]", func() { header_X-From-Where = frp `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // not set auth header framework.NewRequestExpect(f).Port(vhostHTTPPort). @@ -290,7 +290,7 @@ var _ = Describe("[Feature: HTTP]", func() { Ensure() }) - It("Host Header Rewrite", func() { + ginkgo.It("Host Header Rewrite", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) @@ -298,12 +298,12 @@ var _ = Describe("[Feature: HTTP]", func() { localServer := httpserver.New( httpserver.WithBindPort(localPort), httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte(req.Host)) + _, _ = w.Write([]byte(req.Host)) })), ) f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -312,7 +312,7 @@ var _ = Describe("[Feature: HTTP]", func() { host_header_rewrite = rewrite.example.com `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -322,7 +322,7 @@ var _ = Describe("[Feature: HTTP]", func() { Ensure() }) - It("Websocket protocol", func() { + ginkgo.It("Websocket protocol", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) @@ -352,7 +352,7 @@ var _ = Describe("[Feature: HTTP]", func() { f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -360,7 +360,7 @@ var _ = Describe("[Feature: HTTP]", func() { custom_domains = 127.0.0.1 `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)} c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) @@ -374,7 +374,7 @@ var _ = Describe("[Feature: HTTP]", func() { framework.ExpectEqualValues(consts.TestString, string(msg)) }) - It("Ip allow list", func() { + ginkgo.It("Ip allow list", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) serverConf += ` @@ -390,29 +390,27 @@ var _ = Describe("[Feature: HTTP]", func() { bazPort := f.AllocPort() f.RunServer("", newHTTPServer(bazPort, "baz")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http local_port = %d subdomain = foo - ips_allow_list = "" - [bar] type = http local_port = %d subdomain = bar - ips_allow_list = "127.0.0.1/16" + ips_allow_list = 127.0.0.1/16 [baz] type = http local_port = %d subdomain = baz - ips_allow_list = "127.1.0.1/16" + ips_allow_list = 127.1.0.1/16 `, fooPort, barPort, bazPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // The request should pass in case in allow list is empty string framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). diff --git a/test/e2e/basic/server.go b/test/e2e/legacy/basic/server.go similarity index 65% rename from test/e2e/basic/server.go rename to test/e2e/legacy/basic/server.go index 74b421be5b1..3407a165695 100644 --- a/test/e2e/basic/server.go +++ b/test/e2e/legacy/basic/server.go @@ -1,34 +1,36 @@ package basic import ( + "context" "fmt" "net" "strconv" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/port" "github.com/fatedier/frp/test/e2e/pkg/request" - clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Server Manager]", func() { +var _ = ginkgo.Describe("[Feature: Server Manager]", func() { f := framework.NewDefaultFramework() - It("Ports Whitelist", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.It("Ports Whitelist", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig + tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000}) + udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000}) serverConf += ` - allow_ports = 20000-25000,25002,30000-50000 + allow_ports = 10000-11000,11002,12000-13000 ` - tcpPortName := port.GenName("TCP", port.WithRangePorts(20000, 25000)) - udpPortName := port.GenName("UDP", port.WithRangePorts(30000, 50000)) + tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000)) + udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000)) clientConf += fmt.Sprintf(` - [tcp-allowded-in-range] + [tcp-allowed-in-range] type = tcp local_port = {{ .%s }} remote_port = {{ .%s }} @@ -37,8 +39,8 @@ var _ = Describe("[Feature: Server Manager]", func() { [tcp-port-not-allowed] type = tcp local_port = {{ .%s }} - remote_port = 20001 - `, framework.TCPEchoServerPort) + remote_port = %d + `, framework.TCPEchoServerPort, tcpPortNotAllowed) clientConf += fmt.Sprintf(` [tcp-port-unavailable] type = tcp @@ -55,17 +57,17 @@ var _ = Describe("[Feature: Server Manager]", func() { [udp-port-not-allowed] type = udp local_port = {{ .%s }} - remote_port = 20003 - `, framework.UDPEchoServerPort) + remote_port = %d + `, framework.UDPEchoServerPort, udpPortNotAllowed) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // TCP // Allowed in range framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() // Not Allowed - framework.NewRequestExpect(f).Port(25003).ExpectError(true).Ensure() + framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure() // Unavailable, already bind by frps framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure() @@ -76,13 +78,13 @@ var _ = Describe("[Feature: Server Manager]", func() { // Not Allowed framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { - r.UDP().Port(25003) + r.UDP().Port(udpPortNotAllowed) }).ExpectError(true).Ensure() }) - It("Alloc Random Port", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.It("Alloc Random Port", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig adminPort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -97,12 +99,12 @@ var _ = Describe("[Feature: Server Manager]", func() { local_port = {{ .%s }} `, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) - client := clientsdk.New("127.0.0.1", adminPort) + client := f.APIClientForFrpc(adminPort) // tcp random port - status, err := client.GetProxyStatus("tcp") + status, err := client.GetProxyStatus(context.Background(), "tcp") framework.ExpectNoError(err) _, portStr, err := net.SplitHostPort(status.RemoteAddr) @@ -113,7 +115,7 @@ var _ = Describe("[Feature: Server Manager]", func() { framework.NewRequestExpect(f).Port(port).Ensure() // udp random port - status, err = client.GetProxyStatus("udp") + status, err = client.GetProxyStatus(context.Background(), "udp") framework.ExpectNoError(err) _, portStr, err = net.SplitHostPort(status.RemoteAddr) @@ -124,29 +126,29 @@ var _ = Describe("[Feature: Server Manager]", func() { framework.NewRequestExpect(f).Protocol("udp").Port(port).Ensure() }) - It("Port Reuse", func() { - serverConf := consts.DefaultServerConfig + ginkgo.It("Port Reuse", func() { + serverConf := consts.LegacyDefaultServerConfig // Use same port as PortServer serverConf += fmt.Sprintf(` vhost_http_port = {{ .%s }} `, consts.PortServerName) - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [http] type = http local_port = {{ .%s }} custom_domains = example.com `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("example.com") }).PortName(consts.PortServerName).Ensure() }) - It("healthz", func() { - serverConf := consts.DefaultServerConfig + ginkgo.It("healthz", func() { + serverConf := consts.LegacyDefaultServerConfig dashboardPort := f.AllocPort() // Use same port as PortServer @@ -158,14 +160,14 @@ var _ = Describe("[Feature: Server Manager]", func() { dashboard_pwd = admin `, consts.PortServerName, dashboardPort) - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [http] type = http local_port = {{ .%s }} custom_domains = example.com `, framework.HTTPSimpleServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().HTTPPath("/healthz") diff --git a/test/e2e/legacy/basic/tcpmux.go b/test/e2e/legacy/basic/tcpmux.go new file mode 100644 index 00000000000..3872ea04acc --- /dev/null +++ b/test/e2e/legacy/basic/tcpmux.go @@ -0,0 +1,218 @@ +package basic + +import ( + "bufio" + "fmt" + "net" + "net/http" + + "github.com/onsi/ginkgo/v2" + + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/rpc" +) + +var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { + f := framework.NewDefaultFramework() + + getDefaultServerConf := func(httpconnectPort int) string { + conf := consts.LegacyDefaultServerConfig + ` + tcpmux_httpconnect_port = %d + ` + return fmt.Sprintf(conf, httpconnectPort) + } + newServer := func(port int, respContent string) *streamserver.Server { + return streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(port), + streamserver.WithRespContent([]byte(respContent)), + ) + } + + proxyURLWithAuth := func(username, password string, port int) string { + if username == "" { + return fmt.Sprintf("http://127.0.0.1:%d", port) + } + return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, port) + } + + ginkgo.It("Route by HTTP user", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + + fooPort := f.AllocPort() + f.RunServer("", newServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newServer(barPort, "bar")) + + otherPort := f.AllocPort() + f.RunServer("", newServer(otherPort, "other")) + + clientConf := consts.LegacyDefaultClientConfig + clientConf += fmt.Sprintf(` + [foo] + type = tcpmux + multiplexer = httpconnect + local_port = %d + custom_domains = normal.example.com + route_by_http_user = user1 + + [bar] + type = tcpmux + multiplexer = httpconnect + local_port = %d + custom_domains = normal.example.com + route_by_http_user = user2 + + [catchAll] + type = tcpmux + multiplexer = httpconnect + local_port = %d + custom_domains = normal.example.com + `, fooPort, barPort, otherPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // user1 + framework.NewRequestExpect(f).Explain("user1"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user1", "", vhostPort)) + }). + ExpectResp([]byte("foo")). + Ensure() + + // user2 + framework.NewRequestExpect(f).Explain("user2"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user2", "", vhostPort)) + }). + ExpectResp([]byte("bar")). + Ensure() + + // other user + framework.NewRequestExpect(f).Explain("other user"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user3", "", vhostPort)) + }). + ExpectResp([]byte("other")). + Ensure() + }) + + ginkgo.It("Proxy auth", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + + fooPort := f.AllocPort() + f.RunServer("", newServer(fooPort, "foo")) + + clientConf := consts.LegacyDefaultClientConfig + clientConf += fmt.Sprintf(` + [test] + type = tcpmux + multiplexer = httpconnect + local_port = %d + custom_domains = normal.example.com + http_user = test + http_pwd = test + `, fooPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // not set auth header + framework.NewRequestExpect(f).Explain("no auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)) + }). + ExpectError(true). + Ensure() + + // set incorrect auth header + framework.NewRequestExpect(f).Explain("incorrect auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "invalid", vhostPort)) + }). + ExpectError(true). + Ensure() + + // set correct auth header + framework.NewRequestExpect(f).Explain("correct auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "test", vhostPort)) + }). + ExpectResp([]byte("foo")). + Ensure() + }) + + ginkgo.It("TCPMux Passthrough", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + serverConf += ` + tcpmux_passthrough = true + ` + + var ( + respErr error + connectRequestHost string + ) + newServer := func(port int) *streamserver.Server { + return streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(port), + streamserver.WithCustomHandler(func(conn net.Conn) { + defer conn.Close() + + // read HTTP CONNECT request + bufioReader := bufio.NewReader(conn) + req, err := http.ReadRequest(bufioReader) + if err != nil { + respErr = err + return + } + connectRequestHost = req.Host + + // return ok response + res := httppkg.OkResponse() + if res.Body != nil { + defer res.Body.Close() + } + _ = res.Write(conn) + + buf, err := rpc.ReadBytes(conn) + if err != nil { + respErr = err + return + } + _, _ = rpc.WriteBytes(conn, buf) + }), + ) + } + + localPort := f.AllocPort() + f.RunServer("", newServer(localPort)) + + clientConf := consts.LegacyDefaultClientConfig + clientConf += fmt.Sprintf(` + [test] + type = tcpmux + multiplexer = httpconnect + local_port = %d + custom_domains = normal.example.com + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)).Body([]byte("frp")) + }). + ExpectResp([]byte("frp")). + Ensure() + framework.ExpectNoError(respErr) + framework.ExpectEqualValues(connectRequestHost, "normal.example.com") + }) +}) diff --git a/test/e2e/legacy/basic/xtcp.go b/test/e2e/legacy/basic/xtcp.go new file mode 100644 index 00000000000..6e72f2e1695 --- /dev/null +++ b/test/e2e/legacy/basic/xtcp.go @@ -0,0 +1,52 @@ +package basic + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: XTCP]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Fallback To STCP", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig + + bindPortName := port.GenName("XTCP") + clientConf += fmt.Sprintf(` + [foo] + type = stcp + local_port = {{ .%s }} + + [foo-visitor] + type = stcp + role = visitor + server_name = foo + bind_port = -1 + + [bar-visitor] + type = xtcp + role = visitor + server_name = bar + bind_port = {{ .%s }} + keep_tunnel_open = true + fallback_to = foo-visitor + fallback_timeout_ms = 200 + `, framework.TCPEchoServerPort, bindPortName) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Timeout(time.Second) + }). + PortName(bindPortName). + Ensure() + }) +}) diff --git a/test/e2e/legacy/features/bandwidth_limit.go b/test/e2e/legacy/features/bandwidth_limit.go new file mode 100644 index 00000000000..a54866b8794 --- /dev/null +++ b/test/e2e/legacy/features/bandwidth_limit.go @@ -0,0 +1,105 @@ +package features + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Proxy Bandwidth Limit by Client", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [tcp] + type = tcp + local_port = %d + remote_port = %d + bandwidth_limit = 10KB + `, localPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + content := strings.Repeat("a", 50*1024) // 5KB + start := time.Now() + framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { + r.Body([]byte(content)).Timeout(30 * time.Second) + }).ExpectResp([]byte(content)).Ensure() + + duration := time.Since(start) + framework.Logf("request duration: %s", duration.String()) + + framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String()) + }) + + ginkgo.It("Proxy Bandwidth Limit by Server", func() { + // new test plugin server + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewProxyContent{} + return &r + } + pluginPort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + content.BandwidthLimit = "10KB" + content.BandwidthLimitMode = "server" + ret.Content = content + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(pluginPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` + [plugin.test] + addr = 127.0.0.1:%d + path = /handler + ops = NewProxy + `, pluginPort) + clientConf := consts.LegacyDefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [tcp] + type = tcp + local_port = %d + remote_port = %d + `, localPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + content := strings.Repeat("a", 50*1024) // 5KB + start := time.Now() + framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { + r.Body([]byte(content)).Timeout(30 * time.Second) + }).ExpectResp([]byte(content)).Ensure() + + duration := time.Since(start) + framework.Logf("request duration: %s", duration.String()) + + framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String()) + }) +}) diff --git a/test/e2e/features/chaos.go b/test/e2e/legacy/features/chaos.go similarity index 90% rename from test/e2e/features/chaos.go rename to test/e2e/legacy/features/chaos.go index 2049fc77965..9fccc4cb5ce 100644 --- a/test/e2e/features/chaos.go +++ b/test/e2e/legacy/features/chaos.go @@ -4,15 +4,15 @@ import ( "fmt" "time" - "github.com/fatedier/frp/test/e2e/framework" + "github.com/onsi/ginkgo/v2" - . "github.com/onsi/ginkgo" + "github.com/fatedier/frp/test/e2e/framework" ) -var _ = Describe("[Feature: Chaos]", func() { +var _ = ginkgo.Describe("[Feature: Chaos]", func() { f := framework.NewDefaultFramework() - It("reconnect after frps restart", func() { + ginkgo.It("reconnect after frps restart", func() { serverPort := f.AllocPort() serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(` [common] @@ -41,7 +41,7 @@ var _ = Describe("[Feature: Chaos]", func() { framework.NewRequestExpect(f).Port(remotePort).Ensure() // 2. stop frps, expect request failed - ps.Stop() + _ = ps.Stop() time.Sleep(200 * time.Millisecond) framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() @@ -52,7 +52,7 @@ var _ = Describe("[Feature: Chaos]", func() { framework.NewRequestExpect(f).Port(remotePort).Ensure() // 4. stop frpc, expect request failed - pc.Stop() + _ = pc.Stop() time.Sleep(200 * time.Millisecond) framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() diff --git a/test/e2e/features/group.go b/test/e2e/legacy/features/group.go similarity index 78% rename from test/e2e/features/group.go rename to test/e2e/legacy/features/group.go index ea6b781fd89..57c143c5ab1 100644 --- a/test/e2e/features/group.go +++ b/test/e2e/legacy/features/group.go @@ -6,16 +6,16 @@ import ( "sync" "time" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/mock/server/httpserver" "github.com/fatedier/frp/test/e2e/mock/server/streamserver" "github.com/fatedier/frp/test/e2e/pkg/request" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Group]", func() { +var _ = ginkgo.Describe("[Feature: Group]", func() { f := framework.NewDefaultFramework() newHTTPServer := func(port int, respContent string) *httpserver.Server { @@ -48,22 +48,20 @@ var _ = Describe("[Feature: Group]", func() { return true }) } - for i := 0; i < 10; i++ { - wait.Add(1) - go func() { - defer wait.Done() + for range 10 { + wait.Go(func() { expectFn() - }() + }) } wait.Wait() return results } - Describe("Load Balancing", func() { - It("TCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.Describe("Load Balancing", func() { + ginkgo.It("TCP", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig fooPort := f.AllocPort() fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) @@ -90,11 +88,11 @@ var _ = Describe("[Feature: Group]", func() { group_key = 123 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) fooCount := 0 barCount := 0 - for i := 0; i < 10; i++ { + for i := range 10 { framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool { switch string(resp.Content) { case "foo": @@ -112,10 +110,10 @@ var _ = Describe("[Feature: Group]", func() { }) }) - Describe("Health Check", func() { - It("TCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.Describe("Health Check", func() { + ginkgo.It("TCP", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig fooPort := f.AllocPort() fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) @@ -146,11 +144,11 @@ var _ = Describe("[Feature: Group]", func() { health_check_interval_s = 1 `, fooPort, remotePort, barPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // check foo and bar is ok results := []string{} - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { results = append(results, string(resp.Content)) return true @@ -161,7 +159,7 @@ var _ = Describe("[Feature: Group]", func() { // close bar server, check foo is ok barServer.Close() time.Sleep(2 * time.Second) - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() } @@ -169,7 +167,7 @@ var _ = Describe("[Feature: Group]", func() { f.RunServer("", barServer) time.Sleep(2 * time.Second) results = []string{} - for i := 0; i < 10; i++ { + for range 10 { framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { results = append(results, string(resp.Content)) return true @@ -178,12 +176,12 @@ var _ = Describe("[Feature: Group]", func() { framework.ExpectContainElements(results, []string{"foo", "bar"}) }) - It("HTTP", func() { + ginkgo.It("HTTP", func() { vhostPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig fooPort := f.AllocPort() fooServer := newHTTPServer(fooPort, "foo") @@ -215,7 +213,30 @@ var _ = Describe("[Feature: Group]", func() { health_check_url = /healthz `, fooPort, barPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) + + // send first HTTP request + var contents []string + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + Ensure(func(resp *request.Response) bool { + contents = append(contents, string(resp.Content)) + return true + }) + + // send second HTTP request, should be forwarded to another service + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + Ensure(func(resp *request.Response) bool { + contents = append(contents, string(resp.Content)) + return true + }) + + framework.ExpectContainElements(contents, []string{"foo", "bar"}) // check foo and bar is ok results := doFooBarHTTPRequest(vhostPort, "example.com") diff --git a/test/e2e/features/heartbeat.go b/test/e2e/legacy/features/heartbeat.go similarity index 81% rename from test/e2e/features/heartbeat.go rename to test/e2e/legacy/features/heartbeat.go index b0732c37f00..3c1758f2c94 100644 --- a/test/e2e/features/heartbeat.go +++ b/test/e2e/legacy/features/heartbeat.go @@ -4,15 +4,15 @@ import ( "fmt" "time" - "github.com/fatedier/frp/test/e2e/framework" + "github.com/onsi/ginkgo/v2" - . "github.com/onsi/ginkgo" + "github.com/fatedier/frp/test/e2e/framework" ) -var _ = Describe("[Feature: Heartbeat]", func() { +var _ = ginkgo.Describe("[Feature: Heartbeat]", func() { f := framework.NewDefaultFramework() - It("disable application layer heartbeat", func() { + ginkgo.It("disable application layer heartbeat", func() { serverPort := f.AllocPort() serverConf := fmt.Sprintf(` [common] @@ -38,7 +38,7 @@ var _ = Describe("[Feature: Heartbeat]", func() { `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort) // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() diff --git a/test/e2e/features/monitor.go b/test/e2e/legacy/features/monitor.go similarity index 74% rename from test/e2e/features/monitor.go rename to test/e2e/legacy/features/monitor.go index d8b656a87d0..0b19a85eeb7 100644 --- a/test/e2e/features/monitor.go +++ b/test/e2e/legacy/features/monitor.go @@ -5,26 +5,26 @@ import ( "strings" "time" + "github.com/onsi/ginkgo/v2" + "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/request" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Monitor]", func() { +var _ = ginkgo.Describe("[Feature: Monitor]", func() { f := framework.NewDefaultFramework() - It("Prometheus metrics", func() { + ginkgo.It("Prometheus metrics", func() { dashboardPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` enable_prometheus = true dashboard_addr = 0.0.0.0 dashboard_port = %d `, dashboardPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` [tcp] @@ -33,7 +33,7 @@ var _ = Describe("[Feature: Monitor]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() time.Sleep(500 * time.Millisecond) @@ -41,7 +41,7 @@ var _ = Describe("[Feature: Monitor]", func() { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { r.HTTP().Port(dashboardPort).HTTPPath("/metrics") }).Ensure(func(resp *request.Response) bool { - log.Trace("prometheus metrics response: \n%s", resp.Content) + log.Tracef("prometheus metrics response: \n%s", resp.Content) if resp.Code != 200 { return false } diff --git a/test/e2e/features/real_ip.go b/test/e2e/legacy/features/real_ip.go similarity index 74% rename from test/e2e/features/real_ip.go rename to test/e2e/legacy/features/real_ip.go index 424d4049eb1..dae74e565be 100644 --- a/test/e2e/features/real_ip.go +++ b/test/e2e/legacy/features/real_ip.go @@ -6,6 +6,9 @@ import ( "net" "net/http" + "github.com/onsi/ginkgo/v2" + pp "github.com/pires/go-proxyproto" + "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" @@ -13,17 +16,14 @@ import ( "github.com/fatedier/frp/test/e2e/mock/server/streamserver" "github.com/fatedier/frp/test/e2e/pkg/request" "github.com/fatedier/frp/test/e2e/pkg/rpc" - - . "github.com/onsi/ginkgo" - pp "github.com/pires/go-proxyproto" ) -var _ = Describe("[Feature: Real IP]", func() { +var _ = ginkgo.Describe("[Feature: Real IP]", func() { f := framework.NewDefaultFramework() - It("HTTP X-Forwarded-For", func() { + ginkgo.It("HTTP X-Forwarded-For", func() { vhostHTTPPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) @@ -31,12 +31,12 @@ var _ = Describe("[Feature: Real IP]", func() { localServer := httpserver.New( httpserver.WithBindPort(localPort), httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte(req.Header.Get("X-Forwarded-For"))) + _, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For"))) })), ) f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -44,7 +44,7 @@ var _ = Describe("[Feature: Real IP]", func() { custom_domains = normal.example.com `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort). RequestModify(func(r *request.Request) { @@ -52,13 +52,12 @@ var _ = Describe("[Feature: Real IP]", func() { }). ExpectResp([]byte("127.0.0.1")). Ensure() - }) - Describe("Proxy Protocol", func() { - It("TCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.Describe("Proxy Protocol", func() { + ginkgo.It("TCP", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig localPort := f.AllocPort() localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), @@ -67,7 +66,7 @@ var _ = Describe("[Feature: Real IP]", func() { rd := bufio.NewReader(c) ppHeader, err := pp.Read(rd) if err != nil { - log.Error("read proxy protocol error: %v", err) + log.Errorf("read proxy protocol error: %v", err) return } @@ -77,7 +76,7 @@ var _ = Describe("[Feature: Real IP]", func() { } buf := []byte(ppHeader.SourceAddr.String()) - rpc.WriteBytes(c, buf) + _, _ = rpc.WriteBytes(c, buf) } })) f.RunServer("", localServer) @@ -91,10 +90,10 @@ var _ = Describe("[Feature: Real IP]", func() { proxy_protocol_version = v2 `, localPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { - log.Trace("ProxyProtocol get SourceAddr: %s", string(resp.Content)) + log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content)) addr, err := net.ResolveTCPAddr("tcp", string(resp.Content)) if err != nil { return false @@ -106,13 +105,13 @@ var _ = Describe("[Feature: Real IP]", func() { }) }) - It("HTTP", func() { + ginkgo.It("HTTP", func() { vhostHTTPPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig localPort := f.AllocPort() var srcAddrRecord string @@ -122,7 +121,7 @@ var _ = Describe("[Feature: Real IP]", func() { rd := bufio.NewReader(c) ppHeader, err := pp.Read(rd) if err != nil { - log.Error("read proxy protocol error: %v", err) + log.Errorf("read proxy protocol error: %v", err) return } srcAddrRecord = ppHeader.SourceAddr.String() @@ -137,13 +136,13 @@ var _ = Describe("[Feature: Real IP]", func() { proxy_protocol_version = v2 `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) { r.HTTP().HTTPHost("normal.example.com") }).Ensure(framework.ExpectResponseCode(404)) - log.Trace("ProxyProtocol get SourceAddr: %s", srcAddrRecord) + log.Tracef("proxy protocol get SourceAddr: %s", srcAddrRecord) addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord) framework.ExpectNoError(err, srcAddrRecord) framework.ExpectEqualValues("127.0.0.1", addr.IP.String()) diff --git a/test/e2e/plugin/client.go b/test/e2e/legacy/plugin/client.go similarity index 80% rename from test/e2e/plugin/client.go rename to test/e2e/legacy/plugin/client.go index afbd0a2c7fd..ca2942803d3 100644 --- a/test/e2e/plugin/client.go +++ b/test/e2e/legacy/plugin/client.go @@ -4,6 +4,9 @@ import ( "crypto/tls" "fmt" "strconv" + "strings" + + "github.com/onsi/ginkgo/v2" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/test/e2e/framework" @@ -12,17 +15,16 @@ import ( "github.com/fatedier/frp/test/e2e/pkg/cert" "github.com/fatedier/frp/test/e2e/pkg/port" "github.com/fatedier/frp/test/e2e/pkg/request" - - . "github.com/onsi/ginkgo" ) -var _ = Describe("[Feature: Client-Plugins]", func() { +var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { f := framework.NewDefaultFramework() - Describe("UnixDomainSocket", func() { - It("Expose a unix domain socket echo server", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.Describe("UnixDomainSocket", func() { + ginkgo.It("Expose a unix domain socket echo server", func() { + serverConf := consts.LegacyDefaultServerConfig + var clientConf strings.Builder + clientConf.WriteString(consts.LegacyDefaultClientConfig) getProxyConf := func(proxyName string, portName string, extra string) string { return fmt.Sprintf(` @@ -65,10 +67,10 @@ var _ = Describe("[Feature: Client-Plugins]", func() { // build all client config for _, test := range tests { - clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") } // run frps and frpc - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf.String()}) for _, test := range tests { framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() @@ -76,9 +78,9 @@ var _ = Describe("[Feature: Client-Plugins]", func() { }) }) - It("http_proxy", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.It("http_proxy", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -90,7 +92,7 @@ var _ = Describe("[Feature: Client-Plugins]", func() { plugin_http_passwd = 123 `, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // http proxy, no auth info framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { @@ -108,9 +110,9 @@ var _ = Describe("[Feature: Client-Plugins]", func() { }) }) - It("socks5 proxy", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + ginkgo.It("socks5 proxy", func() { + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -122,7 +124,7 @@ var _ = Describe("[Feature: Client-Plugins]", func() { plugin_passwd = 123 `, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // http proxy, no auth info framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { @@ -135,12 +137,12 @@ var _ = Describe("[Feature: Client-Plugins]", func() { }).Ensure() }) - It("static_file", func() { + ginkgo.It("static_file", func() { vhostPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() f.WriteTempFile("test_static_file", "foo") @@ -166,7 +168,7 @@ var _ = Describe("[Feature: Client-Plugins]", func() { plugin_http_passwd = 123 `, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) // from tcp proxy framework.NewRequestExpect(f).Request( @@ -184,15 +186,15 @@ var _ = Describe("[Feature: Client-Plugins]", func() { ).ExpectResp([]byte("foo")).Ensure() }) - It("http2https", func() { - serverConf := consts.DefaultServerConfig + ginkgo.It("http2https", func() { + serverConf := consts.LegacyDefaultServerConfig vhostHTTPPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [http2https] type = http custom_domains = example.com @@ -200,13 +202,13 @@ var _ = Describe("[Feature: Client-Plugins]", func() { plugin_local_addr = 127.0.0.1:%d `, localPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) localServer := httpserver.New( httpserver.WithBindPort(localPort), - httpserver.WithTlsConfig(tlsConfig), + httpserver.WithTLSConfig(tlsConfig), httpserver.WithResponse([]byte("test")), ) f.RunServer("", localServer) @@ -220,21 +222,21 @@ var _ = Describe("[Feature: Client-Plugins]", func() { Ensure() }) - It("https2http", func() { + ginkgo.It("https2http", func() { generator := &cert.SelfSignedCertGenerator{} artifacts, err := generator.Generate("example.com") framework.ExpectNoError(err) crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPSPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_https_port = %d `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [https2http] type = https custom_domains = example.com @@ -244,7 +246,7 @@ var _ = Describe("[Feature: Client-Plugins]", func() { plugin_key_path = %s `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) localServer := httpserver.New( httpserver.WithBindPort(localPort), @@ -264,21 +266,21 @@ var _ = Describe("[Feature: Client-Plugins]", func() { Ensure() }) - It("https2https", func() { + ginkgo.It("https2https", func() { generator := &cert.SelfSignedCertGenerator{} artifacts, err := generator.Generate("example.com") framework.ExpectNoError(err) crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPSPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_https_port = %d `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [https2https] type = https custom_domains = example.com @@ -288,14 +290,14 @@ var _ = Describe("[Feature: Client-Plugins]", func() { plugin_key_path = %s `, localPort, crtPath, keyPath) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) localServer := httpserver.New( httpserver.WithBindPort(localPort), httpserver.WithResponse([]byte("test")), - httpserver.WithTlsConfig(tlsConfig), + httpserver.WithTLSConfig(tlsConfig), ) f.RunServer("", localServer) diff --git a/test/e2e/plugin/server.go b/test/e2e/legacy/plugin/server.go similarity index 70% rename from test/e2e/plugin/server.go rename to test/e2e/legacy/plugin/server.go index 2af1eba8496..b00b1bac357 100644 --- a/test/e2e/plugin/server.go +++ b/test/e2e/legacy/plugin/server.go @@ -4,25 +4,26 @@ import ( "fmt" "time" + "github.com/onsi/ginkgo/v2" + plugin "github.com/fatedier/frp/pkg/plugin/server" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" - - . "github.com/onsi/ginkgo" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" ) -var _ = Describe("[Feature: Server-Plugins]", func() { +var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { f := framework.NewDefaultFramework() - Describe("Login", func() { + ginkgo.Describe("Login", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.LoginContent{} return &r } - It("Auth for custom meta token", func() { + ginkgo.It("Auth for custom meta token", func() { localPort := f.AllocPort() clientAddressGot := false @@ -40,17 +41,17 @@ var _ = Describe("[Feature: Server-Plugins]", func() { } return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.user-manager] addr = 127.0.0.1:%d path = /handler ops = Login `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -63,14 +64,14 @@ var _ = Describe("[Feature: Server-Plugins]", func() { `, framework.TCPEchoServerPort, remotePort) remotePort2 := f.AllocPort() - invalidTokenClientConf := consts.DefaultClientConfig + fmt.Sprintf(` + invalidTokenClientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [tcp2] type = tcp local_port = {{ .%s }} remote_port = %d `, framework.TCPEchoServerPort, remotePort2) - f.RunProcesses([]string{serverConf}, []string{clientConf, invalidTokenClientConf}) + f.RunProcesses(serverConf, []string{clientConf, invalidTokenClientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure() @@ -79,14 +80,14 @@ var _ = Describe("[Feature: Server-Plugins]", func() { }) }) - Describe("NewProxy", func() { + ginkgo.Describe("NewProxy", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.NewProxyContent{} return &r } - It("Validate Info", func() { + ginkgo.It("Validate Info", func() { localPort := f.AllocPort() handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response @@ -98,17 +99,17 @@ var _ = Describe("[Feature: Server-Plugins]", func() { } return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = NewProxy `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -118,12 +119,12 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) - It("Mofify RemotePort", func() { + ginkgo.It("Modify RemotePort", func() { localPort := f.AllocPort() remotePort := f.AllocPort() handler := func(req *plugin.Request) *plugin.Response { @@ -133,17 +134,17 @@ var _ = Describe("[Feature: Server-Plugins]", func() { ret.Content = content return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = NewProxy `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] @@ -152,20 +153,20 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = 0 `, framework.TCPEchoServerPort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() }) }) - Describe("CloseProxy", func() { + ginkgo.Describe("CloseProxy", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.CloseProxyContent{} return &r } - It("Validate Info", func() { + ginkgo.It("Validate Info", func() { localPort := f.AllocPort() var recordProxyName string handler := func(req *plugin.Request) *plugin.Response { @@ -174,17 +175,17 @@ var _ = Describe("[Feature: Server-Plugins]", func() { recordProxyName = content.ProxyName return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = CloseProxy `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -194,12 +195,12 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - _, clients := f.RunProcesses([]string{serverConf}, []string{clientConf}) + _, clients := f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() for _, c := range clients { - c.Stop() + _ = c.Stop() } time.Sleep(1 * time.Second) @@ -208,29 +209,29 @@ var _ = Describe("[Feature: Server-Plugins]", func() { }) }) - Describe("Ping", func() { + ginkgo.Describe("Ping", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.PingContent{} return &r } - It("Validate Info", func() { + ginkgo.It("Validate Info", func() { localPort := f.AllocPort() var record string handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response content := req.Content.(*plugin.PingContent) - record = content.Ping.PrivilegeKey + record = content.PrivilegeKey ret.Unchange = true return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler @@ -238,7 +239,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` heartbeat_interval = 1 authenticate_heartbeats = true @@ -249,7 +250,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -258,29 +259,29 @@ var _ = Describe("[Feature: Server-Plugins]", func() { }) }) - Describe("NewWorkConn", func() { + ginkgo.Describe("NewWorkConn", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.NewWorkConnContent{} return &r } - It("Validate Info", func() { + ginkgo.It("Validate Info", func() { localPort := f.AllocPort() var record string handler := func(req *plugin.Request) *plugin.Response { var ret plugin.Response content := req.Content.(*plugin.NewWorkConnContent) - record = content.NewWorkConn.RunID + record = content.RunID ret.Unchange = true return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler @@ -288,7 +289,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] type = tcp @@ -296,7 +297,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -304,13 +305,13 @@ var _ = Describe("[Feature: Server-Plugins]", func() { }) }) - Describe("NewUserConn", func() { + ginkgo.Describe("NewUserConn", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.NewUserConnContent{} return &r } - It("Validate Info", func() { + ginkgo.It("Validate Info", func() { localPort := f.AllocPort() var record string @@ -321,11 +322,11 @@ var _ = Describe("[Feature: Server-Plugins]", func() { ret.Unchange = true return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler @@ -333,7 +334,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] type = tcp @@ -341,7 +342,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() @@ -349,13 +350,13 @@ var _ = Describe("[Feature: Server-Plugins]", func() { }) }) - Describe("HTTPS Protocol", func() { + ginkgo.Describe("HTTPS Protocol", func() { newFunc := func() *plugin.Request { var r plugin.Request r.Content = &plugin.NewUserConnContent{} return &r } - It("Validate Login Info, disable tls verify", func() { + ginkgo.It("Validate Login Info, disable tls verify", func() { localPort := f.AllocPort() var record string @@ -368,11 +369,11 @@ var _ = Describe("[Feature: Server-Plugins]", func() { } tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = https://127.0.0.1:%d path = /handler @@ -380,7 +381,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] type = tcp @@ -388,7 +389,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() { remote_port = %d `, framework.TCPEchoServerPort, remotePort) - f.RunProcesses([]string{serverConf}, []string{clientConf}) + f.RunProcesses(serverConf, []string{clientConf}) framework.NewRequestExpect(f).Port(remotePort).Ensure() diff --git a/test/e2e/mock/server/httpserver/server.go b/test/e2e/mock/server/httpserver/server.go index a811ac2700a..fb80288f02c 100644 --- a/test/e2e/mock/server/httpserver/server.go +++ b/test/e2e/mock/server/httpserver/server.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "strconv" + "time" ) type Server struct { @@ -30,13 +31,6 @@ func New(options ...Option) *Server { return s } -func WithBindAddr(addr string) Option { - return func(s *Server) *Server { - s.bindAddr = addr - return s - } -} - func WithBindPort(port int) Option { return func(s *Server) *Server { s.bindPort = port @@ -44,7 +38,7 @@ func WithBindPort(port int) Option { } } -func WithTlsConfig(tlsConfig *tls.Config) Option { +func WithTLSConfig(tlsConfig *tls.Config) Option { return func(s *Server) *Server { s.tlsConfig = tlsConfig return s @@ -61,7 +55,7 @@ func WithHandler(h http.Handler) Option { func WithResponse(resp []byte) Option { return func(s *Server) *Server { s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(resp) + _, _ = w.Write(resp) }) return s } @@ -74,16 +68,21 @@ func (s *Server) Run() error { addr := net.JoinHostPort(s.bindAddr, strconv.Itoa(s.bindPort)) hs := &http.Server{ - Addr: addr, - Handler: s.handler, - TLSConfig: s.tlsConfig, + Addr: addr, + Handler: s.handler, + TLSConfig: s.tlsConfig, + ReadHeaderTimeout: time.Minute, } s.hs = hs if s.tlsConfig == nil { - go hs.Serve(s.l) + go func() { + _ = hs.Serve(s.l) + }() } else { - go hs.ServeTLS(s.l, "", "") + go func() { + _ = hs.ServeTLS(s.l, "", "") + }() } return nil } diff --git a/test/e2e/mock/server/oidcserver/oidcserver.go b/test/e2e/mock/server/oidcserver/oidcserver.go new file mode 100644 index 00000000000..eb7ce2e4aa1 --- /dev/null +++ b/test/e2e/mock/server/oidcserver/oidcserver.go @@ -0,0 +1,246 @@ +// 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 oidcserver provides a minimal mock OIDC server for e2e testing. +// It implements three endpoints: +// - /.well-known/openid-configuration (discovery) +// - /jwks (JSON Web Key Set) +// - /token (client_credentials grant) +package oidcserver + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "net" + "net/http" + "strconv" + "sync/atomic" + "time" +) + +type Server struct { + bindAddr string + bindPort int + l net.Listener + hs *http.Server + + privateKey *rsa.PrivateKey + kid string + + clientID string + clientSecret string + audience string + subject string + expiresIn int // seconds; 0 means omit expires_in from token response + + tokenRequestCount atomic.Int64 +} + +const maxTokenRequestBodySize = 1 << 20 + +type Option func(*Server) + +func WithBindPort(port int) Option { + return func(s *Server) { s.bindPort = port } +} + +func WithExpiresIn(seconds int) Option { + return func(s *Server) { s.expiresIn = seconds } +} + +func New(options ...Option) *Server { + s := &Server{ + bindAddr: "127.0.0.1", + kid: "test-key-1", + clientID: "test-client", + clientSecret: "test-secret", + audience: "frps", + subject: "test-service", + expiresIn: 3600, + } + for _, opt := range options { + opt(s) + } + return s +} + +func (s *Server) Run() error { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("generate RSA key: %w", err) + } + s.privateKey = key + + s.l, err = net.Listen("tcp", net.JoinHostPort(s.bindAddr, strconv.Itoa(s.bindPort))) + if err != nil { + return err + } + s.bindPort = s.l.Addr().(*net.TCPAddr).Port + + mux := http.NewServeMux() + mux.HandleFunc("/.well-known/openid-configuration", s.handleDiscovery) + mux.HandleFunc("/jwks", s.handleJWKS) + mux.HandleFunc("/token", s.handleToken) + + s.hs = &http.Server{ + Handler: mux, + ReadHeaderTimeout: time.Minute, + } + go func() { _ = s.hs.Serve(s.l) }() + return nil +} + +func (s *Server) Close() error { + if s.hs != nil { + return s.hs.Close() + } + return nil +} + +func (s *Server) BindAddr() string { return s.bindAddr } +func (s *Server) BindPort() int { return s.bindPort } + +func (s *Server) Issuer() string { + return fmt.Sprintf("http://%s:%d", s.bindAddr, s.bindPort) +} + +func (s *Server) TokenEndpoint() string { + return s.Issuer() + "/token" +} + +// TokenRequestCount returns the number of successful token requests served. +func (s *Server) TokenRequestCount() int64 { + return s.tokenRequestCount.Load() +} + +func (s *Server) handleDiscovery(w http.ResponseWriter, _ *http.Request) { + issuer := s.Issuer() + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "issuer": issuer, + "token_endpoint": issuer + "/token", + "jwks_uri": issuer + "/jwks", + "response_types_supported": []string{"code"}, + "subject_types_supported": []string{"public"}, + "id_token_signing_alg_values_supported": []string{"RS256"}, + }) +} + +func (s *Server) handleJWKS(w http.ResponseWriter, _ *http.Request) { + pub := &s.privateKey.PublicKey + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "keys": []map[string]any{ + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": s.kid, + "n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()), + "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()), + }, + }, + }) +} + +func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + r.Body = http.MaxBytesReader(w, r.Body, maxTokenRequestBodySize) + if err := r.ParseForm(); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "invalid_request", + }) + return + } + + if r.Form.Get("grant_type") != "client_credentials" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "unsupported_grant_type", + }) + return + } + + // Accept credentials from Basic Auth or form body. + clientID, clientSecret, ok := r.BasicAuth() + if !ok { + clientID = r.Form.Get("client_id") + clientSecret = r.Form.Get("client_secret") + } + if clientID != s.clientID || clientSecret != s.clientSecret { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _ = json.NewEncoder(w).Encode(map[string]any{ + "error": "invalid_client", + }) + return + } + + token, err := s.signJWT() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := map[string]any{ + "access_token": token, + "token_type": "Bearer", + } + if s.expiresIn > 0 { + resp["expires_in"] = s.expiresIn + } + + s.tokenRequestCount.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + +func (s *Server) signJWT() (string, error) { + now := time.Now() + header, _ := json.Marshal(map[string]string{ + "alg": "RS256", + "kid": s.kid, + "typ": "JWT", + }) + claims, _ := json.Marshal(map[string]any{ + "iss": s.Issuer(), + "sub": s.subject, + "aud": s.audience, + "iat": now.Unix(), + "exp": now.Add(1 * time.Hour).Unix(), + }) + + headerB64 := base64.RawURLEncoding.EncodeToString(header) + claimsB64 := base64.RawURLEncoding.EncodeToString(claims) + signingInput := headerB64 + "." + claimsB64 + + h := sha256.Sum256([]byte(signingInput)) + sig, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, h[:]) + if err != nil { + return "", err + } + return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil +} diff --git a/test/e2e/mock/server/streamserver/server.go b/test/e2e/mock/server/streamserver/server.go index 1dde353a9db..9e237d96fba 100644 --- a/test/e2e/mock/server/streamserver/server.go +++ b/test/e2e/mock/server/streamserver/server.go @@ -127,7 +127,7 @@ func (s *Server) handle(c net.Conn) { if len(s.respContent) > 0 { buf = s.respContent } - rpc.WriteBytes(c, buf) + _, _ = rpc.WriteBytes(c, buf) } } diff --git a/test/e2e/pkg/cert/generator.go b/test/e2e/pkg/cert/generator.go index 5c11b226d1e..fb22522bec0 100644 --- a/test/e2e/pkg/cert/generator.go +++ b/test/e2e/pkg/cert/generator.go @@ -22,8 +22,8 @@ type Artifacts struct { ResourceVersion string } -// CertGenerator is an interface to provision the serving certificate. -type CertGenerator interface { +// Generator is an interface to provision the serving certificate. +type Generator interface { // Generate returns a Artifacts struct. Generate(CommonName string) (*Artifacts, error) // SetCA sets the PEM-encoded CA private key and CA cert for signing the generated serving cert. diff --git a/test/e2e/pkg/cert/selfsigned.go b/test/e2e/pkg/cert/selfsigned.go index 4f9ed57378a..82762d0990c 100644 --- a/test/e2e/pkg/cert/selfsigned.go +++ b/test/e2e/pkg/cert/selfsigned.go @@ -23,7 +23,7 @@ type SelfSignedCertGenerator struct { caCert []byte } -var _ CertGenerator = &SelfSignedCertGenerator{} +var _ Generator = &SelfSignedCertGenerator{} // SetCA sets the PEM-encoded CA private key and CA cert for signing the generated serving cert. func (cp *SelfSignedCertGenerator) SetCA(caKey, caCert []byte) { diff --git a/test/e2e/plugin/utils.go b/test/e2e/pkg/plugin/plugin.go similarity index 73% rename from test/e2e/plugin/utils.go rename to test/e2e/pkg/plugin/plugin.go index c0d7db3d3db..b807a060a0d 100644 --- a/test/e2e/plugin/utils.go +++ b/test/e2e/pkg/plugin/plugin.go @@ -11,14 +11,14 @@ import ( "github.com/fatedier/frp/test/e2e/mock/server/httpserver" ) -type PluginHandler func(req *plugin.Request) *plugin.Response +type Handler func(req *plugin.Request) *plugin.Response type NewPluginRequest func() *plugin.Request -func NewHTTPPluginServer(port int, newFunc NewPluginRequest, handler PluginHandler, tlsConfig *tls.Config) *httpserver.Server { +func NewHTTPPluginServer(port int, newFunc NewPluginRequest, handler Handler, tlsConfig *tls.Config) *httpserver.Server { return httpserver.New( httpserver.WithBindPort(port), - httpserver.WithTlsConfig(tlsConfig), + httpserver.WithTLSConfig(tlsConfig), httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { r := newFunc() buf, err := io.ReadAll(req.Body) @@ -26,7 +26,7 @@ func NewHTTPPluginServer(port int, newFunc NewPluginRequest, handler PluginHandl w.WriteHeader(500) return } - log.Trace("plugin request: %s", string(buf)) + log.Tracef("plugin request: %s", string(buf)) err = json.Unmarshal(buf, &r) if err != nil { w.WriteHeader(500) @@ -34,8 +34,8 @@ func NewHTTPPluginServer(port int, newFunc NewPluginRequest, handler PluginHandl } resp := handler(r) buf, _ = json.Marshal(resp) - log.Trace("plugin response: %s", string(buf)) - w.Write(buf) + log.Tracef("plugin response: %s", string(buf)) + _, _ = w.Write(buf) })), ) } diff --git a/test/e2e/pkg/port/port.go b/test/e2e/pkg/port/port.go index 298892e7737..82ac8586361 100644 --- a/test/e2e/pkg/port/port.go +++ b/test/e2e/pkg/port/port.go @@ -10,8 +10,8 @@ import ( ) type Allocator struct { - reserved sets.Int - used sets.Int + reserved sets.Set[int] + used sets.Set[int] mu sync.Mutex } @@ -20,8 +20,8 @@ type Allocator struct { // Reserved ports: 13, 17 func NewAllocator(from int, to int, mod int, index int) *Allocator { pa := &Allocator{ - reserved: sets.NewInt(), - used: sets.NewInt(), + reserved: sets.New[int](), + used: sets.New[int](), } for i := from; i <= to; i++ { @@ -52,13 +52,13 @@ func (pa *Allocator) GetByName(portName string) int { pa.mu.Lock() defer pa.mu.Unlock() - for i := 0; i < 20; i++ { + for range 20 { port := pa.getByRange(builder.rangePortFrom, builder.rangePortTo) if port == 0 { return 0 } - l, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + l, err := net.Listen("tcp", net.JoinHostPort("0.0.0.0", strconv.Itoa(port))) if err != nil { // Maybe not controlled by us, mark it used. pa.used.Insert(port) @@ -66,7 +66,7 @@ func (pa *Allocator) GetByName(portName string) int { } l.Close() - udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort("0.0.0.0", strconv.Itoa(port))) if err != nil { continue } @@ -79,6 +79,7 @@ func (pa *Allocator) GetByName(portName string) int { udpConn.Close() pa.used.Insert(port) + pa.reserved.Delete(port) return port } return 0 diff --git a/test/e2e/pkg/port/util.go b/test/e2e/pkg/port/util.go index 9cf1204f5e7..073bf3c7787 100644 --- a/test/e2e/pkg/port/util.go +++ b/test/e2e/pkg/port/util.go @@ -26,16 +26,17 @@ func unmarshalFromName(name string) (*nameBuilder, error) { builder.name = arrs[1] case 4: builder.name = arrs[1] - if fromPort, err := strconv.Atoi(arrs[2]); err != nil { + fromPort, err := strconv.Atoi(arrs[2]) + if err != nil { return nil, fmt.Errorf("error range port from") - } else { - builder.rangePortFrom = fromPort } - if toPort, err := strconv.Atoi(arrs[3]); err != nil { + builder.rangePortFrom = fromPort + + toPort, err := strconv.Atoi(arrs[3]) + if err != nil { return nil, fmt.Errorf("error range port to") - } else { - builder.rangePortTo = toPort } + builder.rangePortTo = toPort default: return nil, fmt.Errorf("error port name format") } diff --git a/test/e2e/pkg/process/process.go b/test/e2e/pkg/process/process.go index e6721e1da33..0e05b90d146 100644 --- a/test/e2e/pkg/process/process.go +++ b/test/e2e/pkg/process/process.go @@ -3,21 +3,45 @@ package process import ( "bytes" "context" + "errors" + "fmt" "os/exec" + "strings" + "sync" + "time" ) +// SafeBuffer is a thread-safe wrapper around bytes.Buffer. +// It is safe to call Write and String concurrently. +type SafeBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *SafeBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *SafeBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} + type Process struct { cmd *exec.Cmd cancel context.CancelFunc - errorOutput *bytes.Buffer - stdOutput *bytes.Buffer + errorOutput *SafeBuffer + stdOutput *SafeBuffer - beforeStopHandler func() - stopped bool -} + done chan struct{} + closeOne sync.Once + waitErr error -func New(path string, params []string) *Process { - return NewWithEnvs(path, params, nil) + started bool + stopped bool } func NewWithEnvs(path string, params []string, envs []string) *Process { @@ -27,30 +51,53 @@ func NewWithEnvs(path string, params []string, envs []string) *Process { p := &Process{ cmd: cmd, cancel: cancel, + done: make(chan struct{}), } - p.errorOutput = bytes.NewBufferString("") - p.stdOutput = bytes.NewBufferString("") + p.errorOutput = &SafeBuffer{} + p.stdOutput = &SafeBuffer{} cmd.Stderr = p.errorOutput cmd.Stdout = p.stdOutput return p } func (p *Process) Start() error { - return p.cmd.Start() + if p.started { + return errors.New("process already started") + } + p.started = true + + err := p.cmd.Start() + if err != nil { + p.waitErr = err + p.closeDone() + return err + } + go func() { + p.waitErr = p.cmd.Wait() + p.closeDone() + }() + return nil +} + +func (p *Process) closeDone() { + p.closeOne.Do(func() { close(p.done) }) +} + +// Done returns a channel that is closed when the process exits. +func (p *Process) Done() <-chan struct{} { + return p.done } func (p *Process) Stop() error { - if p.stopped { + if p.stopped || !p.started { return nil } defer func() { p.stopped = true }() - if p.beforeStopHandler != nil { - p.beforeStopHandler() - } p.cancel() - return p.cmd.Wait() + <-p.done + return p.waitErr } func (p *Process) ErrorOutput() string { @@ -61,6 +108,34 @@ func (p *Process) StdOutput() string { return p.stdOutput.String() } -func (p *Process) SetBeforeStopHandler(fn func()) { - p.beforeStopHandler = fn +func (p *Process) Output() string { + return p.stdOutput.String() + p.errorOutput.String() +} + +// CountOutput returns how many times pattern appears in the current accumulated output. +func (p *Process) CountOutput(pattern string) int { + return strings.Count(p.Output(), pattern) +} + +// WaitForOutput polls the combined process output until the pattern is found +// count time(s) or the timeout is reached. It also returns early if the process exits. +func (p *Process) WaitForOutput(pattern string, count int, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + output := p.Output() + if strings.Count(output, pattern) >= count { + return nil + } + select { + case <-p.Done(): + // Process exited, check one last time. + output = p.Output() + if strings.Count(output, pattern) >= count { + return nil + } + return fmt.Errorf("process exited before %d occurrence(s) of %q found", count, pattern) + case <-time.After(25 * time.Millisecond): + } + } + return fmt.Errorf("timeout waiting for %d occurrence(s) of %q", count, pattern) } diff --git a/test/e2e/pkg/relay/halfopen.go b/test/e2e/pkg/relay/halfopen.go new file mode 100644 index 00000000000..34389e53315 --- /dev/null +++ b/test/e2e/pkg/relay/halfopen.go @@ -0,0 +1,208 @@ +// 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 relay + +import ( + "fmt" + "io" + "net" + "strconv" + "sync" + "time" +) + +// HalfOpen forwards TCP connections until Blackhole is called. A blackholed +// pair stops forwarding but deliberately retains the upstream socket so the +// peer sees a real half-open connection until the relay is closed. +type HalfOpen struct { + bindAddr string + bindPort int + upstreamAddr string + + listener net.Listener + done chan struct{} + accepted chan struct{} + + mu sync.Mutex + pairs []*connectionPair + + wg sync.WaitGroup + closeOnce sync.Once +} + +type connectionPair struct { + downstream net.Conn + upstream net.Conn + + mu sync.Mutex + blackholed bool +} + +func New(upstreamAddr string) *HalfOpen { + return &HalfOpen{ + bindAddr: "127.0.0.1", + upstreamAddr: upstreamAddr, + done: make(chan struct{}), + accepted: make(chan struct{}, 1), + } +} + +func (r *HalfOpen) Run() error { + listener, err := net.Listen("tcp", net.JoinHostPort(r.bindAddr, strconv.Itoa(r.bindPort))) + if err != nil { + return err + } + r.listener = listener + r.bindPort = listener.Addr().(*net.TCPAddr).Port + + r.wg.Add(1) + go r.acceptLoop() + return nil +} + +func (r *HalfOpen) acceptLoop() { + defer r.wg.Done() + for { + downstream, err := r.listener.Accept() + if err != nil { + return + } + upstream, err := net.DialTimeout("tcp", r.upstreamAddr, 3*time.Second) + if err != nil { + _ = downstream.Close() + continue + } + + pair := &connectionPair{downstream: downstream, upstream: upstream} + r.mu.Lock() + r.pairs = append(r.pairs, pair) + r.mu.Unlock() + select { + case r.accepted <- struct{}{}: + default: + } + + r.wg.Add(1) + go r.servePair(pair) + } +} + +func (r *HalfOpen) servePair(pair *connectionPair) { + defer r.wg.Done() + copyDone := make(chan struct{}, 2) + go func() { + _, _ = io.Copy(pair.upstream, pair.downstream) + copyDone <- struct{}{} + }() + go func() { + _, _ = io.Copy(pair.downstream, pair.upstream) + copyDone <- struct{}{} + }() + + completed := 0 + select { + case <-copyDone: + completed = 1 + if !pair.isBlackholed() { + _ = pair.downstream.Close() + _ = pair.upstream.Close() + } + case <-r.done: + _ = pair.downstream.Close() + _ = pair.upstream.Close() + } + for completed < 2 { + <-copyDone + completed++ + } + + if pair.isBlackholed() { + <-r.done + _ = pair.downstream.Close() + _ = pair.upstream.Close() + } +} + +func (r *HalfOpen) WaitForConnections(count int, timeout time.Duration) error { + timer := time.NewTimer(timeout) + defer timer.Stop() + for { + r.mu.Lock() + accepted := len(r.pairs) + r.mu.Unlock() + if accepted >= count { + return nil + } + select { + case <-r.accepted: + case <-r.done: + return fmt.Errorf("relay closed after accepting %d of %d connections", accepted, count) + case <-timer.C: + return fmt.Errorf("timed out after accepting %d of %d connections", accepted, count) + } + } +} + +// Blackhole uses a one-based connection index in accept order. +func (r *HalfOpen) Blackhole(index int) error { + r.mu.Lock() + if index <= 0 || index > len(r.pairs) { + accepted := len(r.pairs) + r.mu.Unlock() + return fmt.Errorf("connection %d is unavailable; accepted %d", index, accepted) + } + pair := r.pairs[index-1] + r.mu.Unlock() + + pair.mu.Lock() + if pair.blackholed { + pair.mu.Unlock() + return nil + } + pair.blackholed = true + pair.mu.Unlock() + + now := time.Now() + _ = pair.downstream.SetDeadline(now) + _ = pair.upstream.SetDeadline(now) + return nil +} + +func (r *HalfOpen) Close() error { + r.closeOnce.Do(func() { + close(r.done) + if r.listener != nil { + _ = r.listener.Close() + } + r.mu.Lock() + pairs := append([]*connectionPair(nil), r.pairs...) + r.mu.Unlock() + for _, pair := range pairs { + _ = pair.downstream.Close() + _ = pair.upstream.Close() + } + r.wg.Wait() + }) + return nil +} + +func (r *HalfOpen) BindAddr() string { return r.bindAddr } +func (r *HalfOpen) BindPort() int { return r.bindPort } + +func (p *connectionPair) isBlackholed() bool { + p.mu.Lock() + defer p.mu.Unlock() + return p.blackholed +} diff --git a/test/e2e/pkg/request/request.go b/test/e2e/pkg/request/request.go index cf6352f3828..c3b726693f0 100644 --- a/test/e2e/pkg/request/request.go +++ b/test/e2e/pkg/request/request.go @@ -12,20 +12,20 @@ import ( "strconv" "time" + libnet "github.com/fatedier/golib/net" + + httppkg "github.com/fatedier/frp/pkg/util/http" "github.com/fatedier/frp/test/e2e/pkg/rpc" - "github.com/fatedier/frp/test/e2e/pkg/utils" - libdial "github.com/fatedier/golib/net/dial" ) type Request struct { protocol string // for all protocol - addr string - port int - body []byte - timeout time.Duration - resolver *net.Resolver + addr string + port int + body []byte + timeout time.Duration // for http or https method string @@ -114,7 +114,7 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request { } func (r *Request) HTTPAuth(user, password string) *Request { - r.authValue = utils.BasicAuth(user, password) + r.authValue = httppkg.BasicAuth(user, password) return r } @@ -133,18 +133,16 @@ func (r *Request) Body(content []byte) *Request { return r } -func (r *Request) Resolver(resolver *net.Resolver) *Request { - r.resolver = resolver - return r -} - func (r *Request) Do() (*Response, error) { var ( conn net.Conn err error ) - addr := net.JoinHostPort(r.addr, strconv.Itoa(r.port)) + addr := r.addr + if r.port > 0 { + addr = net.JoinHostPort(r.addr, strconv.Itoa(r.port)) + } // for protocol http and https if r.protocol == "http" || r.protocol == "https" { return r.sendHTTPRequest(r.method, fmt.Sprintf("%s://%s%s", r.protocol, addr, r.path), @@ -156,16 +154,16 @@ func (r *Request) Do() (*Response, error) { if r.protocol != "tcp" { return nil, fmt.Errorf("only tcp protocol is allowed for proxy") } - proxyType, proxyAddress, auth, err := libdial.ParseProxyURL(r.proxyURL) + proxyType, proxyAddress, auth, err := libnet.ParseProxyURL(r.proxyURL) if err != nil { return nil, fmt.Errorf("parse ProxyURL error: %v", err) } - conn, err = libdial.Dial(addr, libdial.WithProxy(proxyType, proxyAddress), libdial.WithProxyAuth(auth)) + conn, err = libnet.Dial(addr, libnet.WithProxy(proxyType, proxyAddress), libnet.WithProxyAuth(auth)) if err != nil { return nil, err } } else { - dialer := &net.Dialer{Resolver: r.resolver} + dialer := &net.Dialer{} switch r.protocol { case "tcp": conn, err = dialer.Dial("tcp", addr) @@ -181,7 +179,7 @@ func (r *Request) Do() (*Response, error) { defer conn.Close() if r.timeout > 0 { - conn.SetDeadline(time.Now().Add(r.timeout)) + _ = conn.SetDeadline(time.Now().Add(r.timeout)) } buf, err := r.sendRequestByConn(conn, r.body) if err != nil { @@ -199,7 +197,6 @@ type Response struct { func (r *Request) sendHTTPRequest(method, urlstr string, host string, headers map[string]string, proxy string, body []byte, tlsConfig *tls.Config, ) (*Response, error) { - var inBody io.Reader if len(body) != 0 { inBody = bytes.NewReader(body) @@ -222,7 +219,6 @@ func (r *Request) sendHTTPRequest(method, urlstr string, host string, headers ma Timeout: time.Second, KeepAlive: 30 * time.Second, DualStack: true, - Resolver: r.resolver, }).DialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, @@ -240,6 +236,7 @@ func (r *Request) sendHTTPRequest(method, urlstr string, host string, headers ma if err != nil { return nil, err } + defer resp.Body.Close() ret := &Response{Code: resp.StatusCode, Header: resp.Header} buf, err := io.ReadAll(resp.Body) diff --git a/test/e2e/pkg/rpc/rpc.go b/test/e2e/pkg/rpc/rpc.go index ee3aef56f22..48b240d7587 100644 --- a/test/e2e/pkg/rpc/rpc.go +++ b/test/e2e/pkg/rpc/rpc.go @@ -4,12 +4,16 @@ import ( "bytes" "encoding/binary" "errors" + "fmt" "io" ) func WriteBytes(w io.Writer, buf []byte) (int, error) { out := bytes.NewBuffer(nil) - binary.Write(out, binary.BigEndian, int64(len(buf))) + if err := binary.Write(out, binary.BigEndian, int64(len(buf))); err != nil { + return 0, err + } + out.Write(buf) return w.Write(out.Bytes()) } @@ -19,6 +23,9 @@ func ReadBytes(r io.Reader) ([]byte, error) { if err := binary.Read(r, binary.BigEndian, &length); err != nil { return nil, err } + if length < 0 || length > 10*1024*1024 { + return nil, fmt.Errorf("invalid length") + } buffer := make([]byte, length) n, err := io.ReadFull(r, buffer) if err != nil { diff --git a/test/e2e/pkg/sdk/client/client.go b/test/e2e/pkg/sdk/client/client.go deleted file mode 100644 index 2bca92e7457..00000000000 --- a/test/e2e/pkg/sdk/client/client.go +++ /dev/null @@ -1,133 +0,0 @@ -package client - -import ( - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "strconv" - "strings" - - "github.com/fatedier/frp/client" - "github.com/fatedier/frp/test/e2e/pkg/utils" -) - -type Client struct { - address string - authUser string - authPwd string -} - -func New(host string, port int) *Client { - return &Client{ - address: net.JoinHostPort(host, strconv.Itoa(port)), - } -} - -func (c *Client) SetAuth(user, pwd string) { - c.authUser = user - c.authPwd = pwd -} - -func (c *Client) GetProxyStatus(name string) (*client.ProxyStatusResp, error) { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/status", nil) - if err != nil { - return nil, err - } - content, err := c.do(req) - if err != nil { - return nil, err - } - allStatus := &client.StatusResp{} - if err = json.Unmarshal([]byte(content), &allStatus); err != nil { - return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content)) - } - for _, s := range allStatus.TCP { - if s.Name == name { - return &s, nil - } - } - for _, s := range allStatus.UDP { - if s.Name == name { - return &s, nil - } - } - for _, s := range allStatus.HTTP { - if s.Name == name { - return &s, nil - } - } - for _, s := range allStatus.HTTPS { - if s.Name == name { - return &s, nil - } - } - for _, s := range allStatus.STCP { - if s.Name == name { - return &s, nil - } - } - for _, s := range allStatus.XTCP { - if s.Name == name { - return &s, nil - } - } - for _, s := range allStatus.SUDP { - if s.Name == name { - return &s, nil - } - } - return nil, fmt.Errorf("no proxy status found") -} - -func (c *Client) Reload() error { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload", nil) - if err != nil { - return err - } - _, err = c.do(req) - return err -} - -func (c *Client) GetConfig() (string, error) { - req, err := http.NewRequest("GET", "http://"+c.address+"/api/config", nil) - if err != nil { - return "", err - } - return c.do(req) -} - -func (c *Client) UpdateConfig(content string) error { - req, err := http.NewRequest("PUT", "http://"+c.address+"/api/config", strings.NewReader(content)) - if err != nil { - return err - } - _, err = c.do(req) - return err -} - -func (c *Client) setAuthHeader(req *http.Request) { - if c.authUser != "" || c.authPwd != "" { - req.Header.Set("Authorization", utils.BasicAuth(c.authUser, c.authPwd)) - } -} - -func (c *Client) do(req *http.Request) (string, error) { - c.setAuthHeader(req) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return "", fmt.Errorf("api status code [%d]", resp.StatusCode) - } - buf, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - return string(buf), nil -} diff --git a/test/e2e/pkg/ssh/client.go b/test/e2e/pkg/ssh/client.go new file mode 100644 index 00000000000..d2525871cf9 --- /dev/null +++ b/test/e2e/pkg/ssh/client.go @@ -0,0 +1,89 @@ +package ssh + +import ( + "net" + + libio "github.com/fatedier/golib/io" + "golang.org/x/crypto/ssh" +) + +type TunnelClient struct { + localAddr string + sshServer string + commands string + + sshConn *ssh.Client + ln net.Listener +} + +func NewTunnelClient(localAddr string, sshServer string, commands string) *TunnelClient { + return &TunnelClient{ + localAddr: localAddr, + sshServer: sshServer, + commands: commands, + } +} + +func (c *TunnelClient) Start() error { + config := &ssh.ClientConfig{ + User: "v0", + HostKeyCallback: func(string, net.Addr, ssh.PublicKey) error { return nil }, + } + + conn, err := ssh.Dial("tcp", c.sshServer, config) + if err != nil { + return err + } + c.sshConn = conn + + l, err := conn.Listen("tcp", "0.0.0.0:80") + if err != nil { + return err + } + c.ln = l + ch, req, err := conn.OpenChannel("session", []byte("")) + if err != nil { + return err + } + defer ch.Close() + go ssh.DiscardRequests(req) + + type command struct { + Cmd string + } + _, err = ch.SendRequest("exec", false, ssh.Marshal(command{Cmd: c.commands})) + if err != nil { + return err + } + + go c.serveListener() + return nil +} + +func (c *TunnelClient) Close() { + if c.sshConn != nil { + _ = c.sshConn.Close() + } + if c.ln != nil { + _ = c.ln.Close() + } +} + +func (c *TunnelClient) serveListener() { + for { + conn, err := c.ln.Accept() + if err != nil { + return + } + go c.handleConn(conn) + } +} + +func (c *TunnelClient) handleConn(conn net.Conn) { + defer conn.Close() + local, err := net.Dial("tcp", c.localAddr) + if err != nil { + return + } + _, _, _ = libio.Join(local, conn) +} diff --git a/test/e2e/pkg/utils/utils.go b/test/e2e/pkg/utils/utils.go deleted file mode 100644 index 7b177e14bb0..00000000000 --- a/test/e2e/pkg/utils/utils.go +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -import ( - "encoding/base64" -) - -func BasicAuth(username, passwd string) string { - auth := username + ":" + passwd - return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) -} diff --git a/test/e2e/v1/basic/annotations.go b/test/e2e/v1/basic/annotations.go new file mode 100644 index 00000000000..78e94a167db --- /dev/null +++ b/test/e2e/v1/basic/annotations.go @@ -0,0 +1,54 @@ +package basic + +import ( + "fmt" + "io" + "net/http" + + "github.com/onsi/ginkgo/v2" + "github.com/tidwall/gjson" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" +) + +var _ = ginkgo.Describe("[Feature: Annotations]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Set Proxy Annotations", func() { + webPort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + webServer.port = %d + `, webPort) + + p1Port := f.AllocPort() + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "p1" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + [proxies.annotations] + "frp.e2e.test/foo" = "value1" + "frp.e2e.test/bar" = "value2" + `, framework.TCPEchoServerPort, p1Port) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(p1Port).Ensure() + + // check annotations in frps + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/proxy/tcp/%s", webPort, "p1")) + framework.ExpectNoError(err) + framework.ExpectEqual(resp.StatusCode, 200) + defer resp.Body.Close() + content, err := io.ReadAll(resp.Body) + framework.ExpectNoError(err) + + annotations := gjson.Get(string(content), "conf.annotations").Map() + framework.ExpectEqual("value1", annotations["frp.e2e.test/foo"].String()) + framework.ExpectEqual("value2", annotations["frp.e2e.test/bar"].String()) + }) +}) diff --git a/test/e2e/v1/basic/basic.go b/test/e2e/v1/basic/basic.go new file mode 100644 index 00000000000..8994ba73d56 --- /dev/null +++ b/test/e2e/v1/basic/basic.go @@ -0,0 +1,531 @@ +package basic + +import ( + "crypto/tls" + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Basic]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("TCP && UDP", func() { + types := []string{"tcp", "udp"} + for _, t := range types { + proxyType := t + ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { + serverConf := consts.DefaultServerConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) + + localPortName := "" + protocol := "tcp" + switch proxyType { + case "tcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + case "udp": + localPortName = framework.UDPEchoServerPort + protocol = "udp" + } + getProxyConf := func(proxyName string, portName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "%s" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `+extra, proxyName, proxyType, localPortName, portName) + } + + tests := []struct { + proxyName string + portName string + extraConfig string + }{ + { + proxyName: "normal", + portName: port.GenName("Normal"), + }, + { + proxyName: "with-encryption", + portName: port.GenName("WithEncryption"), + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + portName: port.GenName("WithCompression"), + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + portName: port.GenName("WithEncryptionAndCompression"), + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + } + + // build all client config + for _, test := range tests { + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") + } + // run frps and frpc + f.RunProcesses(serverConf, []string{clientConf.String()}) + + for _, test := range tests { + framework.NewRequestExpect(f). + Protocol(protocol). + PortName(test.portName). + Explain(test.proxyName). + Ensure() + } + }) + } + }) + + ginkgo.Describe("HTTP", func() { + ginkgo.It("proxy to HTTP server", func() { + serverConf := consts.DefaultServerConfig + vhostHTTPPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) + + getProxyConf := func(proxyName string, customDomains string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "http" + localPort = {{ .%s }} + customDomains = %s + `+extra, proxyName, framework.HTTPSimpleServerPort, customDomains) + } + + tests := []struct { + proxyName string + customDomains string + extraConfig string + }{ + { + proxyName: "normal", + }, + { + proxyName: "with-encryption", + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + { + proxyName: "multiple-custom-domains", + customDomains: `["a.example.com", "b.example.com"]`, + }, + } + + // build all client config + for i, test := range tests { + if tests[i].customDomains == "" { + tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com") + } + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") + } + // run frps and frpc + f.RunProcesses(serverConf, []string{clientConf.String()}) + + for _, test := range tests { + for domain := range strings.SplitSeq(test.customDomains, ",") { + domain = strings.TrimSpace(domain) + domain = strings.TrimLeft(domain, "[\"") + domain = strings.TrimRight(domain, "]\"") + framework.NewRequestExpect(f). + Explain(test.proxyName + "-" + domain). + Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost(domain) + }). + Ensure() + } + } + + // not exist host + framework.NewRequestExpect(f). + Explain("not exist host"). + Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("not-exist.example.com") + }). + Ensure(framework.ExpectResponseCode(404)) + }) + }) + + ginkgo.Describe("HTTPS", func() { + ginkgo.It("proxy to HTTPS server", func() { + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) + getProxyConf := func(proxyName string, customDomains string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "https" + localPort = %d + customDomains = %s + `+extra, proxyName, localPort, customDomains) + } + + tests := []struct { + proxyName string + customDomains string + extraConfig string + }{ + { + proxyName: "normal", + }, + { + proxyName: "with-encryption", + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + { + proxyName: "multiple-custom-domains", + customDomains: `["a.example.com", "b.example.com"]`, + }, + } + + // build all client config + for i, test := range tests { + if tests[i].customDomains == "" { + tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com") + } + clientConf.WriteString(getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n") + } + // run frps and frpc + f.RunProcesses(serverConf, []string{clientConf.String()}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithTLSConfig(tlsConfig), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + for _, test := range tests { + for domain := range strings.SplitSeq(test.customDomains, ",") { + domain = strings.TrimSpace(domain) + domain = strings.TrimLeft(domain, "[\"") + domain = strings.TrimRight(domain, "]\"") + framework.NewRequestExpect(f). + Explain(test.proxyName + "-" + domain). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost(domain).TLSConfig(&tls.Config{ + ServerName: domain, + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + } + } + + // not exist host + notExistDomain := "not-exist.example.com" + framework.NewRequestExpect(f). + Explain("not exist host"). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost(notExistDomain).TLSConfig(&tls.Config{ + ServerName: notExistDomain, + InsecureSkipVerify: true, + }) + }). + ExpectError(true). + Ensure() + }) + }) + + ginkgo.Describe("STCP && SUDP && XTCP", func() { + types := []string{"stcp", "sudp", "xtcp"} + for _, t := range types { + proxyType := t + ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { + serverConf := consts.DefaultServerConfig + var clientServerConf strings.Builder + clientServerConf.WriteString(consts.DefaultClientConfig + "\nuser = \"user1\"") + var clientVisitorConf strings.Builder + clientVisitorConf.WriteString(consts.DefaultClientConfig + "\nuser = \"user1\"") + var clientUser2VisitorConf strings.Builder + clientUser2VisitorConf.WriteString(consts.DefaultClientConfig + "\nuser = \"user2\"") + + localPortName := "" + protocol := "tcp" + switch proxyType { + case "stcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + case "sudp": + localPortName = framework.UDPEchoServerPort + protocol = "udp" + case "xtcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + ginkgo.Skip("stun server is not stable") + } + + correctSK := "abc" + wrongSK := "123" + + getProxyServerConf := func(proxyName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "%s" + secretKey = "%s" + localPort = {{ .%s }} + `+extra, proxyName, proxyType, correctSK, localPortName) + } + getProxyVisitorConf := func(proxyName string, portName, visitorSK, extra string) string { + return fmt.Sprintf(` + [[visitors]] + name = "%s" + type = "%s" + serverName = "%s" + secretKey = "%s" + bindPort = {{ .%s }} + `+extra, proxyName, proxyType, proxyName, visitorSK, portName) + } + + tests := []struct { + proxyName string + bindPortName string + visitorSK string + commonExtraConfig string + proxyExtraConfig string + visitorExtraConfig string + expectError bool + deployUser2Client bool + // skipXTCP is used to skip xtcp test case + skipXTCP bool + }{ + { + proxyName: "normal", + bindPortName: port.GenName("Normal"), + visitorSK: correctSK, + skipXTCP: true, + }, + { + proxyName: "with-encryption", + bindPortName: port.GenName("WithEncryption"), + visitorSK: correctSK, + commonExtraConfig: "transport.useEncryption = true", + skipXTCP: true, + }, + { + proxyName: "with-compression", + bindPortName: port.GenName("WithCompression"), + visitorSK: correctSK, + commonExtraConfig: "transport.useCompression = true", + skipXTCP: true, + }, + { + proxyName: "with-encryption-and-compression", + bindPortName: port.GenName("WithEncryptionAndCompression"), + visitorSK: correctSK, + commonExtraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + skipXTCP: true, + }, + { + proxyName: "with-error-sk", + bindPortName: port.GenName("WithErrorSK"), + visitorSK: wrongSK, + expectError: true, + }, + { + proxyName: "allowed-user", + bindPortName: port.GenName("AllowedUser"), + visitorSK: correctSK, + proxyExtraConfig: `allowUsers = ["another", "user2"]`, + visitorExtraConfig: `serverUser = "user1"`, + deployUser2Client: true, + }, + { + proxyName: "not-allowed-user", + bindPortName: port.GenName("NotAllowedUser"), + visitorSK: correctSK, + proxyExtraConfig: `allowUsers = ["invalid"]`, + visitorExtraConfig: `serverUser = "user1"`, + expectError: true, + }, + { + proxyName: "allow-all", + bindPortName: port.GenName("AllowAll"), + visitorSK: correctSK, + proxyExtraConfig: `allowUsers = ["*"]`, + visitorExtraConfig: `serverUser = "user1"`, + deployUser2Client: true, + }, + } + + // build all client config + for _, test := range tests { + clientServerConf.WriteString(getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n") + } + for _, test := range tests { + config := getProxyVisitorConf( + test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig, + ) + "\n" + if test.deployUser2Client { + clientUser2VisitorConf.WriteString(config) + } else { + clientVisitorConf.WriteString(config) + } + } + // run frps and frpc + f.RunProcesses(serverConf, []string{clientServerConf.String(), clientVisitorConf.String(), clientUser2VisitorConf.String()}) + + for _, test := range tests { + timeout := time.Second + if t == "xtcp" { + if test.skipXTCP { + continue + } + timeout = 10 * time.Second + } + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Timeout(timeout) + }). + Protocol(protocol). + PortName(test.bindPortName). + Explain(test.proxyName). + ExpectError(test.expectError). + Ensure() + } + }) + } + }) + + ginkgo.Describe("TCPMUX", func() { + ginkgo.It("Type tcpmux", func() { + serverConf := consts.DefaultServerConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) + + tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") + serverConf += fmt.Sprintf(` + tcpmuxHTTPConnectPort = {{ .%s }} + `, tcpmuxHTTPConnectPortName) + + getProxyConf := func(proxyName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = {{ .%s }} + customDomains = ["%s"] + `+extra, proxyName, port.GenName(proxyName), proxyName) + } + + tests := []struct { + proxyName string + extraConfig string + }{ + { + proxyName: "normal", + }, + { + proxyName: "with-encryption", + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + } + + // build all client config + for _, test := range tests { + clientConf.WriteString(getProxyConf(test.proxyName, test.extraConfig) + "\n") + + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName))) + f.RunServer(port.GenName(test.proxyName), localServer) + } + + // run frps and frpc + f.RunProcesses(serverConf, []string{clientConf.String()}) + + // Request without HTTP connect should get error + framework.NewRequestExpect(f). + PortName(tcpmuxHTTPConnectPortName). + ExpectError(true). + Explain("request without HTTP connect expect error"). + Ensure() + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", f.PortByName(tcpmuxHTTPConnectPortName)) + // Request with incorrect connect hostname + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr("invalid").Proxy(proxyURL) + }).ExpectError(true).Explain("request without HTTP connect expect error").Ensure() + + // Request with correct connect hostname + for _, test := range tests { + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr(test.proxyName).Proxy(proxyURL) + }).ExpectResp([]byte(test.proxyName)).Explain(test.proxyName).Ensure() + } + }) + }) +}) diff --git a/test/e2e/v1/basic/client.go b/test/e2e/v1/basic/client.go new file mode 100644 index 00000000000..ec2011ea4c4 --- /dev/null +++ b/test/e2e/v1/basic/client.go @@ -0,0 +1,136 @@ +package basic + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: ClientManage]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Update && Reload API", func() { + serverConf := consts.DefaultServerConfig + + adminPort := f.AllocPort() + + p1Port := f.AllocPort() + p2Port := f.AllocPort() + p3Port := f.AllocPort() + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.port = %d + + [[proxies]] + name = "p1" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + + [[proxies]] + name = "p2" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + + [[proxies]] + name = "p3" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, adminPort, + framework.TCPEchoServerPort, p1Port, + framework.TCPEchoServerPort, p2Port, + framework.TCPEchoServerPort, p3Port) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(p1Port).Ensure() + framework.NewRequestExpect(f).Port(p2Port).Ensure() + framework.NewRequestExpect(f).Port(p3Port).Ensure() + + client := f.APIClientForFrpc(adminPort) + conf, err := client.GetConfig(context.Background()) + framework.ExpectNoError(err) + + newP2Port := f.AllocPort() + // change p2 port and remove p3 proxy + newClientConf := strings.ReplaceAll(conf, strconv.Itoa(p2Port), strconv.Itoa(newP2Port)) + p3Index := strings.LastIndex(newClientConf, "[[proxies]]") + if p3Index >= 0 { + newClientConf = newClientConf[:p3Index] + } + + err = client.UpdateConfig(context.Background(), newClientConf) + framework.ExpectNoError(err) + + err = client.Reload(context.Background(), true) + framework.ExpectNoError(err) + time.Sleep(time.Second) + + framework.NewRequestExpect(f).Port(p1Port).Explain("p1 port").Ensure() + framework.NewRequestExpect(f).Port(p2Port).Explain("original p2 port").ExpectError(true).Ensure() + framework.NewRequestExpect(f).Port(newP2Port).Explain("new p2 port").Ensure() + framework.NewRequestExpect(f).Port(p3Port).Explain("p3 port").ExpectError(true).Ensure() + }) + + ginkgo.It("healthz", func() { + serverConf := consts.DefaultServerConfig + + dashboardPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "0.0.0.0" + webServer.port = %d + webServer.user = "admin" + webServer.password = "admin" + `, dashboardPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/healthz") + }).Port(dashboardPort).ExpectResp([]byte("")).Ensure() + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/") + }).Port(dashboardPort). + Ensure(framework.ExpectResponseCode(401)) + }) + + ginkgo.It("stop", func() { + serverConf := consts.DefaultServerConfig + + adminPort := f.AllocPort() + testPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.port = %d + + [[proxies]] + name = "test" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, adminPort, framework.TCPEchoServerPort, testPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(testPort).Ensure() + + client := f.APIClientForFrpc(adminPort) + err := client.Stop(context.Background()) + framework.ExpectNoError(err) + + time.Sleep(3 * time.Second) + + // frpc stopped so the port is not listened, expect error + framework.NewRequestExpect(f).Port(testPort).ExpectError(true).Ensure() + }) +}) diff --git a/test/e2e/v1/basic/client_server.go b/test/e2e/v1/basic/client_server.go new file mode 100644 index 00000000000..3a2fa28d15c --- /dev/null +++ b/test/e2e/v1/basic/client_server.go @@ -0,0 +1,345 @@ +package basic + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/cert" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +type generalTestConfigures struct { + server string + client string + clientPrefix string + client2 string + client2Prefix string + testDelay time.Duration + expectError bool +} + +func renderBindPortConfig(protocol string) string { + switch protocol { + case "kcp": + return fmt.Sprintf(`kcpBindPort = {{ .%s }}`, consts.PortServerName) + case "quic": + return fmt.Sprintf(`quicBindPort = {{ .%s }}`, consts.PortServerName) + default: + return "" + } +} + +func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + if configures.clientPrefix != "" { + clientConf = configures.clientPrefix + } + + serverConf += fmt.Sprintf(` + %s + `, configures.server) + + tcpPortName := port.GenName("TCP") + udpPortName := port.GenName("UDP") + clientConf += fmt.Sprintf(` + %s + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, configures.client, + framework.TCPEchoServerPort, tcpPortName, + framework.UDPEchoServerPort, udpPortName, + ) + + clientConfs := []string{clientConf} + if configures.client2 != "" { + client2Conf := consts.DefaultClientConfig + if configures.client2Prefix != "" { + client2Conf = configures.client2Prefix + } + client2Conf += fmt.Sprintf(` + %s + `, configures.client2) + clientConfs = append(clientConfs, client2Conf) + } + + f.RunProcesses(serverConf, clientConfs) + + if configures.testDelay > 0 { + time.Sleep(configures.testDelay) + } + + framework.NewRequestExpect(f).PortName(tcpPortName).ExpectError(configures.expectError).Explain("tcp proxy").Ensure() + framework.NewRequestExpect(f).Protocol("udp"). + PortName(udpPortName).ExpectError(configures.expectError).Explain("udp proxy").Ensure() +} + +// defineClientServerTest test a normal tcp and udp proxy with specified TestConfigures. +func defineClientServerTest(desc string, f *framework.Framework, configures *generalTestConfigures) { + ginkgo.It(desc, func() { + runClientServerTest(f, configures) + }) +} + +var _ = ginkgo.Describe("[Feature: Client-Server]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Protocol", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + configures := &generalTestConfigures{ + server: fmt.Sprintf(` + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(`transport.protocol = "%s"`, protocol), + } + defineClientServerTest(protocol, f, configures) + } + }) + + // wss is special, it needs to be tested separately. + // frps only supports ws, so there should be a proxy to terminate TLS before frps. + ginkgo.Describe("Protocol wss", func() { + wssPort := f.AllocPort() + configures := &generalTestConfigures{ + clientPrefix: fmt.Sprintf(` + serverAddr = "127.0.0.1" + serverPort = %d + loginFailExit = false + transport.protocol = "wss" + log.level = "trace" + `, wssPort), + // Due to the fact that frps cannot directly accept wss connections, we use the https2http plugin of another frpc to terminate TLS. + 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, + } + + defineClientServerTest("wss", f, configures) + }) + + ginkgo.Describe("Authentication", func() { + defineClientServerTest("Token Correct", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: `auth.token = "123456"`, + }) + + defineClientServerTest("Token Incorrect", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: `auth.token = "invalid"`, + expectError: true, + }) + }) + + ginkgo.Describe("TLS", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + // Since v0.50.0, the default value of tls_enable has been changed to true. + // Therefore, here it needs to be set as false to test the scenario of turning it off. + defineClientServerTest("Disable TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(`transport.tls.enable = false + transport.protocol = "%s" + `, protocol), + }) + } + + defineClientServerTest("enable tls force, client with TLS", f, &generalTestConfigures{ + server: "transport.tls.force = true", + }) + defineClientServerTest("enable tls force, client without TLS", f, &generalTestConfigures{ + server: "transport.tls.force = true", + client: "transport.tls.enable = false", + expectError: true, + }) + }) + + ginkgo.Describe("TLS with custom certificate", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + + var ( + caCrtPath string + serverCrtPath, serverKeyPath string + clientCrtPath, clientKeyPath string + ) + ginkgo.JustBeforeEach(func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("127.0.0.1") + framework.ExpectNoError(err) + + caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert)) + serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert)) + serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key)) + generator.SetCA(artifacts.CACert, artifacts.CAKey) + _, err = generator.Generate("127.0.0.1") + framework.ExpectNoError(err) + clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert)) + clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key)) + }) + + for _, protocol := range supportProtocols { + tmp := protocol + + ginkgo.It("one-way authentication: "+tmp, func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + transport.tls.trustedCaFile = "%s" + `, renderBindPortConfig(tmp), caCrtPath), + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + `, tmp, clientCrtPath, clientKeyPath), + }) + }) + + ginkgo.It("mutual authentication: "+tmp, func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, renderBindPortConfig(tmp), serverCrtPath, serverKeyPath, caCrtPath), + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, tmp, clientCrtPath, clientKeyPath, caCrtPath), + }) + }) + } + }) + + ginkgo.Describe("TLS with custom certificate and specified server name", func() { + var ( + caCrtPath string + serverCrtPath, serverKeyPath string + clientCrtPath, clientKeyPath string + ) + ginkgo.JustBeforeEach(func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + + caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert)) + serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert)) + serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key)) + generator.SetCA(artifacts.CACert, artifacts.CAKey) + _, err = generator.Generate("example.com") + framework.ExpectNoError(err) + clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert)) + clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key)) + }) + + ginkgo.It("mutual authentication", func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, serverCrtPath, serverKeyPath, caCrtPath), + client: fmt.Sprintf(` + transport.tls.serverName = "example.com" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, clientCrtPath, clientKeyPath, caCrtPath), + }) + }) + + ginkgo.It("mutual authentication with incorrect server name", func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, serverCrtPath, serverKeyPath, caCrtPath), + client: fmt.Sprintf(` + transport.tls.serverName = "invalid.com" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, clientCrtPath, clientKeyPath, caCrtPath), + expectError: true, + }) + }) + }) + + ginkgo.Describe("TLS with disableCustomTLSFirstByte set to false", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.disableCustomTLSFirstByte = false + `, protocol), + }) + } + }) + + ginkgo.Describe("IPv6 bind address", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + defineClientServerTest("IPv6 bind address: "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + bindAddr = "::" + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(` + transport.protocol = "%s" + `, protocol), + }) + } + }) + + ginkgo.Describe("Use same port for bindPort and vhostHTTPSPort", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + defineClientServerTest("Use same port for bindPort and vhostHTTPSPort: "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + vhostHTTPSPort = {{ .%s }} + %s + `, consts.PortServerName, renderBindPortConfig(protocol)), + // transport.tls.disableCustomTLSFirstByte should set to false when vhostHTTPSPort is same as bindPort + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.disableCustomTLSFirstByte = false + `, protocol), + }) + } + }) +}) diff --git a/test/e2e/v1/basic/cmd.go b/test/e2e/v1/basic/cmd.go new file mode 100644 index 00000000000..44aa35990cb --- /dev/null +++ b/test/e2e/v1/basic/cmd.go @@ -0,0 +1,108 @@ +package basic + +import ( + "strconv" + "strings" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +const ( + ConfigValidStr = "syntax is ok" +) + +var _ = ginkgo.Describe("[Feature: Cmd]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Verify", func() { + ginkgo.It("frps valid", func() { + path := f.GenerateConfigFile(` + bindAddr = "0.0.0.0" + bindPort = 7000 + `) + _, output, err := f.RunFrps("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + ginkgo.It("frps invalid", func() { + path := f.GenerateConfigFile(` + bindAddr = "0.0.0.0" + bindPort = 70000 + `) + _, output, err := f.RunFrps("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + ginkgo.It("frpc valid", func() { + path := f.GenerateConfigFile(` + serverAddr = "0.0.0.0" + serverPort = 7000 + `) + _, output, err := f.RunFrpc("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + ginkgo.It("frpc invalid", func() { + path := f.GenerateConfigFile(` + serverAddr = "0.0.0.0" + serverPort = 7000 + transport.protocol = "invalid" + `) + _, output, err := f.RunFrpc("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + }) + + ginkgo.Describe("Single proxy", func() { + ginkgo.It("TCP", func() { + serverPort := f.AllocPort() + _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort)) + framework.ExpectNoError(err) + + localPort := f.PortByName(framework.TCPEchoServerPort) + remotePort := f.AllocPort() + _, _, err = f.RunFrpc("tcp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", + "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "tcp_test") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("UDP", func() { + serverPort := f.AllocPort() + _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort)) + framework.ExpectNoError(err) + + localPort := f.PortByName(framework.UDPEchoServerPort) + remotePort := f.AllocPort() + _, _, err = f.RunFrpc("udp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", + "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "udp_test") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Protocol("udp"). + Port(remotePort).Ensure() + }) + + ginkgo.It("HTTP", func() { + serverPort := f.AllocPort() + vhostHTTPPort := f.AllocPort() + _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost-http-port", strconv.Itoa(vhostHTTPPort)) + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("http", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", + "-n", "udp_test", "-l", strconv.Itoa(f.PortByName(framework.HTTPSimpleServerPort)), + "--custom-domain", "test.example.com") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("test.example.com") + }). + Ensure() + }) + }) +}) diff --git a/test/e2e/v1/basic/config.go b/test/e2e/v1/basic/config.go new file mode 100644 index 00000000000..e290109e98d --- /dev/null +++ b/test/e2e/v1/basic/config.go @@ -0,0 +1,168 @@ +package basic + +import ( + "context" + "fmt" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: Config]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Template", func() { + ginkgo.It("render by env", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + serverConf += fmt.Sprintf(` + auth.token = "{{ %s{{ .Envs.FRP_TOKEN }}%s }}" + `, "`", "`") + + clientConf += fmt.Sprintf(` + auth.token = "{{ %s{{ .Envs.FRP_TOKEN }}%s }}" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, "`", "`", framework.TCPEchoServerPort, portName) + + f.SetEnvs([]string{"FRP_TOKEN=123"}) + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("Range ports mapping", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + adminPort := f.AllocPort() + + localPortsRange := "13010-13012,13014" + remotePortsRange := "23010-23012,23014" + escapeTemplate := func(s string) string { + return "{{ `" + s + "` }}" + } + clientConf += fmt.Sprintf(` + webServer.port = %d + + %s + [[proxies]] + name = "tcp-%s" + type = "tcp" + localPort = %s + remotePort = %s + %s + `, adminPort, + escapeTemplate(fmt.Sprintf(`{{- range $_, $v := parseNumberRangePair "%s" "%s" }}`, localPortsRange, remotePortsRange)), + escapeTemplate("{{ $v.First }}"), + escapeTemplate("{{ $v.First }}"), + escapeTemplate("{{ $v.Second }}"), + escapeTemplate("{{- end }}"), + ) + + f.RunProcesses(serverConf, []string{clientConf}) + + client := f.APIClientForFrpc(adminPort) + checkProxyFn := func(name string, localPort, remotePort int) { + status, err := client.GetProxyStatus(context.Background(), name) + framework.ExpectNoError(err) + + framework.ExpectContainSubstring(status.LocalAddr, fmt.Sprintf(":%d", localPort)) + framework.ExpectContainSubstring(status.RemoteAddr, fmt.Sprintf(":%d", remotePort)) + } + checkProxyFn("tcp-13010", 13010, 23010) + checkProxyFn("tcp-13011", 13011, 23011) + checkProxyFn("tcp-13012", 13012, 23012) + checkProxyFn("tcp-13014", 13014, 23014) + }) + }) + + ginkgo.Describe("Includes", func() { + ginkgo.It("split tcp proxies into different files", func() { + serverPort := f.AllocPort() + serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + bindAddr = "0.0.0.0" + bindPort = %d + `, serverPort)) + + remotePort := f.AllocPort() + proxyConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, f.PortByName(framework.TCPEchoServerPort), remotePort)) + + remotePort2 := f.AllocPort() + proxyConfigPath2 := f.GenerateConfigFile(fmt.Sprintf(` + [[proxies]] + name = "tcp2" + type = "tcp" + localPort = %d + remotePort = %d + `, f.PortByName(framework.TCPEchoServerPort), remotePort2)) + + clientConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + serverPort = %d + includes = ["%s","%s"] + `, serverPort, proxyConfigPath, proxyConfigPath2)) + + _, _, err := f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + framework.NewRequestExpect(f).Port(remotePort2).Ensure() + }) + }) + + ginkgo.Describe("Support Formats", func() { + ginkgo.It("YAML", func() { + serverConf := fmt.Sprintf(` +bindPort: {{ .%s }} +log: + level: trace +`, port.GenName("Server")) + + remotePort := f.AllocPort() + clientConf := fmt.Sprintf(` +serverPort: {{ .%s }} +log: + level: trace + +proxies: +- name: tcp + type: tcp + localPort: {{ .%s }} + remotePort: %d +`, port.GenName("Server"), framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("JSON", func() { + serverConf := fmt.Sprintf(`{"bindPort": {{ .%s }}, "log": {"level": "trace"}}`, port.GenName("Server")) + + remotePort := f.AllocPort() + clientConf := fmt.Sprintf(`{"serverPort": {{ .%s }}, "log": {"level": "trace"}, +"proxies": [{"name": "tcp", "type": "tcp", "localPort": {{ .%s }}, "remotePort": %d}]}`, + port.GenName("Server"), framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + }) +}) diff --git a/test/e2e/v1/basic/http.go b/test/e2e/v1/basic/http.go new file mode 100644 index 00000000000..d982df26367 --- /dev/null +++ b/test/e2e/v1/basic/http.go @@ -0,0 +1,604 @@ +package basic + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/gorilla/websocket" + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: HTTP]", func() { + f := framework.NewDefaultFramework() + + getDefaultServerConf := func(vhostHTTPPort int) string { + conf := consts.DefaultServerConfig + ` + vhostHTTPPort = %d + ` + return fmt.Sprintf(conf, vhostHTTPPort) + } + newHTTPServer := func(port int, respContent string) *httpserver.Server { + return httpserver.New( + httpserver.WithBindPort(port), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte(respContent))), + ) + } + + ginkgo.It("HTTP route by locations", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + locations = ["/","/foo"] + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + locations = ["/bar"] + `, fooPort, barPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + tests := []struct { + path string + expectResp string + desc string + }{ + {path: "/foo", expectResp: "foo", desc: "foo path"}, + {path: "/bar", expectResp: "bar", desc: "bar path"}, + {path: "/other", expectResp: "foo", desc: "other path"}, + } + + for _, test := range tests { + framework.NewRequestExpect(f).Explain(test.desc).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPPath(test.path) + }). + ExpectResp([]byte(test.expectResp)). + Ensure() + } + }) + + ginkgo.It("HTTP route by HTTP user", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + otherPort := f.AllocPort() + f.RunServer("", newHTTPServer(otherPort, "other")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user1" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user2" + + [[proxies]] + name = "catchAll" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + `, fooPort, barPort, otherPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // user1 + framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user1", "") + }). + ExpectResp([]byte("foo")). + Ensure() + + // user2 + framework.NewRequestExpect(f).Explain("user2").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user2", "") + }). + ExpectResp([]byte("bar")). + Ensure() + + // other user + framework.NewRequestExpect(f).Explain("other user").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user3", "") + }). + ExpectResp([]byte("other")). + Ensure() + }) + + ginkgo.It("HTTP proxy mode uses proxy auth consistently", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + backendPort := f.AllocPort() + f.RunServer("", newHTTPServer(backendPort, "PRIVATE")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "protected" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "alice" + httpUser = "alice" + httpPassword = "secret" + `, backendPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + proxyURLWithAuth := func(username, password string) string { + if username == "" { + return fmt.Sprintf("http://127.0.0.1:%d", vhostHTTPPort) + } + return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, vhostHTTPPort) + } + + framework.NewRequestExpect(f).Explain("direct no auth").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + Ensure(framework.ExpectResponseCode(http.StatusNotFound)) + + framework.NewRequestExpect(f).Explain("direct correct auth").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("alice", "secret") + }). + ExpectResp([]byte("PRIVATE")). + Ensure() + + framework.NewRequestExpect(f).Explain("direct wrong auth").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("alice", "wrong") + }). + Ensure(framework.ExpectResponseCode(http.StatusUnauthorized)) + + framework.NewRequestExpect(f).Explain("proxy correct proxy auth"). + RequestModify(func(r *request.Request) { + r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "secret")) + }). + ExpectResp([]byte("PRIVATE")). + Ensure() + + framework.NewRequestExpect(f).Explain("proxy wrong proxy auth"). + RequestModify(func(r *request.Request) { + r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "wrong")) + }). + Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired)) + + framework.NewRequestExpect(f).Explain("proxy request ignores authorization header"). + RequestModify(func(r *request.Request) { + r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("", "")).HTTPAuth("alice", "secret") + }). + Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired)) + + framework.NewRequestExpect(f).Explain("proxy wrong proxy auth with correct authorization"). + RequestModify(func(r *request.Request) { + r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "wrong")).HTTPAuth("alice", "secret") + }). + Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired)) + }) + + ginkgo.It("HTTP Basic Auth", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = {{ .%s }} + customDomains = ["normal.example.com"] + httpUser = "test" + httpPassword = "test" + `, framework.HTTPSimpleServerPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // not set auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + Ensure(framework.ExpectResponseCode(401)) + + // set incorrect auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "invalid") + }). + Ensure(framework.ExpectResponseCode(401)) + + // set correct auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "test") + }). + Ensure() + }) + + ginkgo.It("Wildcard domain", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = {{ .%s }} + customDomains = ["*.example.com"] + `, framework.HTTPSimpleServerPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // not match host + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("not-match.test.com") + }). + Ensure(framework.ExpectResponseCode(404)) + + // test.example.com match *.example.com + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("test.example.com") + }). + Ensure() + + // sub.test.example.com match *.example.com + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("sub.test.example.com") + }). + Ensure() + }) + + ginkgo.It("Subdomain", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + serverConf += ` + subdomainHost = "example.com" + ` + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + subdomain = "foo" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + subdomain = "bar" + `, fooPort, barPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // foo + framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("foo.example.com") + }). + ExpectResp([]byte("foo")). + Ensure() + + // bar + framework.NewRequestExpect(f).Explain("bar subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("bar.example.com") + }). + ExpectResp([]byte("bar")). + Ensure() + }) + + ginkgo.It("Modify request headers", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-From-Where"))) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + requestHeaders.set.x-from-where = "frp" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + ExpectResp([]byte("frp")). // local http server will write this X-From-Where header to response body + Ensure() + }) + + ginkgo.It("Modify response headers", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + responseHeaders.set.x-from-where = "frp" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + Ensure(func(res *request.Response) bool { + return res.Header.Get("X-From-Where") == "frp" + }) + }) + + ginkgo.It("Host Header Rewrite", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Host)) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + hostHeaderRewrite = "rewrite.example.com" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + ExpectResp([]byte("rewrite.example.com")). // local http server will write host header to response body + Ensure() + }) + + ginkgo.It("Websocket protocol", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + upgrader := websocket.Upgrader{} + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + c, err := upgrader.Upgrade(w, req, nil) + if err != nil { + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + break + } + err = c.WriteMessage(mt, message) + if err != nil { + break + } + } + })), + ) + + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["127.0.0.1"] + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)} + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + framework.ExpectNoError(err) + + err = c.WriteMessage(websocket.TextMessage, []byte(consts.TestString)) + framework.ExpectNoError(err) + + _, msg, err := c.ReadMessage() + framework.ExpectNoError(err) + framework.ExpectEqualValues(consts.TestString, string(msg)) + }) + + ginkgo.It("vhostHTTPTimeout", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + serverConf += ` + vhostHTTPTimeout = 2 + ` + + delayDuration := 0 * time.Second + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + time.Sleep(delayDuration) + _, _ = w.Write([]byte(req.Host)) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(time.Second) + }). + ExpectResp([]byte("normal.example.com")). + Ensure() + + delayDuration = 3 * time.Second + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(5 * time.Second) + }). + Ensure(framework.ExpectResponseCode(504)) + }) + + ginkgo.It("Ip allow list", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + serverConf += ` + subdomainHost = "example.com" + ` + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + bazPort := f.AllocPort() + f.RunServer("", newHTTPServer(bazPort, "baz")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + subdomain = "foo" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + subdomain = "bar" + ipsAllowList = ["127.0.0.1/16"] + + [[proxies]] + name = "baz" + type = "http" + localPort = %d + subdomain = "baz" + ipsAllowList = ["127.1.0.1/16"] + `, fooPort, barPort, bazPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // no allow list configured: request passes through + framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("foo.example.com") + }). + ExpectResp([]byte("foo")). + Ensure() + + // client ip matches the allow list: request passes through + framework.NewRequestExpect(f).Explain("bar subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("bar.example.com") + }). + ExpectResp([]byte("bar")). + Ensure() + + // client ip does not match the allow list: request is rejected + framework.NewRequestExpect(f).Explain("baz subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("baz.example.com") + }). + Ensure(framework.ExpectResponseCode(403)) + }) +}) diff --git a/test/e2e/v1/basic/oidc.go b/test/e2e/v1/basic/oidc.go new file mode 100644 index 00000000000..19539c76511 --- /dev/null +++ b/test/e2e/v1/basic/oidc.go @@ -0,0 +1,192 @@ +// 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 basic + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/oidcserver" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: OIDC]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("should work with OIDC authentication", func() { + oidcSrv := oidcserver.New(oidcserver.WithBindPort(f.AllocPort())) + f.RunServer("", oidcSrv) + + portName := port.GenName("TCP") + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, oidcSrv.Issuer()) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "test-secret" +auth.oidc.tokenEndpointURL = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, oidcSrv.TokenEndpoint(), framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should authenticate heartbeats with OIDC", func() { + oidcSrv := oidcserver.New(oidcserver.WithBindPort(f.AllocPort())) + f.RunServer("", oidcSrv) + + serverPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d +log.level = "trace" +auth.method = "oidc" +auth.additionalScopes = ["HeartBeats"] +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, serverPort, oidcSrv.Issuer()) + + clientConf := fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = %d +loginFailExit = false +log.level = "trace" +auth.method = "oidc" +auth.additionalScopes = ["HeartBeats"] +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "test-secret" +auth.oidc.tokenEndpointURL = "%s" +transport.heartbeatInterval = 1 + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = %d +remotePort = %d +`, serverPort, oidcSrv.TokenEndpoint(), f.PortByName(framework.TCPEchoServerPort), remotePort) + + serverConfigPath := f.GenerateConfigFile(serverConf) + clientConfigPath := f.GenerateConfigFile(clientConf) + + _, _, err := f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + clientProcess, _, err := f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + + // Wait for several authenticated heartbeat cycles instead of a fixed sleep. + err = clientProcess.WaitForOutput("send heartbeat to server", 3, 10*time.Second) + framework.ExpectNoError(err) + + // Proxy should still work: heartbeat auth has not failed. + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("should work when token has no expires_in", func() { + oidcSrv := oidcserver.New( + oidcserver.WithBindPort(f.AllocPort()), + oidcserver.WithExpiresIn(0), + ) + f.RunServer("", oidcSrv) + + portName := port.GenName("TCP") + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, oidcSrv.Issuer()) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.additionalScopes = ["HeartBeats"] +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "test-secret" +auth.oidc.tokenEndpointURL = "%s" +transport.heartbeatInterval = 1 + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, oidcSrv.TokenEndpoint(), framework.TCPEchoServerPort, portName) + + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).PortName(portName).Ensure() + + countAfterLogin := oidcSrv.TokenRequestCount() + + // Wait for several heartbeat cycles instead of a fixed sleep. + // Each heartbeat fetches a fresh token in non-caching mode. + err := clientProcesses[0].WaitForOutput("send heartbeat to server", 3, 10*time.Second) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + + // Each heartbeat should have fetched a new token (non-caching mode). + countAfterHeartbeats := oidcSrv.TokenRequestCount() + framework.ExpectTrue( + countAfterHeartbeats > countAfterLogin, + "expected additional token requests for heartbeats, got %d before and %d after", + countAfterLogin, countAfterHeartbeats, + ) + }) + + ginkgo.It("should reject invalid OIDC credentials", func() { + oidcSrv := oidcserver.New(oidcserver.WithBindPort(f.AllocPort())) + f.RunServer("", oidcSrv) + + portName := port.GenName("TCP") + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.issuer = "%s" +auth.oidc.audience = "frps" +`, oidcSrv.Issuer()) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` +auth.method = "oidc" +auth.oidc.clientID = "test-client" +auth.oidc.clientSecret = "wrong-secret" +auth.oidc.tokenEndpointURL = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, oidcSrv.TokenEndpoint(), framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure() + }) +}) diff --git a/test/e2e/v1/basic/server.go b/test/e2e/v1/basic/server.go new file mode 100644 index 00000000000..04ae01b7cf3 --- /dev/null +++ b/test/e2e/v1/basic/server.go @@ -0,0 +1,194 @@ +package basic + +import ( + "context" + "fmt" + "net" + "strconv" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Server Manager]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Ports Whitelist", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000}) + udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000}) + + serverConf += ` + allowPorts = [ + { start = 10000, end = 11000 }, + { single = 11002 }, + { start = 12000, end = 13000 }, + ] + ` + + tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000)) + udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000)) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp-allowed-in-range" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, tcpPortName) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp-port-not-allowed" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, tcpPortNotAllowed) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp-port-unavailable" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, consts.PortServerName) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "udp-allowed-in-range" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.UDPEchoServerPort, udpPortName) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "udp-port-not-allowed" + type = "udp" + localPort = {{ .%s }} + remotePort = %d + `, framework.UDPEchoServerPort, udpPortNotAllowed) + + f.RunProcesses(serverConf, []string{clientConf}) + + // TCP + // Allowed in range + framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() + + // Not Allowed + framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure() + + // Unavailable, already bind by frps + framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure() + + // UDP + // Allowed in range + framework.NewRequestExpect(f).Protocol("udp").PortName(udpPortName).Ensure() + + // Not Allowed + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.UDP().Port(udpPortNotAllowed) + }).ExpectError(true).Ensure() + }) + + ginkgo.It("Alloc Random Port", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + adminPort := f.AllocPort() + clientConf += fmt.Sprintf(` + webServer.port = %d + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + `, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + client := f.APIClientForFrpc(adminPort) + + // tcp random port + status, err := client.GetProxyStatus(context.Background(), "tcp") + framework.ExpectNoError(err) + + _, portStr, err := net.SplitHostPort(status.RemoteAddr) + framework.ExpectNoError(err) + port, err := strconv.Atoi(portStr) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(port).Ensure() + + // udp random port + status, err = client.GetProxyStatus(context.Background(), "udp") + framework.ExpectNoError(err) + + _, portStr, err = net.SplitHostPort(status.RemoteAddr) + framework.ExpectNoError(err) + port, err = strconv.Atoi(portStr) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Protocol("udp").Port(port).Ensure() + }) + + ginkgo.It("Port Reuse", func() { + serverConf := consts.DefaultServerConfig + // Use same port as PortServer + serverConf += fmt.Sprintf(` + vhostHTTPPort = {{ .%s }} + `, consts.PortServerName) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http" + type = "http" + localPort = {{ .%s }} + customDomains = ["example.com"] + `, framework.HTTPSimpleServerPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }).PortName(consts.PortServerName).Ensure() + }) + + ginkgo.It("healthz", func() { + serverConf := consts.DefaultServerConfig + dashboardPort := f.AllocPort() + + // Use same port as PortServer + serverConf += fmt.Sprintf(` + vhostHTTPPort = {{ .%s }} + webServer.addr = "0.0.0.0" + webServer.port = %d + webServer.user = "admin" + webServer.password = "admin" + `, consts.PortServerName, dashboardPort) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http" + type = "http" + localPort = {{ .%s }} + customDomains = ["example.com"] + `, framework.HTTPSimpleServerPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/healthz") + }).Port(dashboardPort).ExpectResp([]byte("")).Ensure() + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/") + }).Port(dashboardPort). + Ensure(framework.ExpectResponseCode(401)) + }) +}) diff --git a/test/e2e/v1/basic/tcpmux.go b/test/e2e/v1/basic/tcpmux.go new file mode 100644 index 00000000000..9b839faa8df --- /dev/null +++ b/test/e2e/v1/basic/tcpmux.go @@ -0,0 +1,223 @@ +package basic + +import ( + "bufio" + "fmt" + "net" + "net/http" + + "github.com/onsi/ginkgo/v2" + + httppkg "github.com/fatedier/frp/pkg/util/http" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/rpc" +) + +var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { + f := framework.NewDefaultFramework() + + getDefaultServerConf := func(httpconnectPort int) string { + conf := consts.DefaultServerConfig + ` + tcpmuxHTTPConnectPort = %d + ` + return fmt.Sprintf(conf, httpconnectPort) + } + newServer := func(port int, respContent string) *streamserver.Server { + return streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(port), + streamserver.WithRespContent([]byte(respContent)), + ) + } + + proxyURLWithAuth := func(username, password string, port int) string { + if username == "" { + return fmt.Sprintf("http://127.0.0.1:%d", port) + } + return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, port) + } + + ginkgo.It("Route by HTTP user", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + + fooPort := f.AllocPort() + f.RunServer("", newServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newServer(barPort, "bar")) + + otherPort := f.AllocPort() + f.RunServer("", newServer(otherPort, "other")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user1" + + [[proxies]] + name = "bar" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user2" + + [[proxies]] + name = "catchAll" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + `, fooPort, barPort, otherPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // user1 + framework.NewRequestExpect(f).Explain("user1"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user1", "", vhostPort)) + }). + ExpectResp([]byte("foo")). + Ensure() + + // user2 + framework.NewRequestExpect(f).Explain("user2"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user2", "", vhostPort)) + }). + ExpectResp([]byte("bar")). + Ensure() + + // other user + framework.NewRequestExpect(f).Explain("other user"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user3", "", vhostPort)) + }). + ExpectResp([]byte("other")). + Ensure() + }) + + ginkgo.It("Proxy auth", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + + fooPort := f.AllocPort() + f.RunServer("", newServer(fooPort, "foo")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + httpUser = "test" + httpPassword = "test" + `, fooPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // not set auth header + framework.NewRequestExpect(f).Explain("no auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)) + }). + ExpectError(true). + Ensure() + + // set incorrect auth header + framework.NewRequestExpect(f).Explain("incorrect auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "invalid", vhostPort)) + }). + ExpectError(true). + Ensure() + + // set correct auth header + framework.NewRequestExpect(f).Explain("correct auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "test", vhostPort)) + }). + ExpectResp([]byte("foo")). + Ensure() + }) + + ginkgo.It("TCPMux Passthrough", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + serverConf += ` + tcpmuxPassthrough = true + ` + + var ( + respErr error + connectRequestHost string + ) + newServer := func(port int) *streamserver.Server { + return streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(port), + streamserver.WithCustomHandler(func(conn net.Conn) { + defer conn.Close() + + // read HTTP CONNECT request + bufioReader := bufio.NewReader(conn) + req, err := http.ReadRequest(bufioReader) + if err != nil { + respErr = err + return + } + connectRequestHost = req.Host + + // return ok response + res := httppkg.OkResponse() + if res.Body != nil { + defer res.Body.Close() + } + _ = res.Write(conn) + + buf, err := rpc.ReadBytes(conn) + if err != nil { + respErr = err + return + } + _, _ = rpc.WriteBytes(conn, buf) + }), + ) + } + + localPort := f.AllocPort() + f.RunServer("", newServer(localPort)) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)).Body([]byte("frp")) + }). + ExpectResp([]byte("frp")). + Ensure() + framework.ExpectNoError(respErr) + framework.ExpectEqualValues(connectRequestHost, "normal.example.com") + }) +}) diff --git a/test/e2e/v1/basic/token_source.go b/test/e2e/v1/basic/token_source.go new file mode 100644 index 00000000000..d12dd4de49c --- /dev/null +++ b/test/e2e/v1/basic/token_source.go @@ -0,0 +1,377 @@ +// Copyright 2025 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 basic + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: TokenSource]", func() { + f := framework.NewDefaultFramework() + + createExecTokenScript := func(name string) string { + scriptPath := filepath.Join(f.TempDirectory, name) + scriptContent := `#!/bin/sh +printf '%s\n' "$1" +` + err := os.WriteFile(scriptPath, []byte(scriptContent), 0o600) + framework.ExpectNoError(err) + err = os.Chmod(scriptPath, 0o700) + framework.ExpectNoError(err) + return scriptPath + } + + ginkgo.Describe("File-based token loading", func() { + ginkgo.It("should work with file tokenSource", func() { + // Create a temporary token file + tmpDir := f.TempDirectory + tokenFile := filepath.Join(tmpDir, "test_token") + tokenContent := "test-token-123" + + err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with tokenSource + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, tokenFile) + + // Client config with matching token + clientConf += fmt.Sprintf(` +auth.token = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, tokenContent, framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should work with client tokenSource", func() { + // Create a temporary token file + tmpDir := f.TempDirectory + tokenFile := filepath.Join(tmpDir, "client_token") + tokenContent := "client-token-456" + + err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with matching token + serverConf += fmt.Sprintf(` +auth.token = "%s" +`, tokenContent) + + // Client config with tokenSource + clientConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, tokenFile, framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should work with both server and client tokenSource", func() { + // Create temporary token files + tmpDir := f.TempDirectory + serverTokenFile := filepath.Join(tmpDir, "server_token") + clientTokenFile := filepath.Join(tmpDir, "client_token") + tokenContent := "shared-token-789" + + err := os.WriteFile(serverTokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + err = os.WriteFile(clientTokenFile, []byte(tokenContent), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with tokenSource + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, serverTokenFile) + + // Client config with tokenSource + clientConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, clientTokenFile, framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + + ginkgo.It("should fail with mismatched tokens", func() { + // Create temporary token files with different content + tmpDir := f.TempDirectory + serverTokenFile := filepath.Join(tmpDir, "server_token") + clientTokenFile := filepath.Join(tmpDir, "client_token") + + err := os.WriteFile(serverTokenFile, []byte("server-token"), 0o600) + framework.ExpectNoError(err) + + err = os.WriteFile(clientTokenFile, []byte("client-token"), 0o600) + framework.ExpectNoError(err) + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + + // Server config with tokenSource + serverConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, serverTokenFile) + + // Client config with different tokenSource + clientConf += fmt.Sprintf(` +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = {{ .%s }} +remotePort = {{ .%s }} +`, clientTokenFile, framework.TCPEchoServerPort, portName) + + f.RunProcesses(serverConf, []string{clientConf}) + + // This should fail due to token mismatch - the client should not be able to connect + // We expect the request to fail because the proxy tunnel is not established + framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure() + }) + + ginkgo.It("should fail with non-existent token file", func() { + tmpDir := f.TempDirectory + nonExistentFile := filepath.Join(tmpDir, "non_existent_token") + + serverPort := f.AllocPort() + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d +auth.tokenSource.type = "file" +auth.tokenSource.file.path = "%s" +`, serverPort, nonExistentFile) + + serverConfigPath := f.GenerateConfigFile(serverConf) + + _, _, _ = f.RunFrps("-c", serverConfigPath) + + // Server should have failed to start, so the port should not be listening. + conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(serverPort)), 1*time.Second) + if err == nil { + conn.Close() + } + framework.ExpectTrue(err != nil, "server should not be listening on port %d", serverPort) + }) + }) + + ginkgo.Describe("Exec-based token loading", func() { + ginkgo.It("should work with server tokenSource", func() { + execValue := "exec-server-value" + scriptPath := createExecTokenScript("server_token_exec.sh") + + serverPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d + +auth.tokenSource.type = "exec" +auth.tokenSource.exec.command = %q +auth.tokenSource.exec.args = [%q] +`, serverPort, scriptPath, execValue) + + clientConf := fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = %d +loginFailExit = false +auth.token = %q + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = %d +remotePort = %d +`, serverPort, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort) + + serverConfigPath := f.GenerateConfigFile(serverConf) + clientConfigPath := f.GenerateConfigFile(clientConf) + + _, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec") + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("should work with client tokenSource", func() { + execValue := "exec-client-value" + scriptPath := createExecTokenScript("client_token_exec.sh") + + serverPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d + +auth.token = %q +`, serverPort, execValue) + + clientConf := fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = %d +loginFailExit = false + +auth.tokenSource.type = "exec" +auth.tokenSource.exec.command = %q +auth.tokenSource.exec.args = [%q] + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = %d +remotePort = %d +`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort) + + serverConfigPath := f.GenerateConfigFile(serverConf) + clientConfigPath := f.GenerateConfigFile(clientConf) + + _, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec") + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("should work with both server and client tokenSource", func() { + execValue := "exec-shared-value" + scriptPath := createExecTokenScript("shared_token_exec.sh") + + serverPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d + +auth.tokenSource.type = "exec" +auth.tokenSource.exec.command = %q +auth.tokenSource.exec.args = [%q] +`, serverPort, scriptPath, execValue) + + clientConf := fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = %d +loginFailExit = false + +auth.tokenSource.type = "exec" +auth.tokenSource.exec.command = %q +auth.tokenSource.exec.args = [%q] + +[[proxies]] +name = "tcp" +type = "tcp" +localPort = %d +remotePort = %d +`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort) + + serverConfigPath := f.GenerateConfigFile(serverConf) + clientConfigPath := f.GenerateConfigFile(clientConf) + + _, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec") + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("should fail validation without allow-unsafe", func() { + execValue := "exec-unsafe-value" + scriptPath := createExecTokenScript("unsafe_token_exec.sh") + + serverPort := f.AllocPort() + serverConf := fmt.Sprintf(` +bindAddr = "0.0.0.0" +bindPort = %d + +auth.tokenSource.type = "exec" +auth.tokenSource.exec.command = %q +auth.tokenSource.exec.args = [%q] +`, serverPort, scriptPath, execValue) + + serverConfigPath := f.GenerateConfigFile(serverConf) + + _, output, err := f.RunFrps("verify", "-c", serverConfigPath) + framework.ExpectNoError(err) + framework.ExpectContainSubstring(output, "unsafe feature \"TokenSourceExec\" is not enabled") + }) + }) +}) diff --git a/test/e2e/v1/basic/wire.go b/test/e2e/v1/basic/wire.go new file mode 100644 index 00000000000..bf4752872ea --- /dev/null +++ b/test/e2e/v1/basic/wire.go @@ -0,0 +1,312 @@ +package basic + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: WireProtocol]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("v1 tcp and udp proxies", func() { + serverConf := consts.DefaultServerConfig + tcpPortName := port.GenName("WireV1TCP") + udpPortName := port.GenName("WireV1UDP") + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + transport.wireProtocol = "v1" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, tcpPortName, framework.UDPEchoServerPort, udpPortName) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() + framework.NewRequestExpect(f).Protocol("udp").PortName(udpPortName).Ensure() + }) + + ginkgo.It("v2 tcp and udp proxies", func() { + serverConf := consts.DefaultServerConfig + tcpPortName := port.GenName("WireV2TCP") + udpPortName := port.GenName("WireV2UDP") + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + transport.wireProtocol = "v2" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, tcpPortName, framework.UDPEchoServerPort, udpPortName) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() + framework.NewRequestExpect(f).Protocol("udp").PortName(udpPortName).Ensure() + }) + + ginkgo.It("v2 stcp visitor", func() { + serverConf := consts.DefaultServerConfig + bindPortName := port.GenName("WireV2STCP") + clientServerConf := consts.DefaultClientConfig + fmt.Sprintf(` + user = "user1" + transport.wireProtocol = "v2" + + [[proxies]] + name = "stcp" + type = "stcp" + secretKey = "abc" + localPort = {{ .%s }} + `, framework.TCPEchoServerPort) + clientVisitorConf := consts.DefaultClientConfig + fmt.Sprintf(` + user = "user1" + transport.wireProtocol = "v2" + + [[visitors]] + name = "stcp-visitor" + type = "stcp" + serverName = "stcp" + secretKey = "abc" + bindPort = {{ .%s }} + `, bindPortName) + + f.RunProcesses(serverConf, []string{clientServerConf, clientVisitorConf}) + + framework.NewRequestExpect(f).PortName(bindPortName).Ensure() + }) + + for _, tc := range []struct { + name string + proxyWireConfig string + visitorWireConfig string + extraProxyConfig string + extraVisitorConfig string + }{ + { + name: "default sudp visitor", + }, + { + name: "v2 binary raw sudp visitor", + proxyWireConfig: `transport.wireProtocol = "v2"`, + visitorWireConfig: `transport.wireProtocol = "v2"`, + }, + { + name: "v1 JSON proxy -> v2 Binary visitor transcode", + proxyWireConfig: `transport.wireProtocol = "v1"`, + visitorWireConfig: `transport.wireProtocol = "v2"`, + extraProxyConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + extraVisitorConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + { + name: "v2 Binary proxy -> v1 JSON visitor transcode", + proxyWireConfig: `transport.wireProtocol = "v2"`, + visitorWireConfig: `transport.wireProtocol = "v1"`, + }, + } { + ginkgo.It(tc.name, func() { + serverConf := consts.DefaultServerConfig + bindPortName := port.GenName("WireSUDP") + clientServerConf := consts.DefaultClientConfig + fmt.Sprintf(` + user = "user1" + %s + + [[proxies]] + name = "sudp" + type = "sudp" + secretKey = "abc" + localPort = {{ .%s }} + %s + `, tc.proxyWireConfig, framework.UDPEchoServerPort, tc.extraProxyConfig) + clientVisitorConf := consts.DefaultClientConfig + fmt.Sprintf(` + user = "user1" + %s + + [[visitors]] + name = "sudp-visitor" + type = "sudp" + serverName = "sudp" + secretKey = "abc" + bindPort = {{ .%s }} + %s + `, tc.visitorWireConfig, bindPortName, tc.extraVisitorConfig) + + f.RunProcesses(serverConf, []string{clientServerConf, clientVisitorConf}) + + framework.NewRequestExpect(f).Protocol("udp").PortName(bindPortName).Ensure() + }) + } + + ginkgo.It("reports client wire protocol", func() { + webPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + webServer.port = %d + `, webPort) + + v1PortName := port.GenName("WireReportV1") + v1ClientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientID = "wire-v1" + transport.wireProtocol = "v1" + + [[proxies]] + name = "v1" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, v1PortName) + + v2PortName := port.GenName("WireReportV2") + v2ClientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientID = "wire-v2" + transport.wireProtocol = "v2" + + [[proxies]] + name = "v2" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, v2PortName) + + f.RunProcesses(serverConf, []string{v1ClientConf, v2ClientConf}) + + framework.NewRequestExpect(f).PortName(v1PortName).Ensure() + framework.NewRequestExpect(f).PortName(v2PortName).Ensure() + expectClientWireProtocol(webPort, "wire-v1", "v1") + expectClientWireProtocol(webPort, "wire-v2", "v2") + }) +}) + +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"` +} + +func expectClientWireProtocol(webPort int, clientID string, wireProtocol string) { + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/clients", webPort)) + framework.ExpectNoError(err) + defer resp.Body.Close() + framework.ExpectEqual(resp.StatusCode, 200) + + content, err := io.ReadAll(resp.Body) + framework.ExpectNoError(err) + + var clients []wireClientInfo + framework.ExpectNoError(json.Unmarshal(content, &clients)) + for _, client := range clients { + if client.ClientID == clientID { + framework.ExpectEqual(client.WireProtocol, wireProtocol) + return + } + } + framework.Failf("client %q not found in /api/clients response: %s", clientID, string(content)) +} diff --git a/test/e2e/v1/basic/xtcp.go b/test/e2e/v1/basic/xtcp.go new file mode 100644 index 00000000000..57905f0dc09 --- /dev/null +++ b/test/e2e/v1/basic/xtcp.go @@ -0,0 +1,53 @@ +package basic + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: XTCP]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Fallback To STCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + bindPortName := port.GenName("XTCP") + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "stcp" + localPort = {{ .%s }} + + [[visitors]] + name = "foo-visitor" + type = "stcp" + serverName = "foo" + bindPort = -1 + + [[visitors]] + name = "bar-visitor" + type = "xtcp" + serverName = "bar" + bindPort = {{ .%s }} + keepTunnelOpen = true + fallbackTo = "foo-visitor" + fallbackTimeoutMs = 200 + `, framework.TCPEchoServerPort, bindPortName) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Timeout(time.Second) + }). + PortName(bindPortName). + Ensure() + }) +}) diff --git a/test/e2e/v1/features/bandwidth_limit.go b/test/e2e/v1/features/bandwidth_limit.go new file mode 100644 index 00000000000..11503adbf24 --- /dev/null +++ b/test/e2e/v1/features/bandwidth_limit.go @@ -0,0 +1,108 @@ +package features + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Proxy Bandwidth Limit by Client", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + transport.bandwidthLimit = "10KB" + `, localPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + content := strings.Repeat("a", 50*1024) // 5KB + start := time.Now() + framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { + r.Body([]byte(content)).Timeout(30 * time.Second) + }).ExpectResp([]byte(content)).Ensure() + + duration := time.Since(start) + framework.Logf("request duration: %s", duration.String()) + + framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String()) + }) + + ginkgo.It("Proxy Bandwidth Limit by Server", func() { + // new test plugin server + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewProxyContent{} + return &r + } + pluginPort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + content.BandwidthLimit = "10KB" + content.BandwidthLimitMode = "server" + ret.Content = content + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(pluginPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewProxy"] + `, pluginPort) + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, localPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + content := strings.Repeat("a", 50*1024) // 5KB + start := time.Now() + framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { + r.Body([]byte(content)).Timeout(30 * time.Second) + }).ExpectResp([]byte(content)).Ensure() + + duration := time.Since(start) + framework.Logf("request duration: %s", duration.String()) + + framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String()) + }) +}) diff --git a/test/e2e/v1/features/chaos.go b/test/e2e/v1/features/chaos.go new file mode 100644 index 00000000000..8c515251163 --- /dev/null +++ b/test/e2e/v1/features/chaos.go @@ -0,0 +1,64 @@ +package features + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" +) + +var _ = ginkgo.Describe("[Feature: Chaos]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("reconnect after frps restart", func() { + serverPort := f.AllocPort() + serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + bindAddr = "0.0.0.0" + bindPort = %d + `, serverPort)) + + remotePort := f.AllocPort() + clientConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + serverPort = %d + log.level = "trace" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)) + + // 1. start frps and frpc, expect request success + ps, _, err := f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + + pc, _, err := f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + // 2. stop frps, expect request failed + _ = ps.Stop() + framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() + + // 3. restart frps, expect request success + successCount := pc.CountOutput("[tcp] start proxy success") + _, _, err = f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + framework.ExpectNoError(pc.WaitForOutput("[tcp] start proxy success", successCount+1, 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + // 4. stop frpc, expect request failed + _ = pc.Stop() + framework.ExpectNoError(framework.WaitForTCPUnreachable(fmt.Sprintf("127.0.0.1:%d", remotePort), 100*time.Millisecond, 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() + + // 5. restart frpc, expect request success + newPc, _, err := f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + framework.ExpectNoError(newPc.WaitForOutput("[tcp] start proxy success", 1, 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) +}) diff --git a/test/e2e/v1/features/control_replacement.go b/test/e2e/v1/features/control_replacement.go new file mode 100644 index 00000000000..dbb95f41af8 --- /dev/null +++ b/test/e2e/v1/features/control_replacement.go @@ -0,0 +1,300 @@ +// 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 features + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/relay" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: ControlReplacement]", func() { + f := framework.NewDefaultFramework() + + for _, wireProtocol := range []string{"v1", "v2"} { + for _, tcpMux := range []bool{true, false} { + ginkgo.It(fmt.Sprintf("recovers a %s control through a half-open relay with tcpMux=%t", wireProtocol, tcpMux), func() { + runHalfOpenControlReplacement(f, wireProtocol, tcpMux) + }) + } + } +}) + +func runHalfOpenControlReplacement(f *framework.Framework, wireProtocol string, tcpMux bool) { + serverPort := f.AllocPort() + dashboardPort := f.AllocPort() + remotePort := f.AllocPort() + heartbeatTimeout := int64(-1) + if !tcpMux { + heartbeatTimeout = 3 + } + + serverConfig := fmt.Sprintf(` +bindAddr = "127.0.0.1" +bindPort = %d +log.level = "trace" +transport.tcpMux = %t +transport.tcpMuxKeepaliveInterval = 30 +transport.heartbeatTimeout = %d +webServer.addr = "127.0.0.1" +webServer.port = %d +webServer.pprofEnable = true +enablePrometheus = true +`, serverPort, tcpMux, heartbeatTimeout, dashboardPort) + serverConfigPath := f.WriteTempFile("issue-5391-frps.toml", serverConfig) + serverProcess, _, err := f.StartFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", serverPort), 5*time.Second)) + + halfOpenRelay := relay.New(fmt.Sprintf("127.0.0.1:%d", serverPort)) + f.RunServer("", halfOpenRelay) + + heartbeatInterval := int64(-1) + clientHeartbeatTimeout := int64(-1) + if !tcpMux { + heartbeatInterval = 1 + clientHeartbeatTimeout = 3 + } + clientConfig := fmt.Sprintf(` +serverAddr = "127.0.0.1" +serverPort = %d +clientID = "issue-5391" +loginFailExit = false +log.level = "trace" +transport.wireProtocol = %q +transport.tcpMux = %t +transport.tcpMuxKeepaliveInterval = 1 +transport.heartbeatInterval = %d +transport.heartbeatTimeout = %d +transport.tls.enable = false + +[[proxies]] +name = "issue-5391-tcp" +type = "tcp" +localPort = %d +remotePort = %d +`, halfOpenRelay.BindPort(), wireProtocol, tcpMux, heartbeatInterval, clientHeartbeatTimeout, + f.PortByName(framework.TCPEchoServerPort), remotePort) + clientConfigPath := f.WriteTempFile("issue-5391-frpc.toml", clientConfig) + clientProcess, _, err := f.StartFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + framework.ExpectNoError(halfOpenRelay.WaitForConnections(1, 5*time.Second)) + framework.ExpectNoError(clientProcess.WaitForOutput("login to server success", 1, 10*time.Second)) + framework.ExpectNoError(clientProcess.WaitForOutput("[issue-5391-tcp] start proxy success", 1, 10*time.Second)) + framework.ExpectNoError(waitForReplacementState(dashboardPort, remotePort, 10*time.Second)) + + replacementCount := 1 + if tcpMux { + replacementCount = 3 + } + for i := 0; i < replacementCount; i++ { + connectionIndex := 1 + if tcpMux { + connectionIndex = i + 1 + } + framework.ExpectNoError(halfOpenRelay.Blackhole(connectionIndex)) + if tcpMux { + framework.ExpectNoError(halfOpenRelay.WaitForConnections(connectionIndex+1, 15*time.Second)) + } + framework.ExpectNoError(clientProcess.WaitForOutput("login to server success", i+2, 15*time.Second)) + framework.ExpectNoError(clientProcess.WaitForOutput("[issue-5391-tcp] start proxy success", i+2, 15*time.Second)) + framework.ExpectNoError(waitForReplacementState(dashboardPort, remotePort, 10*time.Second)) + } + + _ = clientProcess.Stop() + select { + case <-clientProcess.Done(): + case <-time.After(5 * time.Second): + framework.Failf("frpc did not exit") + } + framework.ExpectNoError(waitForReplacementShutdown(dashboardPort, 10*time.Second)) + framework.ExpectNoError(halfOpenRelay.Close()) + framework.ExpectNoError(waitForNoHandoffWaiters(dashboardPort, 5*time.Second)) + + _ = serverProcess.Stop() + select { + case <-serverProcess.Done(): + case <-time.After(5 * time.Second): + framework.Failf("frps did not exit") + } +} + +func waitForReplacementState(dashboardPort, remotePort int, timeout time.Duration) error { + return waitForLifecycleCondition(timeout, func() error { + metricsBody, err := getLifecycleEndpoint(dashboardPort, "/metrics") + if err != nil { + return err + } + if err := expectMetricValue(metricsBody, "frp_server_client_counts", "", 1); err != nil { + return err + } + if err := expectMetricValue(metricsBody, "frp_server_proxy_counts", `type="tcp"`, 1); err != nil { + return err + } + clients, err := getOnlineLifecycleClients(dashboardPort) + if err != nil { + return err + } + if len(clients) != 1 || clients[0].ClientID != "issue-5391" { + return fmt.Errorf("expected one online client, got %+v", clients) + } + profile, err := getLifecycleEndpoint(dashboardPort, "/debug/pprof/goroutine?debug=2") + if err != nil { + return err + } + if err := expectNoHandoffWaiter(profile, "after replacement"); err != nil { + return err + } + resp, err := request.New(). + TCP(). + Port(remotePort). + Timeout(time.Second). + Body([]byte(consts.TestString)). + Do() + if err != nil { + return err + } + if string(resp.Content) != consts.TestString { + return fmt.Errorf("unexpected proxy response %q", resp.Content) + } + return nil + }) +} + +func waitForReplacementShutdown(dashboardPort int, timeout time.Duration) error { + return waitForLifecycleCondition(timeout, func() error { + metricsBody, err := getLifecycleEndpoint(dashboardPort, "/metrics") + if err != nil { + return err + } + if err := expectMetricValue(metricsBody, "frp_server_client_counts", "", 0); err != nil { + return err + } + if err := expectMetricValue(metricsBody, "frp_server_proxy_counts", `type="tcp"`, 0); err != nil { + return err + } + clients, err := getOnlineLifecycleClients(dashboardPort) + if err != nil { + return err + } + if len(clients) != 0 { + return fmt.Errorf("expected no online clients, got %+v", clients) + } + return nil + }) +} + +func waitForNoHandoffWaiters(dashboardPort int, timeout time.Duration) error { + return waitForLifecycleCondition(timeout, func() error { + profile, err := getLifecycleEndpoint(dashboardPort, "/debug/pprof/goroutine?debug=2") + if err != nil { + return err + } + return expectNoHandoffWaiter(profile, "after relay shutdown") + }) +} + +type lifecycleClient struct { + ClientID string `json:"clientID"` +} + +func expectNoHandoffWaiter(profile, phase string) error { + if strings.Contains(profile, "(*Control).WaitForHandoff") { + return fmt.Errorf("control handoff waiter remained %s", phase) + } + return nil +} + +func getOnlineLifecycleClients(dashboardPort int) ([]lifecycleClient, error) { + body, err := getLifecycleEndpoint(dashboardPort, "/api/clients?status=online") + if err != nil { + return nil, err + } + var clients []lifecycleClient + if err := json.Unmarshal([]byte(body), &clients); err != nil { + return nil, err + } + return clients, nil +} + +func getLifecycleEndpoint(port int, path string) (string, error) { + client := &http.Client{Timeout: time.Second} + resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d%s", port, path)) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("GET %s returned %s", path, resp.Status) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil +} + +func expectMetricValue(body, name, labels string, want float64) error { + prefix := name + if labels != "" { + prefix += "{" + labels + "}" + } + for line := range strings.SplitSeq(body, "\n") { + fields := strings.Fields(line) + if len(fields) != 2 || fields[0] != prefix { + continue + } + got, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return err + } + if got != want { + return fmt.Errorf("metric %s = %v, want %v", prefix, got, want) + } + return nil + } + return fmt.Errorf("metric %s not found", prefix) +} + +func waitForLifecycleCondition(timeout time.Duration, condition func() error) error { + timer := time.NewTimer(timeout) + defer timer.Stop() + ticker := time.NewTicker(25 * time.Millisecond) + defer ticker.Stop() + var lastErr error + for { + err := condition() + if err == nil { + return nil + } + lastErr = err + select { + case <-ticker.C: + case <-timer.C: + return fmt.Errorf("condition was not met: %w", lastErr) + } + } +} diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go new file mode 100644 index 00000000000..85b938a94c8 --- /dev/null +++ b/test/e2e/v1/features/group.go @@ -0,0 +1,407 @@ +package features + +import ( + "crypto/tls" + "fmt" + "strconv" + "sync" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Group]", func() { + f := framework.NewDefaultFramework() + + newHTTPServer := func(port int, respContent string) *httpserver.Server { + return httpserver.New( + httpserver.WithBindPort(port), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte(respContent))), + ) + } + + validateFooBarResponse := func(resp *request.Response) bool { + if string(resp.Content) == "foo" || string(resp.Content) == "bar" { + return true + } + return false + } + + doFooBarHTTPRequest := func(vhostPort int, host string) []string { + results := []string{} + var wait sync.WaitGroup + var mu sync.Mutex + expectFn := func() { + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost(host) + }). + Ensure(validateFooBarResponse, func(resp *request.Response) bool { + mu.Lock() + defer mu.Unlock() + results = append(results, string(resp.Content)) + return true + }) + } + for range 10 { + wait.Go(func() { + expectFn() + }) + } + + wait.Wait() + return results + } + + ginkgo.Describe("Load Balancing", func() { + ginkgo.It("TCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar"))) + f.RunServer("", barServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + + [[proxies]] + name = "bar" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + `, fooPort, remotePort, barPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + fooCount := 0 + barCount := 0 + for i := range 10 { + framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool { + switch string(resp.Content) { + case "foo": + fooCount++ + case "bar": + barCount++ + default: + return false + } + return true + }) + } + + framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) + }) + + ginkgo.It("HTTPS", func() { + vhostHTTPSPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + clientConf := consts.DefaultClientConfig + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + + fooPort := f.AllocPort() + fooServer := httpserver.New( + httpserver.WithBindPort(fooPort), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("foo"))), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := httpserver.New( + httpserver.WithBindPort(barPort), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("bar"))), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", barServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "https" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + + [[proxies]] + name = "bar" + type = "https" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + `, fooPort, barPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + fooCount := 0 + barCount := 0 + for i := range 10 { + framework.NewRequestExpect(f). + Explain("times " + strconv.Itoa(i)). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + Ensure(func(resp *request.Response) bool { + switch string(resp.Content) { + case "foo": + fooCount++ + case "bar": + barCount++ + default: + return false + } + return true + }) + } + + framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) + }) + + ginkgo.It("TCPMux httpconnect", func() { + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + tcpmuxHTTPConnectPort = %d + `, vhostPort) + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar"))) + f.RunServer("", barServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["tcpmux-group.example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + + [[proxies]] + name = "bar" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["tcpmux-group.example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + `, fooPort, barPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", vhostPort) + fooCount := 0 + barCount := 0 + for i := range 10 { + framework.NewRequestExpect(f). + Explain("times " + strconv.Itoa(i)). + RequestModify(func(r *request.Request) { + r.Addr("tcpmux-group.example.com").Proxy(proxyURL) + }). + Ensure(func(resp *request.Response) bool { + switch string(resp.Content) { + case "foo": + fooCount++ + case "bar": + barCount++ + default: + return false + } + return true + }) + } + + framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) + }) + }) + + ginkgo.Describe("Health Check", func() { + ginkgo.It("TCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar"))) + f.RunServer("", barServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "tcp" + healthCheck.intervalSeconds = 1 + + [[proxies]] + name = "bar" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "tcp" + healthCheck.intervalSeconds = 1 + `, fooPort, remotePort, barPort, remotePort) + + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) + + // check foo and bar is ok + results := []string{} + for range 10 { + framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { + results = append(results, string(resp.Content)) + return true + }) + } + framework.ExpectContainElements(results, []string{"foo", "bar"}) + + // close bar server, check foo is ok + failedCount := clientProcesses[0].CountOutput("[bar] health check failed") + barServer.Close() + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check failed", failedCount+1, 5*time.Second)) + for range 10 { + framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() + } + + // resume bar server, check foo and bar is ok + successCount := clientProcesses[0].CountOutput("[bar] health check success") + f.RunServer("", barServer) + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check success", successCount+1, 5*time.Second)) + results = []string{} + for range 10 { + framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { + results = append(results, string(resp.Content)) + return true + }) + } + framework.ExpectContainElements(results, []string{"foo", "bar"}) + }) + + ginkgo.It("HTTP", func() { + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostPort) + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := newHTTPServer(fooPort, "foo") + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := newHTTPServer(barPort, "bar") + f.RunServer("", barServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "http" + healthCheck.intervalSeconds = 1 + healthCheck.path = "/healthz" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "http" + healthCheck.intervalSeconds = 1 + healthCheck.path = "/healthz" + `, fooPort, barPort) + + _, clientProcesses := f.RunProcesses(serverConf, []string{clientConf}) + + // send first HTTP request + var contents []string + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + Ensure(func(resp *request.Response) bool { + contents = append(contents, string(resp.Content)) + return true + }) + + // send second HTTP request, should be forwarded to another service + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + Ensure(func(resp *request.Response) bool { + contents = append(contents, string(resp.Content)) + return true + }) + + framework.ExpectContainElements(contents, []string{"foo", "bar"}) + + // check foo and bar is ok + results := doFooBarHTTPRequest(vhostPort, "example.com") + framework.ExpectContainElements(results, []string{"foo", "bar"}) + + // close bar server, check foo is ok + failedCount := clientProcesses[0].CountOutput("[bar] health check failed") + barServer.Close() + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check failed", failedCount+1, 5*time.Second)) + results = doFooBarHTTPRequest(vhostPort, "example.com") + framework.ExpectContainElements(results, []string{"foo"}) + framework.ExpectNotContainElements(results, []string{"bar"}) + + // resume bar server, check foo and bar is ok + successCount := clientProcesses[0].CountOutput("[bar] health check success") + f.RunServer("", barServer) + framework.ExpectNoError(clientProcesses[0].WaitForOutput("[bar] health check success", successCount+1, 5*time.Second)) + results = doFooBarHTTPRequest(vhostPort, "example.com") + framework.ExpectContainElements(results, []string{"foo", "bar"}) + }) + }) +}) diff --git a/test/e2e/v1/features/heartbeat.go b/test/e2e/v1/features/heartbeat.go new file mode 100644 index 00000000000..08a1b5d96ad --- /dev/null +++ b/test/e2e/v1/features/heartbeat.go @@ -0,0 +1,47 @@ +package features + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" +) + +var _ = ginkgo.Describe("[Feature: Heartbeat]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("disable application layer heartbeat", func() { + serverPort := f.AllocPort() + serverConf := fmt.Sprintf(` + bindAddr = "0.0.0.0" + bindPort = %d + transport.heartbeatTimeout = -1 + transport.tcpMuxKeepaliveInterval = 2 + `, serverPort) + + remotePort := f.AllocPort() + clientConf := fmt.Sprintf(` + serverPort = %d + log.level = "trace" + transport.heartbeatInterval = -1 + transport.heartbeatTimeout = -1 + transport.tcpMuxKeepaliveInterval = 2 + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort) + + // run frps and frpc + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() + + time.Sleep(5 * time.Second) + framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() + }) +}) diff --git a/test/e2e/v1/features/monitor.go b/test/e2e/v1/features/monitor.go new file mode 100644 index 00000000000..f70dce16400 --- /dev/null +++ b/test/e2e/v1/features/monitor.go @@ -0,0 +1,55 @@ +package features + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Monitor]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Prometheus metrics", func() { + dashboardPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + enablePrometheus = true + webServer.addr = "0.0.0.0" + webServer.port = %d + `, dashboardPort) + + clientConf := consts.DefaultClientConfig + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + time.Sleep(500 * time.Millisecond) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(dashboardPort).HTTPPath("/metrics") + }).Ensure(func(resp *request.Response) bool { + log.Tracef("prometheus metrics response: \n%s", resp.Content) + if resp.Code != 200 { + return false + } + if !strings.Contains(string(resp.Content), "traffic_in") { + return false + } + return true + }) + }) +}) diff --git a/test/e2e/v1/features/real_ip.go b/test/e2e/v1/features/real_ip.go new file mode 100644 index 00000000000..6bebfc2b31c --- /dev/null +++ b/test/e2e/v1/features/real_ip.go @@ -0,0 +1,324 @@ +package features + +import ( + "bufio" + "crypto/tls" + "fmt" + "net" + "net/http" + + "github.com/onsi/ginkgo/v2" + pp "github.com/pires/go-proxyproto" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/rpc" +) + +var _ = ginkgo.Describe("[Feature: Real IP]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("HTTP X-forwarded-For", func() { + ginkgo.It("Client Without Header", func() { + vhostHTTPPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For"))) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + ExpectResp([]byte("127.0.0.1")). + Ensure() + }) + + ginkgo.It("Client With Header", func() { + vhostHTTPPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For"))) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + r.HTTP().HTTPHeaders(map[string]string{"x-forwarded-for": "2.2.2.2"}) + }). + ExpectResp([]byte("2.2.2.2, 127.0.0.1")). + Ensure() + }) + + ginkgo.It("http2https plugin", func() { + vhostHTTPPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + localPort := f.AllocPort() + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + customDomains = ["normal.example.com"] + [proxies.plugin] + type = "http2https" + localAddr = "127.0.0.1:%d" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For"))) + })), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + r.HTTP().HTTPHeaders(map[string]string{"x-forwarded-for": "2.2.2.2, 3.3.3.3"}) + }). + ExpectResp([]byte("2.2.2.2, 3.3.3.3, 127.0.0.1")). + Ensure() + }) + + ginkgo.It("https2http plugin", func() { + vhostHTTPSPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "https" + customDomains = ["normal.example.com"] + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:%d" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For"))) + })), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f).Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("normal.example.com"). + HTTPHeaders(map[string]string{"x-forwarded-for": "2.2.2.2"}). + TLSConfig(&tls.Config{ServerName: "normal.example.com", InsecureSkipVerify: true}) + }). + ExpectResp([]byte("2.2.2.2, 127.0.0.1")). + Ensure() + }) + }) + + ginkgo.Describe("Proxy Protocol", func() { + ginkgo.It("TCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Errorf("read proxy protocol error: %v", err) + return + } + + for { + if _, err := rpc.ReadBytes(rd); err != nil { + return + } + + buf := []byte(ppHeader.SourceAddr.String()) + _, _ = rpc.WriteBytes(c, buf) + } + })) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + transport.proxyProtocolVersion = "v2" + `, localPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { + log.Tracef("proxy protocol get SourceAddr: %s", string(resp.Content)) + addr, err := net.ResolveTCPAddr("tcp", string(resp.Content)) + if err != nil { + return false + } + if addr.IP.String() != "127.0.0.1" { + return false + } + return true + }) + }) + + ginkgo.It("UDP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.UDP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Errorf("read proxy protocol error: %v", err) + return + } + + // Read the actual UDP content after proxy protocol header + if _, err := rpc.ReadBytes(rd); err != nil { + return + } + + buf := []byte(ppHeader.SourceAddr.String()) + _, _ = rpc.WriteBytes(c, buf) + })) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "udp" + type = "udp" + localPort = %d + remotePort = %d + transport.proxyProtocolVersion = "v2" + `, localPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Protocol("udp").Port(remotePort).Ensure(func(resp *request.Response) bool { + log.Tracef("udp proxy protocol get SourceAddr: %s", string(resp.Content)) + addr, err := net.ResolveUDPAddr("udp", string(resp.Content)) + if err != nil { + return false + } + if addr.IP.String() != "127.0.0.1" { + return false + } + return true + }) + }) + + ginkgo.It("HTTP", func() { + vhostHTTPPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + var srcAddrRecord string + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Errorf("read proxy protocol error: %v", err) + return + } + srcAddrRecord = ppHeader.SourceAddr.String() + })) + f.RunServer("", localServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + transport.proxyProtocolVersion = "v2" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }).Ensure(framework.ExpectResponseCode(404)) + + log.Tracef("proxy protocol get SourceAddr: %s", srcAddrRecord) + addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord) + framework.ExpectNoError(err, srcAddrRecord) + framework.ExpectEqualValues("127.0.0.1", addr.IP.String()) + }) + }) +}) diff --git a/test/e2e/v1/features/ssh_tunnel.go b/test/e2e/v1/features/ssh_tunnel.go new file mode 100644 index 00000000000..c120eab0743 --- /dev/null +++ b/test/e2e/v1/features/ssh_tunnel.go @@ -0,0 +1,200 @@ +package features + +import ( + "crypto/tls" + "fmt" + "net" + "strconv" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/ssh" +) + +var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("tcp", func() { + sshPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + sshTunnelGateway.bindPort = %d + `, sshPort) + + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) + + localPort := f.PortByName(framework.TCPEchoServerPort) + remotePort := f.AllocPort() + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + fmt.Sprintf("tcp --remote-port %d", remotePort), + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("http", func() { + sshPort := f.AllocPort() + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + sshTunnelGateway.bindPort = %d + `, vhostPort, sshPort) + + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) + + localPort := f.PortByName(framework.HTTPSimpleServerPort) + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + "http --custom-domain test.example.com", + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("test.example.com") + }). + Ensure() + }) + + ginkgo.It("https", func() { + sshPort := f.AllocPort() + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPSPort = %d + sshTunnelGateway.bindPort = %d + `, vhostPort, sshPort) + + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) + + localPort := f.AllocPort() + testDomain := "test.example.com" + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + fmt.Sprintf("https --custom-domain %s", testDomain), + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithTLSConfig(tlsConfig), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + time.Sleep(time.Second) + framework.NewRequestExpect(f). + Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost(testDomain).TLSConfig(&tls.Config{ + ServerName: testDomain, + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("tcpmux", func() { + sshPort := f.AllocPort() + tcpmuxPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + tcpmuxHTTPConnectPort = %d + sshTunnelGateway.bindPort = %d + `, tcpmuxPort, sshPort) + + f.RunProcesses(serverConf, nil) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) + + localPort := f.AllocPort() + testDomain := "test.example.com" + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + fmt.Sprintf("tcpmux --mux=httpconnect --custom-domain %s", testDomain), + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + localServer := streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(localPort), + streamserver.WithRespContent([]byte("test")), + ) + f.RunServer("", localServer) + + time.Sleep(time.Second) + // Request without HTTP connect should get error + framework.NewRequestExpect(f). + Port(tcpmuxPort). + ExpectError(true). + Explain("request without HTTP connect expect error"). + Ensure() + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", tcpmuxPort) + // Request with incorrect connect hostname + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr("invalid").Proxy(proxyURL) + }).ExpectError(true).Explain("request without HTTP connect expect error").Ensure() + + // Request with correct connect hostname + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr(testDomain).Proxy(proxyURL) + }).ExpectResp([]byte("test")).Ensure() + }) + + ginkgo.It("stcp", func() { + sshPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + sshTunnelGateway.bindPort = %d + `, sshPort) + + bindPort := f.AllocPort() + visitorConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[visitors]] + name = "stcp-test-visitor" + type = "stcp" + serverName = "stcp-test" + secretKey = "abcdefg" + bindPort = %d + `, bindPort) + + f.RunProcesses(serverConf, []string{visitorConf}) + framework.ExpectNoError(framework.WaitForTCPReady(net.JoinHostPort("127.0.0.1", strconv.Itoa(sshPort)), 5*time.Second)) + + localPort := f.PortByName(framework.TCPEchoServerPort) + tc := ssh.NewTunnelClient( + fmt.Sprintf("127.0.0.1:%d", localPort), + fmt.Sprintf("127.0.0.1:%d", sshPort), + "stcp -n stcp-test --sk=abcdefg --allow-users=\"*\"", + ) + framework.ExpectNoError(tc.Start()) + defer tc.Close() + + time.Sleep(time.Second) + + framework.NewRequestExpect(f). + Port(bindPort). + Ensure() + }) +}) diff --git a/test/e2e/v1/features/store.go b/test/e2e/v1/features/store.go new file mode 100644 index 00000000000..76a9b05aef7 --- /dev/null +++ b/test/e2e/v1/features/store.go @@ -0,0 +1,322 @@ +package features + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Store]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Store API", func() { + ginkgo.It("create proxy via API and verify connection", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort), 5*time.Second)) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("update proxy via API", func() { + adminPort := f.AllocPort() + remotePort1 := f.AllocPort() + remotePort2 := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort1, + }, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort1), 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort1).Ensure() + + proxyConfig["tcp"].(map[string]any)["remotePort"] = remotePort2 + proxyBody, _ = json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/test-tcp").HTTPParams("PUT", "", "/api/store/proxies/test-tcp", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort2), 5*time.Second)) + framework.ExpectNoError(framework.WaitForTCPUnreachable(fmt.Sprintf("127.0.0.1:%d", remotePort1), 100*time.Millisecond, 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort2).Ensure() + framework.NewRequestExpect(f).Port(remotePort1).ExpectError(true).Ensure() + }) + + ginkgo.It("delete proxy via API", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", remotePort), 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/test-tcp").HTTPParams("DELETE", "", "/api/store/proxies/test-tcp", nil) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + framework.ExpectNoError(framework.WaitForTCPUnreachable(fmt.Sprintf("127.0.0.1:%d", remotePort), 100*time.Millisecond, 5*time.Second)) + framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() + }) + + ginkgo.It("list and get proxy via API", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + proxyConfig := map[string]any{ + "name": "test-tcp", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + } + proxyBody, _ := json.Marshal(proxyConfig) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(proxyBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 && strings.Contains(string(resp.Content), "test-tcp") + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/test-tcp") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 && strings.Contains(string(resp.Content), "test-tcp") + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/nonexistent") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 404 + }) + }) + + ginkgo.It("store disabled returns 404", func() { + adminPort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + `, adminPort) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies") + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 404 + }) + }) + + ginkgo.It("rejects mismatched type block", func() { + adminPort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + invalidBody, _ := json.Marshal(map[string]any{ + "name": "bad-proxy", + "type": "tcp", + "udp": map[string]any{ + "localPort": 1234, + }, + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(invalidBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 400 + }) + }) + + ginkgo.It("rejects path/body name mismatch on update", func() { + adminPort := f.AllocPort() + remotePort := f.AllocPort() + + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "127.0.0.1" + webServer.port = %d + + [store] + path = "%s/store.json" + `, adminPort, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + framework.ExpectNoError(framework.WaitForTCPReady(fmt.Sprintf("127.0.0.1:%d", adminPort), 5*time.Second)) + + createBody, _ := json.Marshal(map[string]any{ + "name": "proxy-a", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies").HTTPParams("POST", "", "/api/store/proxies", map[string]string{ + "Content-Type": "application/json", + }).Body(createBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 200 + }) + + updateBody, _ := json.Marshal(map[string]any{ + "name": "proxy-b", + "type": "tcp", + "tcp": map[string]any{ + "localIP": "127.0.0.1", + "localPort": f.PortByName(framework.TCPEchoServerPort), + "remotePort": remotePort, + }, + }) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(adminPort).HTTPPath("/api/store/proxies/proxy-a").HTTPParams("PUT", "", "/api/store/proxies/proxy-a", map[string]string{ + "Content-Type": "application/json", + }).Body(updateBody) + }).Ensure(func(resp *request.Response) bool { + return resp.Code == 400 + }) + }) + }) +}) diff --git a/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go new file mode 100644 index 00000000000..f2afad4194b --- /dev/null +++ b/test/e2e/v1/plugin/client.go @@ -0,0 +1,540 @@ +package plugin + +import ( + "bufio" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + + "github.com/onsi/ginkgo/v2" + pp "github.com/pires/go-proxyproto" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/cert" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("UnixDomainSocket", func() { + ginkgo.It("Expose a unix domain socket echo server", func() { + serverConf := consts.DefaultServerConfig + var clientConf strings.Builder + clientConf.WriteString(consts.DefaultClientConfig) + + getProxyConf := func(proxyName string, portName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "tcp" + remotePort = {{ .%s }} + `+extra, proxyName, portName) + fmt.Sprintf(` + [proxies.plugin] + type = "unix_domain_socket" + unixPath = "{{ .%s }}" + `, framework.UDSEchoServerAddr) + } + + tests := []struct { + proxyName string + portName string + extraConfig string + }{ + { + proxyName: "normal", + portName: port.GenName("Normal"), + }, + { + proxyName: "with-encryption", + portName: port.GenName("WithEncryption"), + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + portName: port.GenName("WithCompression"), + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + portName: port.GenName("WithEncryptionAndCompression"), + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + } + + // build all client config + for _, test := range tests { + clientConf.WriteString(getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n") + } + // run frps and frpc + f.RunProcesses(serverConf, []string{clientConf.String()}) + + for _, test := range tests { + framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() + } + }) + }) + + ginkgo.It("http_proxy", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "http_proxy" + httpUser = "abc" + httpPassword = "123" + `, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // http proxy, no auth info + framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { + r.HTTP().Proxy("http://127.0.0.1:" + strconv.Itoa(remotePort)) + }).Ensure(framework.ExpectResponseCode(407)) + + // http proxy, correct auth + framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { + r.HTTP().Proxy("http://abc:123@127.0.0.1:" + strconv.Itoa(remotePort)) + }).Ensure() + + // connect TCP server by CONNECT method + framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { + r.TCP().Proxy("http://abc:123@127.0.0.1:" + strconv.Itoa(remotePort)) + }) + }) + + ginkgo.It("socks5 proxy", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "socks5" + username = "abc" + password = "123" + `, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + // http proxy, no auth info + framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { + r.TCP().Proxy("socks5://127.0.0.1:" + strconv.Itoa(remotePort)) + }).ExpectError(true).Ensure() + + // http proxy, correct auth + framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { + r.TCP().Proxy("socks5://abc:123@127.0.0.1:" + strconv.Itoa(remotePort)) + }).Ensure() + }) + + ginkgo.It("static_file", func() { + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + f.WriteTempFile("test_static_file", "foo") + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "static_file" + localPath = "%s" + + [[proxies]] + name = "http" + type = "http" + customDomains = ["example.com"] + [proxies.plugin] + type = "static_file" + localPath = "%s" + + [[proxies]] + name = "http-with-auth" + type = "http" + customDomains = ["other.example.com"] + [proxies.plugin] + type = "static_file" + localPath = "%s" + httpUser = "abc" + httpPassword = "123" + `, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory) + + f.RunProcesses(serverConf, []string{clientConf}) + + // from tcp proxy + framework.NewRequestExpect(f).Request( + framework.NewHTTPRequest().HTTPPath("/test_static_file").Port(remotePort), + ).ExpectResp([]byte("foo")).Ensure() + + // from http proxy without auth + framework.NewRequestExpect(f).Request( + framework.NewHTTPRequest().HTTPHost("example.com").HTTPPath("/test_static_file").Port(vhostPort), + ).ExpectResp([]byte("foo")).Ensure() + + // from http proxy with auth + framework.NewRequestExpect(f).Request( + framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPAuth("abc", "123"), + ).ExpectResp([]byte("foo")).Ensure() + }) + + ginkgo.It("http2https", func() { + serverConf := consts.DefaultServerConfig + vhostHTTPPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http2https" + type = "http" + customDomains = ["example.com"] + [proxies.plugin] + type = "http2https" + localAddr = "127.0.0.1:%d" + `, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithTLSConfig(tlsConfig), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("https2http", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "https2http" + type = "https" + customDomains = ["example.com"] + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses(serverConf, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("https2https", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "https2https" + type = "https" + customDomains = ["example.com"] + [proxies.plugin] + type = "https2https" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses(serverConf, []string{clientConf}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithResponse([]byte("test")), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.Describe("http2http", func() { + ginkgo.It("host header rewrite", func() { + serverConf := consts.DefaultServerConfig + + localPort := f.AllocPort() + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http2http" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "http2http" + localAddr = "127.0.0.1:%d" + hostHeaderRewrite = "rewrite.test.com" + `, remotePort, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Host)) + })), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(remotePort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + ExpectResp([]byte("rewrite.test.com")). + Ensure() + }) + + ginkgo.It("set request header", func() { + serverConf := consts.DefaultServerConfig + + localPort := f.AllocPort() + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http2http" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "http2http" + localAddr = "127.0.0.1:%d" + requestHeaders.set.x-from-where = "frp" + `, remotePort, localPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("x-from-where"))) + })), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(remotePort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + ExpectResp([]byte("frp")). + Ensure() + }) + }) + + ginkgo.It("tls2raw", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("tls2raw_server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("tls2raw_server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "tls2raw-test" + type = "https" + customDomains = ["example.com"] + [proxies.plugin] + type = "tls2raw" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses(serverConf, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("tls2raw with proxy protocol v2", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("tls2raw_proxy_protocol_server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("tls2raw_proxy_protocol_server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "tls2raw-proxy-protocol-test" + type = "https" + customDomains = ["example.com"] + transport.proxyProtocolVersion = "v2" + [proxies.plugin] + type = "tls2raw" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses(serverConf, []string{clientConf}) + + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + + writeResp := func(body string) { + _, _ = fmt.Fprintf(c, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", len(body), body) + } + + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Errorf("read proxy protocol error: %v", err) + writeResp("missing proxy protocol") + return + } + if ppHeader.Version != 2 { + log.Errorf("unexpected proxy protocol version: %d", ppHeader.Version) + writeResp("unexpected proxy protocol version") + return + } + srcAddr, ok := ppHeader.SourceAddr.(*net.TCPAddr) + if !ok || srcAddr.IP.String() != "127.0.0.1" { + log.Errorf("unexpected proxy protocol source address: %v", ppHeader.SourceAddr) + writeResp("unexpected proxy protocol source address") + return + } + + req, err := http.ReadRequest(rd) + if err != nil { + log.Errorf("read http request after proxy protocol error: %v", err) + writeResp("missing http request") + return + } + _, _ = io.Copy(io.Discard, req.Body) + _ = req.Body.Close() + + writeResp("test") + })) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) +}) diff --git a/test/e2e/v1/plugin/server.go b/test/e2e/v1/plugin/server.go new file mode 100644 index 00000000000..fd684ef5d4e --- /dev/null +++ b/test/e2e/v1/plugin/server.go @@ -0,0 +1,416 @@ +package plugin + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" +) + +var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Login", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.LoginContent{} + return &r + } + + ginkgo.It("Auth for custom meta token", func() { + localPort := f.AllocPort() + + clientAddressGot := false + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.LoginContent) + if content.ClientAddress != "" { + clientAddressGot = true + } + if content.Metas["token"] == "123" { + ret.Unchange = true + } else { + ret.Reject = true + ret.RejectReason = "invalid token" + } + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "user-manager" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["Login"] + `, localPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + metadatas.token = "123" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + remotePort2 := f.AllocPort() + invalidTokenClientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "tcp2" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort2) + + f.RunProcesses(serverConf, []string{clientConf, invalidTokenClientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure() + + framework.ExpectTrue(clientAddressGot) + }) + }) + + ginkgo.Describe("NewProxy", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewProxyContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + if content.ProxyName == "tcp" { + ret.Unchange = true + } else { + ret.Reject = true + } + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewProxy"] + `, localPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("Modify RemotePort", func() { + localPort := f.AllocPort() + remotePort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + content.RemotePort = remotePort + ret.Content = content + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewProxy"] + `, localPort) + clientConf := consts.DefaultClientConfig + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = 0 + `, framework.TCPEchoServerPort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + }) + + ginkgo.Describe("CloseProxy", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.CloseProxyContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + var recordProxyName string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.CloseProxyContent) + recordProxyName = content.ProxyName + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["CloseProxy"] + `, localPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + _, clients := f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + for _, c := range clients { + _ = c.Stop() + } + + time.Sleep(1 * time.Second) + + framework.ExpectEqual(recordProxyName, "tcp") + }) + }) + + ginkgo.Describe("Ping", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.PingContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.PingContent) + record = content.PrivilegeKey + ret.Unchange = true + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["Ping"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + transport.heartbeatInterval = 1 + auth.additionalScopes = ["HeartBeats"] + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + time.Sleep(3 * time.Second) + framework.ExpectNotEqual("", record) + }) + }) + + ginkgo.Describe("NewWorkConn", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewWorkConnContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewWorkConnContent) + record = content.RunID + ret.Unchange = true + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewWorkConn"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.ExpectNotEqual("", record) + }) + }) + + ginkgo.Describe("NewUserConn", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewUserConnContent{} + return &r + } + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewUserConnContent) + record = content.RemoteAddr + ret.Unchange = true + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewUserConn"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.ExpectNotEqual("", record) + }) + }) + + ginkgo.Describe("HTTPS Protocol", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewUserConnContent{} + return &r + } + ginkgo.It("Validate Login Info, disable tls verify", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewUserConnContent) + record = content.RemoteAddr + ret.Unchange = true + return &ret + } + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "https://127.0.0.1:%d" + path = "/handler" + ops = ["NewUserConn"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses(serverConf, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.ExpectNotEqual("", record) + }) + }) +}) diff --git a/web/frpc/.babelrc b/web/frpc/.babelrc deleted file mode 100644 index b5510156ea9..00000000000 --- a/web/frpc/.babelrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "presets": [ - ["es2015", { "modules": false }] - ], - "plugins": [ - [ - "component", - { - "libraryName": "element-ui", - "styleLibraryName": "theme-chalk" - } - ] - ] -} diff --git a/web/frpc/.gitignore b/web/frpc/.gitignore index 3cd34b42d53..38adffa64e8 100644 --- a/web/frpc/.gitignore +++ b/web/frpc/.gitignore @@ -1,6 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules .DS_Store -node_modules/ -dist/ -npm-debug.log +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json .idea -.vscode/settings.json +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/frpc/.prettierrc.json b/web/frpc/.prettierrc.json new file mode 100644 index 00000000000..f8dec5b1897 --- /dev/null +++ b/web/frpc/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/web/frpc/Makefile b/web/frpc/Makefile index 620a9689151..adf015e50a2 100644 --- a/web/frpc/Makefile +++ b/web/frpc/Makefile @@ -1,6 +1,16 @@ -.PHONY: dist build -build: +.PHONY: dist install build preview lint + +install: + @cd .. && npm install + +build: install @npm run build -dev: +dev: @npm run dev + +preview: + @npm run preview + +lint: + @npm run lint diff --git a/web/frpc/README.md b/web/frpc/README.md new file mode 100644 index 00000000000..acbdbb087ee --- /dev/null +++ b/web/frpc/README.md @@ -0,0 +1,25 @@ +# frpc-dashboard + +## Project Setup + +```sh +yarn install +``` + +### Compile and Hot-Reload for Development + +```sh +make dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +make build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +make lint +``` diff --git a/web/frpc/auto-imports.d.ts b/web/frpc/auto-imports.d.ts new file mode 100644 index 00000000000..9d2400790b5 --- /dev/null +++ b/web/frpc/auto-imports.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @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 new file mode 100644 index 00000000000..262d5831f00 --- /dev/null +++ b/web/frpc/components.d.ts @@ -0,0 +1,52 @@ +/* eslint-disable */ +// @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'] + ConfigSection: typeof import('./src/components/ConfigSection.vue')['default'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default'] + ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default'] + ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default'] + ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default'] + ProxyCard: typeof import('./src/components/ProxyCard.vue')['default'] + ProxyFormLayout: typeof import('./src/components/proxy-form/ProxyFormLayout.vue')['default'] + ProxyHealthSection: typeof import('./src/components/proxy-form/ProxyHealthSection.vue')['default'] + ProxyHttpSection: typeof import('./src/components/proxy-form/ProxyHttpSection.vue')['default'] + ProxyLoadBalanceSection: typeof import('./src/components/proxy-form/ProxyLoadBalanceSection.vue')['default'] + ProxyMetadataSection: typeof import('./src/components/proxy-form/ProxyMetadataSection.vue')['default'] + ProxyNatSection: typeof import('./src/components/proxy-form/ProxyNatSection.vue')['default'] + ProxyRemoteSection: typeof import('./src/components/proxy-form/ProxyRemoteSection.vue')['default'] + ProxyTransportSection: typeof import('./src/components/proxy-form/ProxyTransportSection.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + StatusPills: typeof import('./src/components/StatusPills.vue')['default'] + StringListEditor: typeof import('./src/components/StringListEditor.vue')['default'] + 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'] + } + export interface GlobalDirectives { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/assets/frpc/embed.go b/web/frpc/embed.go similarity index 51% rename from assets/frpc/embed.go rename to web/frpc/embed.go index 3fc0d3a5524..77b4863e9e2 100644 --- a/assets/frpc/embed.go +++ b/web/frpc/embed.go @@ -1,3 +1,5 @@ +//go:build !noweb + package frpc import ( @@ -6,9 +8,9 @@ import ( "github.com/fatedier/frp/assets" ) -//go:embed static/* -var content embed.FS +//go:embed dist +var EmbedFS embed.FS func init() { - assets.Register(content) + assets.Register(EmbedFS) } diff --git a/web/frpc/embed_stub.go b/web/frpc/embed_stub.go new file mode 100644 index 00000000000..591a770a563 --- /dev/null +++ b/web/frpc/embed_stub.go @@ -0,0 +1,3 @@ +//go:build noweb + +package frpc diff --git a/web/frpc/eslint.config.js b/web/frpc/eslint.config.js new file mode 100644 index 00000000000..a4cce6d4b14 --- /dev/null +++ b/web/frpc/eslint.config.js @@ -0,0 +1,36 @@ +import pluginVue from 'eslint-plugin-vue' +import vueTsEslintConfig from '@vue/eslint-config-typescript' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default [ + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + { + name: 'app/files-to-ignore', + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], + }, + ...pluginVue.configs['flat/essential'], + ...vueTsEslintConfig(), + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'vue/multi-word-component-names': [ + 'error', + { + ignores: ['Overview'], + }, + ], + }, + }, + skipFormatting, +] diff --git a/web/frpc/index.html b/web/frpc/index.html new file mode 100644 index 00000000000..a893c9c1d5f --- /dev/null +++ b/web/frpc/index.html @@ -0,0 +1,15 @@ + + + + + + + frp client + + + +
+ + + + diff --git a/web/frpc/package.json b/web/frpc/package.json index 65d0c4682bb..17e8c5944b0 100644 --- a/web/frpc/package.json +++ b/web/frpc/package.json @@ -1,46 +1,42 @@ { - "name": "frpc-web", - "description": "An admin web ui for frp client.", - "author": "fatedier", + "name": "frpc-dashboard", + "version": "0.0.1", "private": true, + "type": "module", "scripts": { - "dev": "webpack-dev-server -d --inline --hot --env.dev", - "build": "rimraf dist && webpack -p --progress --hide-modules" + "dev": "vite", + "build": "run-p type-check build-only", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit", + "lint": "eslint . --fix", + "lint:check": "eslint ." }, "dependencies": { - "element-ui": "^2.5.3", - "vue": "^2.5.22", - "vue-resource": "^1.5.1", - "vue-router": "^3.0.2", - "whatwg-fetch": "^3.0.0" - }, - "engines": { - "node": ">=6" + "element-plus": "^2.14.3", + "pinia": "^3.0.4", + "vue": "^3.5.40", + "vue-router": "^5.2.0" }, "devDependencies": { - "autoprefixer": "^9.4.7", - "babel-core": "^6.26.3", - "babel-eslint": "^10.0.1", - "babel-loader": "^7.1.5", - "babel-plugin-component": "^1.1.1", - "babel-preset-es2015": "^6.24.1", - "css-loader": "^2.1.0", - "eslint": "^5.12.1", - "eslint-config-enough": "^0.3.4", - "eslint-loader": "^2.1.1", - "file-loader": "^3.0.1", - "html-loader": "^0.5.5", - "html-webpack-plugin": "^2.24.1", - "less": "^3.9.0", - "less-loader": "^4.1.0", - "postcss-loader": "^3.0.0", - "rimraf": "^2.6.3", - "style-loader": "^0.23.1", - "url-loader": "^1.1.2", - "vue-loader": "^15.6.2", - "vue-template-compiler": "^2.5.22", - "webpack": "^2.7.0", - "webpack-cli": "^3.2.1", - "webpack-dev-server": "^3.1.14" + "@types/node": "24", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.8.1", + "@vueuse/core": "^14.3.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", + "npm-run-all": "^4.1.5", + "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.3.8" } } diff --git a/web/frpc/postcss.config.js b/web/frpc/postcss.config.js deleted file mode 100644 index af65640308a..00000000000 --- a/web/frpc/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: [ - require('autoprefixer')() - ] -} \ No newline at end of file diff --git a/assets/frpc/static/favicon.ico b/web/frpc/public/favicon.ico similarity index 100% rename from assets/frpc/static/favicon.ico rename to web/frpc/public/favicon.ico diff --git a/web/frpc/src/App.vue b/web/frpc/src/App.vue index 0dd5c972d51..b75c8a6915f 100644 --- a/web/frpc/src/App.vue +++ b/web/frpc/src/App.vue @@ -1,73 +1,528 @@ - - diff --git a/web/frpc/src/api/frpc.ts b/web/frpc/src/api/frpc.ts new file mode 100644 index 00000000000..c7af90c0aa5 --- /dev/null +++ b/web/frpc/src/api/frpc.ts @@ -0,0 +1,92 @@ +import { http } from './http' +import type { + StatusResponse, + ProxyListResp, + ProxyDefinition, + VisitorListResp, + VisitorDefinition, +} from '../types' + +export const getStatus = () => { + return http.get('/api/status') +} + +export const getConfig = () => { + return http.get('/api/config') +} + +export const putConfig = (content: string) => { + return http.put('/api/config', content) +} + +export const reloadConfig = () => { + return http.get('/api/reload') +} + +// Config lookup API (any source) +export const getProxyConfig = (name: string) => { + return http.get( + `/api/proxy/${encodeURIComponent(name)}/config`, + ) +} + +export const getVisitorConfig = (name: string) => { + return http.get( + `/api/visitor/${encodeURIComponent(name)}/config`, + ) +} + +// Store API - Proxies +export const listStoreProxies = () => { + return http.get('/api/store/proxies') +} + +export const getStoreProxy = (name: string) => { + return http.get( + `/api/store/proxies/${encodeURIComponent(name)}`, + ) +} + +export const createStoreProxy = (config: ProxyDefinition) => { + return http.post('/api/store/proxies', config) +} + +export const updateStoreProxy = (name: string, config: ProxyDefinition) => { + return http.put( + `/api/store/proxies/${encodeURIComponent(name)}`, + config, + ) +} + +export const deleteStoreProxy = (name: string) => { + return http.delete(`/api/store/proxies/${encodeURIComponent(name)}`) +} + +// Store API - Visitors +export const listStoreVisitors = () => { + return http.get('/api/store/visitors') +} + +export const getStoreVisitor = (name: string) => { + return http.get( + `/api/store/visitors/${encodeURIComponent(name)}`, + ) +} + +export const createStoreVisitor = (config: VisitorDefinition) => { + return http.post('/api/store/visitors', config) +} + +export const updateStoreVisitor = ( + name: string, + config: VisitorDefinition, +) => { + return http.put( + `/api/store/visitors/${encodeURIComponent(name)}`, + config, + ) +} + +export const deleteStoreVisitor = (name: string) => { + return http.delete(`/api/store/visitors/${encodeURIComponent(name)}`) +} diff --git a/web/frpc/src/api/http.ts b/web/frpc/src/api/http.ts new file mode 100644 index 00000000000..5f5b8c64e4f --- /dev/null +++ b/web/frpc/src/api/http.ts @@ -0,0 +1,92 @@ +// http.ts - Base HTTP client + +class HTTPError extends Error { + status: number + statusText: string + + constructor(status: number, statusText: string, message?: string) { + super(message || statusText) + this.status = status + this.statusText = statusText + } +} + +async function request(url: string, options: RequestInit = {}): Promise { + const defaultOptions: RequestInit = { + credentials: 'include', + } + + const response = await fetch(url, { ...defaultOptions, ...options }) + + if (!response.ok) { + throw new HTTPError( + response.status, + response.statusText, + `HTTP ${response.status}`, + ) + } + + // Handle empty response (e.g. 204 No Content) + if (response.status === 204) { + return {} as T + } + + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + return response.json() + } + return response.text() as unknown as T +} + +export const http = { + get: (url: string, options?: RequestInit) => + request(url, { ...options, method: 'GET' }), + post: (url: string, body?: any, options?: RequestInit) => { + const headers: HeadersInit = { ...options?.headers } + let requestBody = body + + if ( + body && + typeof body === 'object' && + !(body instanceof FormData) && + !(body instanceof Blob) + ) { + if (!('Content-Type' in headers)) { + ;(headers as any)['Content-Type'] = 'application/json' + } + requestBody = JSON.stringify(body) + } + + return request(url, { + ...options, + method: 'POST', + headers, + body: requestBody, + }) + }, + put: (url: string, body?: any, options?: RequestInit) => { + const headers: HeadersInit = { ...options?.headers } + let requestBody = body + + if ( + body && + typeof body === 'object' && + !(body instanceof FormData) && + !(body instanceof Blob) + ) { + if (!('Content-Type' in headers)) { + ;(headers as any)['Content-Type'] = 'application/json' + } + requestBody = JSON.stringify(body) + } + + return request(url, { + ...options, + method: 'PUT', + headers, + body: requestBody, + }) + }, + delete: (url: string, options?: RequestInit) => + request(url, { ...options, method: 'DELETE' }), +} diff --git a/web/frpc/src/assets/css/_form-layout.scss b/web/frpc/src/assets/css/_form-layout.scss new file mode 100644 index 00000000000..ca84433f657 --- /dev/null +++ b/web/frpc/src/assets/css/_form-layout.scss @@ -0,0 +1,33 @@ +@use '@shared/css/mixins' as *; + +/* Shared form layout styles for proxy/visitor form sections */ +.field-row { + display: grid; + gap: 16px; + align-items: start; +} + +.field-row.two-col { + grid-template-columns: 1fr 1fr; +} + +.field-row.three-col { + grid-template-columns: 1fr 1fr 1fr; +} + +.field-grow { + min-width: 0; +} + +.switch-field :deep(.el-form-item__content) { + min-height: 32px; + display: flex; + align-items: center; +} + +@include mobile { + .field-row.two-col, + .field-row.three-col { + grid-template-columns: 1fr; + } +} diff --git a/web/frpc/src/assets/css/dark.css b/web/frpc/src/assets/css/dark.css new file mode 100644 index 00000000000..1d090aec5b1 --- /dev/null +++ b/web/frpc/src/assets/css/dark.css @@ -0,0 +1,193 @@ +/* Dark mode styles */ +html.dark { + --el-bg-color: #212121; + --el-bg-color-page: #181818; + --el-bg-color-overlay: #303030; + --el-fill-color-blank: #212121; + --el-border-color: #404040; + --el-border-color-light: #353535; + --el-border-color-lighter: #2a2a2a; + --el-text-color-primary: #e5e7eb; + --el-text-color-secondary: #888888; + --el-text-color-placeholder: #afafaf; + background-color: #212121; + color-scheme: dark; +} + +/* Scrollbar */ +html.dark ::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +html.dark ::-webkit-scrollbar-track { + background: #303030; +} + +html.dark ::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 3px; +} + +html.dark ::-webkit-scrollbar-thumb:hover { + background: #505050; +} + +/* Form */ +html.dark .el-form-item__label { + color: #e5e7eb; +} + +/* Input */ +html.dark .el-input__wrapper { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #404040 inset; +} + +html.dark .el-input__wrapper:hover { + box-shadow: 0 0 0 1px #505050 inset; +} + +html.dark .el-input__wrapper.is-focus { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; +} + +html.dark .el-input__inner { + color: #e5e7eb; +} + +html.dark .el-input__inner::placeholder { + color: #afafaf; +} + +html.dark .el-textarea__inner { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #404040 inset; + color: #e5e7eb; +} + +html.dark .el-textarea__inner:hover { + box-shadow: 0 0 0 1px #505050 inset; +} + +html.dark .el-textarea__inner:focus { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; +} + +/* Select */ +html.dark .el-select__wrapper { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #404040 inset; +} + +html.dark .el-select__wrapper:hover { + box-shadow: 0 0 0 1px #505050 inset; +} + +html.dark .el-select__selected-item { + color: #e5e7eb; +} + +html.dark .el-select__placeholder { + color: #afafaf; +} + +html.dark .el-select-dropdown { + background: #303030; + border-color: #404040; +} + +html.dark .el-select-dropdown__item { + color: #e5e7eb; +} + +html.dark .el-select-dropdown__item:hover { + background: #3a3a3a; +} + +html.dark .el-select-dropdown__item.is-selected { + color: var(--el-color-primary); +} + +html.dark .el-select-dropdown__item.is-disabled { + color: #666666; +} + +/* Tag */ +html.dark .el-tag--info { + background: #303030; + border-color: #404040; + color: #b0b0b0; +} + +/* Button */ +html.dark .el-button--default { + background: #303030; + border-color: #404040; + color: #e5e7eb; +} + +html.dark .el-button--default:hover { + background: #3a3a3a; + border-color: #505050; + color: #e5e7eb; +} + +/* Card */ +html.dark .el-card { + background: #212121; + border-color: #353535; + color: #b0b0b0; +} + +html.dark .el-card__header { + border-bottom-color: #353535; + color: #e5e7eb; +} + +/* Dialog */ +html.dark .el-dialog { + background: #212121; +} + +html.dark .el-dialog__title { + color: #e5e7eb; +} + +/* Message */ +html.dark .el-message { + background: #303030; + border-color: #404040; +} + +html.dark .el-message--success { + background: #1e3d2e; + border-color: #3d6b4f; +} + +html.dark .el-message--warning { + background: #3d3020; + border-color: #6b5020; +} + +html.dark .el-message--error { + background: #3d2027; + border-color: #5c2d2d; +} + +/* Loading */ +html.dark .el-loading-mask { + background-color: rgba(33, 33, 33, 0.9); +} + +/* Overlay */ +html.dark .el-overlay { + background-color: rgba(0, 0, 0, 0.6); +} + +/* Tooltip */ +html.dark .el-tooltip__popper { + background: #303030 !important; + border-color: #404040 !important; + color: #e5e7eb !important; +} diff --git a/web/frpc/src/assets/css/var.css b/web/frpc/src/assets/css/var.css new file mode 100644 index 00000000000..388a0521fb2 --- /dev/null +++ b/web/frpc/src/assets/css/var.css @@ -0,0 +1,117 @@ +:root { + /* Text colors */ + --color-text-primary: #303133; + --color-text-secondary: #606266; + --color-text-muted: #909399; + --color-text-light: #c0c4cc; + --color-text-placeholder: #a8abb2; + + /* Background colors */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f9f9f9; + --color-bg-tertiary: #fafafa; + --color-bg-surface: #ffffff; + --color-bg-muted: #f4f4f5; + --color-bg-input: #ffffff; + --color-bg-hover: #efefef; + --color-bg-active: #eaeaea; + + /* Border colors */ + --color-border: #dcdfe6; + --color-border-light: #e4e7ed; + --color-border-lighter: #ebeef5; + --color-border-extra-light: #f2f6fc; + + /* Status colors */ + --color-primary: #409eff; + --color-primary-light: #ecf5ff; + --color-success: #67c23a; + --color-warning: #e6a23c; + --color-danger: #f56c6c; + --color-danger-dark: #c45656; + --color-danger-light: #fef0f0; + --color-info: #909399; + + /* Button colors */ + --color-btn-primary: #303133; + --color-btn-primary-hover: #4a4d5c; + + /* Element Plus mapping */ + --el-color-primary: var(--color-primary); + --el-color-success: var(--color-success); + --el-color-warning: var(--color-warning); + --el-color-danger: var(--color-danger); + --el-color-info: var(--color-info); + + --el-text-color-primary: var(--color-text-primary); + --el-text-color-regular: var(--color-text-secondary); + --el-text-color-secondary: var(--color-text-muted); + --el-text-color-placeholder: var(--color-text-placeholder); + + --el-bg-color: var(--color-bg-primary); + --el-bg-color-page: var(--color-bg-secondary); + --el-bg-color-overlay: var(--color-bg-primary); + + --el-border-color: var(--color-border); + --el-border-color-light: var(--color-border-light); + --el-border-color-lighter: var(--color-border-lighter); + --el-border-color-extra-light: var(--color-border-extra-light); + + --el-fill-color-blank: var(--color-bg-primary); + --el-fill-color-light: var(--color-bg-tertiary); + --el-fill-color: var(--color-bg-tertiary); + --el-fill-color-dark: var(--color-bg-hover); + --el-fill-color-darker: var(--color-bg-active); + + /* Input */ + --el-input-bg-color: var(--color-bg-input); + --el-input-border-color: var(--color-border); + --el-input-hover-border-color: var(--color-border-light); + + /* Dialog */ + --el-dialog-bg-color: var(--color-bg-primary); + --el-overlay-color: rgba(0, 0, 0, 0.5); +} + +html.dark { + /* Text colors */ + --color-text-primary: #e5e7eb; + --color-text-secondary: #b0b0b0; + --color-text-muted: #888888; + --color-text-light: #666666; + --color-text-placeholder: #afafaf; + + /* Background colors */ + --color-bg-primary: #212121; + --color-bg-secondary: #181818; + --color-bg-tertiary: #303030; + --color-bg-surface: #303030; + --color-bg-muted: #303030; + --color-bg-input: #2f2f2f; + --color-bg-hover: #3a3a3a; + --color-bg-active: #454545; + + /* Border colors */ + --color-border: #404040; + --color-border-light: #353535; + --color-border-lighter: #2a2a2a; + --color-border-extra-light: #222222; + + /* Status colors */ + --color-primary: #409eff; + --color-danger: #f87171; + --color-danger-dark: #f87171; + --color-danger-light: #3d2027; + --color-info: #888888; + + /* Button colors */ + --color-btn-primary: #404040; + --color-btn-primary-hover: #505050; + + /* Dark overrides */ + --el-text-color-regular: var(--color-text-primary); + --el-overlay-color: rgba(0, 0, 0, 0.7); + + background-color: #181818; + color-scheme: dark; +} diff --git a/web/frpc/src/assets/favicon.ico b/web/frpc/src/assets/favicon.ico deleted file mode 100644 index 4347765571a..00000000000 Binary files a/web/frpc/src/assets/favicon.ico and /dev/null differ diff --git a/web/frpc/src/assets/icons/github.svg b/web/frpc/src/assets/icons/github.svg new file mode 100644 index 00000000000..160a7939320 --- /dev/null +++ b/web/frpc/src/assets/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/frpc/src/assets/icons/logo.svg b/web/frpc/src/assets/icons/logo.svg new file mode 100644 index 00000000000..fee4a82a394 --- /dev/null +++ b/web/frpc/src/assets/icons/logo.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/web/frpc/src/components/ConfigField.vue b/web/frpc/src/components/ConfigField.vue new file mode 100644 index 00000000000..67be4ba0dc1 --- /dev/null +++ b/web/frpc/src/components/ConfigField.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/web/frpc/src/components/ConfigSection.vue b/web/frpc/src/components/ConfigSection.vue new file mode 100644 index 00000000000..1a795afb932 --- /dev/null +++ b/web/frpc/src/components/ConfigSection.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/web/frpc/src/components/Configure.vue b/web/frpc/src/components/Configure.vue deleted file mode 100644 index ffda46458e3..00000000000 --- a/web/frpc/src/components/Configure.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - diff --git a/web/frpc/src/components/KeyValueEditor.vue b/web/frpc/src/components/KeyValueEditor.vue new file mode 100644 index 00000000000..9b4676703b8 --- /dev/null +++ b/web/frpc/src/components/KeyValueEditor.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/web/frpc/src/components/Overview.vue b/web/frpc/src/components/Overview.vue deleted file mode 100644 index c25c3adb3a8..00000000000 --- a/web/frpc/src/components/Overview.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/web/frpc/src/components/ProxyCard.vue b/web/frpc/src/components/ProxyCard.vue new file mode 100644 index 00000000000..93f815a6a65 --- /dev/null +++ b/web/frpc/src/components/ProxyCard.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/web/frpc/src/components/StatusPills.vue b/web/frpc/src/components/StatusPills.vue new file mode 100644 index 00000000000..054dfaadcbf --- /dev/null +++ b/web/frpc/src/components/StatusPills.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/web/frpc/src/components/StringListEditor.vue b/web/frpc/src/components/StringListEditor.vue new file mode 100644 index 00000000000..c48c2083575 --- /dev/null +++ b/web/frpc/src/components/StringListEditor.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyAuthSection.vue b/web/frpc/src/components/proxy-form/ProxyAuthSection.vue new file mode 100644 index 00000000000..54e39b3da6f --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyAuthSection.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyBackendSection.vue b/web/frpc/src/components/proxy-form/ProxyBackendSection.vue new file mode 100644 index 00000000000..633d3e61308 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyBackendSection.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyBaseSection.vue b/web/frpc/src/components/proxy-form/ProxyBaseSection.vue new file mode 100644 index 00000000000..f0c01bd6249 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyBaseSection.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyFormLayout.vue b/web/frpc/src/components/proxy-form/ProxyFormLayout.vue new file mode 100644 index 00000000000..cb954c9a549 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyFormLayout.vue @@ -0,0 +1,50 @@ + + + diff --git a/web/frpc/src/components/proxy-form/ProxyHealthSection.vue b/web/frpc/src/components/proxy-form/ProxyHealthSection.vue new file mode 100644 index 00000000000..6e146d9dcc5 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyHealthSection.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyHttpSection.vue b/web/frpc/src/components/proxy-form/ProxyHttpSection.vue new file mode 100644 index 00000000000..beccc96f9ef --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyHttpSection.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyLoadBalanceSection.vue b/web/frpc/src/components/proxy-form/ProxyLoadBalanceSection.vue new file mode 100644 index 00000000000..ef22e07af8c --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyLoadBalanceSection.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyMetadataSection.vue b/web/frpc/src/components/proxy-form/ProxyMetadataSection.vue new file mode 100644 index 00000000000..adc627aff52 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyMetadataSection.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyNatSection.vue b/web/frpc/src/components/proxy-form/ProxyNatSection.vue new file mode 100644 index 00000000000..191aeb27103 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyNatSection.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyRemoteSection.vue b/web/frpc/src/components/proxy-form/ProxyRemoteSection.vue new file mode 100644 index 00000000000..cba7652e40f --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyRemoteSection.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/web/frpc/src/components/proxy-form/ProxyTransportSection.vue b/web/frpc/src/components/proxy-form/ProxyTransportSection.vue new file mode 100644 index 00000000000..b290d59bce8 --- /dev/null +++ b/web/frpc/src/components/proxy-form/ProxyTransportSection.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/web/frpc/src/components/visitor-form/VisitorBaseSection.vue b/web/frpc/src/components/visitor-form/VisitorBaseSection.vue new file mode 100644 index 00000000000..8e5e526dcd5 --- /dev/null +++ b/web/frpc/src/components/visitor-form/VisitorBaseSection.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/web/frpc/src/components/visitor-form/VisitorConnectionSection.vue b/web/frpc/src/components/visitor-form/VisitorConnectionSection.vue new file mode 100644 index 00000000000..0b52a8740d3 --- /dev/null +++ b/web/frpc/src/components/visitor-form/VisitorConnectionSection.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/web/frpc/src/components/visitor-form/VisitorFormLayout.vue b/web/frpc/src/components/visitor-form/VisitorFormLayout.vue new file mode 100644 index 00000000000..ba94246336f --- /dev/null +++ b/web/frpc/src/components/visitor-form/VisitorFormLayout.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/frpc/src/components/visitor-form/VisitorPluginSection.vue b/web/frpc/src/components/visitor-form/VisitorPluginSection.vue new file mode 100644 index 00000000000..ecbe5ce7063 --- /dev/null +++ b/web/frpc/src/components/visitor-form/VisitorPluginSection.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/web/frpc/src/components/visitor-form/VisitorTransportSection.vue b/web/frpc/src/components/visitor-form/VisitorTransportSection.vue new file mode 100644 index 00000000000..f7d2fadbfad --- /dev/null +++ b/web/frpc/src/components/visitor-form/VisitorTransportSection.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/web/frpc/src/components/visitor-form/VisitorXtcpSection.vue b/web/frpc/src/components/visitor-form/VisitorXtcpSection.vue new file mode 100644 index 00000000000..2bca4b9b49e --- /dev/null +++ b/web/frpc/src/components/visitor-form/VisitorXtcpSection.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/web/frpc/src/composables/useResponsive.ts b/web/frpc/src/composables/useResponsive.ts new file mode 100644 index 00000000000..f6cf177439d --- /dev/null +++ b/web/frpc/src/composables/useResponsive.ts @@ -0,0 +1,8 @@ +import { useBreakpoints } from '@vueuse/core' + +const breakpoints = useBreakpoints({ mobile: 0, desktop: 768 }) + +export function useResponsive() { + const isMobile = breakpoints.smaller('desktop') // < 768px + return { isMobile } +} diff --git a/web/frpc/src/env.d.ts b/web/frpc/src/env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/web/frpc/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/frpc/src/index.html b/web/frpc/src/index.html deleted file mode 100644 index 791291f1eea..00000000000 --- a/web/frpc/src/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - frp client admin UI - - - -
- - - - - diff --git a/web/frpc/src/main.js b/web/frpc/src/main.js deleted file mode 100644 index 337eb479824..00000000000 --- a/web/frpc/src/main.js +++ /dev/null @@ -1,52 +0,0 @@ -import Vue from 'vue' -// import ElementUI from 'element-ui' -import { - Button, - Form, - FormItem, - Row, - Col, - Table, - TableColumn, - Menu, - MenuItem, - MessageBox, - Message, - Input -} from 'element-ui' -import lang from 'element-ui/lib/locale/lang/en' -import locale from 'element-ui/lib/locale' -import 'element-ui/lib/theme-chalk/index.css' -import './utils/less/custom.less' - -import App from './App.vue' -import router from './router' -import 'whatwg-fetch' - -locale.use(lang) - -Vue.use(Button) -Vue.use(Form) -Vue.use(FormItem) -Vue.use(Row) -Vue.use(Col) -Vue.use(Table) -Vue.use(TableColumn) -Vue.use(Menu) -Vue.use(MenuItem) -Vue.use(Input) - -Vue.prototype.$msgbox = MessageBox; -Vue.prototype.$confirm = MessageBox.confirm -Vue.prototype.$message = Message - -//Vue.use(ElementUI) - -Vue.config.productionTip = false - -new Vue({ - el: '#app', - router, - template: '', - components: { App } -}) diff --git a/web/frpc/src/main.ts b/web/frpc/src/main.ts new file mode 100644 index 00000000000..c433176b7f4 --- /dev/null +++ b/web/frpc/src/main.ts @@ -0,0 +1,15 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import 'element-plus/theme-chalk/dark/css-vars.css' +import App from './App.vue' +import router from './router' + +import './assets/css/var.css' +import './assets/css/dark.css' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/web/frpc/src/router/index.js b/web/frpc/src/router/index.js deleted file mode 100644 index 572b1ca4540..00000000000 --- a/web/frpc/src/router/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue' -import Router from 'vue-router' -import Overview from '../components/Overview.vue' -import Configure from '../components/Configure.vue' - -Vue.use(Router) - -export default new Router({ - routes: [{ - path: '/', - name: 'Overview', - component: Overview - },{ - path: '/configure', - name: 'Configure', - component: Configure, - }] -}) diff --git a/web/frpc/src/router/index.ts b/web/frpc/src/router/index.ts new file mode 100644 index 00000000000..75457207d4f --- /dev/null +++ b/web/frpc/src/router/index.ts @@ -0,0 +1,88 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import { ElMessage } from 'element-plus' +import ClientConfigure from '../views/ClientConfigure.vue' +import ProxyDetail from '../views/ProxyDetail.vue' +import ProxyEdit from '../views/ProxyEdit.vue' +import ProxyList from '../views/ProxyList.vue' +import VisitorDetail from '../views/VisitorDetail.vue' +import VisitorEdit from '../views/VisitorEdit.vue' +import VisitorList from '../views/VisitorList.vue' +import { useProxyStore } from '../stores/proxy' + +const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/', + redirect: '/proxies', + }, + { + path: '/proxies', + name: 'ProxyList', + component: ProxyList, + }, + { + path: '/proxies/detail/:name', + name: 'ProxyDetail', + component: ProxyDetail, + }, + { + path: '/proxies/create', + name: 'ProxyCreate', + component: ProxyEdit, + meta: { requiresStore: true }, + }, + { + path: '/proxies/:name/edit', + name: 'ProxyEdit', + component: ProxyEdit, + meta: { requiresStore: true }, + }, + { + path: '/visitors', + name: 'VisitorList', + component: VisitorList, + }, + { + path: '/visitors/detail/:name', + name: 'VisitorDetail', + component: VisitorDetail, + }, + { + path: '/visitors/create', + name: 'VisitorCreate', + component: VisitorEdit, + meta: { requiresStore: true }, + }, + { + path: '/visitors/:name/edit', + name: 'VisitorEdit', + component: VisitorEdit, + meta: { requiresStore: true }, + }, + { + path: '/config', + name: 'ClientConfigure', + component: ClientConfigure, + }, + ], +}) + +router.beforeEach(async (to) => { + if (!to.matched.some((record) => record.meta.requiresStore)) { + return true + } + + const proxyStore = useProxyStore() + const enabled = await proxyStore.checkStoreEnabled() + if (enabled) { + return true + } + + ElMessage.warning( + 'Store is disabled. Enable Store in frpc config to create or edit store entries.', + ) + return { name: 'ProxyList' } +}) + +export default router diff --git a/web/frpc/src/stores/client.ts b/web/frpc/src/stores/client.ts new file mode 100644 index 00000000000..4f576b4b709 --- /dev/null +++ b/web/frpc/src/stores/client.ts @@ -0,0 +1,28 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getConfig, putConfig, reloadConfig } from '../api/frpc' + +export const useClientStore = defineStore('client', () => { + const config = ref('') + const loading = ref(false) + + const fetchConfig = async () => { + loading.value = true + try { + config.value = await getConfig() + } finally { + loading.value = false + } + } + + const saveConfig = async (text: string) => { + await putConfig(text) + config.value = text + } + + const reload = async () => { + await reloadConfig() + } + + return { config, loading, fetchConfig, saveConfig, reload } +}) diff --git a/web/frpc/src/stores/proxy.ts b/web/frpc/src/stores/proxy.ts new file mode 100644 index 00000000000..b7b6449bd91 --- /dev/null +++ b/web/frpc/src/stores/proxy.ts @@ -0,0 +1,132 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { ProxyStatus, ProxyDefinition } from '../types' +import { + getStatus, + listStoreProxies, + getStoreProxy, + createStoreProxy, + updateStoreProxy, + deleteStoreProxy, +} from '../api/frpc' + +export const useProxyStore = defineStore('proxy', () => { + const proxies = ref([]) + const storeProxies = ref([]) + const storeEnabled = ref(false) + const storeChecked = ref(false) + const loading = ref(false) + const storeLoading = ref(false) + const error = ref(null) + + const fetchStatus = async () => { + loading.value = true + error.value = null + try { + const json = await getStatus() + const list: ProxyStatus[] = [] + for (const key in json) { + for (const ps of json[key]) { + list.push(ps) + } + } + proxies.value = list + } catch (err: any) { + error.value = err.message + throw err + } finally { + loading.value = false + } + } + + const fetchStoreProxies = async () => { + storeLoading.value = true + try { + const res = await listStoreProxies() + storeProxies.value = res.proxies || [] + storeEnabled.value = true + storeChecked.value = true + } catch (err: any) { + if (err?.status === 404) { + storeEnabled.value = false + } + storeChecked.value = true + } finally { + storeLoading.value = false + } + } + + const checkStoreEnabled = async () => { + if (storeChecked.value) return storeEnabled.value + await fetchStoreProxies() + return storeEnabled.value + } + + const createProxy = async (data: ProxyDefinition) => { + await createStoreProxy(data) + await fetchStoreProxies() + } + + const updateProxy = async (name: string, data: ProxyDefinition) => { + await updateStoreProxy(name, data) + await fetchStoreProxies() + } + + const deleteProxy = async (name: string) => { + await deleteStoreProxy(name) + await fetchStoreProxies() + } + + const toggleProxy = async (name: string, enabled: boolean) => { + const def = await getStoreProxy(name) + const block = (def as any)[def.type] + if (block) { + block.enabled = enabled + } + await updateStoreProxy(name, def) + await fetchStatus() + await fetchStoreProxies() + } + + const storeProxyWithStatus = (def: ProxyDefinition): ProxyStatus => { + const block = (def as any)[def.type] + const enabled = block?.enabled !== false + + const localIP = block?.localIP || '127.0.0.1' + const localPort = block?.localPort + const local_addr = localPort != null ? `${localIP}:${localPort}` : '' + const remotePort = block?.remotePort + const remote_addr = remotePort != null ? `:${remotePort}` : '' + const plugin = block?.plugin?.type || '' + + const status = proxies.value.find((p) => p.name === def.name) + return { + name: def.name, + type: def.type, + status: !enabled ? 'disabled' : (status?.status || 'waiting'), + err: status?.err || '', + local_addr: status?.local_addr || local_addr, + remote_addr: status?.remote_addr || remote_addr, + plugin: status?.plugin || plugin, + source: 'store', + } + } + + return { + proxies, + storeProxies, + storeEnabled, + storeChecked, + loading, + storeLoading, + error, + fetchStatus, + fetchStoreProxies, + checkStoreEnabled, + createProxy, + updateProxy, + deleteProxy, + toggleProxy, + storeProxyWithStatus, + } +}) diff --git a/web/frpc/src/stores/visitor.ts b/web/frpc/src/stores/visitor.ts new file mode 100644 index 00000000000..9b817c39f93 --- /dev/null +++ b/web/frpc/src/stores/visitor.ts @@ -0,0 +1,68 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { VisitorDefinition } from '../types' +import { + listStoreVisitors, + createStoreVisitor, + updateStoreVisitor, + deleteStoreVisitor, +} from '../api/frpc' + +export const useVisitorStore = defineStore('visitor', () => { + const storeVisitors = ref([]) + const storeEnabled = ref(false) + const storeChecked = ref(false) + const loading = ref(false) + const error = ref(null) + + const fetchStoreVisitors = async () => { + loading.value = true + try { + const res = await listStoreVisitors() + storeVisitors.value = res.visitors || [] + storeEnabled.value = true + storeChecked.value = true + } catch (err: any) { + if (err?.status === 404) { + storeEnabled.value = false + } + storeChecked.value = true + } finally { + loading.value = false + } + } + + const checkStoreEnabled = async () => { + if (storeChecked.value) return storeEnabled.value + await fetchStoreVisitors() + return storeEnabled.value + } + + const createVisitor = async (data: VisitorDefinition) => { + await createStoreVisitor(data) + await fetchStoreVisitors() + } + + const updateVisitor = async (name: string, data: VisitorDefinition) => { + await updateStoreVisitor(name, data) + await fetchStoreVisitors() + } + + const deleteVisitor = async (name: string) => { + await deleteStoreVisitor(name) + await fetchStoreVisitors() + } + + return { + storeVisitors, + storeEnabled, + storeChecked, + loading, + error, + fetchStoreVisitors, + checkStoreEnabled, + createVisitor, + updateVisitor, + deleteVisitor, + } +}) diff --git a/web/frpc/src/svg.d.ts b/web/frpc/src/svg.d.ts new file mode 100644 index 00000000000..2f7dabe59c1 --- /dev/null +++ b/web/frpc/src/svg.d.ts @@ -0,0 +1,5 @@ +declare module '*.svg?component' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} diff --git a/web/frpc/src/types/constants.ts b/web/frpc/src/types/constants.ts new file mode 100644 index 00000000000..b135f8b6fce --- /dev/null +++ b/web/frpc/src/types/constants.ts @@ -0,0 +1,32 @@ +export const PROXY_TYPES = [ + 'tcp', + 'udp', + 'http', + 'https', + 'tcpmux', + 'stcp', + 'sudp', + 'xtcp', +] as const + +export type ProxyType = (typeof PROXY_TYPES)[number] + +export const VISITOR_TYPES = ['stcp', 'sudp', 'xtcp'] as const + +export type VisitorType = (typeof VISITOR_TYPES)[number] + +export const PLUGIN_TYPES = [ + '', + 'http2https', + 'http_proxy', + 'https2http', + 'https2https', + 'http2http', + 'socks5', + 'static_file', + 'unix_domain_socket', + 'tls2raw', + 'virtual_net', +] as const + +export type PluginType = (typeof PLUGIN_TYPES)[number] diff --git a/web/frpc/src/types/index.ts b/web/frpc/src/types/index.ts new file mode 100644 index 00000000000..c24111ef90b --- /dev/null +++ b/web/frpc/src/types/index.ts @@ -0,0 +1,5 @@ +export * from './constants' +export * from './proxy-status' +export * from './proxy-store' +export * from './proxy-form' +export * from './proxy-converters' diff --git a/web/frpc/src/types/proxy-converters.ts b/web/frpc/src/types/proxy-converters.ts new file mode 100644 index 00000000000..eb4c4c17420 --- /dev/null +++ b/web/frpc/src/types/proxy-converters.ts @@ -0,0 +1,481 @@ +import type { ProxyType, VisitorType } from './constants' +import type { ProxyFormData, VisitorFormData } from './proxy-form' +import { createDefaultProxyForm, createDefaultVisitorForm } from './proxy-form' +import type { ProxyDefinition, VisitorDefinition } from './proxy-store' + +// ======================================== +// CONVERTERS: Form -> Store API +// ======================================== + +export function formToStoreProxy(form: ProxyFormData): ProxyDefinition { + const block: Record = {} + + // Enabled (nil/true = enabled, false = disabled) + if (!form.enabled) { + block.enabled = false + } + + // Backend - LocalIP/LocalPort + if (form.pluginType === '') { + if (form.localIP && form.localIP !== '127.0.0.1') { + block.localIP = form.localIP + } + if (form.localPort != null) { + block.localPort = form.localPort + } + } else { + block.plugin = { + type: form.pluginType, + ...form.pluginConfig, + } + } + + // Transport + if ( + form.useEncryption || + form.useCompression || + form.bandwidthLimit || + (form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') || + form.proxyProtocolVersion + ) { + block.transport = {} + if (form.useEncryption) block.transport.useEncryption = true + if (form.useCompression) block.transport.useCompression = true + if (form.bandwidthLimit) block.transport.bandwidthLimit = form.bandwidthLimit + if (form.bandwidthLimitMode && form.bandwidthLimitMode !== 'client') { + block.transport.bandwidthLimitMode = form.bandwidthLimitMode + } + if (form.proxyProtocolVersion) { + block.transport.proxyProtocolVersion = form.proxyProtocolVersion + } + } + + // Load Balancer + if (form.loadBalancerGroup) { + block.loadBalancer = { + group: form.loadBalancerGroup, + } + if (form.loadBalancerGroupKey) { + block.loadBalancer.groupKey = form.loadBalancerGroupKey + } + } + + // Health Check + if (form.healthCheckType) { + block.healthCheck = { + type: form.healthCheckType, + } + if (form.healthCheckTimeoutSeconds != null) { + block.healthCheck.timeoutSeconds = form.healthCheckTimeoutSeconds + } + if (form.healthCheckMaxFailed != null) { + block.healthCheck.maxFailed = form.healthCheckMaxFailed + } + if (form.healthCheckIntervalSeconds != null) { + block.healthCheck.intervalSeconds = form.healthCheckIntervalSeconds + } + if (form.healthCheckPath) { + block.healthCheck.path = form.healthCheckPath + } + if (form.healthCheckHTTPHeaders.length > 0) { + block.healthCheck.httpHeaders = form.healthCheckHTTPHeaders + } + } + + // Metadata + if (form.metadatas.length > 0) { + block.metadatas = Object.fromEntries( + form.metadatas.map((m) => [m.key, m.value]), + ) + } + + // Annotations + if (form.annotations.length > 0) { + block.annotations = Object.fromEntries( + form.annotations.map((a) => [a.key, a.value]), + ) + } + + // Type-specific fields + if ((form.type === 'tcp' || form.type === 'udp') && form.remotePort != null) { + block.remotePort = form.remotePort + } + + if (form.type === 'http' || form.type === 'https' || form.type === 'tcpmux') { + if (form.customDomains.length > 0) { + block.customDomains = form.customDomains.filter(Boolean) + } + if (form.subdomain) { + block.subdomain = form.subdomain + } + } + + if (form.type === 'http') { + if (form.locations.length > 0) { + block.locations = form.locations.filter(Boolean) + } + if (form.httpUser) block.httpUser = form.httpUser + if (form.httpPassword) block.httpPassword = form.httpPassword + if (form.hostHeaderRewrite) block.hostHeaderRewrite = form.hostHeaderRewrite + if (form.routeByHTTPUser) block.routeByHTTPUser = form.routeByHTTPUser + + if (form.requestHeaders.length > 0) { + block.requestHeaders = { + set: Object.fromEntries( + form.requestHeaders.map((h) => [h.key, h.value]), + ), + } + } + if (form.responseHeaders.length > 0) { + block.responseHeaders = { + set: Object.fromEntries( + form.responseHeaders.map((h) => [h.key, h.value]), + ), + } + } + } + + if (form.type === 'tcpmux') { + if (form.httpUser) block.httpUser = form.httpUser + if (form.httpPassword) block.httpPassword = form.httpPassword + if (form.routeByHTTPUser) block.routeByHTTPUser = form.routeByHTTPUser + if (form.multiplexer && form.multiplexer !== 'httpconnect') { + block.multiplexer = form.multiplexer + } + } + + if (form.type === 'stcp' || form.type === 'sudp' || form.type === 'xtcp') { + if (form.secretKey) block.secretKey = form.secretKey + if (form.allowUsers.length > 0) { + block.allowUsers = form.allowUsers.filter(Boolean) + } + } + + if (form.type === 'xtcp' && form.natTraversalDisableAssistedAddrs) { + block.natTraversal = { + disableAssistedAddrs: true, + } + } + + return withStoreProxyBlock( + { + name: form.name, + type: form.type, + }, + form.type, + block, + ) +} + +export function formToStoreVisitor(form: VisitorFormData): VisitorDefinition { + const block: Record = {} + + if (!form.enabled) { + block.enabled = false + } + + if (form.useEncryption || form.useCompression) { + block.transport = {} + if (form.useEncryption) block.transport.useEncryption = true + if (form.useCompression) block.transport.useCompression = true + } + + if (form.secretKey) block.secretKey = form.secretKey + if (form.serverUser) block.serverUser = form.serverUser + if (form.serverName) block.serverName = form.serverName + if (form.bindAddr && form.bindAddr !== '127.0.0.1') { + block.bindAddr = form.bindAddr + } + if (form.bindPort != null) { + 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 + } + if (form.keepTunnelOpen) { + block.keepTunnelOpen = true + } + if (form.maxRetriesAnHour != null) { + block.maxRetriesAnHour = form.maxRetriesAnHour + } + if (form.minRetryInterval != null) { + block.minRetryInterval = form.minRetryInterval + } + if (form.fallbackTo) { + block.fallbackTo = form.fallbackTo + } + if (form.fallbackTimeoutMs != null) { + block.fallbackTimeoutMs = form.fallbackTimeoutMs + } + if (form.natTraversalDisableAssistedAddrs) { + block.natTraversal = { + disableAssistedAddrs: true, + } + } + } + + return withStoreVisitorBlock( + { + name: form.name, + type: form.type, + }, + form.type, + block, + ) +} + +// ======================================== +// CONVERTERS: Store API -> Form +// ======================================== + +function getStoreProxyBlock(config: ProxyDefinition): Record { + switch (config.type) { + case 'tcp': + return config.tcp || {} + case 'udp': + return config.udp || {} + case 'http': + return config.http || {} + case 'https': + return config.https || {} + case 'tcpmux': + return config.tcpmux || {} + case 'stcp': + return config.stcp || {} + case 'sudp': + return config.sudp || {} + case 'xtcp': + return config.xtcp || {} + } +} + +function withStoreProxyBlock( + payload: ProxyDefinition, + type: ProxyType, + block: Record, +): ProxyDefinition { + switch (type) { + case 'tcp': + payload.tcp = block + break + case 'udp': + payload.udp = block + break + case 'http': + payload.http = block + break + case 'https': + payload.https = block + break + case 'tcpmux': + payload.tcpmux = block + break + case 'stcp': + payload.stcp = block + break + case 'sudp': + payload.sudp = block + break + case 'xtcp': + payload.xtcp = block + break + } + return payload +} + +function getStoreVisitorBlock(config: VisitorDefinition): Record { + switch (config.type) { + case 'stcp': + return config.stcp || {} + case 'sudp': + return config.sudp || {} + case 'xtcp': + return config.xtcp || {} + } +} + +function withStoreVisitorBlock( + payload: VisitorDefinition, + type: VisitorType, + block: Record, +): VisitorDefinition { + switch (type) { + case 'stcp': + payload.stcp = block + break + case 'sudp': + payload.sudp = block + break + case 'xtcp': + payload.xtcp = block + break + } + return payload +} + +export function storeProxyToForm(config: ProxyDefinition): ProxyFormData { + const c = getStoreProxyBlock(config) + const form = createDefaultProxyForm() + + form.name = config.name || '' + form.type = config.type || 'tcp' + form.enabled = c.enabled !== false + + // Backend + form.localIP = c.localIP || '127.0.0.1' + form.localPort = c.localPort + if (c.plugin?.type) { + form.pluginType = c.plugin.type + form.pluginConfig = { ...c.plugin } + delete form.pluginConfig.type + } + + // Transport + if (c.transport) { + form.useEncryption = c.transport.useEncryption || false + form.useCompression = c.transport.useCompression || false + form.bandwidthLimit = c.transport.bandwidthLimit || '' + form.bandwidthLimitMode = c.transport.bandwidthLimitMode || 'client' + form.proxyProtocolVersion = c.transport.proxyProtocolVersion || '' + } + + // Load Balancer + if (c.loadBalancer) { + form.loadBalancerGroup = c.loadBalancer.group || '' + form.loadBalancerGroupKey = c.loadBalancer.groupKey || '' + } + + // Health Check + if (c.healthCheck) { + form.healthCheckType = c.healthCheck.type || '' + form.healthCheckTimeoutSeconds = c.healthCheck.timeoutSeconds + form.healthCheckMaxFailed = c.healthCheck.maxFailed + form.healthCheckIntervalSeconds = c.healthCheck.intervalSeconds + form.healthCheckPath = c.healthCheck.path || '' + form.healthCheckHTTPHeaders = c.healthCheck.httpHeaders || [] + } + + // Metadata + if (c.metadatas) { + form.metadatas = Object.entries(c.metadatas).map(([key, value]) => ({ + key, + value: String(value), + })) + } + + // Annotations + if (c.annotations) { + form.annotations = Object.entries(c.annotations).map(([key, value]) => ({ + key, + value: String(value), + })) + } + + // Type-specific fields + form.remotePort = c.remotePort + + // Domain config + if (Array.isArray(c.customDomains)) { + form.customDomains = c.customDomains + } else if (c.customDomains) { + form.customDomains = [c.customDomains] + } + form.subdomain = c.subdomain || '' + + // HTTP specific + if (Array.isArray(c.locations)) { + form.locations = c.locations + } else if (c.locations) { + form.locations = [c.locations] + } + form.httpUser = c.httpUser || '' + form.httpPassword = c.httpPassword || '' + form.hostHeaderRewrite = c.hostHeaderRewrite || '' + form.routeByHTTPUser = c.routeByHTTPUser || '' + + // Header operations + if (c.requestHeaders?.set) { + form.requestHeaders = Object.entries(c.requestHeaders.set).map( + ([key, value]) => ({ key, value: String(value) }), + ) + } + if (c.responseHeaders?.set) { + form.responseHeaders = Object.entries(c.responseHeaders.set).map( + ([key, value]) => ({ key, value: String(value) }), + ) + } + + // TCPMux + form.multiplexer = c.multiplexer || 'httpconnect' + + // Secure types + form.secretKey = c.secretKey || '' + if (Array.isArray(c.allowUsers)) { + form.allowUsers = c.allowUsers + } else if (c.allowUsers) { + form.allowUsers = [c.allowUsers] + } + + // XTCP NAT traversal + form.natTraversalDisableAssistedAddrs = + c.natTraversal?.disableAssistedAddrs || false + + return form +} + +export function storeVisitorToForm( + config: VisitorDefinition, +): VisitorFormData { + const c = getStoreVisitorBlock(config) + const form = createDefaultVisitorForm() + + form.name = config.name || '' + form.type = config.type || 'stcp' + form.enabled = c.enabled !== false + + // Transport + if (c.transport) { + form.useEncryption = c.transport.useEncryption || false + form.useCompression = c.transport.useCompression || false + } + + // Base fields + form.secretKey = c.secretKey || '' + form.serverUser = c.serverUser || '' + form.serverName = c.serverName || '' + 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 + form.maxRetriesAnHour = c.maxRetriesAnHour + form.minRetryInterval = c.minRetryInterval + form.fallbackTo = c.fallbackTo || '' + form.fallbackTimeoutMs = c.fallbackTimeoutMs + form.natTraversalDisableAssistedAddrs = + c.natTraversal?.disableAssistedAddrs || false + + return form +} diff --git a/web/frpc/src/types/proxy-form.ts b/web/frpc/src/types/proxy-form.ts new file mode 100644 index 00000000000..793fe516b36 --- /dev/null +++ b/web/frpc/src/types/proxy-form.ts @@ -0,0 +1,174 @@ +import type { ProxyType, VisitorType } from './constants' + +export interface ProxyFormData { + // Base fields (ProxyBaseConfig) + name: string + type: ProxyType + enabled: boolean + + // Backend (ProxyBackend) + localIP: string + localPort: number | undefined + pluginType: string + pluginConfig: Record + + // Transport (ProxyTransport) + useEncryption: boolean + useCompression: boolean + bandwidthLimit: string + bandwidthLimitMode: string + proxyProtocolVersion: string + + // Load Balancer (LoadBalancerConfig) + loadBalancerGroup: string + loadBalancerGroupKey: string + + // Health Check (HealthCheckConfig) + healthCheckType: string + healthCheckTimeoutSeconds: number | undefined + healthCheckMaxFailed: number | undefined + healthCheckIntervalSeconds: number | undefined + healthCheckPath: string + healthCheckHTTPHeaders: Array<{ name: string; value: string }> + + // Metadata & Annotations + metadatas: Array<{ key: string; value: string }> + annotations: Array<{ key: string; value: string }> + + // TCP/UDP specific + remotePort: number | undefined + + // Domain (HTTP/HTTPS/TCPMux) - DomainConfig + customDomains: string[] + subdomain: string + + // HTTP specific (HTTPProxyConfig) + locations: string[] + httpUser: string + httpPassword: string + hostHeaderRewrite: string + requestHeaders: Array<{ key: string; value: string }> + responseHeaders: Array<{ key: string; value: string }> + routeByHTTPUser: string + + // TCPMux specific + multiplexer: string + + // STCP/SUDP/XTCP specific + secretKey: string + allowUsers: string[] + + // XTCP specific (NatTraversalConfig) + natTraversalDisableAssistedAddrs: boolean +} + +export interface VisitorFormData { + // Base fields (VisitorBaseConfig) + name: string + type: VisitorType + enabled: boolean + + // Transport (VisitorTransport) + useEncryption: boolean + useCompression: boolean + + // Connection + secretKey: string + serverUser: string + serverName: string + bindAddr: string + bindPort: number | undefined + + // Visitor plugin + pluginType: '' | 'virtual_net' + pluginDestinationIP: string + + // XTCP specific (XTCPVisitorConfig) + protocol: string + keepTunnelOpen: boolean + maxRetriesAnHour: number | undefined + minRetryInterval: number | undefined + fallbackTo: string + fallbackTimeoutMs: number | undefined + natTraversalDisableAssistedAddrs: boolean +} + +export function createDefaultProxyForm(): ProxyFormData { + return { + name: '', + type: 'tcp', + enabled: true, + + localIP: '127.0.0.1', + localPort: undefined, + pluginType: '', + pluginConfig: {}, + + useEncryption: false, + useCompression: false, + bandwidthLimit: '', + bandwidthLimitMode: 'client', + proxyProtocolVersion: '', + + loadBalancerGroup: '', + loadBalancerGroupKey: '', + + healthCheckType: '', + healthCheckTimeoutSeconds: undefined, + healthCheckMaxFailed: undefined, + healthCheckIntervalSeconds: undefined, + healthCheckPath: '', + healthCheckHTTPHeaders: [], + + metadatas: [], + annotations: [], + + remotePort: undefined, + + customDomains: [], + subdomain: '', + + locations: [], + httpUser: '', + httpPassword: '', + hostHeaderRewrite: '', + requestHeaders: [], + responseHeaders: [], + routeByHTTPUser: '', + + multiplexer: 'httpconnect', + + secretKey: '', + allowUsers: [], + + natTraversalDisableAssistedAddrs: false, + } +} + +export function createDefaultVisitorForm(): VisitorFormData { + return { + name: '', + type: 'stcp', + enabled: true, + + useEncryption: false, + useCompression: false, + + secretKey: '', + serverUser: '', + serverName: '', + bindAddr: '127.0.0.1', + bindPort: undefined, + + pluginType: '', + pluginDestinationIP: '', + + protocol: 'quic', + keepTunnelOpen: false, + maxRetriesAnHour: undefined, + minRetryInterval: undefined, + fallbackTo: '', + fallbackTimeoutMs: undefined, + natTraversalDisableAssistedAddrs: false, + } +} diff --git a/web/frpc/src/types/proxy-status.ts b/web/frpc/src/types/proxy-status.ts new file mode 100644 index 00000000000..3b4ea648f10 --- /dev/null +++ b/web/frpc/src/types/proxy-status.ts @@ -0,0 +1,13 @@ +export interface ProxyStatus { + name: string + type: string + status: string + err: string + local_addr: string + plugin: string + remote_addr: string + source?: 'store' | 'config' + [key: string]: any +} + +export type StatusResponse = Record diff --git a/web/frpc/src/types/proxy-store.ts b/web/frpc/src/types/proxy-store.ts new file mode 100644 index 00000000000..f7ef95b065d --- /dev/null +++ b/web/frpc/src/types/proxy-store.ts @@ -0,0 +1,30 @@ +import type { ProxyType, VisitorType } from './constants' + +export interface ProxyDefinition { + name: string + type: ProxyType + tcp?: Record + udp?: Record + http?: Record + https?: Record + tcpmux?: Record + stcp?: Record + sudp?: Record + xtcp?: Record +} + +export interface VisitorDefinition { + name: string + type: VisitorType + stcp?: Record + sudp?: Record + xtcp?: Record +} + +export interface ProxyListResp { + proxies: ProxyDefinition[] +} + +export interface VisitorListResp { + visitors: VisitorDefinition[] +} diff --git a/web/frpc/src/utils/format.ts b/web/frpc/src/utils/format.ts new file mode 100644 index 00000000000..11cd398f142 --- /dev/null +++ b/web/frpc/src/utils/format.ts @@ -0,0 +1,33 @@ +export function formatDistanceToNow(date: Date): string { + const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000) + + let interval = seconds / 31536000 + if (interval > 1) return Math.floor(interval) + ' years ago' + + interval = seconds / 2592000 + if (interval > 1) return Math.floor(interval) + ' months ago' + + interval = seconds / 86400 + if (interval > 1) return Math.floor(interval) + ' days ago' + + interval = seconds / 3600 + if (interval > 1) return Math.floor(interval) + ' hours ago' + + interval = seconds / 60 + if (interval > 1) return Math.floor(interval) + ' minutes ago' + + return Math.floor(seconds) + ' seconds ago' +} + +export function formatFileSize(bytes: number): string { + if (!Number.isFinite(bytes) || bytes < 0) return '0 B' + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + // Prevent index out of bounds for extremely large numbers + const unit = sizes[i] || sizes[sizes.length - 1] + const val = bytes / Math.pow(k, i) + + return parseFloat(val.toFixed(2)) + ' ' + unit +} diff --git a/web/frpc/src/utils/less/custom.less b/web/frpc/src/utils/less/custom.less deleted file mode 100644 index b83a4007466..00000000000 --- a/web/frpc/src/utils/less/custom.less +++ /dev/null @@ -1,22 +0,0 @@ -@color: red; - -.el-form-item { - span { - margin-left: 15px; - } -} - -.demo-table-expand { - font-size: 0; - - label { - width: 90px; - color: #99a9bf; - } - - .el-form-item { - margin-right: 0; - margin-bottom: 0; - width: 50%; - } -} diff --git a/web/frpc/src/utils/status.js b/web/frpc/src/utils/status.js deleted file mode 100644 index 9ccc418e658..00000000000 --- a/web/frpc/src/utils/status.js +++ /dev/null @@ -1,13 +0,0 @@ -class ProxyStatus { - constructor(status) { - this.name = status.name - this.type = status.type - this.status = status.status - this.err = status.err - this.local_addr = status.local_addr - this.plugin = status.plugin - this.remote_addr = status.remote_addr - } -} - -export {ProxyStatus} diff --git a/web/frpc/src/views/ClientConfigure.vue b/web/frpc/src/views/ClientConfigure.vue new file mode 100644 index 00000000000..aa9c4c4e17c --- /dev/null +++ b/web/frpc/src/views/ClientConfigure.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/web/frpc/src/views/ProxyDetail.vue b/web/frpc/src/views/ProxyDetail.vue new file mode 100644 index 00000000000..9b238b4a85f --- /dev/null +++ b/web/frpc/src/views/ProxyDetail.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/web/frpc/src/views/ProxyEdit.vue b/web/frpc/src/views/ProxyEdit.vue new file mode 100644 index 00000000000..0a45d695355 --- /dev/null +++ b/web/frpc/src/views/ProxyEdit.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/web/frpc/src/views/ProxyList.vue b/web/frpc/src/views/ProxyList.vue new file mode 100644 index 00000000000..61661983969 --- /dev/null +++ b/web/frpc/src/views/ProxyList.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/web/frpc/src/views/VisitorDetail.vue b/web/frpc/src/views/VisitorDetail.vue new file mode 100644 index 00000000000..b038b1e5d5c --- /dev/null +++ b/web/frpc/src/views/VisitorDetail.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/web/frpc/src/views/VisitorEdit.vue b/web/frpc/src/views/VisitorEdit.vue new file mode 100644 index 00000000000..f095103d740 --- /dev/null +++ b/web/frpc/src/views/VisitorEdit.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/web/frpc/src/views/VisitorList.vue b/web/frpc/src/views/VisitorList.vue new file mode 100644 index 00000000000..506b57f251b --- /dev/null +++ b/web/frpc/src/views/VisitorList.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/web/frpc/test/config-field.test.ts b/web/frpc/test/config-field.test.ts new file mode 100644 index 00000000000..64986274145 --- /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 00000000000..69e3410bf17 --- /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/frpc/tsconfig.json b/web/frpc/tsconfig.json new file mode 100644 index 00000000000..8795fb5cec2 --- /dev/null +++ b/web/frpc/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"], + "@shared/*": ["../shared/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/web/frpc/tsconfig.node.json b/web/frpc/tsconfig.node.json new file mode 100644 index 00000000000..a8583534f27 --- /dev/null +++ b/web/frpc/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.mts"] +} diff --git a/web/frpc/vite.config.mts b/web/frpc/vite.config.mts new file mode 100644 index 00000000000..209a91fd554 --- /dev/null +++ b/web/frpc/vite.config.mts @@ -0,0 +1,61 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import svgLoader from 'vite-svg-loader' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import ElementPlus from 'unplugin-element-plus/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: '', + plugins: [ + vue(), + svgLoader(), + ElementPlus({}), + AutoImport({ + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@shared': fileURLToPath(new URL('../shared', import.meta.url)), + }, + dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'], + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@use "@shared/css/_index.scss" as *;`, + }, + }, + }, + build: { + assetsDir: '', + chunkSizeWarningLimit: 1000, + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + }, + server: { + allowedHosts: process.env.ALLOWED_HOSTS + ? process.env.ALLOWED_HOSTS.split(',') + : [], + proxy: { + '/api': { + target: process.env.VITE_API_URL || 'http://127.0.0.1:7400', + changeOrigin: true, + }, + }, + }, +}) diff --git a/web/frpc/webpack.config.js b/web/frpc/webpack.config.js deleted file mode 100644 index 6947920524d..00000000000 --- a/web/frpc/webpack.config.js +++ /dev/null @@ -1,107 +0,0 @@ -const path = require('path') -var webpack = require('webpack') -var HtmlWebpackPlugin = require('html-webpack-plugin') -var VueLoaderPlugin = require('vue-loader/lib/plugin') -var url = require('url') -var publicPath = '' - -module.exports = (options = {}) => ({ - entry: { - vendor: './src/main' - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: options.dev ? '[name].js' : '[name].js?[chunkhash]', - chunkFilename: '[id].js?[chunkhash]', - publicPath: options.dev ? '/assets/' : publicPath - }, - resolve: { - extensions: ['.js', '.vue', '.json'], - alias: { - 'vue$': 'vue/dist/vue.esm.js', - '@': path.resolve(__dirname, 'src'), - } - }, - module: { - rules: [{ - test: /\.vue$/, - loader: 'vue-loader' - }, { - test: /\.js$/, - use: ['babel-loader'], - exclude: /node_modules/ - }, { - test: /\.html$/, - use: [{ - loader: 'html-loader', - options: { - root: path.resolve(__dirname, 'src'), - attrs: ['img:src', 'link:href'] - } - }] - }, { - test: /\.less$/, - loader: 'style-loader!css-loader!postcss-loader!less-loader' - }, { - test: /\.css$/, - use: ['style-loader', 'css-loader', 'postcss-loader'] - }, { - test: /favicon\.png$/, - use: [{ - loader: 'file-loader', - options: { - name: '[name].[ext]?[hash]' - } - }] - }, { - test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, - exclude: /favicon\.png$/, - use: [{ - loader: 'url-loader', - options: { - limit: 10000 - } - }] - }] - }, - plugins: [ - new webpack.optimize.CommonsChunkPlugin({ - names: ['vendor', 'manifest'] - }), - new HtmlWebpackPlugin({ - favicon: 'src/assets/favicon.ico', - template: 'src/index.html' - }), - new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'), - new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: '"production"' - } - }), - new webpack.optimize.UglifyJsPlugin({ - sourceMap: false, - comments: false, - compress: { - warnings: false - } - }), - new VueLoaderPlugin() - ], - devServer: { - host: '127.0.0.1', - port: 8010, - proxy: { - '/api/': { - target: 'http://127.0.0.1:8080', - changeOrigin: true, - pathRewrite: { - '^/api': '' - } - } - }, - historyApiFallback: { - index: url.parse(options.dev ? '/assets/' : publicPath).pathname - } - }//, - //devtool: options.dev ? '#eval-source-map' : '#source-map' -}) diff --git a/web/frpc/yarn.lock b/web/frpc/yarn.lock deleted file mode 100644 index 8135e37ec4b..00000000000 --- a/web/frpc/yarn.lock +++ /dev/null @@ -1,6012 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/generator@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" - integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== - dependencies: - "@babel/types" "^7.12.11" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-function-name@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" - integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== - dependencies: - "@babel/helper-get-function-arity" "^7.12.10" - "@babel/template" "^7.12.7" - "@babel/types" "^7.12.11" - -"@babel/helper-get-function-arity@^7.12.10": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" - integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== - dependencies: - "@babel/types" "^7.12.10" - -"@babel/helper-module-imports@7.0.0-beta.35": - version "7.0.0-beta.35" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.35.tgz#308e350e731752cdb4d0f058df1d704925c64e0a" - integrity sha512-vaC1KyIZSuyWb3Lj277fX0pxivyHwuDU4xZsofqgYAbkDxNieMg2vuhzP5AgMweMY7fCQUMTi+BgPqTLjkxXFg== - dependencies: - "@babel/types" "7.0.0-beta.35" - lodash "^4.2.0" - -"@babel/helper-split-export-declaration@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" - integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== - dependencies: - "@babel/types" "^7.12.11" - -"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" - integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" - integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== - -"@babel/template@^7.12.7": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" - integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" - -"@babel/traverse@^7.7.0": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" - integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== - dependencies: - "@babel/code-frame" "^7.12.11" - "@babel/generator" "^7.12.11" - "@babel/helper-function-name" "^7.12.11" - "@babel/helper-split-export-declaration" "^7.12.11" - "@babel/parser" "^7.12.11" - "@babel/types" "^7.12.12" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/types@7.0.0-beta.35": - version "7.0.0-beta.35" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.35.tgz#cf933a9a9a38484ca724b335b88d83726d5ab960" - integrity sha512-y9XT11CozHDgjWcTdxmhSj13rJVXpa5ZXwjjOiTedjaM0ba5ItqdS02t31EhPl7HtOWxsZkYCCUNrSfrOisA6w== - dependencies: - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^2.0.0" - -"@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.7", "@babel/types@^7.7.0": - version "7.12.12" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" - integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== - dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@sindresorhus/is@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" - integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== - -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/node@*": - version "14.14.22" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" - integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== - -"@vue/component-compiler-utils@^3.1.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d" - integrity sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw== - dependencies: - consolidate "^0.15.1" - hash-sum "^1.0.2" - lru-cache "^4.1.2" - merge-source-map "^1.1.0" - postcss "^7.0.14" - postcss-selector-parser "^6.0.2" - source-map "~0.6.1" - vue-template-es2015-compiler "^1.9.0" - optionalDependencies: - prettier "^1.18.2" - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-dynamic-import@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" - integrity sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ= - dependencies: - acorn "^4.0.3" - -acorn-jsx@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== - -acorn@^4.0.3: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= - -acorn@^5.0.0: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== - -acorn@^6.0.7: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^1.1.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" - integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= - -ajv-keywords@^3.1.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^4.7.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.9.1: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -ast-types@0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" - integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -async-validator@~1.8.1: - version "1.8.5" - resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-1.8.5.tgz#dc3e08ec1fd0dddb67e60842f02c0cd1cec6d7f0" - integrity sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA== - dependencies: - babel-runtime "6.x" - -async@^2.1.2, async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -autoprefixer@^9.4.7: - version "9.8.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" - integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - colorette "^1.2.1" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.26.0, babel-core@^6.26.3: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-eslint@^10.0.1: - version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" - integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.7.0" - "@babel/traverse" "^7.7.0" - "@babel/types" "^7.7.0" - eslint-visitor-keys "^1.0.0" - resolve "^1.12.0" - -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" - integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" - integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" - integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" - integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" - integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" - integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" - integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI= - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" - integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-vue-jsx-merge-props@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6" - integrity sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg== - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-loader@^7.1.5: - version "7.1.5" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68" - integrity sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw== - dependencies: - find-cache-dir "^1.0.0" - loader-utils "^1.0.2" - mkdirp "^0.5.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" - integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-component@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-component/-/babel-plugin-component-1.1.1.tgz#9b023a23ff5c9aae0fd56c5a18b9cab8c4d45eea" - integrity sha512-WUw887kJf2GH80Ng/ZMctKZ511iamHNqPhd9uKo14yzisvV7Wt1EckIrb8oq/uCz3B3PpAW7Xfl7AkTLDYT6ag== - dependencies: - "@babel/helper-module-imports" "7.0.0-beta.35" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" - integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" - integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" - integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= - dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-plugin-transform-es2015-classes@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" - integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" - integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" - integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" - integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-for-of@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" - integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" - integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" - integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" - integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ= - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.2" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" - integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" - integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" - integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg= - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" - integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" - integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" - integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" - integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" - integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" - integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" - integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" - integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-regenerator@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" - integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= - dependencies: - regenerator-transform "^0.10.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-preset-es2015@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" - integrity sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk= - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.24.1" - babel-plugin-transform-es2015-classes "^6.24.1" - babel-plugin-transform-es2015-computed-properties "^6.24.1" - babel-plugin-transform-es2015-destructuring "^6.22.0" - babel-plugin-transform-es2015-duplicate-keys "^6.24.1" - babel-plugin-transform-es2015-for-of "^6.22.0" - babel-plugin-transform-es2015-function-name "^6.24.1" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-plugin-transform-es2015-modules-systemjs "^6.24.1" - babel-plugin-transform-es2015-modules-umd "^6.24.1" - babel-plugin-transform-es2015-object-super "^6.24.1" - babel-plugin-transform-es2015-parameters "^6.24.1" - babel-plugin-transform-es2015-shorthand-properties "^6.24.1" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.24.1" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.22.0" - babel-plugin-transform-es2015-unicode-regex "^6.24.1" - babel-plugin-transform-regenerator "^6.24.1" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bluebird@^3.1.1, bluebird@^3.4.7: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.12.0: - version "4.16.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.1.tgz#bf757a2da376b3447b800a16f0f1c96358138766" - integrity sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA== - dependencies: - caniuse-lite "^1.0.30001173" - colorette "^1.2.1" - electron-to-chromium "^1.3.634" - escalade "^3.1.1" - node-releases "^1.1.69" - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-request@^2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" - integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= - dependencies: - clone-response "1.0.2" - get-stream "3.0.0" - http-cache-semantics "3.8.1" - keyv "3.0.0" - lowercase-keys "1.0.0" - normalize-url "2.0.1" - responselike "1.0.2" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" - integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= - dependencies: - no-case "^2.2.0" - upper-case "^1.1.1" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^5.0.0, camelcase@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173: - version "1.0.30001179" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001179.tgz#b0803883b4471a6c62066fb1752756f8afc699c8" - integrity sha512-blMmO0QQujuUWZKyVrD1msR4WNDAqb/UPO1Sw2WWsQ7deoM5bJiicKnWJ1Y0NS/aGINSnKPIWBMw5luX+NDUCA== - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.4.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-css@4.2.x: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== - dependencies: - source-map "~0.6.0" - -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -clone-response@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - -commander@2.17.x: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -commander@~2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -consolidate@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" - integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw== - dependencies: - bluebird "^3.1.1" - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -copy-anything@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.1.tgz#2afbce6da684bdfcbec93752fa762819cb480d9a" - integrity sha512-lA57e7viQHOdPQcrytv5jFeudZZOXuyk47lZym279FiDQ8jeZomXiGuVf6ffMKkJ+3TIai3J1J3yi6M+/4U35g== - dependencies: - is-what "^3.7.1" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^6.0.0, cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-loader@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" - integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== - dependencies: - camelcase "^5.2.0" - icss-utils "^4.1.0" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.14" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^2.0.6" - postcss-modules-scope "^2.1.0" - postcss-modules-values "^2.0.0" - postcss-value-parser "^3.3.0" - schema-utils "^1.0.0" - -css-select@^2.0.2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-what@^3.2.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" - integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -de-indent@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" - integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.1, debug@^3.2.6: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -deepmerge@^1.2.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" - integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== - -default-gateway@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-converter@^0.2: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -electron-to-chromium@^1.3.634: - version "1.3.644" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.644.tgz#c89721733ec26b8d117275fb6b2acbeb3d45a6b6" - integrity sha512-N7FLvjDPADxad+OXXBuYfcvDvCBG0aW8ZZGr7G91sZMviYbnQJFxdSvUus4SJ0K7Q8dzMxE+Wx1d/CrJIIJ0sw== - -element-ui@^2.5.3: - version "2.15.0" - resolved "https://registry.yarnpkg.com/element-ui/-/element-ui-2.15.0.tgz#de9b73a8d1e3e3b50e82b923a5fa95295239bd41" - integrity sha512-9z/1+b7V8fvp08OnKUEW4/BZ72kT+IhuKR9cTMz3XoCTKmEsqLLb32XjbO/DznSFaaiFbOYU93G7WtkvrCAL9A== - dependencies: - async-validator "~1.8.1" - babel-helper-vue-jsx-merge-props "^2.0.0" - deepmerge "^1.2.0" - normalize-wheel "^1.0.1" - resize-observer-polyfill "^1.5.0" - throttle-debounce "^1.0.1" - -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^3.3.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" - integrity sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24= - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - object-assign "^4.0.1" - tapable "^0.2.7" - -enhanced-resolve@^4.1.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" - integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -errno@^0.1.1, errno@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es6-templates@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4" - integrity sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ= - dependencies: - recast "~0.11.12" - through "~2.3.6" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-config-enough@^0.3.4: - version "0.3.8" - resolved "https://registry.yarnpkg.com/eslint-config-enough/-/eslint-config-enough-0.3.8.tgz#62f19fc470bde83b7ad848ce338af93171e6a637" - integrity sha512-kp3wK5KRENv6DFG3b/++wlBPlah/euyWJqQSqsh9cFUcsDN+LVhVKFznqzRB/MXma4MmeRNKKw+Na1wzNpjkvg== - -eslint-loader@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.2.1.tgz#28b9c12da54057af0845e2a6112701a2f6bf8337" - integrity sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg== - dependencies: - loader-fs-cache "^1.0.0" - loader-utils "^1.0.2" - object-assign "^4.0.1" - object-hash "^1.1.4" - rimraf "^2.6.1" - -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.3.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint@^5.12.1: - version "5.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" - integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.9.1" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^4.0.3" - eslint-utils "^1.3.1" - eslint-visitor-keys "^1.0.0" - espree "^5.0.1" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.7.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^6.2.2" - js-yaml "^3.13.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.11" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^5.5.1" - strip-ansi "^4.0.0" - strip-json-comments "^2.0.1" - table "^5.2.3" - text-table "^0.2.0" - -espree@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a" - integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== - dependencies: - acorn "^6.0.7" - acorn-jsx "^5.0.0" - eslint-visitor-keys "^1.0.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esquery@^1.0.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== - -eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fastparse@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" - integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== - -faye-websocket@^0.11.3: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - -file-loader@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" - integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== - dependencies: - loader-utils "^1.0.2" - schema-utils "^1.0.0" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-cache-dir@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" - integrity sha1-yN765XyKUqinhPnjHFfHQumToLk= - dependencies: - commondir "^1.0.1" - mkdirp "^0.5.1" - pkg-dir "^1.0.0" - -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - -follow-redirects@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" - integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -from2@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" - integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49" - integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-stream@3.0.0, get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob@^7.0.3, glob@^7.1.2, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0, globals@^11.7.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -got@^8.0.3: - version "8.3.2" - resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" - integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== - dependencies: - "@sindresorhus/is" "^0.7.0" - cacheable-request "^2.1.1" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - into-stream "^3.1.0" - is-retry-allowed "^1.1.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - mimic-response "^1.0.0" - p-cancelable "^0.4.0" - p-timeout "^2.0.1" - pify "^3.0.0" - safe-buffer "^5.1.1" - timed-out "^4.0.1" - url-parse-lax "^3.0.0" - url-to-options "^1.0.1" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbol-support-x@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" - integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has-to-string-tag-x@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" - integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== - dependencies: - has-symbol-support-x "^1.4.1" - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-sum@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" - integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ= - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -he@1.2.x, he@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-entities@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" - integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== - -html-loader@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea" - integrity sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog== - dependencies: - es6-templates "^0.2.3" - fastparse "^1.1.1" - html-minifier "^3.5.8" - loader-utils "^1.1.0" - object-assign "^4.1.1" - -html-minifier@^3.2.3, html-minifier@^3.5.8: - version "3.5.21" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" - integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== - dependencies: - camel-case "3.0.x" - clean-css "4.2.x" - commander "2.17.x" - he "1.2.x" - param-case "2.1.x" - relateurl "0.2.x" - uglify-js "3.4.x" - -html-webpack-plugin@^2.24.1: - version "2.30.1" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz#7f9c421b7ea91ec460f56527d78df484ee7537d5" - integrity sha1-f5xCG36pHsRg9WUn1430hO51N9U= - dependencies: - bluebird "^3.4.7" - html-minifier "^3.2.3" - loader-utils "^0.2.16" - lodash "^4.17.3" - pretty-error "^2.0.2" - toposort "^1.0.0" - -htmlparser2@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -http-cache-semantics@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.5.1: - version "0.5.3" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" - integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== - -http-proxy-middleware@0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy@^1.17.0: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -iconv-lite@0.4.24, iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -icss-utils@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - dependencies: - postcss "^7.0.14" - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -image-size@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= - -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-fresh@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inquirer@^6.2.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -internal-ip@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" - integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== - dependencies: - default-gateway "^4.2.0" - ipaddr.js "^1.9.0" - -interpret@^1.0.0, interpret@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -into-stream@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" - integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= - dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" - -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.9.1, ipaddr.js@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arguments@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" - integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== - dependencies: - call-bind "^1.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-core-module@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" - integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== - -is-path-cwd@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-retry-allowed@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-what@^3.7.1: - version "3.12.0" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.12.0.tgz#f4405ce4bd6dd420d3ced51a026fb90e03705e55" - integrity sha512-2ilQz5/f/o9V7WRWJQmpFYNmQFZ9iM+OXRonZKcYgTkCzjb949Vi4h282PD1UfmgHk666rcWonbRJ++KI41VGw== - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isurl@^1.0.0-alpha5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" - integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== - dependencies: - has-to-string-tag-x "^1.2.0" - is-object "^1.0.1" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-loader@^0.5.4: - version "0.5.7" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" - integrity sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w== - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - -json3@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@^0.5.0, json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - -keyv@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" - integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== - dependencies: - json-buffer "3.0.0" - -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -less-loader@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.1.0.tgz#2c1352c5b09a4f84101490274fd51674de41363e" - integrity sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg== - dependencies: - clone "^2.1.1" - loader-utils "^1.1.0" - pify "^3.0.0" - -less@^3.9.0: - version "3.13.1" - resolved "https://registry.yarnpkg.com/less/-/less-3.13.1.tgz#0ebc91d2a0e9c0c6735b83d496b0ab0583077909" - integrity sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw== - dependencies: - copy-anything "^2.0.1" - tslib "^1.10.0" - optionalDependencies: - errno "^0.1.1" - graceful-fs "^4.1.2" - image-size "~0.5.0" - make-dir "^2.1.0" - mime "^1.4.1" - native-request "^1.0.5" - source-map "~0.6.0" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -loader-fs-cache@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz#f08657646d607078be2f0a032f8bd69dd6f277d9" - integrity sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA== - dependencies: - find-cache-dir "^0.1.1" - mkdirp "^0.5.1" - -loader-runner@^2.3.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@^0.2.16: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -loglevel@^1.6.8: - version "1.7.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" - integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" - integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= - -lowercase-keys@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" - integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= - -lowercase-keys@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lru-cache@^4.1.2: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.45.0, "mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - -mime-types@~2.1.17, mime-types@~2.1.24: - version "2.1.28" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== - dependencies: - mime-db "1.45.0" - -mime@1.6.0, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.0.3, mime@^2.4.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" - integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - -nan@^2.12.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -native-request@^1.0.5: - version "1.0.8" - resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb" - integrity sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -neo-async@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -no-case@^2.2.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" - integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== - dependencies: - lower-case "^1.1.1" - -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - -node-libs-browser@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-releases@^1.1.69: - version "1.1.70" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" - integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-url@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" - integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== - dependencies: - prepend-http "^2.0.0" - query-string "^5.0.1" - sort-keys "^2.0.0" - -normalize-wheel@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" - integrity sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU= - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -nth-check@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-hash@^1.1.4: - version "1.3.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" - integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== - -object-is@^1.0.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" - integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - -optionator@^0.8.2: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-cancelable@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" - integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-is-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" - integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - -p-timeout@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" - integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== - dependencies: - p-finally "^1.0.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -param-case@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" - integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= - dependencies: - no-case "^2.2.0" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" - integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= - dependencies: - find-up "^1.0.0" - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -portfinder@^1.0.26: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-load-config@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== - dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" - -postcss-loader@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" - integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== - dependencies: - loader-utils "^1.1.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - schema-utils "^1.0.0" - -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" - integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - postcss-value-parser "^3.3.1" - -postcss-modules-scope@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" - integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^7.0.6" - -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - util-deprecate "^1.0.2" - -postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier@^1.18.2: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== - -pretty-error@^2.0.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" - integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw== - dependencies: - lodash "^4.17.20" - renderkid "^2.0.4" - -private@^0.1.6, private@^0.1.8, private@~0.1.5: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -query-string@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" - integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== - dependencies: - decode-uri-component "^0.2.0" - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -recast@~0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - -regenerate@^1.2.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" - integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.1.6" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp.prototype.flags@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= - dependencies: - jsesc "~0.5.0" - -relateurl@0.2.x: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -renderkid@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.5.tgz#483b1ac59c6601ab30a7a596a5965cabccfdd0a5" - integrity sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ== - dependencies: - css-select "^2.0.2" - dom-converter "^0.2" - htmlparser2 "^3.10.1" - lodash "^4.17.20" - strip-ansi "^3.0.0" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -resize-observer-polyfill@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.10.0, resolve@^1.12.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== - dependencies: - is-core-module "^2.1.0" - path-parse "^1.0.6" - -responselike@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^2.6.1, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-async@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -rxjs@^6.4.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -selfsigned@^1.10.8: - version "1.10.8" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" - integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== - dependencies: - node-forge "^0.10.0" - -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sockjs-client@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.0.tgz#2f8ff5d4b659e0d092f7aba0b7c386bd2aa20add" - integrity sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q== - dependencies: - debug "^3.2.6" - eventsource "^1.0.7" - faye-websocket "^0.11.3" - inherits "^2.0.4" - json3 "^3.3.3" - url-parse "^1.4.7" - -sockjs@^0.3.21: - version "0.3.21" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" - integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== - dependencies: - faye-websocket "^0.11.3" - uuid "^3.4.0" - websocket-driver "^0.7.4" - -sort-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" - integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= - dependencies: - is-plain-obj "^1.0.0" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-json-comments@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -style-loader@^0.23.1: - version "0.23.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" - integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== - dependencies: - loader-utils "^1.1.0" - schema-utils "^1.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.1.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -tapable@^0.2.7, tapable@~0.2.5: - version "0.2.9" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.9.tgz#af2d8bbc9b04f74ee17af2b4d9048f807acd18a8" - integrity sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A== - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -throttle-debounce@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz#51853da37be68a155cb6e827b3514a3c422e89cd" - integrity sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg== - -through@^2.3.6, through@~2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -timed-out@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - -timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -toposort@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" - integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -tslib@^1.10.0, tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -uglify-js@3.4.x: - version "3.4.10" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" - integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== - dependencies: - commander "~2.19.0" - source-map "~0.6.1" - -uglify-js@^2.8.27: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -upper-case@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" - integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-loader@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" - integrity sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg== - dependencies: - loader-utils "^1.1.0" - mime "^2.0.3" - schema-utils "^1.0.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-parse@^1.4.3, url-parse@^1.4.7: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url-to-options@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" - integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2, uuid@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -v8-compile-cache@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" - integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -vue-hot-reload-api@^2.3.0: - version "2.3.4" - resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" - integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== - -vue-loader@^15.6.2: - version "15.9.6" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b" - integrity sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg== - dependencies: - "@vue/component-compiler-utils" "^3.1.0" - hash-sum "^1.0.2" - loader-utils "^1.1.0" - vue-hot-reload-api "^2.3.0" - vue-style-loader "^4.1.0" - -vue-resource@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.5.1.tgz#0f3d685e3254d21800bebd966edcf56c34b3b6e4" - integrity sha512-o6V4wNgeqP+9v9b2bPXrr20CGNQPEXjpbUWdZWq9GJhqVeAGcYoeTtn/D4q059ZiyN0DIrDv/ADrQUmlUQcsmg== - dependencies: - got "^8.0.3" - -vue-router@^3.0.2: - version "3.4.9" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.9.tgz#c016f42030ae2932f14e4748b39a1d9a0e250e66" - integrity sha512-CGAKWN44RqXW06oC+u4mPgHLQQi2t6vLD/JbGRDAXm0YpMv0bgpKuU5bBd7AvMgfTz9kXVRIWKHqRwGEb8xFkA== - -vue-style-loader@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8" - integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ== - dependencies: - hash-sum "^1.0.2" - loader-utils "^1.0.2" - -vue-template-compiler@^2.5.22: - version "2.6.12" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" - integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg== - dependencies: - de-indent "^1.0.2" - he "^1.1.0" - -vue-template-es2015-compiler@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" - integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== - -vue@^2.5.22: - version "2.6.12" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123" - integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg== - -watchpack-chokidar2@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" - integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.3.1: - version "1.7.5" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" - integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.1" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -webpack-cli@^3.2.1: - version "3.3.12" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" - integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== - dependencies: - chalk "^2.4.2" - cross-spawn "^6.0.5" - enhanced-resolve "^4.1.1" - findup-sync "^3.0.0" - global-modules "^2.0.0" - import-local "^2.0.0" - interpret "^1.4.0" - loader-utils "^1.4.0" - supports-color "^6.1.0" - v8-compile-cache "^2.1.1" - yargs "^13.3.2" - -webpack-dev-middleware@^3.7.2: - version "3.7.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" - integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.4" - mkdirp "^0.5.1" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-server@^3.1.14: - version "3.11.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz#695ebced76a4929f0d5de7fd73fafe185fe33708" - integrity sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.1.8" - compression "^1.7.4" - connect-history-api-fallback "^1.6.0" - debug "^4.1.1" - del "^4.1.1" - express "^4.17.1" - html-entities "^1.3.1" - http-proxy-middleware "0.19.1" - import-local "^2.0.0" - internal-ip "^4.3.0" - ip "^1.1.5" - is-absolute-url "^3.0.3" - killable "^1.0.1" - loglevel "^1.6.8" - opn "^5.5.0" - p-retry "^3.0.1" - portfinder "^1.0.26" - schema-utils "^1.0.0" - selfsigned "^1.10.8" - semver "^6.3.0" - serve-index "^1.9.1" - sockjs "^0.3.21" - sockjs-client "^1.5.0" - spdy "^4.0.2" - strip-ansi "^3.0.1" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.7.2" - webpack-log "^2.0.0" - ws "^6.2.1" - yargs "^13.3.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-sources@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.7.0.tgz#b2a1226804373ffd3d03ea9c6bd525067034f6b1" - integrity sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg== - dependencies: - acorn "^5.0.0" - acorn-dynamic-import "^2.0.0" - ajv "^4.7.0" - ajv-keywords "^1.1.1" - async "^2.1.2" - enhanced-resolve "^3.3.0" - interpret "^1.0.0" - json-loader "^0.5.4" - json5 "^0.5.1" - loader-runner "^2.3.0" - loader-utils "^0.2.16" - memory-fs "~0.4.1" - mkdirp "~0.5.0" - node-libs-browser "^2.0.0" - source-map "^0.5.3" - supports-color "^3.1.0" - tapable "~0.2.5" - uglify-js "^2.8.27" - watchpack "^1.3.1" - webpack-sources "^1.0.1" - yargs "^6.0.0" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-fetch@^3.0.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz#605a2cd0a7146e5db141e29d1c62ab84c0c4c868" - integrity sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A== - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.14, which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - -ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" - integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" - integrity sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw= - dependencies: - camelcase "^3.0.0" - -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^6.0.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" - integrity sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^4.2.0" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" diff --git a/web/frps/.babelrc b/web/frps/.babelrc deleted file mode 100644 index b5510156ea9..00000000000 --- a/web/frps/.babelrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "presets": [ - ["es2015", { "modules": false }] - ], - "plugins": [ - [ - "component", - { - "libraryName": "element-ui", - "styleLibraryName": "theme-chalk" - } - ] - ] -} diff --git a/web/frps/.gitignore b/web/frps/.gitignore index 3cd34b42d53..38adffa64e8 100644 --- a/web/frps/.gitignore +++ b/web/frps/.gitignore @@ -1,6 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules .DS_Store -node_modules/ -dist/ -npm-debug.log +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json .idea -.vscode/settings.json +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/frps/.prettierrc.json b/web/frps/.prettierrc.json new file mode 100644 index 00000000000..f8dec5b1897 --- /dev/null +++ b/web/frps/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/web/frps/Makefile b/web/frps/Makefile index ceffed7d9e3..adf015e50a2 100644 --- a/web/frps/Makefile +++ b/web/frps/Makefile @@ -1,7 +1,16 @@ -.PHONY: dist build +.PHONY: dist install build preview lint -build: +install: + @cd .. && npm install + +build: install @npm run build -dev: install +dev: @npm run dev + +preview: + @npm run preview + +lint: + @npm run lint diff --git a/web/frps/README.md b/web/frps/README.md new file mode 100644 index 00000000000..a2e08e66d98 --- /dev/null +++ b/web/frps/README.md @@ -0,0 +1,25 @@ +# frps-dashboard + +## Project Setup + +```sh +yarn install +``` + +### Compile and Hot-Reload for Development + +```sh +make dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +make build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +make lint +``` diff --git a/web/frps/auto-imports.d.ts b/web/frps/auto-imports.d.ts new file mode 100644 index 00000000000..9d2400790b5 --- /dev/null +++ b/web/frps/auto-imports.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @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 new file mode 100644 index 00000000000..e177245b556 --- /dev/null +++ b/web/frps/components.d.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +// @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'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ProxyCard: typeof import('./src/components/ProxyCard.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + StatCard: typeof import('./src/components/StatCard.vue')['default'] + Traffic: typeof import('./src/components/Traffic.vue')['default'] + } + export interface GlobalDirectives { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/web/frps/embed.go b/web/frps/embed.go new file mode 100644 index 00000000000..a1264d7ca9a --- /dev/null +++ b/web/frps/embed.go @@ -0,0 +1,16 @@ +//go:build !noweb + +package frps + +import ( + "embed" + + "github.com/fatedier/frp/assets" +) + +//go:embed dist +var EmbedFS embed.FS + +func init() { + assets.Register(EmbedFS) +} diff --git a/web/frps/embed_stub.go b/web/frps/embed_stub.go new file mode 100644 index 00000000000..35c9d9c8196 --- /dev/null +++ b/web/frps/embed_stub.go @@ -0,0 +1,3 @@ +//go:build noweb + +package frps diff --git a/web/frps/eslint.config.js b/web/frps/eslint.config.js new file mode 100644 index 00000000000..e13d4e48b42 --- /dev/null +++ b/web/frps/eslint.config.js @@ -0,0 +1,36 @@ +import pluginVue from 'eslint-plugin-vue' +import vueTsEslintConfig from '@vue/eslint-config-typescript' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default [ + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + { + name: 'app/files-to-ignore', + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], + }, + ...pluginVue.configs['flat/essential'], + ...vueTsEslintConfig(), + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'vue/multi-word-component-names': [ + 'error', + { + ignores: ['Traffic', 'Proxies', 'Clients'], + }, + ], + }, + }, + skipFormatting, +] diff --git a/web/frps/index.html b/web/frps/index.html new file mode 100644 index 00000000000..c43e45b44e9 --- /dev/null +++ b/web/frps/index.html @@ -0,0 +1,14 @@ + + + + + + frp server + + + +
+ + + + diff --git a/web/frps/package.json b/web/frps/package.json index 1c5afaf4fe9..8cc42d50e71 100644 --- a/web/frps/package.json +++ b/web/frps/package.json @@ -1,48 +1,41 @@ { "name": "frps-dashboard", - "description": "A dashboard for frp server.", - "author": "fatedier", + "version": "0.0.1", "private": true, + "type": "module", "scripts": { - "dev": "webpack-dev-server -d --inline --hot --env.dev", - "build": "rimraf dist && webpack -p --progress --hide-modules" + "dev": "vite", + "build": "run-p type-check build-only", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit", + "lint": "eslint . --fix", + "lint:check": "eslint ." }, "dependencies": { - "bootstrap": "^3.3.7", - "echarts": "^3.5.0", - "element-ui": "^2.3.8", - "humanize-plus": "^1.8.2", - "vue": "^2.5.16", - "vue-resource": "^1.2.1", - "vue-router": "^2.3.0", - "whatwg-fetch": "^2.0.3" - }, - "engines": { - "node": ">=6" + "element-plus": "^2.14.3", + "vue": "^3.5.40", + "vue-router": "^5.2.0" }, "devDependencies": { - "autoprefixer": "^6.6.0", - "babel-core": "^6.21.0", - "babel-eslint": "^7.1.1", - "babel-loader": "^6.4.0", - "babel-plugin-component": "^1.1.1", - "babel-preset-es2015": "^6.13.2", - "css-loader": "^0.27.0", - "eslint": "^3.12.2", - "eslint-config-enough": "^0.2.2", - "eslint-loader": "^1.6.3", - "file-loader": "^0.10.1", - "html-loader": "^0.4.5", - "html-webpack-plugin": "^2.24.1", - "less": "^3.0.4", - "less-loader": "^4.1.0", - "postcss-loader": "^1.3.3", - "rimraf": "^2.5.4", - "style-loader": "^0.13.2", - "url-loader": "^1.0.1", - "vue-loader": "^15.0.10", - "vue-template-compiler": "^2.1.8", - "webpack": "^2.2.0-rc.4", - "webpack-dev-server": "^3.1.4" + "@types/node": "24", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.8.1", + "@vueuse/core": "^14.3.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", + "npm-run-all": "^4.1.5", + "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.3.8" } } diff --git a/web/frps/postcss.config.js b/web/frps/postcss.config.js deleted file mode 100644 index af65640308a..00000000000 --- a/web/frps/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: [ - require('autoprefixer')() - ] -} \ No newline at end of file diff --git a/assets/frps/static/favicon.ico b/web/frps/public/favicon.ico similarity index 100% rename from assets/frps/static/favicon.ico rename to web/frps/public/favicon.ico diff --git a/web/frps/src/App.vue b/web/frps/src/App.vue index 79a7981aaca..11b2790af1e 100644 --- a/web/frps/src/App.vue +++ b/web/frps/src/App.vue @@ -1,81 +1,519 @@ - diff --git a/web/frps/src/api/client.ts b/web/frps/src/api/client.ts new file mode 100644 index 00000000000..7a3982b7ba3 --- /dev/null +++ b/web/frps/src/api/client.ts @@ -0,0 +1,30 @@ +import { buildQueryString, http } from './http' +import type { V2Page } from './http' +import type { ClientInfoData, ClientListV2Params } from '../types/client' + +export const getClients = () => { + return http.get('../api/clients') +} + +export const getClientsV2 = (params: ClientListV2Params = {}) => { + return http.getV2>( + `../api/v2/clients${buildQueryString({ + page: params.page, + pageSize: params.pageSize, + status: + params.status && params.status !== 'all' ? params.status : undefined, + q: params.q || undefined, + user: params.user, + clientID: params.clientID || undefined, + runID: params.runID || undefined, + })}`, + ) +} + +export const getClient = (key: string) => { + return http.get(`../api/clients/${key}`) +} + +export const getClientV2 = (key: string) => { + return http.getV2(`../api/v2/clients/${encodeURIComponent(key)}`) +} diff --git a/web/frps/src/api/http.ts b/web/frps/src/api/http.ts new file mode 100644 index 00000000000..2f59c1b7360 --- /dev/null +++ b/web/frps/src/api/http.ts @@ -0,0 +1,124 @@ +// http.ts - Base HTTP client + +class HTTPError extends Error { + status: number + statusText: string + + constructor(status: number, statusText: string, message?: string) { + super(message || statusText) + this.status = status + this.statusText = statusText + } +} + +export interface V2Envelope { + code: number + msg: string + data: T +} + +export interface V2Page { + total: number + page: number + pageSize: number + items: T[] +} + +type QueryParamValue = string | number | boolean | null | undefined + +async function request(url: string, options: RequestInit = {}): Promise { + const defaultOptions: RequestInit = { + credentials: 'include', + } + + const response = await fetch(url, { ...defaultOptions, ...options }) + + if (!response.ok) { + throw new HTTPError( + response.status, + response.statusText, + `HTTP ${response.status}`, + ) + } + + // Handle empty response (e.g. 204 No Content) + if (response.status === 204) { + return {} as T + } + + return response.json() +} + +async function requestV2( + url: string, + options: RequestInit = {}, +): Promise { + const defaultOptions: RequestInit = { + credentials: 'include', + } + + const response = await fetch(url, { ...defaultOptions, ...options }) + const envelope = (await response.json().catch(() => null)) as + | V2Envelope + | null + + if (!response.ok) { + throw new HTTPError( + response.status, + response.statusText, + envelope?.msg || `HTTP ${response.status}`, + ) + } + + if (!envelope || typeof envelope.code !== 'number') { + throw new Error('Invalid API v2 response') + } + + if (envelope.code >= 400) { + throw new HTTPError(envelope.code, envelope.msg, envelope.msg) + } + + return envelope.data +} + +export const buildQueryString = ( + params: Record, +): string => { + const query = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (value === null || value === undefined) continue + query.append(key, String(value)) + } + const text = query.toString() + return text ? `?${text}` : '' +} + +export const http = { + get: (url: string, options?: RequestInit) => + request(url, { ...options, method: 'GET' }), + getV2: (url: string, options?: RequestInit) => + requestV2(url, { ...options, method: 'GET' }), + postV2: (url: string, body?: any, options?: RequestInit) => + requestV2(url, { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(body), + }), + post: (url: string, body?: any, options?: RequestInit) => + request(url, { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(body), + }), + put: (url: string, body?: any, options?: RequestInit) => + request(url, { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(body), + }), + delete: (url: string, options?: RequestInit) => + request(url, { ...options, method: 'DELETE' }), +} diff --git a/web/frps/src/api/proxy.ts b/web/frps/src/api/proxy.ts new file mode 100644 index 00000000000..de07cac790a --- /dev/null +++ b/web/frps/src/api/proxy.ts @@ -0,0 +1,118 @@ +import { buildQueryString, http } from './http' +import { formatUnixSeconds } from '../utils/format' +import type { V2Page } from './http' +import type { + GetProxyResponse, + ProxyListV2Params, + ProxyStatsInfo, + ProxyV2Info, + ProxyV2Spec, + ProxyV2SpecBlocks, + ProxyV2Type, + TrafficResponse, +} from '../types/proxy' + +export interface SystemPruneResponse { + type: 'offline_proxies' + cleared: number + total: number +} + +export const getProxiesByType = (type: string) => { + return http.get(`../api/proxy/${type}`) +} + +export const getProxiesV2 = async (params: ProxyListV2Params = {}) => { + const page = await http.getV2>( + `../api/v2/proxies${buildQueryString({ + page: params.page, + pageSize: params.pageSize, + status: + params.status && params.status !== 'all' ? params.status : undefined, + q: params.q || undefined, + type: params.type || undefined, + user: params.user, + clientID: params.clientID || undefined, + })}`, + ) + + return { + ...page, + items: page.items.map(toLegacyProxyStats), + } +} + +const getActiveProxySpec = ( + spec: ProxyV2Spec, +): ProxyV2SpecBlocks[ProxyV2Type] => { + switch (spec.type) { + case 'tcp': + return spec.tcp + case 'udp': + return spec.udp + case 'http': + return spec.http + case 'https': + return spec.https + case 'tcpmux': + return spec.tcpmux + case 'stcp': + return spec.stcp + case 'sudp': + return spec.sudp + case 'xtcp': + return spec.xtcp + default: + return assertNever(spec) + } +} + +const assertNever = (value: never): never => { + throw new Error(`Unsupported proxy spec: ${JSON.stringify(value)}`) +} + +export const toLegacyProxyStats = (proxy: ProxyV2Info): ProxyStatsInfo => { + const type = proxy.spec.type + const activeSpec = getActiveProxySpec(proxy.spec) + + return { + name: proxy.name, + type, + conf: proxy.status.phase === 'offline' ? null : activeSpec, + user: proxy.user, + clientID: proxy.clientID, + todayTrafficIn: proxy.status.todayTrafficIn, + todayTrafficOut: proxy.status.todayTrafficOut, + curConns: proxy.status.curConns, + lastStartTime: formatUnixSeconds(proxy.status.lastStartAt), + lastCloseTime: formatUnixSeconds(proxy.status.lastCloseAt), + status: proxy.status.phase, + } +} + +export const getProxy = (type: string, name: string) => { + return http.get(`../api/proxy/${type}/${name}`) +} + +export const getProxyByNameV2 = async (name: string) => { + const proxy = await http.getV2( + `../api/v2/proxies/${encodeURIComponent(name)}`, + ) + return toLegacyProxyStats(proxy) +} + +export const getProxyByName = (name: string) => { + return http.get(`../api/proxies/${name}`) +} + +export const getProxyTraffic = (name: string) => { + return http.getV2( + `../api/v2/proxies/${encodeURIComponent(name)}/traffic`, + ) +} + +export const clearOfflineProxies = () => { + return http.postV2( + '../api/v2/system/prune?type=offline_proxies', + ) +} diff --git a/web/frps/src/api/server.ts b/web/frps/src/api/server.ts new file mode 100644 index 00000000000..f6621ca84cb --- /dev/null +++ b/web/frps/src/api/server.ts @@ -0,0 +1,6 @@ +import { http } from './http' +import type { ServerInfo } from '../types/server' + +export const getServerInfo = () => { + return http.getV2('../api/v2/system/info') +} diff --git a/web/frps/src/assets/css/dark.css b/web/frps/src/assets/css/dark.css new file mode 100644 index 00000000000..bb08805c5b8 --- /dev/null +++ b/web/frps/src/assets/css/dark.css @@ -0,0 +1,212 @@ +/* Dark mode styles */ +html.dark { + --el-bg-color: #1e1e2e; + --el-bg-color-page: #181825; + --el-bg-color-overlay: #27293d; + --el-fill-color-blank: #1e1e2e; + --el-border-color: #3a3d5c; + --el-border-color-light: #313348; + --el-border-color-lighter: #2a2a3e; + --el-text-color-primary: #e5e7eb; + --el-text-color-secondary: #888888; + --el-text-color-placeholder: #afafaf; + background-color: #1e1e2e; + color-scheme: dark; +} + +/* Scrollbar */ +html.dark ::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +html.dark ::-webkit-scrollbar-track { + background: #27293d; +} + +html.dark ::-webkit-scrollbar-thumb { + background: #3a3d5c; + border-radius: 3px; +} + +html.dark ::-webkit-scrollbar-thumb:hover { + background: #4a4d6c; +} + +/* Form */ +html.dark .el-form-item__label { + color: #e5e7eb; +} + +/* Input */ +html.dark .el-input__wrapper { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #3a3d5c inset; +} + +html.dark .el-input__wrapper:hover { + box-shadow: 0 0 0 1px #4a4d6c inset; +} + +html.dark .el-input__wrapper.is-focus { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; +} + +html.dark .el-input__inner { + color: #e5e7eb; +} + +html.dark .el-input__inner::placeholder { + color: #afafaf; +} + +html.dark .el-textarea__inner { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #3a3d5c inset; + color: #e5e7eb; +} + +html.dark .el-textarea__inner:hover { + box-shadow: 0 0 0 1px #4a4d6c inset; +} + +html.dark .el-textarea__inner:focus { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; +} + +/* Select */ +html.dark .el-select__wrapper { + background: var(--color-bg-input); + box-shadow: 0 0 0 1px #3a3d5c inset; +} + +html.dark .el-select__wrapper:hover { + box-shadow: 0 0 0 1px #4a4d6c inset; +} + +html.dark .el-select__selected-item { + color: #e5e7eb; +} + +html.dark .el-select__placeholder { + color: #afafaf; +} + +html.dark .el-select-dropdown { + background: #27293d; + border-color: #3a3d5c; +} + +html.dark .el-select-dropdown__item { + color: #e5e7eb; +} + +html.dark .el-select-dropdown__item:hover { + background: #2a2a3e; +} + +html.dark .el-select-dropdown__item.is-selected { + color: var(--el-color-primary); +} + +html.dark .el-select-dropdown__item.is-disabled { + color: #666666; +} + +/* Tag */ +html.dark .el-tag--info { + background: #27293d; + border-color: #3a3d5c; + color: #b0b0b0; +} + +/* Button */ +html.dark .el-button--default { + background: #27293d; + border-color: #3a3d5c; + color: #e5e7eb; +} + +html.dark .el-button--default:hover { + background: #2a2a3e; + border-color: #4a4d6c; + color: #e5e7eb; +} + +/* Card */ +html.dark .el-card { + background: #1e1e2e; + border-color: #3a3d5c; + color: #b0b0b0; +} + +html.dark .el-card__header { + border-bottom-color: #3a3d5c; + color: #e5e7eb; +} + +/* Table */ +html.dark .el-table { + background-color: #1e1e2e; + color: #e5e7eb; +} + +html.dark .el-table th { + background-color: #1e1e2e; + color: #e5e7eb; +} + +html.dark .el-table tr { + background-color: #1e1e2e; +} + +html.dark .el-table--striped .el-table__body tr.el-table__row--striped td { + background-color: #181825; +} + +/* Dialog */ +html.dark .el-dialog { + background: #1e1e2e; +} + +html.dark .el-dialog__title { + color: #e5e7eb; +} + +/* Message */ +html.dark .el-message { + background: #27293d; + border-color: #3a3d5c; +} + +html.dark .el-message--success { + background: #1e3d2e; + border-color: #3d6b4f; +} + +html.dark .el-message--warning { + background: #3d3020; + border-color: #6b5020; +} + +html.dark .el-message--error { + background: #3d2027; + border-color: #5c2d2d; +} + +/* Loading */ +html.dark .el-loading-mask { + background-color: rgba(30, 30, 46, 0.9); +} + +/* Overlay */ +html.dark .el-overlay { + background-color: rgba(0, 0, 0, 0.6); +} + +/* Tooltip */ +html.dark .el-tooltip__popper { + background: #27293d !important; + border-color: #3a3d5c !important; + color: #e5e7eb !important; +} diff --git a/web/frps/src/assets/css/var.css b/web/frps/src/assets/css/var.css new file mode 100644 index 00000000000..170fccd7996 --- /dev/null +++ b/web/frps/src/assets/css/var.css @@ -0,0 +1,109 @@ +:root { + /* Text colors */ + --color-text-primary: #303133; + --color-text-secondary: #606266; + --color-text-muted: #909399; + --color-text-light: #c0c4cc; + --color-text-placeholder: #a8abb2; + + /* Background colors */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f9f9f9; + --color-bg-tertiary: #fafafa; + --color-bg-surface: #ffffff; + --color-bg-muted: #f4f4f5; + --color-bg-input: #ffffff; + --color-bg-hover: #efefef; + --color-bg-active: #eaeaea; + + /* Border colors */ + --color-border: #dcdfe6; + --color-border-light: #e4e7ed; + --color-border-lighter: #ebeef5; + --color-border-extra-light: #f2f6fc; + + /* Status colors */ + --color-primary: #409eff; + --color-primary-light: #ecf5ff; + --color-success: #67c23a; + --color-warning: #e6a23c; + --color-danger: #f56c6c; + --color-danger-dark: #c45656; + --color-danger-light: #fef0f0; + --color-info: #909399; + + /* Element Plus mapping */ + --el-color-primary: var(--color-primary); + --el-color-success: var(--color-success); + --el-color-warning: var(--color-warning); + --el-color-danger: var(--color-danger); + --el-color-info: var(--color-info); + + --el-text-color-primary: var(--color-text-primary); + --el-text-color-regular: var(--color-text-secondary); + --el-text-color-secondary: var(--color-text-muted); + --el-text-color-placeholder: var(--color-text-placeholder); + + --el-bg-color: var(--color-bg-primary); + --el-bg-color-page: var(--color-bg-secondary); + --el-bg-color-overlay: var(--color-bg-primary); + + --el-border-color: var(--color-border); + --el-border-color-light: var(--color-border-light); + --el-border-color-lighter: var(--color-border-lighter); + --el-border-color-extra-light: var(--color-border-extra-light); + + --el-fill-color-blank: var(--color-bg-primary); + --el-fill-color-light: var(--color-bg-tertiary); + --el-fill-color: var(--color-bg-tertiary); + --el-fill-color-dark: var(--color-bg-hover); + --el-fill-color-darker: var(--color-bg-active); + + /* Input */ + --el-input-bg-color: var(--color-bg-input); + --el-input-border-color: var(--color-border); + --el-input-hover-border-color: var(--color-border-light); + + /* Dialog */ + --el-dialog-bg-color: var(--color-bg-primary); + --el-overlay-color: rgba(0, 0, 0, 0.5); +} + +html.dark { + /* Text colors */ + --color-text-primary: #e5e7eb; + --color-text-secondary: #b0b0b0; + --color-text-muted: #888888; + --color-text-light: #666666; + --color-text-placeholder: #afafaf; + + /* Background colors */ + --color-bg-primary: #1e1e2e; + --color-bg-secondary: #181825; + --color-bg-tertiary: #27293d; + --color-bg-surface: #27293d; + --color-bg-muted: #27293d; + --color-bg-input: #27293d; + --color-bg-hover: #2a2a3e; + --color-bg-active: #353550; + + /* Border colors */ + --color-border: #3a3d5c; + --color-border-light: #313348; + --color-border-lighter: #2a2a3e; + --color-border-extra-light: #222233; + + /* Status colors */ + --color-primary: #409eff; + --color-danger: #f87171; + --color-danger-dark: #f87171; + --color-danger-light: #3d2027; + --color-info: #888888; + + /* Dark overrides */ + --el-text-color-regular: var(--color-text-primary); + --el-overlay-color: rgba(0, 0, 0, 0.7); + + background-color: #181825; + color-scheme: dark; +} diff --git a/web/frps/src/assets/favicon.ico b/web/frps/src/assets/favicon.ico deleted file mode 100644 index 4347765571a..00000000000 Binary files a/web/frps/src/assets/favicon.ico and /dev/null differ diff --git a/web/frps/src/assets/icons/github.svg b/web/frps/src/assets/icons/github.svg new file mode 100644 index 00000000000..160a7939320 --- /dev/null +++ b/web/frps/src/assets/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/frps/src/assets/icons/logo.svg b/web/frps/src/assets/icons/logo.svg new file mode 100644 index 00000000000..fee4a82a394 --- /dev/null +++ b/web/frps/src/assets/icons/logo.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/web/frps/src/components/ClientCard.vue b/web/frps/src/components/ClientCard.vue new file mode 100644 index 00000000000..4ccd7441512 --- /dev/null +++ b/web/frps/src/components/ClientCard.vue @@ -0,0 +1,264 @@ + + + + + diff --git a/web/frps/src/components/Overview.vue b/web/frps/src/components/Overview.vue deleted file mode 100644 index 1a98a6e3c89..00000000000 --- a/web/frps/src/components/Overview.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxiesHttp.vue b/web/frps/src/components/ProxiesHttp.vue deleted file mode 100644 index 74fcb38d1ca..00000000000 --- a/web/frps/src/components/ProxiesHttp.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxiesHttps.vue b/web/frps/src/components/ProxiesHttps.vue deleted file mode 100644 index 606a07eca2f..00000000000 --- a/web/frps/src/components/ProxiesHttps.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxiesStcp.vue b/web/frps/src/components/ProxiesStcp.vue deleted file mode 100644 index 98def112f6c..00000000000 --- a/web/frps/src/components/ProxiesStcp.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxiesSudp.vue b/web/frps/src/components/ProxiesSudp.vue deleted file mode 100644 index d1844d27600..00000000000 --- a/web/frps/src/components/ProxiesSudp.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxiesTcp.vue b/web/frps/src/components/ProxiesTcp.vue deleted file mode 100644 index 8d48944ec85..00000000000 --- a/web/frps/src/components/ProxiesTcp.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxiesUdp.vue b/web/frps/src/components/ProxiesUdp.vue deleted file mode 100644 index 1556cc47cb3..00000000000 --- a/web/frps/src/components/ProxiesUdp.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - - diff --git a/web/frps/src/components/ProxyCard.vue b/web/frps/src/components/ProxyCard.vue new file mode 100644 index 00000000000..850ebb18950 --- /dev/null +++ b/web/frps/src/components/ProxyCard.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/web/frps/src/components/StatCard.vue b/web/frps/src/components/StatCard.vue new file mode 100644 index 00000000000..c62c1235d32 --- /dev/null +++ b/web/frps/src/components/StatCard.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/web/frps/src/components/Traffic.vue b/web/frps/src/components/Traffic.vue index 7e2928e21ad..9f4d422c307 100644 --- a/web/frps/src/components/Traffic.vue +++ b/web/frps/src/components/Traffic.vue @@ -1,36 +1,259 @@ - - diff --git a/web/frps/src/composables/useResponsive.ts b/web/frps/src/composables/useResponsive.ts new file mode 100644 index 00000000000..f6cf177439d --- /dev/null +++ b/web/frps/src/composables/useResponsive.ts @@ -0,0 +1,8 @@ +import { useBreakpoints } from '@vueuse/core' + +const breakpoints = useBreakpoints({ mobile: 0, desktop: 768 }) + +export function useResponsive() { + const isMobile = breakpoints.smaller('desktop') // < 768px + return { isMobile } +} diff --git a/web/frps/src/env.d.ts b/web/frps/src/env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/web/frps/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/frps/src/index.html b/web/frps/src/index.html deleted file mode 100644 index aebd655a179..00000000000 --- a/web/frps/src/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - frps dashboard - - - -
- - - - - diff --git a/web/frps/src/main.js b/web/frps/src/main.js deleted file mode 100644 index d713c13f66b..00000000000 --- a/web/frps/src/main.js +++ /dev/null @@ -1,48 +0,0 @@ -import Vue from 'vue' -//import ElementUI from 'element-ui' -import { - Button, - Form, - FormItem, - Row, - Col, - Table, - TableColumn, - Popover, - Menu, - Submenu, - MenuItem, - Tag -} from 'element-ui' -import lang from 'element-ui/lib/locale/lang/en' -import locale from 'element-ui/lib/locale' -import 'element-ui/lib/theme-chalk/index.css' -import './utils/less/custom.less' - -import App from './App.vue' -import router from './router' -import 'whatwg-fetch' - -locale.use(lang) - -Vue.use(Button) -Vue.use(Form) -Vue.use(FormItem) -Vue.use(Row) -Vue.use(Col) -Vue.use(Table) -Vue.use(TableColumn) -Vue.use(Popover) -Vue.use(Menu) -Vue.use(Submenu) -Vue.use(MenuItem) -Vue.use(Tag) - -Vue.config.productionTip = false - -new Vue({ - el: '#app', - router, - template: '', - components: { App } -}) diff --git a/web/frps/src/main.ts b/web/frps/src/main.ts new file mode 100644 index 00000000000..53441a4e3b2 --- /dev/null +++ b/web/frps/src/main.ts @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import 'element-plus/theme-chalk/dark/css-vars.css' +import App from './App.vue' +import router from './router' + +import './assets/css/var.css' +import './assets/css/dark.css' + +const app = createApp(App) + +app.use(router) + +app.mount('#app') diff --git a/web/frps/src/router/index.js b/web/frps/src/router/index.js deleted file mode 100644 index 394be1db97b..00000000000 --- a/web/frps/src/router/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import Vue from 'vue' -import Router from 'vue-router' -import Overview from '../components/Overview.vue' -import ProxiesTcp from '../components/ProxiesTcp.vue' -import ProxiesUdp from '../components/ProxiesUdp.vue' -import ProxiesHttp from '../components/ProxiesHttp.vue' -import ProxiesHttps from '../components/ProxiesHttps.vue' -import ProxiesStcp from '../components/ProxiesStcp.vue' -import ProxiesSudp from '../components/ProxiesSudp.vue' - -Vue.use(Router) - -export default new Router({ - routes: [{ - path: '/', - name: 'Overview', - component: Overview - }, { - path: '/proxies/tcp', - name: 'ProxiesTcp', - component: ProxiesTcp - }, { - path: '/proxies/udp', - name: 'ProxiesUdp', - component: ProxiesUdp - }, { - path: '/proxies/http', - name: 'ProxiesHttp', - component: ProxiesHttp - }, { - path: '/proxies/https', - name: 'ProxiesHttps', - component: ProxiesHttps - }, { - path: '/proxies/stcp', - name: 'ProxiesStcp', - component: ProxiesStcp - }, { - path: '/proxies/sudp', - name: 'ProxiesSudp', - component: ProxiesSudp - }] -}) diff --git a/web/frps/src/router/index.ts b/web/frps/src/router/index.ts new file mode 100644 index 00000000000..7689e8868be --- /dev/null +++ b/web/frps/src/router/index.ts @@ -0,0 +1,42 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import ServerOverview from '../views/ServerOverview.vue' +import Clients from '../views/Clients.vue' +import ClientDetail from '../views/ClientDetail.vue' +import Proxies from '../views/Proxies.vue' +import ProxyDetail from '../views/ProxyDetail.vue' + +const router = createRouter({ + history: createWebHashHistory(), + scrollBehavior() { + return { top: 0 } + }, + routes: [ + { + path: '/', + name: 'ServerOverview', + component: ServerOverview, + }, + { + path: '/clients', + name: 'Clients', + component: Clients, + }, + { + path: '/clients/:key', + name: 'ClientDetail', + component: ClientDetail, + }, + { + path: '/proxies/:type?', + name: 'Proxies', + component: Proxies, + }, + { + path: '/proxy/:name', + name: 'ProxyDetail', + component: ProxyDetail, + }, + ], +}) + +export default router diff --git a/web/frps/src/svg.d.ts b/web/frps/src/svg.d.ts new file mode 100644 index 00000000000..2f7dabe59c1 --- /dev/null +++ b/web/frps/src/svg.d.ts @@ -0,0 +1,5 @@ +declare module '*.svg?component' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} diff --git a/web/frps/src/types/client.ts b/web/frps/src/types/client.ts new file mode 100644 index 00000000000..94d59098e4d --- /dev/null +++ b/web/frps/src/types/client.ts @@ -0,0 +1,31 @@ +export interface ClientInfoData { + key: string + user: string + clientID: string + runID: string + version?: string + wireProtocol?: string + hostname: string + clientIP?: string + firstConnectedAt: number + lastConnectedAt: number + disconnectedAt?: number + online: boolean + status?: ClientStatus +} + +export interface ClientStatus { + phase: 'online' | 'offline' + curConns: number + proxyCount: number +} + +export interface ClientListV2Params { + page?: number + pageSize?: number + status?: 'all' | 'online' | 'offline' + q?: string + user?: string + clientID?: string + runID?: string +} diff --git a/web/frps/src/types/proxy.ts b/web/frps/src/types/proxy.ts new file mode 100644 index 00000000000..27606ef55b4 --- /dev/null +++ b/web/frps/src/types/proxy.ts @@ -0,0 +1,129 @@ +export interface ProxyStatsInfo { + name: string + type?: string + conf: any + user: string + clientID: string + todayTrafficIn: number + todayTrafficOut: number + curConns: number + lastStartTime: string + lastCloseTime: string + status: string +} + +export interface GetProxyResponse { + proxies: ProxyStatsInfo[] +} + +export interface ProxyListV2Params { + page?: number + pageSize?: number + status?: 'all' | 'online' | 'offline' + q?: string + type?: string + user?: string + clientID?: string +} + +export interface ProxyV2Info { + name: string + user: string + clientID: string + spec: ProxyV2Spec + status: ProxyV2Status +} + +export interface ProxyV2BaseSpec { + annotations?: Record + metadatas?: Record + transport?: { + useEncryption: boolean + useCompression: boolean + bandwidthLimit: string + bandwidthLimitMode: string + } + loadBalancer?: { + group: string + } +} + +export interface ProxyV2TCPBlock extends ProxyV2BaseSpec { + remotePort?: number +} + +export interface ProxyV2UDPBlock extends ProxyV2BaseSpec { + remotePort?: number +} + +export interface ProxyV2HTTPBlock extends ProxyV2BaseSpec { + customDomains?: string[] + subdomain?: string + locations?: string[] + hostHeaderRewrite?: string +} + +export interface ProxyV2HTTPSBlock extends ProxyV2BaseSpec { + customDomains?: string[] + subdomain?: string +} + +export interface ProxyV2TCPMuxBlock extends ProxyV2BaseSpec { + customDomains?: string[] + subdomain?: string + multiplexer?: string + routeByHTTPUser?: string +} + +export type ProxyV2STCPBlock = ProxyV2BaseSpec + +export type ProxyV2SUDPBlock = ProxyV2BaseSpec + +export type ProxyV2XTCPBlock = ProxyV2BaseSpec + +export interface ProxyV2SpecBlocks { + tcp: ProxyV2TCPBlock + udp: ProxyV2UDPBlock + http: ProxyV2HTTPBlock + https: ProxyV2HTTPSBlock + tcpmux: ProxyV2TCPMuxBlock + stcp: ProxyV2STCPBlock + sudp: ProxyV2SUDPBlock + xtcp: ProxyV2XTCPBlock +} + +export type ProxyV2Type = keyof ProxyV2SpecBlocks + +type ProxyV2SpecFor = { + type: T +} & { + [K in T]: ProxyV2SpecBlocks[K] +} & { + [K in Exclude]?: never +} + +export type ProxyV2Spec = { + [T in ProxyV2Type]: ProxyV2SpecFor +}[ProxyV2Type] + +export interface ProxyV2Status { + phase: 'online' | 'offline' + todayTrafficIn: number + todayTrafficOut: number + curConns: number + lastStartAt?: number + lastCloseAt?: number +} + +export interface TrafficResponse { + name: string + unit: 'bytes' + granularity: 'day' + history: TrafficPoint[] +} + +export interface TrafficPoint { + date: string + trafficIn: number + trafficOut: number +} diff --git a/web/frps/src/types/server.ts b/web/frps/src/types/server.ts new file mode 100644 index 00000000000..b4796e90879 --- /dev/null +++ b/web/frps/src/types/server.ts @@ -0,0 +1,28 @@ +export interface ServerInfo { + version: string + config: ServerInfoConfig + status: ServerInfoStatus +} + +export interface ServerInfoConfig { + bindPort: number + vhostHTTPPort: number + vhostHTTPSPort: number + tcpmuxHTTPConnectPort: number + kcpBindPort: number + quicBindPort: number + subdomainHost: string + maxPoolCount: number + maxPortsPerClient: number + heartbeatTimeout: number + allowPortsStr: string + tlsForce: boolean +} + +export interface ServerInfoStatus { + totalTrafficIn: number + totalTrafficOut: number + curConns: number + clientCounts: number + proxyTypeCount: Record +} diff --git a/web/frps/src/utils/chart.js b/web/frps/src/utils/chart.js deleted file mode 100644 index a8246c0ad93..00000000000 --- a/web/frps/src/utils/chart.js +++ /dev/null @@ -1,186 +0,0 @@ -import Humanize from "humanize-plus" -import echarts from "echarts/lib/echarts" - -import "echarts/theme/macarons" -import "echarts/lib/chart/bar" -import "echarts/lib/chart/pie" -import "echarts/lib/component/tooltip" -import "echarts/lib/component/title" - -function DrawTrafficChart(elementId, trafficIn, trafficOut) { - let myChart = echarts.init(document.getElementById(elementId), 'macarons'); - myChart.showLoading() - - let option = { - title: { - text: 'Network Traffic', - subtext: 'today', - x: 'center' - }, - tooltip: { - trigger: 'item', - formatter: function(v) { - return Humanize.fileSize(v.data.value) + " (" + v.percent + "%)" - } - }, - series: [{ - type: 'pie', - radius: '55%', - center: ['50%', '60%'], - data: [{ - value: trafficIn, - name: 'Traffic In' - }, { - value: trafficOut, - name: 'Traffic Out' - }, ], - itemStyle: { - emphasis: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - } - }] - }; - myChart.setOption(option); - myChart.hideLoading() -} - -function DrawProxyChart(elementId, serverInfo) { - let myChart = echarts.init(document.getElementById(elementId), 'macarons') - myChart.showLoading() - - let option = { - title: { - text: 'Proxies', - subtext: 'now', - x: 'center' - }, - tooltip: { - trigger: 'item', - formatter: function(v) { - return v.data.value - } - }, - series: [{ - type: 'pie', - radius: '55%', - center: ['50%', '60%'], - data: [], - itemStyle: { - emphasis: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - } - }] - }; - - if (serverInfo.proxy_type_count.tcp != null && serverInfo.proxy_type_count.tcp != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.tcp, name: 'TCP'}) - } - if (serverInfo.proxy_type_count.udp != null && serverInfo.proxy_type_count.udp != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.udp, name: 'UDP'}) - } - if (serverInfo.proxy_type_count.http != null && serverInfo.proxy_type_count.http != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.http, name: 'HTTP'}) - } - if (serverInfo.proxy_type_count.https != null && serverInfo.proxy_type_count.https != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.https, name: 'HTTPS'}) - } - if (serverInfo.proxy_type_count.stcp != null && serverInfo.proxy_type_count.stcp != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.stcp, name: 'STCP'}) - } - if (serverInfo.proxy_type_count.sudp != null && serverInfo.proxy_type_count.sudp != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.sudp, name: 'SUDP'}) - } - if (serverInfo.proxy_type_count.xtcp != null && serverInfo.proxy_type_count.xtcp != 0) { - option.series[0].data.push({value: serverInfo.proxy_type_count.xtcp, name: 'XTCP'}) - } - - myChart.setOption(option); - myChart.hideLoading() -} - -// 7 days -function DrawProxyTrafficChart(elementId, trafficInArr, trafficOutArr) { - let params = { - width: '600px', - height: '400px' - } - - let myChart = echarts.init(document.getElementById(elementId), 'macarons', params); - myChart.showLoading() - - trafficInArr = trafficInArr.reverse() - trafficOutArr = trafficOutArr.reverse() - let now = new Date() - now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6) - let dates = new Array() - for (let i = 0; i < 7; i++) { - dates.push(now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate()) - now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) - } - - let option = { - tooltip: { - trigger: 'axis', - axisPointer: { - type: 'shadow' - }, - formatter: function(data) { - let html = '' - if (data.length > 0) { - html += data[0].name + '
' - } - for (let v of data) { - let colorEl = ''; - html += colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '
' - } - return html - } - }, - legend: { - data: ['Traffic In', 'Traffic Out'] - }, - grid: { - left: '3%', - right: '4%', - bottom: '3%', - containLabel: true - }, - xAxis: [{ - type: 'category', - data: dates - }], - yAxis: [{ - type: 'value', - axisLabel: { - formatter: function(value) { - return Humanize.fileSize(value) - } - } - }], - series: [{ - name: 'Traffic In', - type: 'bar', - data: trafficInArr - }, { - - name: 'Traffic Out', - type: 'bar', - data: trafficOutArr - }] - }; - myChart.setOption(option); - myChart.hideLoading() -} - -export { - DrawTrafficChart, - DrawProxyChart, - DrawProxyTrafficChart -} diff --git a/web/frps/src/utils/client.ts b/web/frps/src/utils/client.ts new file mode 100644 index 00000000000..0cb9e5a0768 --- /dev/null +++ b/web/frps/src/utils/client.ts @@ -0,0 +1,65 @@ +import { formatDistanceToNow } from './format' +import type { ClientInfoData, ClientStatus } from '../types/client' + +export class Client { + key: string + user: string + clientID: string + runID: string + version: string + wireProtocol: string + hostname: string + ip: string + firstConnectedAt: Date + lastConnectedAt: Date + disconnectedAt?: Date + online: boolean + status: ClientStatus + + constructor(data: ClientInfoData) { + this.key = data.key + this.user = data.user + this.clientID = data.clientID + this.runID = data.runID + this.version = data.version || '' + this.wireProtocol = data.wireProtocol || '' + this.hostname = data.hostname + this.ip = data.clientIP || '' + this.firstConnectedAt = new Date(data.firstConnectedAt * 1000) + this.lastConnectedAt = new Date(data.lastConnectedAt * 1000) + if (data.disconnectedAt && data.disconnectedAt > 0) { + this.disconnectedAt = new Date(data.disconnectedAt * 1000) + } + this.online = data.online + this.status = data.status || { + phase: this.online ? 'online' : 'offline', + curConns: 0, + proxyCount: 0, + } + } + + get displayName(): string { + if (this.clientID) { + return this.user ? `${this.user}.${this.clientID}` : this.clientID + } + return this.runID + } + + get wireProtocolLabel(): string { + if (!this.wireProtocol) return '' + return `Protocol ${this.wireProtocol}` + } + + get firstConnectedAgo(): string { + return formatDistanceToNow(this.firstConnectedAt) + } + + get lastConnectedAgo(): string { + return formatDistanceToNow(this.lastConnectedAt) + } + + get disconnectedAgo(): string { + if (!this.disconnectedAt) return '' + return formatDistanceToNow(this.disconnectedAt) + } +} diff --git a/web/frps/src/utils/format.ts b/web/frps/src/utils/format.ts new file mode 100644 index 00000000000..cca9e31ab96 --- /dev/null +++ b/web/frps/src/utils/format.ts @@ -0,0 +1,41 @@ +export function formatDistanceToNow(date: Date): string { + const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000) + + let interval = seconds / 31536000 + if (interval > 1) return Math.floor(interval) + ' years ago' + + interval = seconds / 2592000 + if (interval > 1) return Math.floor(interval) + ' months ago' + + interval = seconds / 86400 + if (interval > 1) return Math.floor(interval) + ' days ago' + + interval = seconds / 3600 + if (interval > 1) return Math.floor(interval) + ' hours ago' + + interval = seconds / 60 + if (interval > 1) return Math.floor(interval) + ' minutes ago' + + return Math.floor(seconds) + ' seconds ago' +} + +export function formatUnixSeconds(seconds?: number): string { + if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return '' + + const date = new Date(seconds * 1000) + const pad = (value: number) => value.toString().padStart(2, '0') + return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +} + +export function formatFileSize(bytes: number): string { + if (!Number.isFinite(bytes) || bytes < 0) return '0 B' + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + // Prevent index out of bounds for extremely large numbers + const unit = sizes[i] || sizes[sizes.length - 1] + const val = bytes / Math.pow(k, i) + + return parseFloat(val.toFixed(2)) + ' ' + unit +} diff --git a/web/frps/src/utils/less/custom.less b/web/frps/src/utils/less/custom.less deleted file mode 100644 index b83a4007466..00000000000 --- a/web/frps/src/utils/less/custom.less +++ /dev/null @@ -1,22 +0,0 @@ -@color: red; - -.el-form-item { - span { - margin-left: 15px; - } -} - -.demo-table-expand { - font-size: 0; - - label { - width: 90px; - color: #99a9bf; - } - - .el-form-item { - margin-right: 0; - margin-bottom: 0; - width: 50%; - } -} diff --git a/web/frps/src/utils/proxy.js b/web/frps/src/utils/proxy.js deleted file mode 100644 index 948e13cd8e8..00000000000 --- a/web/frps/src/utils/proxy.js +++ /dev/null @@ -1,104 +0,0 @@ -class BaseProxy { - constructor(proxyStats) { - this.name = proxyStats.name - if (proxyStats.conf != null) { - this.encryption = proxyStats.conf.use_encryption - this.compression = proxyStats.conf.use_compression - } else { - this.encryption = "" - this.compression = "" - } - this.conns = proxyStats.cur_conns - this.traffic_in = proxyStats.today_traffic_in - this.traffic_out = proxyStats.today_traffic_out - this.last_start_time = proxyStats.last_start_time - this.last_close_time = proxyStats.last_close_time - this.status = proxyStats.status - } -} - -class TcpProxy extends BaseProxy { - constructor(proxyStats) { - super(proxyStats) - this.type = "tcp" - if (proxyStats.conf != null) { - this.addr = ":" + proxyStats.conf.remote_port - this.port = proxyStats.conf.remote_port - } else { - this.addr = "" - this.port = "" - } - } -} - -class UdpProxy extends BaseProxy { - constructor(proxyStats) { - super(proxyStats) - this.type = "udp" - if (proxyStats.conf != null) { - this.addr = ":" + proxyStats.conf.remote_port - this.port = proxyStats.conf.remote_port - } else { - this.addr = "" - this.port = "" - } - } -} - -class HttpProxy extends BaseProxy { - constructor(proxyStats, port, subdomain_host) { - super(proxyStats) - this.type = "http" - this.port = port - if (proxyStats.conf != null) { - this.custom_domains = proxyStats.conf.custom_domains - this.host_header_rewrite = proxyStats.conf.host_header_rewrite - this.locations = proxyStats.conf.locations - if (proxyStats.conf.subdomain != "") { - this.subdomain = proxyStats.conf.subdomain + "." + subdomain_host - } else { - this.subdomain = "" - } - } else { - this.custom_domains = "" - this.host_header_rewrite = "" - this.subdomain = "" - this.locations = "" - } - } -} - -class HttpsProxy extends BaseProxy { - constructor(proxyStats, port, subdomain_host) { - super(proxyStats) - this.type = "https" - this.port = port - if (proxyStats.conf != null) { - this.custom_domains = proxyStats.conf.custom_domains - if (proxyStats.conf.subdomain != "") { - this.subdomain = proxyStats.conf.subdomain + "." + subdomain_host - } else { - this.subdomain = "" - } - } else { - this.custom_domains = "" - this.subdomain = "" - } - } -} - -class StcpProxy extends BaseProxy { - constructor(proxyStats) { - super(proxyStats) - this.type = "stcp" - } -} - -class SudpProxy extends BaseProxy { - constructor(proxyStats) { - super(proxyStats) - this.type = "sudp" - } -} - -export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy, StcpProxy, SudpProxy} diff --git a/web/frps/src/utils/proxy.ts b/web/frps/src/utils/proxy.ts new file mode 100644 index 00000000000..a883f097a5c --- /dev/null +++ b/web/frps/src/utils/proxy.ts @@ -0,0 +1,161 @@ +class BaseProxy { + name: string + type: string + annotations: Map + encryption: boolean + compression: boolean + conns: number + trafficIn: number + trafficOut: number + lastStartTime: string + lastCloseTime: string + status: string + user: string + clientID: string + addr: string + port: number + + customDomains: string + hostHeaderRewrite: string + locations: string + subdomain: string + + // TCPMux specific + multiplexer: string + routeByHTTPUser: string + + constructor(proxyStats: any) { + this.name = proxyStats.name + this.type = '' + this.annotations = new Map() + if (proxyStats.conf?.annotations) { + for (const key in proxyStats.conf.annotations) { + this.annotations.set(key, proxyStats.conf.annotations[key]) + } + } + + this.encryption = false + this.compression = false + this.encryption = + proxyStats.conf?.transport?.useEncryption || this.encryption + this.compression = + proxyStats.conf?.transport?.useCompression || this.compression + this.conns = proxyStats.curConns + this.trafficIn = proxyStats.todayTrafficIn + this.trafficOut = proxyStats.todayTrafficOut + this.lastStartTime = proxyStats.lastStartTime + this.lastCloseTime = proxyStats.lastCloseTime + this.status = proxyStats.status + this.user = proxyStats.user || '' + this.clientID = proxyStats.clientID || '' + + this.addr = '' + this.port = 0 + this.customDomains = '' + this.hostHeaderRewrite = '' + this.locations = '' + this.subdomain = '' + this.multiplexer = '' + this.routeByHTTPUser = '' + } +} + +class TCPProxy extends BaseProxy { + constructor(proxyStats: any) { + super(proxyStats) + this.type = 'tcp' + if (proxyStats.conf != null) { + this.addr = ':' + proxyStats.conf.remotePort + this.port = proxyStats.conf.remotePort + } else { + this.addr = '' + this.port = 0 + } + } +} + +class UDPProxy extends BaseProxy { + constructor(proxyStats: any) { + super(proxyStats) + this.type = 'udp' + if (proxyStats.conf != null) { + this.addr = ':' + proxyStats.conf.remotePort + this.port = proxyStats.conf.remotePort + } else { + this.addr = '' + this.port = 0 + } + } +} + +class HTTPProxy extends BaseProxy { + constructor(proxyStats: any, port: number, subdomainHost: string) { + super(proxyStats) + this.type = 'http' + this.port = port + if (proxyStats.conf) { + this.customDomains = proxyStats.conf.customDomains || this.customDomains + this.hostHeaderRewrite = proxyStats.conf.hostHeaderRewrite + this.locations = proxyStats.conf.locations + if (proxyStats.conf.subdomain) { + this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}` + } + } + } +} + +class HTTPSProxy extends BaseProxy { + constructor(proxyStats: any, port: number, subdomainHost: string) { + super(proxyStats) + this.type = 'https' + this.port = port + if (proxyStats.conf != null) { + this.customDomains = proxyStats.conf.customDomains || this.customDomains + if (proxyStats.conf.subdomain) { + this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}` + } + } + } +} + +class TCPMuxProxy extends BaseProxy { + constructor(proxyStats: any, port: number, subdomainHost: string) { + super(proxyStats) + this.type = 'tcpmux' + this.port = port + + if (proxyStats.conf) { + this.customDomains = proxyStats.conf.customDomains || this.customDomains + this.multiplexer = proxyStats.conf.multiplexer || '' + this.routeByHTTPUser = proxyStats.conf.routeByHTTPUser || '' + if (proxyStats.conf.subdomain) { + this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}` + } + } + } +} + +class STCPProxy extends BaseProxy { + constructor(proxyStats: any) { + super(proxyStats) + this.type = 'stcp' + } +} + +class SUDPProxy extends BaseProxy { + constructor(proxyStats: any) { + super(proxyStats) + this.type = 'sudp' + } +} + +export { + BaseProxy, + TCPProxy, + UDPProxy, + TCPMuxProxy, + HTTPProxy, + HTTPSProxy, + STCPProxy, + SUDPProxy, +} diff --git a/web/frps/src/views/ClientDetail.vue b/web/frps/src/views/ClientDetail.vue new file mode 100644 index 00000000000..17a50468830 --- /dev/null +++ b/web/frps/src/views/ClientDetail.vue @@ -0,0 +1,632 @@ + + + + + diff --git a/web/frps/src/views/Clients.vue b/web/frps/src/views/Clients.vue new file mode 100644 index 00000000000..43d9f25b4f1 --- /dev/null +++ b/web/frps/src/views/Clients.vue @@ -0,0 +1,368 @@ + + + + + diff --git a/web/frps/src/views/Proxies.vue b/web/frps/src/views/Proxies.vue new file mode 100644 index 00000000000..734c85a5339 --- /dev/null +++ b/web/frps/src/views/Proxies.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/web/frps/src/views/ProxyDetail.vue b/web/frps/src/views/ProxyDetail.vue new file mode 100644 index 00000000000..b4487dd2056 --- /dev/null +++ b/web/frps/src/views/ProxyDetail.vue @@ -0,0 +1,820 @@ + + + + + diff --git a/web/frps/src/views/ServerOverview.vue b/web/frps/src/views/ServerOverview.vue new file mode 100644 index 00000000000..5e38e1e591c --- /dev/null +++ b/web/frps/src/views/ServerOverview.vue @@ -0,0 +1,451 @@ + + + + + diff --git a/web/frps/test/stat-card.test.ts b/web/frps/test/stat-card.test.ts new file mode 100644 index 00000000000..e5f4cbb628d --- /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/frps/tsconfig.json b/web/frps/tsconfig.json new file mode 100644 index 00000000000..8795fb5cec2 --- /dev/null +++ b/web/frps/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"], + "@shared/*": ["../shared/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/web/frps/tsconfig.node.json b/web/frps/tsconfig.node.json new file mode 100644 index 00000000000..a8583534f27 --- /dev/null +++ b/web/frps/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.mts"] +} diff --git a/web/frps/vite.config.mts b/web/frps/vite.config.mts new file mode 100644 index 00000000000..fda910d4124 --- /dev/null +++ b/web/frps/vite.config.mts @@ -0,0 +1,61 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import svgLoader from 'vite-svg-loader' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import ElementPlus from 'unplugin-element-plus/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: '', + plugins: [ + vue(), + svgLoader(), + ElementPlus({}), + AutoImport({ + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@shared': fileURLToPath(new URL('../shared', import.meta.url)), + }, + dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'], + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@use "@shared/css/_index.scss" as *;`, + }, + }, + }, + build: { + assetsDir: '', + chunkSizeWarningLimit: 1000, + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + }, + server: { + allowedHosts: process.env.ALLOWED_HOSTS + ? process.env.ALLOWED_HOSTS.split(',') + : [], + proxy: { + '/api': { + target: process.env.VITE_API_URL || 'http://127.0.0.1:7500', + changeOrigin: true, + }, + }, + }, +}) diff --git a/web/frps/webpack.config.js b/web/frps/webpack.config.js deleted file mode 100644 index 6947920524d..00000000000 --- a/web/frps/webpack.config.js +++ /dev/null @@ -1,107 +0,0 @@ -const path = require('path') -var webpack = require('webpack') -var HtmlWebpackPlugin = require('html-webpack-plugin') -var VueLoaderPlugin = require('vue-loader/lib/plugin') -var url = require('url') -var publicPath = '' - -module.exports = (options = {}) => ({ - entry: { - vendor: './src/main' - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: options.dev ? '[name].js' : '[name].js?[chunkhash]', - chunkFilename: '[id].js?[chunkhash]', - publicPath: options.dev ? '/assets/' : publicPath - }, - resolve: { - extensions: ['.js', '.vue', '.json'], - alias: { - 'vue$': 'vue/dist/vue.esm.js', - '@': path.resolve(__dirname, 'src'), - } - }, - module: { - rules: [{ - test: /\.vue$/, - loader: 'vue-loader' - }, { - test: /\.js$/, - use: ['babel-loader'], - exclude: /node_modules/ - }, { - test: /\.html$/, - use: [{ - loader: 'html-loader', - options: { - root: path.resolve(__dirname, 'src'), - attrs: ['img:src', 'link:href'] - } - }] - }, { - test: /\.less$/, - loader: 'style-loader!css-loader!postcss-loader!less-loader' - }, { - test: /\.css$/, - use: ['style-loader', 'css-loader', 'postcss-loader'] - }, { - test: /favicon\.png$/, - use: [{ - loader: 'file-loader', - options: { - name: '[name].[ext]?[hash]' - } - }] - }, { - test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, - exclude: /favicon\.png$/, - use: [{ - loader: 'url-loader', - options: { - limit: 10000 - } - }] - }] - }, - plugins: [ - new webpack.optimize.CommonsChunkPlugin({ - names: ['vendor', 'manifest'] - }), - new HtmlWebpackPlugin({ - favicon: 'src/assets/favicon.ico', - template: 'src/index.html' - }), - new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'), - new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: '"production"' - } - }), - new webpack.optimize.UglifyJsPlugin({ - sourceMap: false, - comments: false, - compress: { - warnings: false - } - }), - new VueLoaderPlugin() - ], - devServer: { - host: '127.0.0.1', - port: 8010, - proxy: { - '/api/': { - target: 'http://127.0.0.1:8080', - changeOrigin: true, - pathRewrite: { - '^/api': '' - } - } - }, - historyApiFallback: { - index: url.parse(options.dev ? '/assets/' : publicPath).pathname - } - }//, - //devtool: options.dev ? '#eval-source-map' : '#source-map' -}) diff --git a/web/frps/yarn.lock b/web/frps/yarn.lock deleted file mode 100644 index a0e0bb4e77a..00000000000 --- a/web/frps/yarn.lock +++ /dev/null @@ -1,6377 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/helper-module-imports@7.0.0-beta.35": - version "7.0.0-beta.35" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.35.tgz#308e350e731752cdb4d0f058df1d704925c64e0a" - integrity sha512-vaC1KyIZSuyWb3Lj277fX0pxivyHwuDU4xZsofqgYAbkDxNieMg2vuhzP5AgMweMY7fCQUMTi+BgPqTLjkxXFg== - dependencies: - "@babel/types" "7.0.0-beta.35" - lodash "^4.2.0" - -"@babel/types@7.0.0-beta.35": - version "7.0.0-beta.35" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.35.tgz#cf933a9a9a38484ca724b335b88d83726d5ab960" - integrity sha512-y9XT11CozHDgjWcTdxmhSj13rJVXpa5ZXwjjOiTedjaM0ba5ItqdS02t31EhPl7HtOWxsZkYCCUNrSfrOisA6w== - dependencies: - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^2.0.0" - -"@sindresorhus/is@^0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" - integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== - -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/node@*": - version "14.14.22" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" - integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== - -"@vue/component-compiler-utils@^3.1.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz#8f85182ceed28e9b3c75313de669f83166d11e5d" - integrity sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw== - dependencies: - consolidate "^0.15.1" - hash-sum "^1.0.2" - lru-cache "^4.1.2" - merge-source-map "^1.1.0" - postcss "^7.0.14" - postcss-selector-parser "^6.0.2" - source-map "~0.6.1" - vue-template-es2015-compiler "^1.9.0" - optionalDependencies: - prettier "^1.18.2" - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-dynamic-import@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" - integrity sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ= - dependencies: - acorn "^4.0.3" - -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" - integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s= - dependencies: - acorn "^3.0.4" - -acorn@^3.0.4: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" - integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= - -acorn@^4.0.3: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= - -acorn@^5.0.0, acorn@^5.5.0: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" - integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= - -ajv-keywords@^3.1.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^4.7.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^6.1.0: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-escapes@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" - integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -ast-types@0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" - integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -async-validator@~1.8.1: - version "1.8.5" - resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-1.8.5.tgz#dc3e08ec1fd0dddb67e60842f02c0cd1cec6d7f0" - integrity sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA== - dependencies: - babel-runtime "6.x" - -async@^2.1.2, async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -autoprefixer@^6.3.1, autoprefixer@^6.6.0: - version "6.7.7" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" - integrity sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ= - dependencies: - browserslist "^1.7.6" - caniuse-db "^1.0.30000634" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^5.2.16" - postcss-value-parser "^3.2.3" - -babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.21.0, babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-eslint@^7.1.1: - version "7.2.3" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827" - integrity sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc= - dependencies: - babel-code-frame "^6.22.0" - babel-traverse "^6.23.1" - babel-types "^6.23.0" - babylon "^6.17.0" - -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" - integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" - integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" - integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" - integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" - integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" - integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" - integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI= - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" - integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-vue-jsx-merge-props@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6" - integrity sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg== - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-loader@^6.4.0: - version "6.4.1" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca" - integrity sha1-CzQRLVsHSKjc2/Uaz2+b1C1QuMo= - dependencies: - find-cache-dir "^0.1.1" - loader-utils "^0.2.16" - mkdirp "^0.5.1" - object-assign "^4.0.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" - integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-component@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-component/-/babel-plugin-component-1.1.1.tgz#9b023a23ff5c9aae0fd56c5a18b9cab8c4d45eea" - integrity sha512-WUw887kJf2GH80Ng/ZMctKZ511iamHNqPhd9uKo14yzisvV7Wt1EckIrb8oq/uCz3B3PpAW7Xfl7AkTLDYT6ag== - dependencies: - "@babel/helper-module-imports" "7.0.0-beta.35" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" - integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" - integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" - integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= - dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-plugin-transform-es2015-classes@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" - integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" - integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" - integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" - integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-for-of@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" - integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" - integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" - integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" - integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ= - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.2" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" - integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" - integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" - integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg= - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" - integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" - integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" - integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" - integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" - integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" - integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" - integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" - integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-regenerator@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" - integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= - dependencies: - regenerator-transform "^0.10.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-preset-es2015@^6.13.2: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" - integrity sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk= - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.24.1" - babel-plugin-transform-es2015-classes "^6.24.1" - babel-plugin-transform-es2015-computed-properties "^6.24.1" - babel-plugin-transform-es2015-destructuring "^6.22.0" - babel-plugin-transform-es2015-duplicate-keys "^6.24.1" - babel-plugin-transform-es2015-for-of "^6.22.0" - babel-plugin-transform-es2015-function-name "^6.24.1" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-plugin-transform-es2015-modules-systemjs "^6.24.1" - babel-plugin-transform-es2015-modules-umd "^6.24.1" - babel-plugin-transform-es2015-object-super "^6.24.1" - babel-plugin-transform-es2015-parameters "^6.24.1" - babel-plugin-transform-es2015-shorthand-properties "^6.24.1" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.24.1" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.22.0" - babel-plugin-transform-es2015-unicode-regex "^6.24.1" - babel-plugin-transform-regenerator "^6.24.1" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.17.0, babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -balanced-match@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bluebird@^3.1.1, bluebird@^3.4.7: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -bootstrap@^3.3.7: - version "3.4.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.1.tgz#c3a347d419e289ad11f4033e3c4132b87c081d72" - integrity sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: - version "1.7.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" - integrity sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk= - dependencies: - caniuse-db "^1.0.30000639" - electron-to-chromium "^1.2.7" - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-request@^2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" - integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= - dependencies: - clone-response "1.0.2" - get-stream "3.0.0" - http-cache-semantics "3.8.1" - keyv "3.0.0" - lowercase-keys "1.0.0" - normalize-url "2.0.1" - responselike "1.0.2" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -caller-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" - integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= - dependencies: - callsites "^0.2.0" - -callsites@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" - integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= - -camel-case@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" - integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= - dependencies: - no-case "^2.2.0" - upper-case "^1.1.1" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-api@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" - integrity sha1-tTTnxzTE+B7F++isoq0kNUuWLGw= - dependencies: - browserslist "^1.3.6" - caniuse-db "^1.0.30000529" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30001179" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001179.tgz#7af76065c071b85b1f6aa0cbccd180eb32a71c50" - integrity sha512-YgqeMhTgW75OI7emeIpVTol3E2EQTQJqUJp/R1uA6aYzXOSyvGAGAkadvSdYqB/JPDBkXWwT3fLSxA+AuqttlA== - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.4.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== - -clap@^1.0.9: - version "1.2.3" - resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" - integrity sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA== - dependencies: - chalk "^1.1.3" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-css@4.2.x: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== - dependencies: - source-map "~0.6.0" - -cli-cursor@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= - dependencies: - restore-cursor "^1.0.1" - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -clone-response@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -coa@~1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" - integrity sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0= - dependencies: - q "^1.1.2" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.3.0, color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" - integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= - dependencies: - color-name "^1.0.0" - -color@^0.11.0: - version "0.11.4" - resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" - integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= - dependencies: - clone "^1.0.2" - color-convert "^1.3.0" - color-string "^0.3.0" - -colormin@^1.0.5: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" - integrity sha1-6i90IKcrlogaOKrlnsEkpvcpgTM= - dependencies: - color "^0.11.0" - css-color-names "0.0.4" - has "^1.0.1" - -colors@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= - -commander@2.17.x: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -commander@~2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -consolidate@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" - integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw== - dependencies: - bluebird "^3.1.1" - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -copy-anything@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.1.tgz#2afbce6da684bdfcbec93752fa762819cb480d9a" - integrity sha512-lA57e7viQHOdPQcrytv5jFeudZZOXuyk47lZym279FiDQ8jeZomXiGuVf6ffMKkJ+3TIai3J1J3yi6M+/4U35g== - dependencies: - is-what "^3.7.1" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" - integrity sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A== - dependencies: - is-directory "^0.3.1" - js-yaml "^3.4.3" - minimist "^1.2.0" - object-assign "^4.1.0" - os-homedir "^1.0.1" - parse-json "^2.2.0" - require-from-string "^1.1.0" - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-color-names@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-loader@^0.27.0: - version "0.27.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.27.3.tgz#69ab6f47b69bfb1b5acee61bac2aab14302ff0dc" - integrity sha1-aatvR7ab+xtazuYbrCqrFDAv8Nw= - dependencies: - babel-code-frame "^6.11.0" - css-selector-tokenizer "^0.7.0" - cssnano ">=2.6.1 <4" - loader-utils "^1.0.2" - lodash.camelcase "^4.3.0" - object-assign "^4.0.1" - postcss "^5.0.6" - postcss-modules-extract-imports "^1.0.0" - postcss-modules-local-by-default "^1.0.1" - postcss-modules-scope "^1.0.0" - postcss-modules-values "^1.1.0" - source-list-map "^0.1.7" - -css-select@^2.0.2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-selector-tokenizer@^0.7.0: - version "0.7.3" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" - integrity sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg== - dependencies: - cssesc "^3.0.0" - fastparse "^1.1.2" - -css-what@^3.2.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" - integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -"cssnano@>=2.6.1 <4": - version "3.10.0" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" - integrity sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg= - dependencies: - autoprefixer "^6.3.1" - decamelize "^1.1.2" - defined "^1.0.0" - has "^1.0.1" - object-assign "^4.0.1" - postcss "^5.0.14" - postcss-calc "^5.2.0" - postcss-colormin "^2.1.8" - postcss-convert-values "^2.3.4" - postcss-discard-comments "^2.0.4" - postcss-discard-duplicates "^2.0.1" - postcss-discard-empty "^2.0.1" - postcss-discard-overridden "^0.1.1" - postcss-discard-unused "^2.2.1" - postcss-filter-plugins "^2.0.0" - postcss-merge-idents "^2.1.5" - postcss-merge-longhand "^2.0.1" - postcss-merge-rules "^2.0.3" - postcss-minify-font-values "^1.0.2" - postcss-minify-gradients "^1.0.1" - postcss-minify-params "^1.0.4" - postcss-minify-selectors "^2.0.4" - postcss-normalize-charset "^1.1.0" - postcss-normalize-url "^3.0.7" - postcss-ordered-values "^2.1.0" - postcss-reduce-idents "^2.2.2" - postcss-reduce-initial "^1.0.0" - postcss-reduce-transforms "^1.0.3" - postcss-svgo "^2.1.1" - postcss-unique-selectors "^2.0.2" - postcss-value-parser "^3.2.3" - postcss-zindex "^2.0.1" - -csso@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" - integrity sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U= - dependencies: - clap "^1.0.9" - source-map "^0.5.3" - -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - -de-indent@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" - integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= - -debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.1, debug@^3.2.6: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -deepmerge@^1.2.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" - integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== - -default-gateway@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -doctrine@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -dom-converter@^0.2: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" - integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -echarts@^3.5.0: - version "3.8.5" - resolved "https://registry.yarnpkg.com/echarts/-/echarts-3.8.5.tgz#58e4a51d2743c6fb75257b0dc0a9cf9f5378ac0e" - integrity sha512-E+nnROMfCeiLeoT/fZyX8SE8mKzwkTjyemyoBF543oqjRtjTSKQAVDEihMXy4oC6pJS0tYGdMqCA2ATk8onyRg== - dependencies: - zrender "3.7.4" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -electron-to-chromium@^1.2.7: - version "1.3.644" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.644.tgz#c89721733ec26b8d117275fb6b2acbeb3d45a6b6" - integrity sha512-N7FLvjDPADxad+OXXBuYfcvDvCBG0aW8ZZGr7G91sZMviYbnQJFxdSvUus4SJ0K7Q8dzMxE+Wx1d/CrJIIJ0sw== - -element-ui@^2.3.8: - version "2.15.0" - resolved "https://registry.yarnpkg.com/element-ui/-/element-ui-2.15.0.tgz#de9b73a8d1e3e3b50e82b923a5fa95295239bd41" - integrity sha512-9z/1+b7V8fvp08OnKUEW4/BZ72kT+IhuKR9cTMz3XoCTKmEsqLLb32XjbO/DznSFaaiFbOYU93G7WtkvrCAL9A== - dependencies: - async-validator "~1.8.1" - babel-helper-vue-jsx-merge-props "^2.0.0" - deepmerge "^1.2.0" - normalize-wheel "^1.0.1" - resize-observer-polyfill "^1.5.0" - throttle-debounce "^1.0.1" - -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^3.3.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" - integrity sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24= - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - object-assign "^4.0.1" - tapable "^0.2.7" - -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -errno@^0.1.1, errno@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" - -es6-iterator@^2.0.3, es6-iterator@~2.0.1, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-map@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA= - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - -es6-set@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE= - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - -es6-symbol@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-symbol@^3.1.1, es6-symbol@~3.1.1, es6-symbol@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -es6-templates@^0.2.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4" - integrity sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ= - dependencies: - recast "~0.11.12" - through "~2.3.6" - -es6-weak-map@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" - integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== - dependencies: - d "1" - es5-ext "^0.10.46" - es6-iterator "^2.0.3" - es6-symbol "^3.1.1" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" - integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM= - dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-config-enough@^0.2.2: - version "0.2.10" - resolved "https://registry.yarnpkg.com/eslint-config-enough/-/eslint-config-enough-0.2.10.tgz#56f133ab86767a9e5b77b74fbcc0ad1859a229e5" - integrity sha512-/ykPXJ4LvoKOp4X1zdT11JmBZ3rki1AN7SiAOUxYRhSM094g+45jfDT4pVnhHcxBz0iJppK10iIERCklrDaQVw== - -eslint-loader@^1.6.3: - version "1.9.0" - resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-1.9.0.tgz#7e1be9feddca328d3dcfaef1ad49d5beffe83a13" - integrity sha512-40aN976qSNPyb9ejTqjEthZITpls1SVKtwguahmH1dzGCwQU/vySE+xX33VZmD8csU0ahVNCtFlsPgKqRBiqgg== - dependencies: - loader-fs-cache "^1.0.0" - loader-utils "^1.0.2" - object-assign "^4.0.1" - object-hash "^1.1.4" - rimraf "^2.6.1" - -eslint@^3.12.2: - version "3.19.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" - integrity sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw= - dependencies: - babel-code-frame "^6.16.0" - chalk "^1.1.3" - concat-stream "^1.5.2" - debug "^2.1.1" - doctrine "^2.0.0" - escope "^3.6.0" - espree "^3.4.0" - esquery "^1.0.0" - estraverse "^4.2.0" - esutils "^2.0.2" - file-entry-cache "^2.0.0" - glob "^7.0.3" - globals "^9.14.0" - ignore "^3.2.0" - imurmurhash "^0.1.4" - inquirer "^0.12.0" - is-my-json-valid "^2.10.0" - is-resolvable "^1.0.0" - js-yaml "^3.5.1" - json-stable-stringify "^1.0.0" - levn "^0.3.0" - lodash "^4.0.0" - mkdirp "^0.5.0" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.1" - pluralize "^1.2.1" - progress "^1.1.8" - require-uncached "^1.0.2" - shelljs "^0.7.5" - strip-bom "^3.0.0" - strip-json-comments "~2.0.1" - table "^3.7.8" - text-table "~0.2.0" - user-home "^2.0.0" - -espree@^3.4.0: - version "3.5.4" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" - integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A== - dependencies: - acorn "^5.5.0" - acorn-jsx "^3.0.0" - -esprima@^2.6.0: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esquery@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1, estraverse@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= - dependencies: - d "1" - es5-ext "~0.10.14" - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== - -eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -ext@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== - dependencies: - type "^2.0.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fastparse@^1.1.1, fastparse@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" - integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== - -faye-websocket@^0.11.3: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - -figures@^1.3.5: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" - integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= - dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" - -file-loader@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.10.1.tgz#815034119891fc6441fb5a64c11bc93c22ddd842" - integrity sha1-gVA0EZiR/GRB+1pkwRvJPCLd2EI= - dependencies: - loader-utils "^1.0.2" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-cache-dir@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" - integrity sha1-yN765XyKUqinhPnjHFfHQumToLk= - dependencies: - commondir "^1.0.1" - mkdirp "^0.5.1" - pkg-dir "^1.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -flat-cache@^1.2.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" - integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg== - dependencies: - circular-json "^0.3.1" - graceful-fs "^4.1.2" - rimraf "~2.6.2" - write "^0.2.1" - -flatten@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" - integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== - -follow-redirects@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" - integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -from2@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" - integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -generate-function@^2.0.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" - integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== - dependencies: - is-property "^1.0.2" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= - dependencies: - is-property "^1.0.0" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49" - integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-stream@3.0.0, get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob@^7.0.0, glob@^7.0.3, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^9.14.0, globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -got@^8.0.3: - version "8.3.2" - resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" - integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== - dependencies: - "@sindresorhus/is" "^0.7.0" - cacheable-request "^2.1.1" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - into-stream "^3.1.0" - is-retry-allowed "^1.1.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - mimic-response "^1.0.0" - p-cancelable "^0.4.0" - p-timeout "^2.0.1" - pify "^3.0.0" - safe-buffer "^5.1.1" - timed-out "^4.0.1" - url-parse-lax "^3.0.0" - url-to-options "^1.0.1" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbol-support-x@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" - integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has-to-string-tag-x@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" - integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== - dependencies: - has-symbol-support-x "^1.4.1" - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-sum@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" - integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ= - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -he@1.2.x, he@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-comment-regex@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" - integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== - -html-entities@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" - integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== - -html-loader@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.4.5.tgz#5fbcd87cd63a5c49a7fce2fe56f425e05729c68c" - integrity sha1-X7zYfNY6XEmn/OL+VvQl4Fcpxow= - dependencies: - es6-templates "^0.2.2" - fastparse "^1.1.1" - html-minifier "^3.0.1" - loader-utils "^1.0.2" - object-assign "^4.1.0" - -html-minifier@^3.0.1, html-minifier@^3.2.3: - version "3.5.21" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" - integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== - dependencies: - camel-case "3.0.x" - clean-css "4.2.x" - commander "2.17.x" - he "1.2.x" - param-case "2.1.x" - relateurl "0.2.x" - uglify-js "3.4.x" - -html-webpack-plugin@^2.24.1: - version "2.30.1" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz#7f9c421b7ea91ec460f56527d78df484ee7537d5" - integrity sha1-f5xCG36pHsRg9WUn1430hO51N9U= - dependencies: - bluebird "^3.4.7" - html-minifier "^3.2.3" - loader-utils "^0.2.16" - lodash "^4.17.3" - pretty-error "^2.0.2" - toposort "^1.0.0" - -htmlparser2@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -http-cache-semantics@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.5.1: - version "0.5.3" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" - integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== - -http-proxy-middleware@0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy@^1.17.0: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -humanize-plus@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030" - integrity sha1-pls0RZrWNnrbs3B6gqPJ+RYWcDA= - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^3.2.0: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== - -image-size@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -inquirer@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" - integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34= - dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" - cli-width "^2.0.0" - figures "^1.3.5" - lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" - through "^2.3.6" - -internal-ip@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" - integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== - dependencies: - default-gateway "^4.2.0" - ipaddr.js "^1.9.0" - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -into-stream@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" - integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= - dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" - -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.9.1, ipaddr.js@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= - -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arguments@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" - integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== - dependencies: - call-bind "^1.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-core-module@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-my-ip-valid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" - integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== - -is-my-json-valid@^2.10.0: - version "2.20.5" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz#5eca6a8232a687f68869b7361be1612e7512e5df" - integrity sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A== - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - is-my-ip-valid "^1.0.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" - integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== - -is-path-cwd@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-property@^1.0.0, is-property@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= - -is-regex@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-resolvable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - -is-retry-allowed@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-svg@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" - integrity sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk= - dependencies: - html-comment-regex "^1.1.0" - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-what@^3.7.1: - version "3.12.0" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.12.0.tgz#f4405ce4bd6dd420d3ced51a026fb90e03705e55" - integrity sha512-2ilQz5/f/o9V7WRWJQmpFYNmQFZ9iM+OXRonZKcYgTkCzjb949Vi4h282PD1UfmgHk666rcWonbRJ++KI41VGw== - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isurl@^1.0.0-alpha5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" - integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== - dependencies: - has-to-string-tag-x "^1.2.0" - is-object "^1.0.1" - -js-base64@^2.1.9: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.4.3, js-yaml@^3.5.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@~3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" - integrity sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A= - dependencies: - argparse "^1.0.7" - esprima "^2.6.0" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-loader@^0.5.4: - version "0.5.7" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" - integrity sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - -json3@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@^0.5.0, json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - -jsonpointer@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" - integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg== - -keyv@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" - integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== - dependencies: - json-buffer "3.0.0" - -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -less-loader@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.1.0.tgz#2c1352c5b09a4f84101490274fd51674de41363e" - integrity sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg== - dependencies: - clone "^2.1.1" - loader-utils "^1.1.0" - pify "^3.0.0" - -less@^3.0.4: - version "3.13.1" - resolved "https://registry.yarnpkg.com/less/-/less-3.13.1.tgz#0ebc91d2a0e9c0c6735b83d496b0ab0583077909" - integrity sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw== - dependencies: - copy-anything "^2.0.1" - tslib "^1.10.0" - optionalDependencies: - errno "^0.1.1" - graceful-fs "^4.1.2" - image-size "~0.5.0" - make-dir "^2.1.0" - mime "^1.4.1" - native-request "^1.0.5" - source-map "~0.6.0" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -loader-fs-cache@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz#f08657646d607078be2f0a032f8bd69dd6f277d9" - integrity sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA== - dependencies: - find-cache-dir "^0.1.1" - mkdirp "^0.5.1" - -loader-runner@^2.3.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@^0.2.16: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - -loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - -lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -loglevel@^1.6.8: - version "1.7.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" - integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" - integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= - -lowercase-keys@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" - integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= - -lowercase-keys@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lru-cache@^4.1.2: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -math-expression-evaluator@^1.2.14: - version "1.3.7" - resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz#1b62225db86af06f7ea1fd9576a34af605a5b253" - integrity sha512-nrbaifCl42w37hYd6oRLvoymFK42tWB+WQTMFtksDGQMi5GvlJwnz/CsS30FFAISFLtX+A0csJ0xLiuuyyec7w== - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.45.0, "mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - -mime-types@~2.1.17, mime-types@~2.1.24: - version "2.1.28" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== - dependencies: - mime-db "1.45.0" - -mime@1.6.0, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.0.3, mime@^2.4.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" - integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== - -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.0, mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA= - -nan@^2.12.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -native-request@^1.0.5: - version "1.0.8" - resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb" - integrity sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -neo-async@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -no-case@^2.2.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" - integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== - dependencies: - lower-case "^1.1.1" - -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - -node-libs-browser@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-url@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" - integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== - dependencies: - prepend-http "^2.0.0" - query-string "^5.0.1" - sort-keys "^2.0.0" - -normalize-url@^1.4.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" - integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= - dependencies: - object-assign "^4.0.1" - prepend-http "^1.0.0" - query-string "^4.1.0" - sort-keys "^1.0.0" - -normalize-wheel@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" - integrity sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU= - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -nth-check@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-hash@^1.1.4: - version "1.3.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" - integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== - -object-is@^1.0.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" - integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= - -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - -optionator@^0.8.2: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-homedir@^1.0.0, os-homedir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-cancelable@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" - integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-is-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" - integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= - -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - -p-timeout@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" - integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== - dependencies: - p-finally "^1.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -param-case@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" - integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= - dependencies: - no-case "^2.2.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.1, path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" - integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= - dependencies: - find-up "^1.0.0" - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pluralize@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" - integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU= - -portfinder@^1.0.26: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-calc@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" - integrity sha1-d7rnypKK2FcW4v2kLyYb98HWW14= - dependencies: - postcss "^5.0.2" - postcss-message-helpers "^2.0.0" - reduce-css-calc "^1.2.6" - -postcss-colormin@^2.1.8: - version "2.2.2" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" - integrity sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks= - dependencies: - colormin "^1.0.5" - postcss "^5.0.13" - postcss-value-parser "^3.2.3" - -postcss-convert-values@^2.3.4: - version "2.6.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" - integrity sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0= - dependencies: - postcss "^5.0.11" - postcss-value-parser "^3.1.2" - -postcss-discard-comments@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" - integrity sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0= - dependencies: - postcss "^5.0.14" - -postcss-discard-duplicates@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" - integrity sha1-uavye4isGIFYpesSq8riAmO5GTI= - dependencies: - postcss "^5.0.4" - -postcss-discard-empty@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" - integrity sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU= - dependencies: - postcss "^5.0.14" - -postcss-discard-overridden@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" - integrity sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg= - dependencies: - postcss "^5.0.16" - -postcss-discard-unused@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" - integrity sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM= - dependencies: - postcss "^5.0.14" - uniqs "^2.0.0" - -postcss-filter-plugins@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz#82245fdf82337041645e477114d8e593aa18b8ec" - integrity sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ== - dependencies: - postcss "^5.0.4" - -postcss-load-config@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" - integrity sha1-U56a/J3chiASHr+djDZz4M5Q0oo= - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - postcss-load-options "^1.2.0" - postcss-load-plugins "^2.3.0" - -postcss-load-options@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" - integrity sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw= - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - -postcss-load-plugins@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" - integrity sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI= - dependencies: - cosmiconfig "^2.1.1" - object-assign "^4.1.0" - -postcss-loader@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-1.3.3.tgz#a621ea1fa29062a83972a46f54486771301916eb" - integrity sha1-piHqH6KQYqg5cqRvVEhncTAZFus= - dependencies: - loader-utils "^1.0.2" - object-assign "^4.1.1" - postcss "^5.2.15" - postcss-load-config "^1.2.0" - -postcss-merge-idents@^2.1.5: - version "2.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" - integrity sha1-TFUwMTwI4dWzu/PSu8dH4njuonA= - dependencies: - has "^1.0.1" - postcss "^5.0.10" - postcss-value-parser "^3.1.1" - -postcss-merge-longhand@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" - integrity sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg= - dependencies: - postcss "^5.0.4" - -postcss-merge-rules@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" - integrity sha1-0d9d+qexrMO+VT8OnhDofGG19yE= - dependencies: - browserslist "^1.5.2" - caniuse-api "^1.5.2" - postcss "^5.0.4" - postcss-selector-parser "^2.2.2" - vendors "^1.0.0" - -postcss-message-helpers@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" - integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= - -postcss-minify-font-values@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" - integrity sha1-S1jttWZB66fIR0qzUmyv17vey2k= - dependencies: - object-assign "^4.0.1" - postcss "^5.0.4" - postcss-value-parser "^3.0.2" - -postcss-minify-gradients@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" - integrity sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE= - dependencies: - postcss "^5.0.12" - postcss-value-parser "^3.3.0" - -postcss-minify-params@^1.0.4: - version "1.2.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" - integrity sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM= - dependencies: - alphanum-sort "^1.0.1" - postcss "^5.0.2" - postcss-value-parser "^3.0.2" - uniqs "^2.0.0" - -postcss-minify-selectors@^2.0.4: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" - integrity sha1-ssapjAByz5G5MtGkllCBFDEXNb8= - dependencies: - alphanum-sort "^1.0.2" - has "^1.0.1" - postcss "^5.0.14" - postcss-selector-parser "^2.0.0" - -postcss-modules-extract-imports@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a" - integrity sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw== - dependencies: - postcss "^6.0.1" - -postcss-modules-local-by-default@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" - integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk= - dependencies: - css-selector-tokenizer "^0.7.0" - postcss "^6.0.1" - -postcss-modules-scope@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" - integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A= - dependencies: - css-selector-tokenizer "^0.7.0" - postcss "^6.0.1" - -postcss-modules-values@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" - integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA= - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^6.0.1" - -postcss-normalize-charset@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" - integrity sha1-757nEhLX/nWceO0WL2HtYrXLk/E= - dependencies: - postcss "^5.0.5" - -postcss-normalize-url@^3.0.7: - version "3.0.8" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" - integrity sha1-EI90s/L82viRov+j6kWSJ5/HgiI= - dependencies: - is-absolute-url "^2.0.0" - normalize-url "^1.4.0" - postcss "^5.0.14" - postcss-value-parser "^3.2.3" - -postcss-ordered-values@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" - integrity sha1-7sbCpntsQSqNsgQud/6NpD+VwR0= - dependencies: - postcss "^5.0.4" - postcss-value-parser "^3.0.1" - -postcss-reduce-idents@^2.2.2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" - integrity sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM= - dependencies: - postcss "^5.0.4" - postcss-value-parser "^3.0.2" - -postcss-reduce-initial@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" - integrity sha1-aPgGlfBF0IJjqHmtJA343WT2ROo= - dependencies: - postcss "^5.0.4" - -postcss-reduce-transforms@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" - integrity sha1-/3b02CEkN7McKYpC0uFEQCV3GuE= - dependencies: - has "^1.0.1" - postcss "^5.0.8" - postcss-value-parser "^3.0.1" - -postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" - integrity sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A= - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - util-deprecate "^1.0.2" - -postcss-svgo@^2.1.1: - version "2.1.6" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" - integrity sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0= - dependencies: - is-svg "^2.0.0" - postcss "^5.0.14" - postcss-value-parser "^3.2.3" - svgo "^0.7.0" - -postcss-unique-selectors@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" - integrity sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0= - dependencies: - alphanum-sort "^1.0.1" - postcss "^5.0.4" - uniqs "^2.0.0" - -postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-zindex@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" - integrity sha1-0hCd3AVbka9n/EyzsCWUZjnSryI= - dependencies: - has "^1.0.1" - postcss "^5.0.4" - uniqs "^2.0.0" - -postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.15, postcss@^5.2.16: - version "5.2.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" - integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== - dependencies: - chalk "^1.1.3" - js-base64 "^2.1.9" - source-map "^0.5.6" - supports-color "^3.2.3" - -postcss@^6.0.1: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.14: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prepend-http@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier@^1.18.2: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== - -pretty-error@^2.0.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" - integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw== - dependencies: - lodash "^4.17.20" - renderkid "^2.0.4" - -private@^0.1.6, private@^0.1.8, private@~0.1.5: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -query-string@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" - integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= - dependencies: - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -query-string@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" - integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== - dependencies: - decode-uri-component "^0.2.0" - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - -recast@~0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -reduce-css-calc@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" - integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= - dependencies: - balanced-match "^0.4.2" - math-expression-evaluator "^1.2.14" - reduce-function-call "^1.0.1" - -reduce-function-call@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" - integrity sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ== - dependencies: - balanced-match "^1.0.0" - -regenerate@^1.2.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" - integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.1.6" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp.prototype.flags@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= - dependencies: - jsesc "~0.5.0" - -relateurl@0.2.x: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -renderkid@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.5.tgz#483b1ac59c6601ab30a7a596a5965cabccfdd0a5" - integrity sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ== - dependencies: - css-select "^2.0.2" - dom-converter "^0.2" - htmlparser2 "^3.10.1" - lodash "^4.17.20" - strip-ansi "^3.0.0" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-from-string@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" - integrity sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -require-uncached@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" - integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= - dependencies: - caller-path "^0.1.0" - resolve-from "^1.0.0" - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -resize-observer-polyfill@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" - integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.6, resolve@^1.10.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== - dependencies: - is-core-module "^2.1.0" - path-parse "^1.0.6" - -responselike@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@~2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k= - dependencies: - once "^1.3.0" - -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" - integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -selfsigned@^1.10.8: - version "1.10.8" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" - integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== - dependencies: - node-forge "^0.10.0" - -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shelljs@^0.7.5: - version "0.7.8" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" - integrity sha1-3svPh0sNHl+3LhSxZKloMEjprLM= - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -signal-exit@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sockjs-client@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.0.tgz#2f8ff5d4b659e0d092f7aba0b7c386bd2aa20add" - integrity sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q== - dependencies: - debug "^3.2.6" - eventsource "^1.0.7" - faye-websocket "^0.11.3" - inherits "^2.0.4" - json3 "^3.3.3" - url-parse "^1.4.7" - -sockjs@^0.3.21: - version "0.3.21" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" - integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== - dependencies: - faye-websocket "^0.11.3" - uuid "^3.4.0" - websocket-driver "^0.7.4" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= - dependencies: - is-plain-obj "^1.0.0" - -sort-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" - integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= - dependencies: - is-plain-obj "^1.0.0" - -source-list-map@^0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" - integrity sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY= - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -style-loader@^0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.2.tgz#74533384cf698c7104c7951150b49717adc2f3bb" - integrity sha1-dFMzhM9pjHEEx5URULSXF63C87s= - dependencies: - loader-utils "^1.0.2" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.1.0, supports-color@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0, supports-color@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -svgo@^0.7.0: - version "0.7.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" - integrity sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U= - dependencies: - coa "~1.0.1" - colors "~1.1.2" - csso "~2.3.1" - js-yaml "~3.7.0" - mkdirp "~0.5.1" - sax "~1.2.1" - whet.extend "~0.9.9" - -table@^3.7.8: - version "3.8.3" - resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" - integrity sha1-K7xULw/amGGnVdOUf+/Ys/UThV8= - dependencies: - ajv "^4.7.0" - ajv-keywords "^1.0.0" - chalk "^1.1.1" - lodash "^4.0.0" - slice-ansi "0.0.4" - string-width "^2.0.0" - -tapable@^0.2.7, tapable@~0.2.5: - version "0.2.9" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.9.tgz#af2d8bbc9b04f74ee17af2b4d9048f807acd18a8" - integrity sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A== - -text-table@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -throttle-debounce@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz#51853da37be68a155cb6e827b3514a3c422e89cd" - integrity sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg== - -through@^2.3.6, through@~2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -timed-out@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - -timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -toposort@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" - integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -tslib@^1.10.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" - integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -uglify-js@3.4.x: - version "3.4.10" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" - integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== - dependencies: - commander "~2.19.0" - source-map "~0.6.1" - -uglify-js@^2.8.27: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -upper-case@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" - integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-loader@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" - integrity sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg== - dependencies: - loader-utils "^1.1.0" - mime "^2.0.3" - schema-utils "^1.0.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-parse@^1.4.3, url-parse@^1.4.7: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url-to-options@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" - integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -user-home@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" - integrity sha1-nHC/2Babwdy/SGBODwS4tJzenp8= - dependencies: - os-homedir "^1.0.0" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2, uuid@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vendors@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -vue-hot-reload-api@^2.3.0: - version "2.3.4" - resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" - integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== - -vue-loader@^15.0.10: - version "15.9.6" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b" - integrity sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg== - dependencies: - "@vue/component-compiler-utils" "^3.1.0" - hash-sum "^1.0.2" - loader-utils "^1.1.0" - vue-hot-reload-api "^2.3.0" - vue-style-loader "^4.1.0" - -vue-resource@^1.2.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.5.1.tgz#0f3d685e3254d21800bebd966edcf56c34b3b6e4" - integrity sha512-o6V4wNgeqP+9v9b2bPXrr20CGNQPEXjpbUWdZWq9GJhqVeAGcYoeTtn/D4q059ZiyN0DIrDv/ADrQUmlUQcsmg== - dependencies: - got "^8.0.3" - -vue-router@^2.3.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.8.1.tgz#9833c9ee57ac83beb0269056fefee71713f20695" - integrity sha512-MC4jacHBhTPKtmcfzvaj2N7g6jgJ/Z/eIjZdt+yUaUOM1iKC0OUIlO/xCtz6OZFFTNUJs/1YNro2GN/lE+nOXA== - -vue-style-loader@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8" - integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ== - dependencies: - hash-sum "^1.0.2" - loader-utils "^1.0.2" - -vue-template-compiler@^2.1.8: - version "2.6.12" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" - integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg== - dependencies: - de-indent "^1.0.2" - he "^1.1.0" - -vue-template-es2015-compiler@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" - integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== - -vue@^2.5.16: - version "2.6.12" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123" - integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg== - -watchpack-chokidar2@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" - integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.3.1: - version "1.7.5" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" - integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.1" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -webpack-dev-middleware@^3.7.2: - version "3.7.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" - integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.4" - mkdirp "^0.5.1" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-server@^3.1.4: - version "3.11.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz#695ebced76a4929f0d5de7fd73fafe185fe33708" - integrity sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.1.8" - compression "^1.7.4" - connect-history-api-fallback "^1.6.0" - debug "^4.1.1" - del "^4.1.1" - express "^4.17.1" - html-entities "^1.3.1" - http-proxy-middleware "0.19.1" - import-local "^2.0.0" - internal-ip "^4.3.0" - ip "^1.1.5" - is-absolute-url "^3.0.3" - killable "^1.0.1" - loglevel "^1.6.8" - opn "^5.5.0" - p-retry "^3.0.1" - portfinder "^1.0.26" - schema-utils "^1.0.0" - selfsigned "^1.10.8" - semver "^6.3.0" - serve-index "^1.9.1" - sockjs "^0.3.21" - sockjs-client "^1.5.0" - spdy "^4.0.2" - strip-ansi "^3.0.1" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.7.2" - webpack-log "^2.0.0" - ws "^6.2.1" - yargs "^13.3.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-sources@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^2.2.0-rc.4: - version "2.7.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.7.0.tgz#b2a1226804373ffd3d03ea9c6bd525067034f6b1" - integrity sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg== - dependencies: - acorn "^5.0.0" - acorn-dynamic-import "^2.0.0" - ajv "^4.7.0" - ajv-keywords "^1.1.1" - async "^2.1.2" - enhanced-resolve "^3.3.0" - interpret "^1.0.0" - json-loader "^0.5.4" - json5 "^0.5.1" - loader-runner "^2.3.0" - loader-utils "^0.2.16" - memory-fs "~0.4.1" - mkdirp "~0.5.0" - node-libs-browser "^2.0.0" - source-map "^0.5.3" - supports-color "^3.1.0" - tapable "~0.2.5" - uglify-js "^2.8.27" - watchpack "^1.3.1" - webpack-sources "^1.0.1" - yargs "^6.0.0" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-fetch@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - -whet.extend@~0.9.9: - version "0.9.9" - resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" - integrity sha1-+HfVv2SMl+WqVC+twW1qJZucEaE= - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= - dependencies: - mkdirp "^0.5.1" - -ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" - integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" - integrity sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw= - dependencies: - camelcase "^3.0.0" - -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^6.0.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" - integrity sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^4.2.0" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" - -zrender@3.7.4: - version "3.7.4" - resolved "https://registry.yarnpkg.com/zrender/-/zrender-3.7.4.tgz#f847d53948481ef6d42906d1ea9aeec7acbefdf2" - integrity sha512-5Nz7+L1wIoL0+Pp/iOP56jD6eD017qC9VRSgUBheXBiAHgOBJZ4uh4/g6e83acIwa8RKSyZf/FlceKu5ntUuxQ== diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 00000000000..d31d4b03235 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,8941 @@ +{ + "name": "frp-web", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frp-web", + "workspaces": [ + "shared", + "frpc", + "frps" + ], + "devDependencies": { + "@vue/test-utils": "2.4.11", + "jsdom": "29.1.1", + "vitest": "4.1.10", + "vue": "^3.5.40" + } + }, + "frpc": { + "name": "frpc-dashboard", + "version": "0.0.1", + "dependencies": { + "element-plus": "^2.14.3", + "pinia": "^3.0.4", + "vue": "^3.5.40", + "vue-router": "^5.2.0" + }, + "devDependencies": { + "@types/node": "24", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.8.1", + "@vueuse/core": "^14.3.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", + "npm-run-all": "^4.1.5", + "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.3.8" + } + }, + "frps": { + "name": "frps-dashboard", + "version": "0.0.1", + "dependencies": { + "element-plus": "^2.14.3", + "vue": "^3.5.40", + "vue-router": "^5.2.0" + }, + "devDependencies": { + "@types/node": "24", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.8.1", + "@vueuse/core": "^14.3.0", + "eslint": "^10.8.0", + "eslint-plugin-vue": "^10.10.0", + "npm-run-all": "^4.1.5", + "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.3.8" + } + }, + "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/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.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.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.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.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "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.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "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", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "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": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "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": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "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": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "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": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "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": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "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", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "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", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/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, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "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", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "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/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", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "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": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "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", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "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": { + "@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" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "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.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "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.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.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "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.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", + "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2" + }, + "peerDependencies": { + "eslint": ">= 8.21.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz", + "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.56.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.56.0", + "vue-eslint-parser": "^10.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "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.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/language-core/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/@vue/reactivity": { + "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.40" + } + }, + "node_modules/@vue/runtime-core": { + "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.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "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.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "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.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "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": { + "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.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": { + "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", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "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.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "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.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" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "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" + }, + "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", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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/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", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "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/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "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": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/c12": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", + "giget": "^2.0.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/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/c12/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/c12/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/c12/node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "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", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "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", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "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.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.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "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", + "vue-component-type-helpers": "^3.3.5" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "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", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/errx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "devOptional": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "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.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", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.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", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "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.9.1", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "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" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "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": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "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.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "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": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "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", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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 + }, + "node_modules/frpc-dashboard": { + "resolved": "frpc", + "link": true + }, + "node_modules/frps-dashboard": { + "resolved": "frps", + "link": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "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", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "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", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "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", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "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/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", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "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", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz", + "integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "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/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==", + "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==", + "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", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "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", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@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", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "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.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "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", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "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", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "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", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/brace-expansion": { + "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": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "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/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "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", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "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==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "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": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "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", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.0.tgz", + "integrity": "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.5" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown-string": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rolldown-string/-/rolldown-string-0.2.1.tgz", + "integrity": "sha512-7H8oH5A8+L96pbBTPCt/rZrwayEhZY5/ejhdk9nRODH32H1v7+bfkaCr+kS15DcGQ7VC1HcWdQVNABFYgrMOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.21" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sass": { + "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": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "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", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "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", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "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", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-literal": { + "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": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "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", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.4.tgz", + "integrity": "sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/terser": { + "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": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "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", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "engines": { + "node": ">=12" + }, + "funding": { + "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", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "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", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unctx": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.5.0.tgz", + "integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21", + "unplugin": "^2.3.11" + } + }, + "node_modules/unctx/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/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", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "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": { + "acorn": "^8.16.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "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": "^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": { + "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/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/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/unimport/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/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": { + "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": ">=18.12.0" + } + }, + "node_modules/unplugin-auto-import": { + "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": { + "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": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^4.0.0", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "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", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-element-plus": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/unplugin-element-plus/-/unplugin-element-plus-0.11.2.tgz", + "integrity": "sha512-jr88ePpv43h8cCmVW0SqM73sTD+g1n9Rmy4uMbTh+pSmceH9ZdKteWX9f+twC4aDlP3svdZuKMqLoUNBT2V6Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^4.2.2", + "es-module-lexer": "^2.0.0", + "escape-string-regexp": "^5.0.0", + "rolldown-string": "^0.2.1", + "unplugin": "^2.3.11" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/unplugin-element-plus/node_modules/@nuxt/kit": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz", + "integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==", + "dev": true, + "license": "MIT", + "dependencies": { + "c12": "^3.3.3", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "errx": "^0.1.0", + "exsolve": "^1.0.8", + "ignore": "^7.0.5", + "jiti": "^2.6.1", + "klona": "^2.0.6", + "mlly": "^1.8.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "rc9": "^3.0.0", + "scule": "^1.3.0", + "semver": "^7.7.4", + "tinyglobby": "^0.2.15", + "ufo": "^1.6.3", + "unctx": "^2.5.0", + "untyped": "^2.0.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-element-plus/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/unplugin-element-plus/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unplugin-element-plus/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-vue-components": { + "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": { + "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": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2 || ^4.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "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": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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": { + "@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/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": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", + "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "defu": "^6.1.4", + "jiti": "^2.4.2", + "knitwork": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "devOptional": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-svg-loader": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.1.tgz", + "integrity": "sha512-RPzcXA/EpKJA0585x58DBgs7my2VfeJ+j2j1EoHY4Zh82Y7hV4cR1fElgy2aZi85+QSrcLLoTStQ5uZjD68u+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "svgo": "^3.3.3" + }, + "peerDependencies": { + "vue": ">=3.2.13" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "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", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "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.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": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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==", + "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", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "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": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-router": { + "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": { + "@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": { + "@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": "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.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.3.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "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/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", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "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", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "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": "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/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/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", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "shared": { + "name": "frp-shared" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000000..a72299797e8 --- /dev/null +++ b/web/package.json @@ -0,0 +1,15 @@ +{ + "name": "frp-web", + "private": true, + "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", + "vue": "^3.5.40" + } +} diff --git a/web/shared/components/ActionButton.vue b/web/shared/components/ActionButton.vue new file mode 100644 index 00000000000..6bbaae828e1 --- /dev/null +++ b/web/shared/components/ActionButton.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/web/shared/components/BaseDialog.vue b/web/shared/components/BaseDialog.vue new file mode 100644 index 00000000000..29dc0496506 --- /dev/null +++ b/web/shared/components/BaseDialog.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/web/shared/components/ConfirmDialog.vue b/web/shared/components/ConfirmDialog.vue new file mode 100644 index 00000000000..4de6975a42a --- /dev/null +++ b/web/shared/components/ConfirmDialog.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/web/shared/components/FilterDropdown.vue b/web/shared/components/FilterDropdown.vue new file mode 100644 index 00000000000..8aedd227972 --- /dev/null +++ b/web/shared/components/FilterDropdown.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/web/shared/components/PopoverMenu.vue b/web/shared/components/PopoverMenu.vue new file mode 100644 index 00000000000..8abd91ed78b --- /dev/null +++ b/web/shared/components/PopoverMenu.vue @@ -0,0 +1,303 @@ + + + + + + + + + diff --git a/web/shared/components/PopoverMenuItem.vue b/web/shared/components/PopoverMenuItem.vue new file mode 100644 index 00000000000..8df1e49b9c4 --- /dev/null +++ b/web/shared/components/PopoverMenuItem.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/web/shared/css/_index.scss b/web/shared/css/_index.scss new file mode 100644 index 00000000000..b70de87b2eb --- /dev/null +++ b/web/shared/css/_index.scss @@ -0,0 +1,2 @@ +@forward './variables'; +@forward './mixins'; diff --git a/web/shared/css/_mixins.scss b/web/shared/css/_mixins.scss new file mode 100644 index 00000000000..3861971bd05 --- /dev/null +++ b/web/shared/css/_mixins.scss @@ -0,0 +1,49 @@ +@use './variables' as vars; + +@mixin mobile { + @media (max-width: #{vars.$breakpoint-mobile - 1px}) { + @content; + } +} + +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin flex-column { + display: flex; + flex-direction: column; +} + +@mixin page-scroll { + height: 100%; + overflow-y: auto; + padding: vars.$spacing-xl 40px; + + > * { + max-width: 960px; + margin: 0 auto; + } + + @include mobile { + padding: vars.$spacing-xl; + } +} + +@mixin custom-scrollbar { + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: #d1d1d1; + border-radius: 3px; + } +} diff --git a/web/shared/css/_variables.scss b/web/shared/css/_variables.scss new file mode 100644 index 00000000000..3df1ef193f8 --- /dev/null +++ b/web/shared/css/_variables.scss @@ -0,0 +1,61 @@ +// Typography +$font-size-xs: 11px; +$font-size-sm: 13px; +$font-size-md: 14px; +$font-size-lg: 15px; +$font-size-xl: 18px; + +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; + +// Colors - Text +$color-text-primary: var(--color-text-primary); +$color-text-secondary: var(--color-text-secondary); +$color-text-muted: var(--color-text-muted); +$color-text-light: var(--color-text-light); + +// Colors - Background +$color-bg-primary: var(--color-bg-primary); +$color-bg-secondary: var(--color-bg-secondary); +$color-bg-tertiary: var(--color-bg-tertiary); +$color-bg-muted: var(--color-bg-muted); +$color-bg-hover: var(--color-bg-hover); +$color-bg-active: var(--color-bg-active); + +// Colors - Border +$color-border: var(--color-border); +$color-border-light: var(--color-border-light); +$color-border-lighter: var(--color-border-lighter); + +// Colors - Status +$color-primary: var(--color-primary); +$color-danger: var(--color-danger); +$color-danger-dark: var(--color-danger-dark); +$color-danger-light: var(--color-danger-light); + +// Colors - Button +$color-btn-primary: var(--color-btn-primary); +$color-btn-primary-hover: var(--color-btn-primary-hover); + +// Spacing +$spacing-xs: 4px; +$spacing-sm: 8px; +$spacing-md: 12px; +$spacing-lg: 16px; +$spacing-xl: 20px; + +// Border Radius +$radius-sm: 6px; +$radius-md: 8px; + +// Transitions +$transition-fast: 0.15s ease; +$transition-medium: 0.2s ease; + +// Layout +$header-height: 50px; +$sidebar-width: 200px; + +// Breakpoints +$breakpoint-mobile: 768px; diff --git a/web/shared/package.json b/web/shared/package.json new file mode 100644 index 00000000000..a2d05f93cd6 --- /dev/null +++ b/web/shared/package.json @@ -0,0 +1,4 @@ +{ + "name": "frp-shared", + "private": true +} diff --git a/web/shared/test/action-button.test.ts b/web/shared/test/action-button.test.ts new file mode 100644 index 00000000000..a38629dac41 --- /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 00000000000..7fef29e13e0 --- /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 00000000000..01421df0d5b --- /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 00000000000..a3ecafc1477 --- /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'], + }, + }, + ], + }, +})