From 217e643faf2df714289312f6bc3ae3da3f7eb776 Mon Sep 17 00:00:00 2001 From: Hendrik Schlehlein Date: Fri, 2 Feb 2024 04:37:30 +0000 Subject: [PATCH] rewrite: everything (#202) * revise: codebase * fix: remove old .vscode and .github dir * feat: cache status response packet * add scripts folder with a simple test script inside of it * simplefy `RespondeToServerRequest` a little * improve readability hash implemetation of status cache * Implement sending proxy protocol header to backend server for login requests. * remove redundant test script * remove pk.WriteTo(hash) statement and remove redundant Sum64 call of hash * don't expose cacheResponse method for now * refactor: one port to bind them all (#197) * refactor: listener to only one * refactor: into methods * feat: add config file loading (#194) * feat: add rate limiter (#201) * feat: add server addresses * feat: add config file loading * fix: proxy protocol Signed-off-by: haveachin * feat: add rate limiter Signed-off-by: haveachin * refactor: middleware to filter Signed-off-by: haveachin * refactor: more testability * refactor: listener to only one * refactor: config files * feat: add config file loading * refactor: filter for only one listener * feat: add docs * docs: add config docs * refactor: file loading * refactor: remove unnecessary mutex * docs: clean up readme * feat: add linting, ci and release automation * docs: add rate limiter * fix: linting errors * fix: linting errors * fix: linting errors * chore: clean up * refactor: ServerRequester * feat: add zerolog * chore: clean up --------- Signed-off-by: haveachin --------- Signed-off-by: haveachin Co-authored-by: realDragonium --- .github/FUNDING.yml | 12 + .github/ISSUE_TEMPLATE/bug_report.md | 27 - .github/ISSUE_TEMPLATE/feature_request.md | 20 - .github/workflows/ci.yml | 52 + .github/workflows/lint.yml | 46 + .github/workflows/release.yml | 49 + .github/workflows/test-build-release.yml | 140 -- .gitignore | 31 +- .golangci.yml | 330 ++++ .goreleaser.yml | 95 ++ CONTRIBUTING.md | 45 + Dockerfile | 12 - LICENSE | 862 +++++++--- Makefile | 33 + README.md | 355 +--- api/api.go | 115 -- build/packages/Dockerfile | 12 + build/packages/Dockerfile.goreleaser | 4 + callback/callback.go | 86 - callback/callback_test.go | 159 -- callback/event.go | 60 - callback/event_test.go | 37 - cmd/infrared/.goreleaser.yml | 48 - cmd/infrared/config.go | 32 + cmd/infrared/main.go | 190 +-- config.go | 476 ------ configs/config.yml | 26 + configs/embed.go | 6 + configs/haproxy.cfg | 37 + configs/proxy.yml | 16 + conn.go | 122 -- deployments/docker-compose.dev.yml | 45 + deployments/docker-compose.yml | 11 + docker-compose.yml | 17 - docs/.vitepress/config.mts | 95 ++ docs/.vitepress/theme/custom.css | 24 + docs/.vitepress/theme/index.js | 5 + docs/assets/agplv3_logo.svg | 28 + docs/assets/logo.svg | 48 + docs/assets/logo_black_text.svg | 60 + docs/assets/logo_white_text.svg | 60 + docs/branding.md | 19 + docs/config/cli-and-env-vars.md | 25 + docs/config/index.md | 18 + docs/config/proxies.md | 21 + docs/features/filters.md | 20 + docs/features/forward-player-ips.md | 16 + docs/features/rate-limit-ips.md | 20 + docs/getting-started.md | 79 + docs/index.md | 29 + docs/package-lock.json | 1506 +++++++++++++++++ docs/package.json | 10 + gateway.go | 219 --- gateway_test.go | 635 ------- go.mod | 37 +- go.sum | 517 +----- grafana/README.md | 38 - grafana/dashboard.json | 741 -------- grafana/dashboard.png | Bin 345785 -> 0 bytes pkg/infrared/config/file.go | 147 ++ pkg/infrared/conn.go | 102 ++ pkg/infrared/filter.go | 65 + pkg/infrared/infrared.go | 317 ++++ pkg/infrared/infrared_test.go | 81 + {protocol => pkg/infrared/protocol}/errors.go | 0 .../handshaking/serverbound_handshake.go | 140 ++ .../handshaking/serverbound_handshake_test.go | 149 +- .../protocol/login/clientbound_disconnect.go | 16 + .../login/clientbound_disconnect_test.go | 17 +- .../login/clientbound_encryptionrequest.go | 32 + .../login/serverbound_encryptionresponse.go | 29 + .../protocol/login/serverbound_loginstart.go | 95 ++ .../login/serverbound_loginstart_test.go | 19 +- pkg/infrared/protocol/packet.go | 118 ++ pkg/infrared/protocol/peeker.go | 40 + .../infrared/protocol}/peeker_test.go | 37 +- .../protocol/play/clientbound_disconnect.go | 16 + .../protocol/status/clientbound_response.go | 98 ++ .../status/clientbound_response_test.go | 28 +- .../protocol/status/serverbound_request.go | 13 + .../status/serverbound_request_test.go | 32 + pkg/infrared/protocol/types.go | 265 +++ pkg/infrared/protocol/types_test.go | 174 ++ pkg/infrared/protocol/versions.go | 31 + pkg/infrared/rate_limiter.go | 245 +++ pkg/infrared/server.go | 315 ++++ process/docker.go | 90 - process/portainer.go | 148 -- process/process.go | 14 - protocol/handshaking/serverbound_handshake.go | 99 -- protocol/login/clientbound_disconnect.go | 18 - protocol/login/serverbound_loginstart.go | 25 - protocol/packet.go | 93 - protocol/packet_test.go | 237 --- protocol/peeker.go | 40 - protocol/status/clientbound_response.go | 61 - protocol/status/serverbound_request.go | 15 - protocol/status/serverbound_request_test.go | 29 - protocol/types.go | 256 --- protocol/types_test.go | 415 ----- proxy.go | 448 ----- tools/dos/main.go | 135 ++ tools/malpk/main.go | 51 + 103 files changed, 6502 insertions(+), 6171 deletions(-) create mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/test-build-release.yml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yml create mode 100644 CONTRIBUTING.md delete mode 100644 Dockerfile create mode 100644 Makefile delete mode 100644 api/api.go create mode 100644 build/packages/Dockerfile create mode 100644 build/packages/Dockerfile.goreleaser delete mode 100644 callback/callback.go delete mode 100644 callback/callback_test.go delete mode 100644 callback/event.go delete mode 100644 callback/event_test.go delete mode 100644 cmd/infrared/.goreleaser.yml create mode 100644 cmd/infrared/config.go delete mode 100644 config.go create mode 100644 configs/config.yml create mode 100644 configs/embed.go create mode 100644 configs/haproxy.cfg create mode 100644 configs/proxy.yml delete mode 100644 conn.go create mode 100644 deployments/docker-compose.dev.yml create mode 100644 deployments/docker-compose.yml delete mode 100644 docker-compose.yml create mode 100644 docs/.vitepress/config.mts create mode 100644 docs/.vitepress/theme/custom.css create mode 100644 docs/.vitepress/theme/index.js create mode 100644 docs/assets/agplv3_logo.svg create mode 100644 docs/assets/logo.svg create mode 100644 docs/assets/logo_black_text.svg create mode 100644 docs/assets/logo_white_text.svg create mode 100644 docs/branding.md create mode 100644 docs/config/cli-and-env-vars.md create mode 100644 docs/config/index.md create mode 100644 docs/config/proxies.md create mode 100644 docs/features/filters.md create mode 100644 docs/features/forward-player-ips.md create mode 100644 docs/features/rate-limit-ips.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json delete mode 100644 gateway.go delete mode 100644 gateway_test.go delete mode 100644 grafana/README.md delete mode 100644 grafana/dashboard.json delete mode 100644 grafana/dashboard.png create mode 100644 pkg/infrared/config/file.go create mode 100644 pkg/infrared/conn.go create mode 100644 pkg/infrared/filter.go create mode 100644 pkg/infrared/infrared.go create mode 100644 pkg/infrared/infrared_test.go rename {protocol => pkg/infrared/protocol}/errors.go (100%) create mode 100644 pkg/infrared/protocol/handshaking/serverbound_handshake.go rename {protocol => pkg/infrared/protocol}/handshaking/serverbound_handshake_test.go (53%) create mode 100644 pkg/infrared/protocol/login/clientbound_disconnect.go rename {protocol => pkg/infrared/protocol}/login/clientbound_disconnect_test.go (65%) create mode 100644 pkg/infrared/protocol/login/clientbound_encryptionrequest.go create mode 100644 pkg/infrared/protocol/login/serverbound_encryptionresponse.go create mode 100644 pkg/infrared/protocol/login/serverbound_loginstart.go rename {protocol => pkg/infrared/protocol}/login/serverbound_loginstart_test.go (56%) create mode 100644 pkg/infrared/protocol/packet.go create mode 100644 pkg/infrared/protocol/peeker.go rename {protocol => pkg/infrared/protocol}/peeker_test.go (79%) create mode 100644 pkg/infrared/protocol/play/clientbound_disconnect.go create mode 100644 pkg/infrared/protocol/status/clientbound_response.go rename {protocol => pkg/infrared/protocol}/status/clientbound_response_test.go (69%) create mode 100644 pkg/infrared/protocol/status/serverbound_request.go create mode 100644 pkg/infrared/protocol/status/serverbound_request_test.go create mode 100644 pkg/infrared/protocol/types.go create mode 100644 pkg/infrared/protocol/types_test.go create mode 100644 pkg/infrared/protocol/versions.go create mode 100644 pkg/infrared/rate_limiter.go create mode 100644 pkg/infrared/server.go delete mode 100644 process/docker.go delete mode 100644 process/portainer.go delete mode 100644 process/process.go delete mode 100644 protocol/handshaking/serverbound_handshake.go delete mode 100644 protocol/login/clientbound_disconnect.go delete mode 100644 protocol/login/serverbound_loginstart.go delete mode 100644 protocol/packet.go delete mode 100644 protocol/packet_test.go delete mode 100644 protocol/peeker.go delete mode 100644 protocol/status/clientbound_response.go delete mode 100644 protocol/status/serverbound_request.go delete mode 100644 protocol/status/serverbound_request_test.go delete mode 100644 protocol/types.go delete mode 100644 protocol/types_test.go delete mode 100644 proxy.go create mode 100644 tools/dos/main.go create mode 100644 tools/malpk/main.go diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..4ac9da1d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: haveachin +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: haveachin +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://paypal.me/hendrikschlehlein # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 70e3a2cf..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -What OS are you using? -What version of Infrared are you using? -What do your Infrared proxy config(s) look like? -How do you trigger the bug/issue? - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** (Optional) -If applicable, add screenshots to help explain your problem. -A copy of your logs would be nice, but please be sure to censor any information that you don't want to be public. - -**Additional context** (Optional) -Add any other context of the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index c9608331..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. E.g.: "I'm always frustrated when [...]" - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots regarding the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..007e7e7e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + branches: + - master + - main + pull_request: + +permissions: + contents: read + +env: + GO_VERSION: 1.21 + +jobs: + test: + name: Test + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + - name: Checkout code + uses: actions/checkout@v3 + - name: Test + run: make test + + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + args: build --snapshot \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..c5ea5b91 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,46 @@ +name: Lint + +on: + push: + branches: + - master + - main + pull_request: + +permissions: + contents: read + pull-requests: read + checks: write + +env: + GO_VERSION: 1.21 + +jobs: + golangci: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Require: The version of golangci-lint to use. + # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. + # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. + version: v1.55.2 + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + only-new-issues: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1d1a4214 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Release + +on: + push: + tags: + - "*" + +permissions: + contents: write + packages: write + +env: + GO_VERSION: 1.21 + +jobs: + release: + name: Build & Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml deleted file mode 100644 index e126208d..00000000 --- a/.github/workflows/test-build-release.yml +++ /dev/null @@ -1,140 +0,0 @@ -name: Test, Build, Release - -on: - push: - branches: [ 'master', 'pre-release' ] - pull_request: - branches: [ 'master', 'dev', 'pre-release' ] - -env: - GH_REGISTRY: ghcr.io - REPOSITORY_NAME: ${{ github.repository }} - -jobs: - - # Build job runs on every PR into dev and master and pushes to master - test: - name: Test - strategy: - matrix: - go-version: [ 1.16.x ] - os: [ ubuntu-latest, macos-latest, windows-latest ] - runs-on: ${{ matrix.os }} - steps: - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Test - run: go test ./... - - # Build job runs on every PR into dev and master and pushes to master - # Depends on test job - build-docker: - runs-on: ubuntu-latest - name: Build - needs: [ test ] - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@master - - - name: Build Docker Images - uses: docker/build-push-action@v2 - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - pull: true - push: false - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - # Release job runs on every push to master - # Depends on build job - release-docker: - runs-on: ubuntu-latest - name: Release - if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/pre-release' }} - needs: [ build-docker ] - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Create semver tag and changelog - - name: Bump version and push tag - id: tag_version - uses: mathieudutour/github-tag-action@v5.6 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - release_branches: master - pre_release_branches: pre-release - - # Build Docker Images - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@master - - - name: Extract metadata for Docker - id: meta - uses: docker/metadata-action@v3 - with: - images: | - ${{ env.GH_REGISTRY }}/${{ env.REPOSITORY_NAME }} - ${{ env.REPOSITORY_NAME }} - tags: | - type=semver,pattern={{version}},value=${{ steps.tag_version.outputs.new_tag }} - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ${{ env.GH_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push version ${{ steps.tag_version.outputs.new_tag }} - uses: docker/build-push-action@v2 - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - push: true - cache-from: type=gha - cache-to: type=gha,mode=max - - # Create GitHub release once images have been pushed - - name: Create a GitHub release ${{ steps.tag_version.outputs.new_tag }} - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.tag_version.outputs.new_tag }} - name: Release ${{ steps.tag_version.outputs.new_tag }} - body: ${{ steps.tag_version.outputs.changelog }} - prerelease: contains( ${{ steps.tag_version.outputs.release_type }} , 'pre' ) - - # Build Go Release - - run: git tag ${{ steps.tag_version.outputs.new_tag }} - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 - - name: Run GoReleaser ${{ steps.tag_version.outputs.new_tag }} - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release - workdir: ./cmd/infrared/. - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 55123b3e..a4636156 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,24 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +# Artifacts +out/ +__debug_bin* +dist/ -# Test binary, build with `go test -c` +# Benchmark/Profiler +*.bench *.test +*.prof -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +# Local Dev Files +.dev/ +cmd/infrared/config.yml -# Config files -*.yaml +# VSCode Dev Files +.vscode/ -.idea +# IntelliJ Dev Files +.idea/ + +# Docs +docs/node_modules/ +docs/.vitepress/cache/ +docs/.vitepress/dist/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..dea3674a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,330 @@ +# This code is licensed under the terms of the MIT license https://opensource.org/license/mit +# Copyright (c) 2021 Marat Reymers + +## Golden config for golangci-lint v1.55.2 +# +# This is the best config for golangci-lint based on my experience and opinion. +# It is very strict, but not extremely strict. +# Feel free to adapt and change it for your needs. + +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and names from check. + # Default: [] + exclude: + # std libs + - "^net/http.Client$" + - "^net/http.Cookie$" + - "^net/http.Request$" + - "^net/http.Response$" + - "^net/http.Server$" + - "^net/http.Transport$" + - "^net/url.URL$" + - "^os/exec.Cmd$" + - "^reflect.StructField$" + # public libs + - "^github.com/Shopify/sarama.Config$" + - "^github.com/Shopify/sarama.ProducerMessage$" + - "^github.com/mitchellh/mapstructure.DecoderConfig$" + - "^github.com/prometheus/client_golang/.+Opts$" + - "^github.com/spf13/cobra.Command$" + - "^github.com/spf13/cobra.CompletionOptions$" + - "^github.com/stretchr/testify/mock.Mock$" + - "^github.com/testcontainers/testcontainers-go.+Request$" + - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" + - "^golang.org/x/tools/go/analysis.Analyzer$" + - "^google.golang.org/protobuf/.+Options$" + - "^gopkg.in/yaml.v3.Node$" + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + # Ignore comments when counting lines. + # Default false + ignore-comments: true + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "gofrs' package is not go module" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - execinquery # checks query string in Query function which reads your Go src files and warning it finds + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoinits # checks that no init functions are present in Go code + - gochecksumtype # checks exhaustiveness on Go "sum types" + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - lll # reports long lines + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - musttag # enforces field tags in (un)marshaled structs + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + #- gci # controls golang package import order and makes it always deterministic + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- godox # detects FIXME, TODO and other comment keywords + #- goheader # checks is file header matches to pattern + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- tagalign # checks that struct tags are well aligned + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + #- gochecknoglobals # checks that no global variables exist + #- gomnd # detects magic numbers + #- testpackage # makes you use a separate _test package + + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- goerr113 # [too strict] checks the errors handling expressions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + + ## deprecated + #- deadcode # [deprecated, replaced by unused] finds unused code + #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized + #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes + #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible + #- interfacer # [deprecated] suggests narrower interface types + #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted + #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name + #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs + #- structcheck # [deprecated, replaced by unused] finds unused struct fields + #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants + + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck + - text: 'shadow: declaration of "(err|ctx)" shadows declaration at' + linters: [ govet ] \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..8ca7cf81 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,95 @@ +# Make sure to check the documentation at https://goreleaser.com + +version: 1 + +env: + - REPO_URL=https://github.com/haveachin/infrared + - DOCKER_IMAGE_NAME=haveachin/infrared + +before: + hooks: + - go mod tidy + +builds: + - main: ./cmd/infrared + env: + - CGO_ENABLED=0 + binary: infrared + goos: + - linux + - windows + - darwin + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +dockers: + - image_templates: + - "docker.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-amd64" + - "docker.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest-amd64" + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-amd64" + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest-amd64" + use: buildx + dockerfile: build/packages/Dockerfile.goreleaser + build_flag_templates: + - "--pull" + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.source={{ .Env.REPO_URL }}" + - image_templates: + - "docker.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-arm64v8" + - "docker.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest-arm64v8" + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-arm64v8" + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest-arm64v8" + use: buildx + goarch: arm64 + dockerfile: build/packages/Dockerfile.goreleaser + build_flag_templates: + - "--pull" + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.source={{ .Env.REPO_URL }}" + +docker_manifests: + - name_template: "{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}" + image_templates: + - "{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-amd64" + - "{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-arm64v8" + - name_template: "{{ .Env.DOCKER_IMAGE_NAME }}:latest" + image_templates: + - "{{ .Env.DOCKER_IMAGE_NAME }}:latest-amd64" + - "{{ .Env.DOCKER_IMAGE_NAME }}:latest-arm64v8" + - name_template: "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}" + image_templates: + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-amd64" + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:{{ .Version }}-arm64v8" + - name_template: "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest" + image_templates: + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest-amd64" + - "ghcr.io/{{ .Env.DOCKER_IMAGE_NAME }}:latest-arm64v8" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^chore:" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3427c012 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing + +Feel free to add or modify the source code. On GitHub the best way of doing this is by forking this repository, then cloning your fork with Git to your local system. After adding or modifying the source code, push it back to your fork and open a pull request in this repository. + +If you can't contribute by adding or modifying the source code, then you might be able to reach out to someone who can. +You can also contribute indirectly by donation. + +## Tools + +- Coding + - [Go](https://go.dev/) + - [GNUMake](https://www.gnu.org/software/make/) (Optional) + - [Docker](https://www.docker.com/get-started/) (Optional) + - [golangci-lint](https://golangci-lint.run/) (Optional) +- Docs + - [Node.js](https://nodejs.org/) + + +## Contributing + +TL;DR + +- [Project Layout](https://github.com/golang-standards/project-layout) - where it makes sense. +- [Code Style](https://github.com/uber-go/guide/blob/master/style.md) - where it makes sense. +- [Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) - only the summary is good enough. +- [Versioning](https://semver.org/) + +### Project Layout + +We try to use [golang-standards/project-layout](https://github.com/golang-standards/project-layout) as a reference. This should give Infrared a good foundation to grow on. + +### Commit Messages + +When contributing to this project please follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +specification for writing commit messages, so that changelogs and release versions can be generated automatically. + +Examples can be found here: https://www.conventionalcommits.org/en/v1.0.0/#examples + +Some tooling that can help you author those commit messages are the following plugins: + +- JetBrains Plugin [Conventional Commit](https://plugins.jetbrains.com/plugin/13389-conventional-commit) + by [Edoardo Luppi](https://github.com/lppedd) +- Visual Studio Code + Plugin [Conventional Commits](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits) + by [vivaxy](https://marketplace.visualstudio.com/publishers/vivaxy) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e4387afc..00000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.16.0-buster AS builder -LABEL stage=intermediate -COPY . /infrared -WORKDIR /infrared/cmd/infrared -ENV GO111MODULE=on -RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a --installsuffix cgo -v -tags netgo -ldflags '-extldflags "-static"' -o /main . - -FROM scratch -LABEL maintainer="Hendrik Jonas Schlehlein " -WORKDIR / -COPY --from=builder /main ./ -ENTRYPOINT [ "./main" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9e..be3f7b28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..43567afb --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: docs + +test: + go test -race -timeout 10s ./... + +build: + CGO_ENABLED=0 go build -ldflags "-s -w" -o ./out/infrared ./cmd/infrared + +all: test build + +run: build + ./out/infrared -w .dev/infrared + +bench: + go test -bench=. -run=x -benchmem -memprofile mem.prof -cpuprofile cpu.prof -benchtime=10s > 0.bench + go tool pprof cpu.prof + +dev: + docker compose -f deployments/docker-compose.dev.yml -p infrared up --force-recreate --remove-orphans + +dos: + CGO_ENABLED=0 go build -ldflags "-s -w" -o ./out/dos ./tools/dos + ./out/dos + +malpk: + CGO_ENABLED=0 go build -ldflags "-s -w" -o ./out/malpk ./tools/malpk + ./out/malpk + +docs: + cd ./docs && npm i && npm run docs:dev + +lint: + golangci-lint run diff --git a/README.md b/README.md index fcf4a3ac..155118b7 100644 --- a/README.md +++ b/README.md @@ -1,319 +1,74 @@

- -

+ +

+

Infrared

+

A Minecraft Reverse Proxy

-[![Discord](https://img.shields.io/discord/800456341088370698?label=discord&logo=discord)](https://discord.gg/r98YPRsZAx) -[![Docker Pulls](https://img.shields.io/docker/pulls/haveachin/infrared?logo=docker)](https://hub.docker.com/r/haveachin/infrared) - -![build](https://github.com/haveachin/infrared/actions/workflows/test-build-release.yml/badge.svg) -[![GitHub](https://img.shields.io/github/license/haveachin/infrared)](https://raw.githubusercontent.com/haveachin/infrared/master/LICENSE) - -# Infrared - a Minecraft Proxy - -An ultra lightweight Minecraft reverse proxy and idle placeholder: +

+ + Discord + + + Docker Pulls + +
+ CI +

+ +> [!WARNING] +> Infrared is currently under active development: breaking changes can happen. +> Feedback and contributions are welcome. + +An ultra lightweight Minecraft reverse proxy and status placeholder: Ever wanted to have only one exposed port on your server for multiple Minecraft servers? Then Infrared is the tool you need! -Infrared works as a reverse proxy using a subdomain to connect clients to a specific Minecraft server. -It works similar to Nginx for those of you who are familiar. +Infrared works as a reverse proxy using a sub-/domains to connect clients to a specific Minecraft server. ## Features -- [x] Reverse Proxy -- [x] Display Placeholder Server -- [x] Autostart Server when pinged -- [x] Logger Callback URLs -- [x] HAProxy Protocol Support -- [x] TCPShield/RealIP Protocol Support -- [X] Prometheus Support -- [X] REST API - -## Deploy - -```shell script -$ docker build --no-cache -t haveachin/infrared:latest https://github.com/haveachin/infrared.git && - docker image prune -f --filter label=stage=intermediate && - docker run -d --name infrared --restart=unless-stopped -it -v /usr/local/infrared/configs/:/configs -p 25565:25565/tcp --expose 25565 haveachin/infrared:latest -``` - -## Update - -```shell script -$ docker build --no-cache -t haveachin/infrared:latest https://github.com/haveachin/infrared.git && - docker image prune -f --filter label=stage=intermediate && - docker stop infrared && - docker rm infrared && - docker run -d --name infrared --restart=unless-stopped -it -v /usr/local/infrared/configs/:/configs -p 25565:25565/tcp --expose 25565 haveachin/infrared:latest -``` - -## Environment Variables - -**Info**: Command-line flags override environment variables. - -`INFRARED_CONFIG_PATH` is the path to all your server configs [default: `"./configs/"`] -`INFRARED_RECEIVE_PROXY_PROTOCOL` if Infrared should be able to receive proxy protocol [default: `"false"`] - -`INFRARED_API_ENABLED` if the api should be enabled [default: `"false"`]\ -`INFRARED_API_BIND` change the http bind option [default: `"127.0.0.1:8080"`] - -`INFRARED_PROMETHEUS_ENABLED` enables the Prometheus stats exporter [default: `"false"`]\ -`INFRARED_PROMETHEUS_BIND` specifies what the Prometheus HTTP server should bind to [default: `":9100"`] - -## Command-Line Flags - -`-config-path` specifies the path to all your server configs [default: `"./configs/"`] - -`-receive-proxy-protocol` if Infrared should be able to receive proxy protocol [default: `false`] - -`-enable-prometheus` enables the Prometheus stats exporter [default: `false`] - -`-prometheus-bind` specifies what the Prometheus HTTP server should bind to [default: `:9100`] - -### Example Usage - -`./infrared -config-path="." -receive-proxy-protocol=true -enable-prometheus -prometheus-bind="localhost:9123"` - -## Proxy Config -| Field Name | Type | Required | Default | Description | -|-------------------|---------|----------|------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| domainName | String | true | localhost | Should be [fully qualified domain name](https://en.wikipedia.org/wiki/Domain_name).
Note: Every string is accepted. So `localhost` is also valid. | -| listenTo | String | true | :25565 | The address (usually just the port; so short term `:port`) that the proxy should listen to for incoming connections.
Accepts basically every address format you throw at it. Valid examples: `:25565`, `localhost:25565`, `0.0.0.0:25565`, `127.0.0.1:25565`, `example.de:25565` | -| proxyTo | String | true | | The address that the proxy should send incoming connections to. Accepts Same formats as the `listenTo` field. | -| proxyBind | String | false | | The local IP that is being used to dail to the server on `proxyTo`. (Same as Nginx `proxy-bind`) | -| disconnectMessage | String | false | Sorry {{username}}, but the server is offline. | The message a client sees when he gets disconnected from Infrared due to the server on `proxyTo` won't respond. Currently available placeholders:
- `username` the username of player that tries to connect
- `now` the current server time
- `remoteAddress` the address of the client that tries to connect
- `localAddress` the local address of the server
- `domain` the domain of the proxy (same as `domainName`)
- `proxyTo` the address that the proxy proxies to (same as `proxyTo`)
- `listenTo` the address that Infrared listens on (same as `listenTo`) | -| timeout | Integer | true | 1000 | The time in milliseconds for the proxy to wait for a ping response before the host (the address you proxyTo) will be declared as offline. This "online check" will be resend for every new connection. | -| spoofForcedHost | String | false | | If Infrared should modify the handshake packet to spoof BungeeCords forced_hosts option. | -| proxyProtocol | Boolean | false | false | If Infrared should use HAProxy's Proxy Protocol for IP **forwarding**.
Warning: You should only ever set this to true if you now that the server you `proxyTo` is compatible. | -| realIp | Boolean | false | false | If Infrared should use TCPShield/RealIP Protocol for IP **forwarding**.
Warning: You should only ever set this to true if you now that the server you `proxyTo` is compatible. | -| docker | Object | false | See [Docker](#Docker) | Optional Docker configuration to automatically start a container and stop it again if unused.
Note: Infrared will not take direct connections into account. Be sure to route all traffic that connects to the container through Infrared. | -| onlineStatus | Object | false | | This is the response that Infrared will give when a client asks for the server status and the server is online. | -| offlineStatus | Object | false | See [Response Status](#response-status) | This is the response that Infrared will give when a client asks for the server status and the server is offline. | -| callbackServer | Object | false | See [Callback Server](#callback-server) | Optional callback server configuration to send events as a POST request to a specified URL. | - -### Docker - -| Field Name | Type | Required | Default | Description | -|---------------|--------|----------|------------|-----------------------------------------------------------------------------| -| dnsServer | String | false | 127.0.0.11 | The address of the DNS that resolves the container names. | -| containerName | String | true | | The name of the container that should be automatically started/stopped. | -| portainer | Object | false | | Optional [Portainer](#Portainer) configuration for authorization management.| - -#### Portainer - -More info on [Portainer](https://www.portainer.io/). - -| Field Name | Type | Required | Default | Description | -|------------|--------|----------|---------|-------------------------------------------------------------------------------| -| address | String | true | | URL of the Portainer instance. | -| endpointId | String | true | | The ID typically an integer of the docker endpoint in the portainer instance. | -| username | String | true | | Username for the Portainer user. | -| password | String | true | | Password for the Portainer user. | - -### Response Status - -| Field Name | Type | Required | Default | Description | -|----------------|---------|----------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| versionName | String | false | Infrared 1.18 | The version name of the Minecraft Server. | -| protocolNumber | Integer | true | 757 | The protocol version number. | -| maxPlayers | Integer | false | 20 | The maximum number of players that can join the server.
Note: Infrared will not limit more players from joining. This number is just for display. | -| playersOnline | Integer | false | 0 | The number of online players.
Note: Infrared will not that this number is also just for display. | -| playerSamples | Array | false | | An array of player samples. See [Player Sample](#Player Sample). | -| iconPath | String | false | | The path to the server icon. | -| motd | String | false | | The motto of the day, short MOTD. | - -#### Player Sample - -| Field Name | Type | Required | Default | Description | -|------------|--------|----------|---------|-------------------------| -| Name | String | true | | Username of the player. | -| uuid | String | false | | UUID of the player. | - -### Callback Server - -| Field Name | Type | Required | Default | Description | -|------------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| url | String | true | | URL of the callback server URL. | -| events | Array | true | | A string array of event names. Currently available event names are:
- `Error` will send error logs
- `PlayerJoin` will send player joins
- `PlayerLeave` will send player leaves
- `ContainerStart` will send container starts
- `ContainerStop` will send container stops | - -### Examples +- [X] Reverse Proxy + - [X] Wildcards Support + - [X] Multi-Domain Support +- [X] Status Response Caching +- [X] Proxy Protocol Support +- [X] Ratelimiter -#### Minimal Config +## Useful Links -
-min.example.com +- **[Docs](https://infrared.dev)** +- **[Ask Questions](https://github.com/haveachin/infrared/discussions)** +- [Latest Release](https://github.com/haveachin/infrared/releases/latest) +- [Discord Invite](https://discord.gg/r98YPRsZAx) +- [Contributing](CONTRIBUTING.md) -```json -{ - "domainName": "mc.example.com", - "proxyTo": ":8080" -} -``` - -
- -#### Full Config - -
-full.example.com - -```json -{ - "domainName": "mc.example.com", - "listenTo": ":25565", - "proxyTo": ":8080", - "proxyBind": "0.0.0.0", - "proxyProtocol": false, - "realIp": false, - "timeout": 1000, - "disconnectMessage": "Username: {{username}}\nNow: {{now}}\nRemoteAddress: {{remoteAddress}}\nLocalAddress: {{localAddress}}\nDomain: {{domain}}\nProxyTo: {{proxyTo}}\nListenTo: {{listenTo}}", - "docker": { - "dnsServer": "127.0.0.11", - "containerName": "mc", - "timeout": 30000, - "portainer": { - "address": "localhost:9000", - "endpointId": "1", - "username": "admin", - "password": "foobar" - } - }, - "onlineStatus": { - "versionName": "1.18", - "protocolNumber": 757, - "maxPlayers": 20, - "playersOnline": 2, - "playerSamples": [ - { - "name": "Steve", - "uuid": "8667ba71-b85a-4004-af54-457a9734eed7" - }, - { - "name": "Alex", - "uuid": "ec561538-f3fd-461d-aff5-086b22154bce" - } - ], - "motd": "Join us!" - }, - "offlineStatus": { - "versionName": "1.18", - "protocolNumber": 757, - "maxPlayers": 20, - "playersOnline": 0, - "motd": "Server is currently offline" - }, - "callbackServer": { - "url": "https://mc.example.com/callback", - "events": [ - "Error", - "PlayerJoin", - "PlayerLeave", - "ContainerStart", - "ContainerStop" - ] - } -} -``` - -
- -## Rest API - -**The API should not be accessible from the internet!** +## Build -### Enabling API +Requirements: +- [Go](https://go.dev/) 1.21+ -To enable the API the environment variable `INFRARED_API_ENABLED` must be set to `"true"`. To change the http bind, set -the env variable `INFRARED_API_BIND` to something like `"0.0.0.0:3000"` the default value is `"127.0.0.1:8080"` - -### API Methods - -#### Create new config - -POST `/proxies`\ -Body must contain: -```json -{ -"domainName": "mc.example.com", -"proxyTo": ":8080" -} ``` -But all values (like in a normal config file) can be set. - -The API then will create a file with the name of the domain (if the file exists it will be overwritten) and write the body to it. The proxy can now be visited. - ------ -POST `/proxies/{fileName}`\ -Body must contain: -```json -{ -"domainName": "mc.example.com", -"proxyTo": ":8080" -} +CGO_ENABLED=0 go build -ldflags "-s -w" -o ./out/infrared ./cmd/infrared ``` -But all values (like in a normal config file) can be set. - -The server will create a file with the given filename (if the file exists it will be overwritten) and store the config in it. - - -### Remove config -DELETE `/proxies/{fileName}`\ -Replace `:file` with the name of the proxy configuration file. - -If the file was found it will be unloaded and deleted. Open connections do not close, but no new player can connect anymore. +or `make all` (requires GNU Make). The binary is in the `out/` directory. -## Prometheus exporter -The built-in prometheus exporter can be used to view metrics about infrareds operation. -When the command line flag `-enable-prometheus` is enabled it will bind to `:9100` by default, if you would like to use another port or use an application like [node_exporter](https://github.com/prometheus/node_exporter) that also uses port 9100 on the same machine you can change the port with the `-prometheus-bind` command line flag, example: `-prometheus-bind=":9070"`. -It is recommended to firewall the prometheus exporter with an application like *ufw* or *iptables* to make it only accessible by your own Prometheus instance. -### Prometheus configuration: -Example prometheus.yml configuration: -```yaml -scrape_configs: - - job_name: infrared - static_configs: - - targets: ['infrared-exporter-hostname:port'] -``` - -### Metrics: -* infrared_connected: show the amount of connected players per instance and proxy: - * **Example response:** `infrared_connected{host="proxy.example.com",instance="vps1.example.com:9070",job="infrared"} 10` - * **host:** listenTo domain as specified in the infrared configuration. - * **instance:** what infrared instance the amount of players are connected to. - * **job:** what job was specified in the prometheus configuration. -* infrared_proxies: show the amount of active infrared proxies: - * **Example response:** `infrared_proxies{instance="vps1.example.com:9070",job="infrared"} 5` - * **instance:** what infrared instance has that amount of active proxies. - * **job:** what job was specified in the prometheus configuration. - -## Coding Guidelines - -### Commit Messages - -When contributing to this project please follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) -specification for writing commit messages, so that changelogs and release versions can be generated automatically. - -**Example commit message** - -``` -fix: prevent racing of requests - -Introduce a request id and a reference to latest request. Dismiss -incoming responses other than from latest request. - -Remove timeouts which were used to mitigate the racing issue but are -obsolete now. - -Reviewed-by: Z -Refs: #123 -``` +## Similar Projects -Some tooling that can help you author those commit messages are the following plugins: +* https://github.com/itzg/mc-router -* JetBrains Plugin [Conventional Commit](https://plugins.jetbrains.com/plugin/13389-conventional-commit) - by [Edoardo Luppi](https://github.com/lppedd) -* Visual Studio - Plugin [Conventional Commits](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits) - by [vivaxy](https://marketplace.visualstudio.com/publishers/vivaxy) +## Attributions -## Similar Projects +- [Free Software Foundation](https://commons.wikimedia.org/wiki/File:AGPLv3_Logo.svg), Public domain, via Wikimedia Commons +- [Tnze/go-mc](https://github.com/Tnze/go-mc) 🚀, MIT +- [IGLOU-EU/go-wildcard](https://github.com/IGLOU-EU/go-wildcard), Apache-2.0 +- [cespare/xxhash](https://github.com/cespare/xxhash), MIT +- [google/uuid](https://github.com/google/uuid), BSD-3-Clause +- [pires/go-proxyproto](https://github.com/pires/go-proxyproto), Apache-2.0 +- [spf13/pflag](https://github.com/spf13/pflag), BSD-3-Clause +- [go-yaml/yaml](https://github.com/go-yaml/yaml), Apache-2.0, MIT +- [vitepress](https://github.com/vuejs/vitepress), MIT +- [tollbooth](https://github.com/didip/tollbooth), MIT -* https://github.com/itzg/mc-router +
+

+ +

\ No newline at end of file diff --git a/api/api.go b/api/api.go deleted file mode 100644 index bffef8bf..00000000 --- a/api/api.go +++ /dev/null @@ -1,115 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/haveachin/infrared" - "io/ioutil" - "log" - "net/http" - "os" -) - -// ListenAndServe StartWebserver Start Webserver if environment variable "api-enable" is set to true -func ListenAndServe(configPath string, apiBind string) { - fmt.Println("Starting WebAPI on " + apiBind) - router := chi.NewRouter() - router.Use(middleware.Logger) - - router.Post("/proxies", addProxy(configPath)) - router.Post("/proxies/{fileName}", addProxyWithName(configPath)) - router.Delete("/proxies/{fileName}", removeProxy(configPath)) - - err := http.ListenAndServe(apiBind, router) - if err != nil { - log.Fatal(err) - return - } -} - -func addProxy(configPath string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - rawData, err := ioutil.ReadAll(r.Body) - if err != nil || string(rawData) == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - jsonIsValid := checkJSONAndRegister(rawData, "", configPath) - if jsonIsValid { - w.WriteHeader(http.StatusOK) - return - } else { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("{'error': 'domainName and proxyTo could not be found'}")) - return - } - } -} - -func addProxyWithName(configPath string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - fileName := chi.URLParam(r, "fileName") - - rawData, err := ioutil.ReadAll(r.Body) - if err != nil || string(rawData) == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - jsonIsValid := checkJSONAndRegister(rawData, fileName, configPath) - if jsonIsValid { - w.WriteHeader(http.StatusOK) - return - } else { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("{'error': 'domainName and proxyTo could not be found'}")) - return - } - } -} - -func removeProxy(configPath string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - file := chi.URLParam(r, "fileName") - fmt.Println(file) - - err := os.Remove(configPath + "/" + file) - if err != nil { - w.WriteHeader(http.StatusNoContent) - w.Write([]byte(err.Error())) - return - } - } -} - -// Helper method to check for domainName and proxyTo in a given JSON array -// If the filename is empty the domain will be used as the filename - files with the same name will be overwritten -func checkJSONAndRegister(rawData []byte, filename string, configPath string) (successful bool) { - var cfg infrared.ProxyConfig - err := json.Unmarshal(rawData, &cfg) - if err != nil { - fmt.Println(err) - return false - } - - if cfg.DomainName == "" || cfg.ProxyTo == "" { - return false - } - - path := configPath + "/" + filename - // If fileName is empty use domainName as filename - if filename == "" { - path = configPath + "/" + cfg.DomainName - } - - err = os.WriteFile(path, rawData, 0644) - if err != nil { - fmt.Println(err) - return false - } - - return true -} diff --git a/build/packages/Dockerfile b/build/packages/Dockerfile new file mode 100644 index 00000000..10c82d2d --- /dev/null +++ b/build/packages/Dockerfile @@ -0,0 +1,12 @@ +FROM --platform=$BUILDPLATFORM golang:alpine AS builder +ARG TARGETARCH +WORKDIR /tmp/build +COPY . /tmp/build +ENV GO111MODULE=on +RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -ldflags "-s -w" -o ./out/infrared ./cmd/infrared +RUN chmod +x ./out/infrared + +FROM alpine:latest +COPY --from=builder /tmp/build/out/infrared /usr/bin/ +WORKDIR /etc/infrared +ENTRYPOINT [ "/usr/bin/infrared" ] \ No newline at end of file diff --git a/build/packages/Dockerfile.goreleaser b/build/packages/Dockerfile.goreleaser new file mode 100644 index 00000000..4c3783dc --- /dev/null +++ b/build/packages/Dockerfile.goreleaser @@ -0,0 +1,4 @@ +FROM alpine:latest +COPY infrared /usr/bin/infrared +WORKDIR /etc/infrared +ENTRYPOINT [ "/usr/bin/infrared" ] \ No newline at end of file diff --git a/callback/callback.go b/callback/callback.go deleted file mode 100644 index 2b14684e..00000000 --- a/callback/callback.go +++ /dev/null @@ -1,86 +0,0 @@ -package callback - -import ( - "bytes" - "encoding/json" - "net/http" - "time" -) - -// HTTPClient represents an interface for the Logger to log events with. -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} - -// EventLog -type EventLog struct { - Event string `json:"event"` - Timestamp time.Time `json:"timestamp"` - Payload interface{} `json:"payload"` -} - -func newEventLog(event Event) EventLog { - return EventLog{ - Event: event.EventType(), - Timestamp: time.Now(), - Payload: event, - } -} - -// Logger can post events to an http endpoint -type Logger struct { - client HTTPClient - - URL string - Events []string -} - -func (logger Logger) isValid() bool { - return logger.URL != "" && len(logger.Events) > 0 -} - -// hasEvent checks if Logger.Events contain the given event's type. -func (logger Logger) hasEvent(event Event) bool { - hasEvent := false - for _, e := range logger.Events { - if e == event.EventType() { - hasEvent = true - break - } - } - return hasEvent -} - -// LogEvent posts the given event to an http endpoint if the Logger -// holds a valid URL and the Logger.Events contains given event's type. -func (logger Logger) LogEvent(event Event) (*EventLog, error) { - if logger.client == nil { - logger.client = http.DefaultClient - } - - if !logger.isValid() { - return nil, nil - } - - if !logger.hasEvent(event) { - return nil, nil - } - - eventLog := newEventLog(event) - bb, err := json.Marshal(eventLog) - if err != nil { - return nil, err - } - - request, err := http.NewRequest(http.MethodPost, logger.URL, bytes.NewReader(bb)) - if err != nil { - return nil, err - } - - _, err = logger.client.Do(request) - if err != nil { - return nil, err - } - - return &eventLog, nil -} diff --git a/callback/callback_test.go b/callback/callback_test.go deleted file mode 100644 index ea76f578..00000000 --- a/callback/callback_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package callback - -import ( - "bytes" - "encoding/json" - "net/http" - "testing" -) - -func TestLogger_IsValid(t *testing.T) { - tt := []struct { - logger Logger - result bool - }{ - { - logger: Logger{ - URL: "something", - Events: []string{EventTypeError, EventTypePlayerJoin, EventTypeContainerStart}, - }, - result: true, - }, - { - logger: Logger{ - URL: "something", - Events: []string{EventTypeError}, - }, - result: true, - }, - { - logger: Logger{ - URL: "something", - }, - result: false, - }, - { - logger: Logger{ - Events: []string{EventTypeError}, - }, - result: false, - }, - { - logger: Logger{}, - result: false, - }, - } - - for _, tc := range tt { - if tc.logger.isValid() != tc.result { - t.Fail() - } - } -} - -func TestLogger_HasEvent(t *testing.T) { - tt := []struct { - logger Logger - event Event - result bool - }{ - { - logger: Logger{ - Events: []string{EventTypeError, EventTypePlayerJoin, EventTypeContainerStart}, - }, - event: PlayerJoinEvent{}, - result: true, - }, - { - logger: Logger{ - Events: []string{EventTypeError}, - }, - event: PlayerJoinEvent{}, - result: false, - }, - } - - for _, tc := range tt { - if tc.logger.hasEvent(tc.event) != tc.result { - t.Fail() - } - } -} - -type mockHTTPClient struct { - *testing.T - method string - url string - body *bytes.Buffer -} - -func (mock *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { - if req.Method != mock.method { - mock.Fail() - } - - if req.URL.String() != mock.url { - mock.Fail() - } - - _, err := mock.body.ReadFrom(req.Body) - if err != nil { - mock.Error(err) - } - - return nil, nil -} - -func TestLogger_LogEvent(t *testing.T) { - tt := []struct { - logger Logger - event Event - }{ - { - logger: Logger{ - URL: "https://example.com", - Events: []string{EventTypeError}, - }, - event: ErrorEvent{ - Error: "my error message", - ProxyUID: "example.com@1.2.3.4:25565", - }, - }, - { - logger: Logger{ - URL: "https://example.com", - Events: []string{EventTypePlayerJoin, EventTypePlayerLeave}, - }, - event: PlayerJoinEvent{ - Username: "notch", - RemoteAddress: "1.2.3.4", - TargetAddress: "1.2.3.4", - ProxyUID: "example.com@1.2.3.4:25565", - }, - }, - } - - for _, tc := range tt { - body := bytes.Buffer{} - tc.logger.client = &mockHTTPClient{ - T: t, - method: http.MethodPost, - url: tc.logger.URL, - body: &body, - } - - eventLog, err := tc.logger.LogEvent(tc.event) - if err != nil { - t.Error(err) - } - - bb, err := json.Marshal(eventLog) - if err != nil { - t.Error(err) - } - - if !bytes.Equal(body.Bytes(), bb) { - t.Fail() - } - } -} diff --git a/callback/event.go b/callback/event.go deleted file mode 100644 index e5d6373c..00000000 --- a/callback/event.go +++ /dev/null @@ -1,60 +0,0 @@ -package callback - -const ( - EventTypeError string = "Error" - EventTypePlayerJoin string = "PlayerJoin" - EventTypePlayerLeave string = "PlayerLeave" - EventTypeContainerStart string = "ContainerStart" - EventTypeContainerStop string = "ContainerStop" -) - -type Event interface { - EventType() string -} - -type ErrorEvent struct { - Error string `json:"error"` - ProxyUID string `json:"proxyUid"` -} - -func (event ErrorEvent) EventType() string { - return EventTypeError -} - -type PlayerJoinEvent struct { - Username string `json:"username"` - RemoteAddress string `json:"remoteAddress"` - TargetAddress string `json:"targetAddress"` - ProxyUID string `json:"proxyUid"` -} - -func (event PlayerJoinEvent) EventType() string { - return EventTypePlayerJoin -} - -type PlayerLeaveEvent struct { - Username string `json:"username"` - RemoteAddress string `json:"remoteAddress"` - TargetAddress string `json:"targetAddress"` - ProxyUID string `json:"proxyUid"` -} - -func (event PlayerLeaveEvent) EventType() string { - return EventTypePlayerLeave -} - -type ContainerStartEvent struct { - ProxyUID string `json:"proxyUid"` -} - -func (event ContainerStartEvent) EventType() string { - return EventTypeContainerStart -} - -type ContainerStopEvent struct { - ProxyUID string `json:"proxyUid"` -} - -func (event ContainerStopEvent) EventType() string { - return EventTypeContainerStop -} diff --git a/callback/event_test.go b/callback/event_test.go deleted file mode 100644 index 4c93e19c..00000000 --- a/callback/event_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package callback - -import "testing" - -func TestErrorEvent_EventType(t *testing.T) { - tt := []struct { - event Event - eventType string - }{ - { - event: ErrorEvent{}, - eventType: EventTypeError, - }, - { - event: PlayerJoinEvent{}, - eventType: EventTypePlayerJoin, - }, - { - event: PlayerLeaveEvent{}, - eventType: EventTypePlayerLeave, - }, - { - event: ContainerStartEvent{}, - eventType: EventTypeContainerStart, - }, - { - event: ContainerStopEvent{}, - eventType: EventTypeContainerStop, - }, - } - - for _, tc := range tt { - if tc.event.EventType() != tc.eventType { - t.Fail() - } - } -} diff --git a/cmd/infrared/.goreleaser.yml b/cmd/infrared/.goreleaser.yml deleted file mode 100644 index 9913b707..00000000 --- a/cmd/infrared/.goreleaser.yml +++ /dev/null @@ -1,48 +0,0 @@ -env: - - GO111MODULE=on - - GOPROXY=https://proxy.golang.org -before: - hooks: - - go mod download -builds: - - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - - windows - goarch: - - 386 - - amd64 - - arm - - arm64 - ignore: - - goos: darwin - goarch: 386 - mod_timestamp: '{{ .CommitTimestamp }}' - flags: - - -trimpath - ldflags: - - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser -checksum: - name_template: '{{ .ProjectName }}_checksums.txt' -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' - - Merge pull request - - Merge branch - - go mod tidy -archives: - - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 - format_overrides: - - goos: windows - format: zip \ No newline at end of file diff --git a/cmd/infrared/config.go b/cmd/infrared/config.go new file mode 100644 index 00000000..ba41533d --- /dev/null +++ b/cmd/infrared/config.go @@ -0,0 +1,32 @@ +package main + +import ( + "errors" + "os" + + "github.com/haveachin/infrared/configs" +) + +func createConfigIfNotExist() error { + info, err := os.Stat(configPath) + if errors.Is(err, os.ErrNotExist) { + if err = os.Mkdir(proxiesDir, 0755); err != nil { + return err + } + + return createDefaultConfigFile() + } else if err != nil { + return err + } + + if info.IsDir() { + return errors.New("ir.Config is a directory") + } + + return nil +} + +func createDefaultConfigFile() error { + bb := configs.DefaultInfraredConfig + return os.WriteFile(configPath, bb, 0600) +} diff --git a/cmd/infrared/main.go b/cmd/infrared/main.go index 3501aae7..2eb6098b 100644 --- a/cmd/infrared/main.go +++ b/cmd/infrared/main.go @@ -1,138 +1,128 @@ package main import ( - "flag" - "log" "os" - "strconv" - - "github.com/haveachin/infrared/api" - - "github.com/haveachin/infrared" + "os/signal" + "syscall" + "time" + + ir "github.com/haveachin/infrared/pkg/infrared" + "github.com/haveachin/infrared/pkg/infrared/config" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/pflag" ) const ( - envPrefix = "INFRARED_" - envConfigPath = envPrefix + "CONFIG_PATH" - envReceiveProxyProtocol = envPrefix + "RECEIVE_PROXY_PROTOCOL" - envApiEnabled = envPrefix + "API_ENABLED" - envApiBind = envPrefix + "API_BIND" - envPrometheusEnabled = envPrefix + "PROMETHEUS_ENABLED" - envPrometheusBind = envPrefix + "PROMETHEUS_BIND" -) - -const ( - clfConfigPath = "config-path" - clfReceiveProxyProtocol = "receive-proxy-protocol" - clfPrometheusEnabled = "enable-prometheus" - clfPrometheusBind = "prometheus-bind" + envVarPrefix = "INFRARED" ) var ( - configPath = "./configs" - receiveProxyProtocol = false - prometheusEnabled = false - prometheusBind = ":9100" - apiEnabled = false - apiBind = "127.0.0.1:8080" + configPath = "config.yml" + workingDir = "." + proxiesDir = "./proxies" + logLevel = "info" ) -func envBool(name string, value bool) bool { - envString := os.Getenv(name) - if envString == "" { - return value +func envVarString(p *string, name string) { + key := envVarPrefix + "_" + name + v := os.Getenv(key) + if v == "" { + return } + *p = v +} - envBool, err := strconv.ParseBool(envString) - if err != nil { - return value - } +func initEnvVars() { + envVarString(&configPath, "CONFIG") + envVarString(&workingDir, "WORKING_DIR") + envVarString(&proxiesDir, "PROXIES_DIR") + envVarString(&logLevel, "LOG_LEVEL") +} - return envBool +func initFlags() { + pflag.StringVarP(&configPath, "config", "c", configPath, "path to the config file") + pflag.StringVarP(&workingDir, "working-dir", "w", workingDir, "changes the current working directory") + pflag.StringVarP(&proxiesDir, "proxies-dir", "p", proxiesDir, "path to the proxies directory") + pflag.StringVarP(&logLevel, "log-level", "l", logLevel, "log level [debug, info, warn, error]") + pflag.Parse() } -func envString(name string, value string) string { - envString := os.Getenv(name) - if envString == "" { - return value +func initLogger() { + log.Logger = log.Output(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }) + + var level zerolog.Level + switch logLevel { + case "debug": + level = zerolog.DebugLevel + case "info": + level = zerolog.InfoLevel + case "warn": + level = zerolog.WarnLevel + case "error": + level = zerolog.ErrorLevel + default: + log.Warn(). + Str("level", logLevel). + Msg("Invalid log level; defaulting to info") } - return envString + zerolog.SetGlobalLevel(level) + log.Debug(). + Str("level", logLevel). + Msg("Log level set") } -func initEnv() { - configPath = envString(envConfigPath, configPath) - receiveProxyProtocol = envBool(envReceiveProxyProtocol, receiveProxyProtocol) - apiEnabled = envBool(envApiEnabled, apiEnabled) - apiBind = envString(envApiBind, apiBind) - prometheusEnabled = envBool(envPrometheusEnabled, prometheusEnabled) - prometheusBind = envString(envPrometheusBind, prometheusBind) -} +func main() { + initEnvVars() + initFlags() + initLogger() -func initFlags() { - flag.StringVar(&configPath, clfConfigPath, configPath, "path of all proxy configs") - flag.BoolVar(&receiveProxyProtocol, clfReceiveProxyProtocol, receiveProxyProtocol, "should accept proxy protocol") - flag.BoolVar(&prometheusEnabled, clfPrometheusEnabled, prometheusEnabled, "should run prometheus client exposing metrics") - flag.StringVar(&prometheusBind, clfPrometheusBind, prometheusBind, "bind address and/or port for prometheus") - flag.Parse() -} + log.Info().Msg("Starting Infrared") -func init() { - initEnv() - initFlags() + if err := run(); err != nil { + log.Fatal(). + Err(err). + Msg("Failed to run") + } } -func main() { - log.Println("Loading proxy configs") - - cfgs, err := infrared.LoadProxyConfigsFromPath(configPath, false) - if err != nil { - log.Printf("Failed loading proxy configs from %s; error: %s", configPath, err) - return +func run() error { + if err := os.Chdir(workingDir); err != nil { + return err } - var proxies []*infrared.Proxy - for _, cfg := range cfgs { - proxies = append(proxies, &infrared.Proxy{ - Config: cfg, - }) + if err := createConfigIfNotExist(); err != nil { + return err } - outCfgs := make(chan *infrared.ProxyConfig) - go func() { - if err := infrared.WatchProxyConfigFolder(configPath, outCfgs); err != nil { - log.Println("Failed watching config folder; error:", err) - log.Println("SYSTEM FAILURE: CONFIG WATCHER FAILED") - } - }() + srv := ir.NewWithConfigProvider(config.FileProvider{ + ConfigPath: configPath, + ProxiesPath: proxiesDir, + }) + srv.Logger = log.Logger - gateway := infrared.Gateway{ReceiveProxyProtocol: receiveProxyProtocol} + errChan := make(chan error, 1) go func() { - for { - cfg, ok := <-outCfgs - if !ok { - return - } - - proxy := &infrared.Proxy{Config: cfg} - if err := gateway.RegisterProxy(proxy); err != nil { - log.Println("Failed registering proxy; error:", err) - } - } + errChan <- srv.ListenAndServe() }() - if apiEnabled { - go api.ListenAndServe(configPath, apiBind) - } + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - if prometheusEnabled { - gateway.EnablePrometheus(prometheusBind) - } + log.Info().Msg("System is online") - log.Println("Starting Infrared") - if err := gateway.ListenAndServe(proxies); err != nil { - log.Fatal("Gateway exited; error: ", err) + select { + case sig := <-sigChan: + log.Printf("Received %s", sig.String()) + case err := <-errChan: + if err != nil { + return err + } } - gateway.KeepProcessActive() + return nil } diff --git a/config.go b/config.go deleted file mode 100644 index 07728085..00000000 --- a/config.go +++ /dev/null @@ -1,476 +0,0 @@ -package infrared - -import ( - "bufio" - "encoding/base64" - "encoding/json" - "fmt" - "io/fs" - "io/ioutil" - "log" - "net" - "os" - "path/filepath" - "sync" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/haveachin/infrared/process" - "github.com/haveachin/infrared/protocol" - "github.com/haveachin/infrared/protocol/status" -) - -// ProxyConfig is a data representation of a Proxy configuration -type ProxyConfig struct { - sync.RWMutex - watcher *fsnotify.Watcher - - removeCallback func() - changeCallback func() - dialer *Dialer - process process.Process - - DomainName string `json:"domainName"` - ListenTo string `json:"listenTo"` - ProxyTo string `json:"proxyTo"` - ProxyBind string `json:"proxyBind"` - SpoofForcedHost string `json:"spoofForcedHost"` - ProxyProtocol bool `json:"proxyProtocol"` - RealIP bool `json:"realIp"` - Timeout int `json:"timeout"` - DisconnectMessage string `json:"disconnectMessage"` - Docker DockerConfig `json:"docker"` - OnlineStatus StatusConfig `json:"onlineStatus"` - OfflineStatus StatusConfig `json:"offlineStatus"` - CallbackServer CallbackServerConfig `json:"callbackServer"` -} - -func (cfg *ProxyConfig) Dialer() (*Dialer, error) { - if cfg.dialer != nil { - return cfg.dialer, nil - } - - cfg.dialer = &Dialer{ - Dialer: net.Dialer{ - Timeout: time.Millisecond * time.Duration(cfg.Timeout), - LocalAddr: &net.TCPAddr{ - IP: net.ParseIP(cfg.ProxyBind), - }, - }, - } - return cfg.dialer, nil -} - -type DockerConfig struct { - DNSServer string `json:"dnsServer"` - ContainerName string `json:"containerName"` - Timeout int `json:"timeout"` - Portainer struct { - Address string `json:"address"` - EndpointID string `json:"endpointId"` - Username string `json:"username"` - Password string `json:"password"` - } `json:"portainer"` -} - -func (docker DockerConfig) IsDocker() bool { - return docker.ContainerName != "" -} - -func (docker DockerConfig) IsPortainer() bool { - return docker.ContainerName != "" && - docker.Portainer.Address != "" && - docker.Portainer.EndpointID != "" -} - -type PlayerSample struct { - Name string `json:"name"` - UUID string `json:"uuid"` -} - -type StatusConfig struct { - cachedPacket *protocol.Packet - - VersionName string `json:"versionName"` - ProtocolNumber int `json:"protocolNumber"` - MaxPlayers int `json:"maxPlayers"` - PlayersOnline int `json:"playersOnline"` - PlayerSamples []PlayerSample `json:"playerSamples"` - IconPath string `json:"iconPath"` - MOTD string `json:"motd"` -} - -func (cfg StatusConfig) StatusResponsePacket() (protocol.Packet, error) { - if cfg.cachedPacket != nil { - return *cfg.cachedPacket, nil - } - - var samples []status.PlayerSampleJSON - for _, sample := range cfg.PlayerSamples { - samples = append(samples, status.PlayerSampleJSON{ - Name: sample.Name, - ID: sample.UUID, - }) - } - - responseJSON := status.ResponseJSON{ - Version: status.VersionJSON{ - Name: cfg.VersionName, - Protocol: cfg.ProtocolNumber, - }, - Players: status.PlayersJSON{ - Max: cfg.MaxPlayers, - Online: cfg.PlayersOnline, - Sample: samples, - }, - Description: status.DescriptionJSON{ - Text: cfg.MOTD, - }, - } - - if cfg.IconPath != "" { - img64, err := loadImageAndEncodeToBase64String(cfg.IconPath) - if err != nil { - return protocol.Packet{}, err - } - responseJSON.Favicon = fmt.Sprintf("data:image/png;base64,%s", img64) - } - - bb, err := json.Marshal(responseJSON) - if err != nil { - return protocol.Packet{}, err - } - - packet := status.ClientBoundResponse{ - JSONResponse: protocol.String(bb), - }.Marshal() - - cfg.cachedPacket = &packet - return packet, nil -} - -func loadImageAndEncodeToBase64String(path string) (string, error) { - if path == "" { - return "", nil - } - - imgFile, err := os.Open(path) - if err != nil { - return "", err - } - defer imgFile.Close() - - fileInfo, err := imgFile.Stat() - if err != nil { - return "", err - } - - buffer := make([]byte, fileInfo.Size()) - fileReader := bufio.NewReader(imgFile) - _, err = fileReader.Read(buffer) - if err != nil { - return "", nil - } - - return base64.StdEncoding.EncodeToString(buffer), nil -} - -type CallbackServerConfig struct { - URL string `json:"url"` - Events []string `json:"events"` -} - -func DefaultProxyConfig() ProxyConfig { - return ProxyConfig{ - DomainName: "localhost", - ListenTo: ":25565", - Timeout: 1000, - DisconnectMessage: "Sorry {{username}}, but the server is offline.", - Docker: DockerConfig{ - DNSServer: "127.0.0.11", - Timeout: 300000, - }, - OfflineStatus: StatusConfig{ - VersionName: "Infrared 1.18", - ProtocolNumber: 757, - MaxPlayers: 20, - MOTD: "Powered by Infrared", - }, - } -} - -func ReadFilePaths(path string, recursive bool) ([]string, error) { - if recursive { - return readFilePathsRecursively(path) - } - - return readFilePaths(path) -} - -func readFilePathsRecursively(path string) ([]string, error) { - var filePaths []string - - err := filepath.WalkDir(path, func(path string, dir fs.DirEntry, err error) error { - if err != nil { - return err - } - - if dir.IsDir() { - return nil - } - - // check the type of file that is behind symlinks link - if dir.Type()&os.ModeSymlink == os.ModeSymlink { - linkedToDir, err := isLinkedToDir(path) - if err != nil { - return err - } - - if linkedToDir { - return nil - } - } - - filePaths = append(filePaths, path) - return nil - }) - - return filePaths, err -} - -func readFilePaths(path string) ([]string, error) { - var filePaths []string - files, err := ioutil.ReadDir(path) - if err != nil { - return nil, err - } - - for _, file := range files { - if file.IsDir() { - continue - } - - fullPathFile := filepath.Join(path, file.Name()) - - // check the type of file that is behind symlinks link - if file.Mode()&os.ModeSymlink == os.ModeSymlink { - linkedToDir, err := isLinkedToDir(fullPathFile) - if err != nil { - return nil, err - } - - if linkedToDir { - continue - } - } - - filePaths = append(filePaths, fullPathFile) - } - - return filePaths, err -} - -func isLinkedToDir(path string) (bool, error) { - linkedFile, err := filepath.EvalSymlinks(path) - if err != nil { - return false, err - } - - linkedFileInfo, err := os.Lstat(linkedFile) - if err != nil { - return false, err - } - - return linkedFileInfo.IsDir(), nil -} - -func LoadProxyConfigsFromPath(path string, recursive bool) ([]*ProxyConfig, error) { - filePaths, err := ReadFilePaths(path, recursive) - if err != nil { - return nil, err - } - - var cfgs []*ProxyConfig - - for _, filePath := range filePaths { - cfg, err := NewProxyConfigFromPath(filePath) - if err != nil { - return nil, err - } - cfgs = append(cfgs, cfg) - } - - return cfgs, nil -} - -// NewProxyConfigFromPath loads a ProxyConfig from a file path and then starts watching -// it for changes. On change the ProxyConfig will automatically LoadFromPath itself -func NewProxyConfigFromPath(path string) (*ProxyConfig, error) { - log.Println("Loading", path) - - var cfg ProxyConfig - if err := cfg.LoadFromPath(path); err != nil { - return nil, err - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - cfg.watcher = watcher - - go func() { - defer watcher.Close() - log.Printf("Starting to watch %s", path) - cfg.watch(path, time.Millisecond*50) - log.Printf("Stopping to watch %s", path) - }() - - if err := watcher.Add(path); err != nil { - return nil, err - } - - return &cfg, err -} - -func (cfg *ProxyConfig) watch(path string, interval time.Duration) { - // The interval protects the watcher from write event spams - // This is necessary due to how some text editors handle file safes - tick := time.Tick(interval) - var lastEvent *fsnotify.Event - - for { - select { - case <-tick: - if lastEvent == nil { - continue - } - cfg.onConfigWrite(*lastEvent) - lastEvent = nil - case event, ok := <-cfg.watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Remove == fsnotify.Remove { - cfg.removeCallback() - return - } - if event.Op&fsnotify.Write == fsnotify.Write { - lastEvent = &event - } - case err, ok := <-cfg.watcher.Errors: - if !ok { - return - } - log.Printf("Failed watching %s; error %s", path, err) - } - } -} - -func (cfg *ProxyConfig) onConfigWrite(event fsnotify.Event) { - log.Println("Updating", event.Name) - if err := cfg.LoadFromPath(event.Name); err != nil { - log.Printf("Failed update on %s; error %s", event.Name, err) - return - } - cfg.OnlineStatus.cachedPacket = nil - cfg.OfflineStatus.cachedPacket = nil - cfg.dialer = nil - cfg.process = nil - cfg.changeCallback() -} - -// LoadFromPath loads the ProxyConfig from a file -func (cfg *ProxyConfig) LoadFromPath(path string) error { - cfg.Lock() - defer cfg.Unlock() - - var defaultCfg map[string]interface{} - bb, err := json.Marshal(DefaultProxyConfig()) - if err != nil { - return err - } - - if err := json.Unmarshal(bb, &defaultCfg); err != nil { - return err - } - - bb, err = ioutil.ReadFile(path) - if err != nil { - return err - } - - var loadedCfg map[string]interface{} - if err := json.Unmarshal(bb, &loadedCfg); err != nil { - log.Println(string(bb)) - return err - } - - for k, v := range loadedCfg { - defaultCfg[k] = v - } - - bb, err = json.Marshal(defaultCfg) - if err != nil { - return err - } - - return json.Unmarshal(bb, cfg) -} - -func WatchProxyConfigFolder(path string, out chan *ProxyConfig) error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - defer watcher.Close() - - if err := watcher.Add(path); err != nil { - return err - } - - defer close(out) - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return nil - } - if event.Op&fsnotify.Create == fsnotify.Create { - fileInfo, err := os.Lstat(event.Name) - if err != nil { - log.Printf("%s was created, but we failed to stat it: %v", event.Name, err) - continue - } - - if fileInfo.IsDir() { - continue - } - - // check the type of file that is behind symlinks link - if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { - linkedToDir, err := isLinkedToDir(event.Name) - if err != nil { - return err - } - - if linkedToDir { - continue - } - } - - proxyCfg, err := NewProxyConfigFromPath(event.Name) - if err != nil { - log.Printf("Failed loading %s; error %s", event.Name, err) - continue - } - out <- proxyCfg - } - case err, ok := <-watcher.Errors: - if !ok { - return nil - } - log.Printf("Failed watching %s; error %s", path, err) - } - } -} diff --git a/configs/config.yml b/configs/config.yml new file mode 100644 index 00000000..4af56ffe --- /dev/null +++ b/configs/config.yml @@ -0,0 +1,26 @@ +# Infrared Config + +# Address that Infrared bind and listens to +# +bind: 0.0.0.0:25565 + +# Maximum duration between packets before the client gets timed out. +# +keepAliveTimeout: 30s + +# Filter are hooks that trigger befor a connection is processed. +# They are used as preconditions to validate a connection. +# +filters: + # Rate Limiter will only allow an IP address to connect a specified + # amount of times in a given time frame. + # + rateLimiter: + # Request Limit is the amount of times an IP address can create + # a new connection before it gets blocked. + # + requestLimit: 10 + + # Windows Length is the time frame for the Request Limit. + # + windowLength: 1s \ No newline at end of file diff --git a/configs/embed.go b/configs/embed.go new file mode 100644 index 00000000..7bfab121 --- /dev/null +++ b/configs/embed.go @@ -0,0 +1,6 @@ +package configs + +import _ "embed" + +//go:embed config.yml +var DefaultInfraredConfig []byte diff --git a/configs/haproxy.cfg b/configs/haproxy.cfg new file mode 100644 index 00000000..b5cefef7 --- /dev/null +++ b/configs/haproxy.cfg @@ -0,0 +1,37 @@ +#--------------------------------------------------------------------- +# Example configuration. See the full configuration manual online. +# +# http://www.haproxy.org/download/2.5/doc/configuration.txt +# +#--------------------------------------------------------------------- + +global + maxconn 20000 + log stdout local0 debug + user haproxy + chroot /usr/share/haproxy + pidfile /run/haproxy.pid + daemon + +defaults + log global + +resolvers nameserver + nameserver ns1 1.1.1.1:53 + nameserver ns2 8.8.8.8:53 + +#listen minecraft +# bind :25500 +# mode tcp +# server s1 127.0.0.1:25565 send-proxy-v2 resolvers nameserver + +frontend minecraft_fe + maxconn 2000 + mode tcp + bind :25500 + default_backend minecraft_be + +backend minecraft_be + mode tcp +# server s1 185.232.71.248:25565 send-proxy-v2 resolvers nameserver + server s1 127.0.0.1:25565 send-proxy-v2 resolvers nameserver \ No newline at end of file diff --git a/configs/proxy.yml b/configs/proxy.yml new file mode 100644 index 00000000..eab5d5de --- /dev/null +++ b/configs/proxy.yml @@ -0,0 +1,16 @@ +# This is the domain that players enter in their game client. +# You can have multiple domains here or just one. +# Currently this holds just a wildcard character as a domain +# meaning that is accepts every domain that a player uses. +# Supports '*' and '?' wildcards in the pattern string. +# +domains: + - "*" + +addresses: + - 127.0.0.1:25565 + +# Send a Proxy Protocol v2 Header to the server to +# forward the players IP address +# +#sendProxyProtocol: true \ No newline at end of file diff --git a/conn.go b/conn.go deleted file mode 100644 index ed8a5d22..00000000 --- a/conn.go +++ /dev/null @@ -1,122 +0,0 @@ -package infrared - -import ( - "bufio" - "crypto/cipher" - "github.com/haveachin/infrared/protocol" - "io" - "net" -) - -type PacketWriter interface { - WritePacket(p protocol.Packet) error -} - -type PacketReader interface { - ReadPacket() (protocol.Packet, error) -} - -type PacketPeeker interface { - PeekPacket() (protocol.Packet, error) -} - -type conn struct { - net.Conn - - r *bufio.Reader - w io.Writer -} - -type Listener struct { - net.Listener -} - -func Listen(addr string) (Listener, error) { - l, err := net.Listen("tcp", addr) - return Listener{Listener: l}, err -} - -func (l Listener) Accept() (Conn, error) { - conn, err := l.Listener.Accept() - if err != nil { - return nil, err - } - return wrapConn(conn), nil -} - -// Conn is a minecraft Connection -type Conn interface { - net.Conn - PacketWriter - PacketReader - PacketPeeker - - Reader() *bufio.Reader -} - -// wrapConn warp an net.Conn to infared.conn -func wrapConn(c net.Conn) *conn { - return &conn{ - Conn: c, - r: bufio.NewReader(c), - w: c, - } -} - -type Dialer struct { - net.Dialer -} - -// Dial create a Minecraft connection -func (d Dialer) Dial(addr string) (Conn, error) { - conn, err := d.Dialer.Dial("tcp", addr) - if err != nil { - return nil, err - } - - return wrapConn(conn), nil -} - -func (c *conn) Read(b []byte) (int, error) { - return c.r.Read(b) -} - -func (c *conn) Write(b []byte) (int, error) { - return c.w.Write(b) -} - -// ReadPacket read a Packet from Conn. -func (c *conn) ReadPacket() (protocol.Packet, error) { - return protocol.ReadPacket(c.r) -} - -// PeekPacket peeks a Packet from Conn. -func (c *conn) PeekPacket() (protocol.Packet, error) { - return protocol.PeekPacket(c.r) -} - -//WritePacket write a Packet to Conn. -func (c *conn) WritePacket(p protocol.Packet) error { - pk, err := p.Marshal() - if err != nil { - return err - } - _, err = c.w.Write(pk) - return err -} - -// SetCipher sets the decode/encode stream for this Conn -func (c *conn) SetCipher(ecoStream, decoStream cipher.Stream) { - c.r = bufio.NewReader(cipher.StreamReader{ - S: decoStream, - R: c.Conn, - }) - c.w = cipher.StreamWriter{ - S: ecoStream, - W: c.Conn, - } -} - -func (c *conn) Reader() *bufio.Reader { - return c.r -} diff --git a/deployments/docker-compose.dev.yml b/deployments/docker-compose.dev.yml new file mode 100644 index 00000000..9a4e1f3d --- /dev/null +++ b/deployments/docker-compose.dev.yml @@ -0,0 +1,45 @@ +version: "3" + +services: + minecraft-java-server: + image: itzg/minecraft-server:java17 + container_name: infrared-dev-paper-server + restart: "no" + volumes: + - ../.dev/paper:/data + ports: + - 25566:25565/tcp + environment: + EULA: "TRUE" + VERSION: "1.20.4" + TYPE: PAPER + networks: + - infrared + labels: + - infrared.java.servers.devserver.gateways=[default] + - infrared.java.servers.devserver.domains=[*] + - infrared.java.servers.devserver.address=:25566 + + haproxy: + image: haproxy + container_name: infrared-dev-haproxy + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + volumes: + - ../.dev/haproxy:/usr/local/etc/haproxy:ro + ports: + - 25567:25565/tcp + networks: + - infrared + + redis: + image: redis + container_name: infrared-dev-redis + ports: + - 6379:6379/tcp + networks: + - infrared + +networks: + infrared: + name: infrared diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml new file mode 100644 index 00000000..18c2d387 --- /dev/null +++ b/deployments/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.8" + +services: + infrared: + image: haveachin/infrared:latest + container_name: infrared + restart: always + ports: + - 25565:25565/tcp + volumes: + - ./data/infrared:/etc/infrared diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e29e4b11..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: "3" - -services: - infrared: - build: "." - container_name: "infrared" - restart: "unless-stopped" - stdin_open: true - tty: true - ports: - - "25565:25565/tcp" - volumes: - - "/usr/local/infrared/configs:/configs" - expose: - - "25565" - environment: - INFRARED_CONFIG_PATH: "/configs" diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..b47ca5fe --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,95 @@ +import { defineConfig} from 'vitepress' + +export default defineConfig({ + lang: 'en-US', + title: 'Infrared', + titleTemplate: ':title | Minecraft Proxy', + description: 'Minecraft Proxy', + cleanUrls: true, + head: [ + [ + 'link', + { + rel: 'icon', + type: 'image/x-icon', + href: '/assets/logo.svg', + }, + ], + ], + themeConfig: { + logo: '/assets/logo.svg', + + nav: [ + { + text: 'Features', + items: [ + { text: 'PROXY Protocol', link: '/features/forward-player-ips' }, + { text: 'Rate Limiter', link: '/features/rate-limit-ips' }, + ] + }, + { + text: 'Config', + items: [ + { text: 'Global', link: '/config/' }, + { text: 'Proxies', link: '/config/proxies' }, + { text: 'CLI & Env Vars', link: '/config/cli-and-env-vars' }, + ] + }, + { + text: 'Donate', + items: [ + { text: 'PayPal', link: 'https://paypal.me/hendrikschlehlein' }, + { text: 'Ko-Fi', link: 'https://ko-fi.com/haveachin' }, + { text: 'Liberapay', link: 'https://liberapay.com/haveachin' }, + ] + }, + ], + + sidebar: [ + { text: 'Getting Started', link: '/getting-started' }, + { + text: 'Config', + items: [ + { text: 'Global', link: '/config/' }, + { text: 'Proxies', link: '/config/proxies' }, + { text: 'CLI & Env Vars', link: '/config/cli-and-env-vars' }, + ], + }, + { + text: 'Features', + items: [ + { text: 'Forward Player IPs', link: '/features/forward-player-ips' }, + { + text: 'Filters', + link: '/features/filters', + items: [ + { text: 'Rate Limit IPs', link: '/features/rate-limit-ips' }, + ] + } + ] + }, + { text: 'Report an Issue', link: 'https://github.com/haveachin/infrared/issues' }, + { text: 'Ask in Discussions', link: 'https://github.com/haveachin/infrared/discussions' }, + { text: 'Join our Discord', link: 'https://discord.gg/r98YPRsZAx' }, + { text: 'Branding', link: '/branding' }, + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/haveachin/infrared' }, + { icon: 'discord', link: 'https://discord.gg/r98YPRsZAx' }, + ], + + footer: { + message: 'Released under the AGPL-3.0.', + copyright: 'Copyright © 2019-present Haveachin and Contributors', + }, + + editLink: { + pattern: 'https://github.com/haveachin/infrared/edit/master/website/:path' + }, + + search: { + provider: 'local' + }, + } +}) \ No newline at end of file diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 00000000..4e50ba52 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,24 @@ +/* .vitepress/theme/custom.css */ +:root { + --vp-c-brand-1: #CC0033; + --vp-c-brand-2: #B7002D; + --vp-c-brand-3: #A30028; + --vp-c-brand-light: #b3002d; + --vp-c-brand-lighter: #990026; + --vp-c-brand-dark: #e60039; + --vp-c-brand-darker: #ff0040; + + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: linear-gradient( + 180deg, + #CC0033ff, + #CC0033B0 + ); + + --vp-home-hero-image-filter: blur(40px); + --vp-home-hero-image-background-image: linear-gradient( + 120deg, + #CC003325, + #CC003315 + ); + } \ No newline at end of file diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 00000000..bed9095c --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1,5 @@ +// .vitepress/theme/index.js +import DefaultTheme from 'vitepress/theme' +import './custom.css' + +export default DefaultTheme \ No newline at end of file diff --git a/docs/assets/agplv3_logo.svg b/docs/assets/agplv3_logo.svg new file mode 100644 index 00000000..75e5c4c6 --- /dev/null +++ b/docs/assets/agplv3_logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..a0f6fbb6 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/logo_black_text.svg b/docs/assets/logo_black_text.svg new file mode 100644 index 00000000..2eca26bc --- /dev/null +++ b/docs/assets/logo_black_text.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + Infrared + \ No newline at end of file diff --git a/docs/assets/logo_white_text.svg b/docs/assets/logo_white_text.svg new file mode 100644 index 00000000..f4c250dc --- /dev/null +++ b/docs/assets/logo_white_text.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + Infrared + \ No newline at end of file diff --git a/docs/branding.md b/docs/branding.md new file mode 100644 index 00000000..25afc78e --- /dev/null +++ b/docs/branding.md @@ -0,0 +1,19 @@ +# Branding + +## Logo +![Logo](/assets/logo.svg) + +### Colors +Top Side: #CC0033 +Left Side: #b7002d +Right Side: #a30028 + +## Logo With White Text +![Logo With White Text](/assets/logo_white_text.svg) +Font: [Inter](https://github.com/rsms/inter) +Font-Color: #f9e5ea + +## Logo With Black Text +![Logo With Black Text](/assets/logo_black_text.svg) +Font: [Inter](https://github.com/rsms/inter) +Font-Color: #140005 \ No newline at end of file diff --git a/docs/config/cli-and-env-vars.md b/docs/config/cli-and-env-vars.md new file mode 100644 index 00000000..5465d1c1 --- /dev/null +++ b/docs/config/cli-and-env-vars.md @@ -0,0 +1,25 @@ +# CLI & Env Vars + +## Config Path + +| Environment Variable | CLI Flag | Default | +|----------------------|------------------|--------------| +| `INFRARED_CONFIG` | `--config`, `-c` | `config.yml` | + +## Working Directory + +| Environment Variable | CLI Flag | Default | +|------------------------|-----------------------|---------| +| `INFRARED_WORKING_DIR` | `--working-dir`, `-w` | `.` | + +## Proxies Path + +| Environment Variable | CLI Flag | Default | +|------------------------|-----------------------|-------------| +| `INFRARED_PROXIES_DIR` | `--proxies-dir`, `-p` | `./proxies` | + +## Log Level + +| Environment Variable | CLI Flag | Default | +|----------------------|---------------------|---------| +| `INFRARED_LOG_LEVEL` | `--log-level`, `-l` | `info` | \ No newline at end of file diff --git a/docs/config/index.md b/docs/config/index.md new file mode 100644 index 00000000..34d267b6 --- /dev/null +++ b/docs/config/index.md @@ -0,0 +1,18 @@ +# Config + +On fist start Infrared should generate a `config.yml` file and a `proxies` directory. +Here is a minimal `config.yml` example: + +```yml +# Minimal Infrared Config + +# Address that Infrared bind and listens to +# +bind: 0.0.0.0:25565 + +# Maximum duration between packets before the client gets timed out. +# +keepAliveTimeout: 30s +``` + +[Complete config example](https://github.com/haveachin/infrared/blob/main/configs/config.yml) \ No newline at end of file diff --git a/docs/config/proxies.md b/docs/config/proxies.md new file mode 100644 index 00000000..1fafbf5a --- /dev/null +++ b/docs/config/proxies.md @@ -0,0 +1,21 @@ +# Proxy + +All proxy configs should live in the `proxies` directory. +The proxy directory can be changed via the [Proxies Path](cli-and-env-vars#proxies-path) + +Minimal proxy config example: +```yml [my-server.yml] +# This is the domain that players enter in their game client. +# You can have multiple domains here or just one. +# Currently this holds just a wildcard character as a domain +# meaning that is accepts every domain that a player uses. +# Supports '*' and '?' wildcards in the pattern string. +# +domains: + - "example.com" + +addresses: + - 127.0.0.1:25565 +``` + +[Complete proxy config example](https://github.com/haveachin/infrared/blob/main/configs/proxy.yml) \ No newline at end of file diff --git a/docs/features/filters.md b/docs/features/filters.md new file mode 100644 index 00000000..ec8abf6e --- /dev/null +++ b/docs/features/filters.md @@ -0,0 +1,20 @@ +# Filters + +Filter are hooks that trigger befor a connection is processed. +They are used as preconditions to validate a connection. + +## Use Filters + +To use filters you just need to a this to your [**global config**](../config/index.md): + +```yml +# Filter are hooks that trigger befor a connection is processed. +# They are used as preconditions to validate a connection. +# +filters: +``` + +Now you actually need to add filters to your config. +This is a list of all the filters that currently exist: + +- [Rate Limiter](rate-limit-ips) \ No newline at end of file diff --git a/docs/features/forward-player-ips.md b/docs/features/forward-player-ips.md new file mode 100644 index 00000000..7fb6e969 --- /dev/null +++ b/docs/features/forward-player-ips.md @@ -0,0 +1,16 @@ +# Forward Player IPs + +You can forward the player IPs via proxy protocol. +To enable it in Infrared you just have to change this in you [**proxy config**](../config/proxies.md): +```yml +# Send a Proxy Protocol v2 Header to the server to +# forward the players IP address. +# +#sendProxyProtocol: true // [!code --] +sendProxyProtocol: true // [!code ++] +``` + +## Paper + +In Paper you have to enable it also to work. +See [the Paper documentation on Proxy Protocol](https://docs.papermc.io/paper/reference/global-configuration#proxies_proxy_protocol) for more. \ No newline at end of file diff --git a/docs/features/rate-limit-ips.md b/docs/features/rate-limit-ips.md new file mode 100644 index 00000000..15130e29 --- /dev/null +++ b/docs/features/rate-limit-ips.md @@ -0,0 +1,20 @@ +# Rate Limit IPs + +You can rate limit by IP address using the `rateLimit` filter. +This can be easily activated in your [**global config**](../config/index.md) by adding this: + +```yml{2-16} +filters: + # Rate Limiter will only allow an IP address to connect a specified + # amount of times in a given time frame. + # + rateLimiter: + # Request Limit is the amount of times an IP address can create + # a new connection before it gets blocked. + # + requestLimit: 10 + + # Windows Length is the time frame for the Request Limit. + # + windowLength: 1s +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..123ceb29 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,79 @@ +# Getting Started + +There are multiple ways to install or setup you Infrared instance. Depending on what setup you choose you will have different pro and cons in usage and update cycles. + +## Quick Start + +One of the quickest ways to get started is by just downloading the [latest release of Infrared](https://github.com/haveachin/infrared/releases/) from GitHub for your machine and executing it. + +### Find the binary for your system + +Most common ones are in **bold**. + +| Hardware | OS | for 32-bit | for 64-bit | +|-----------------------------|-----------------|---------------|--------------------| +| PC, VPS or Root Server | Linux based | Linux_i386* | **Linux_x86_64** | +| Raspberry Pi | Raspberry Pi OS | Linux_armv6* | **Linux_arm64** | +| Custom/Prebuild PC | Windows | Windows_i386* | **Windows_x86_64** | +| Intel Mac or MacBook | macOS | - | Darwin_x86_64 | +| M1 or higher Mac or MacBook | macOS | - | Darwin_arm64 | + +\*These architectures are most of the time the correct, but there is more to it. + +### Downloading + +If your system as a desktop environment then you should be able to download your binary by just clicking on the version you want on the releases page. +The URL of your download should look something like this: +``` +https://github.com/haveachin/infrared/releases/download/{version}/infrared_{architecture}.tar.gz +``` +For example: +``` +https://github.com/haveachin/infrared/releases/download/v1.3.4/infrared_Linux_x86_64.tar.gz +``` + +::: tip +If you are using SSH to connect to a remote server and are currently using a desktop environment with a browser you can just right-click the version you need and copy the link. Then paste it into your terminal with Ctrl+Shift+V on GNU/Linux or right-click on Windows. +::: + +Downloading by using the terminal on macOS or GNU/Linux: +```bash +curl -LO https://github.com/haveachin/infrared/releases/download/{version}/infrared_{architecture}.tar.gz +``` + +Downloading by using Powershell on Windows: +```Powershell +Invoke-WebRequest -Uri https://github.com/haveachin/infrared/releases/download/v1.3.4/infrared_Windows_x86_64.zip -OutFile c:\infrared.zip +``` + +### Extracting the binary + +Extracting by using the terminal on macOS or GNU/Linux: +```bash +tar -xzf infrared_{architecture}.tar.gz +``` + +Extracting by using Powershell on Windows: +```Powershell +Expand-Archive c:\infrared.zip -DestinationPath c:\ +``` +## Docker + +[Official Image on Docker Hub](https://hub.docker.com/r/haveachin/infrared) +[Github Registry](https://github.com/haveachin/infrared/pkgs/container/infrared) + +### Docker Compose + +```docker +version: "3.8" + +services: + infrared: + image: haveachin/infrared:latest + container_name: infrared + restart: always + ports: + - 25565:25565/tcp + volumes: + - ./data/infrared:/etc/infrared +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..e6e66b55 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,29 @@ +--- +layout: home + +hero: + name: Infrared + text: The Reverse Proxy for Minecraft + tagline: + image: + src: /assets/logo.svg + alt: Infrared + actions: + - theme: brand + text: Getting Started + link: /getting-started + - theme: alt + text: Contribute + link: https://github.com/haveachin/infrared/blob/main/CONTRIBUTING.md + +features: + - icon: 📖 + title: Free and Open Source + details: Infrared is developed as free software to ensure transparency and integrity. + - icon: 🧩 + title: Build as a Library + details: Use Infrared as a library for your projects and extend it's functionallty easily via it's rich API. + - icon: 🪶 + title: Simple and Lightweight + details: Low memory footprint and build for concurrnecy. +--- \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..008de618 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,1506 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "vitepress": "^1.0.0-rc.40" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", + "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", + "@algolia/autocomplete-shared": "1.9.3" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", + "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", + "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", + "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.20.0.tgz", + "integrity": "sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.20.0" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.20.0.tgz", + "integrity": "sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ==", + "dev": true + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.20.0.tgz", + "integrity": "sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.20.0" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.20.0.tgz", + "integrity": "sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.20.0", + "@algolia/client-search": "4.20.0", + "@algolia/transporter": "4.20.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.20.0.tgz", + "integrity": "sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.20.0", + "@algolia/client-search": "4.20.0", + "@algolia/requester-common": "4.20.0", + "@algolia/transporter": "4.20.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.20.0.tgz", + "integrity": "sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.20.0", + "@algolia/transporter": "4.20.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.20.0.tgz", + "integrity": "sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.20.0", + "@algolia/requester-common": "4.20.0", + "@algolia/transporter": "4.20.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.20.0.tgz", + "integrity": "sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.20.0", + "@algolia/requester-common": "4.20.0", + "@algolia/transporter": "4.20.0" + } + }, + "node_modules/@algolia/logger-common": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.20.0.tgz", + "integrity": "sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ==", + "dev": true + }, + "node_modules/@algolia/logger-console": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.20.0.tgz", + "integrity": "sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA==", + "dev": true, + "dependencies": { + "@algolia/logger-common": "4.20.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.20.0.tgz", + "integrity": "sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.20.0" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.20.0.tgz", + "integrity": "sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng==", + "dev": true + }, + "node_modules/@algolia/requester-node-http": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.20.0.tgz", + "integrity": "sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.20.0" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.20.0.tgz", + "integrity": "sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.20.0", + "@algolia/logger-common": "4.20.0", + "@algolia/requester-common": "4.20.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.5.2.tgz", + "integrity": "sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.5.2.tgz", + "integrity": "sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.5.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.5.2.tgz", + "integrity": "sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.9.3", + "@algolia/autocomplete-preset-algolia": "1.9.3", + "@docsearch/css": "3.5.2", + "algoliasearch": "^4.19.1" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz", + "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz", + "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz", + "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.6", + "@vue/shared": "3.4.15", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz", + "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.4.15", + "@vue/shared": "3.4.15" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz", + "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.6", + "@vue/compiler-core": "3.4.15", + "@vue/compiler-dom": "3.4.15", + "@vue/compiler-ssr": "3.4.15", + "@vue/shared": "3.4.15", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.33", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz", + "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.4.15", + "@vue/shared": "3.4.15" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz", + "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==", + "dev": true + }, + "node_modules/@vue/reactivity": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz", + "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==", + "dev": true, + "dependencies": { + "@vue/shared": "3.4.15" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz", + "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.4.15", + "@vue/shared": "3.4.15" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz", + "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==", + "dev": true, + "dependencies": { + "@vue/runtime-core": "3.4.15", + "@vue/shared": "3.4.15", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz", + "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.4.15", + "@vue/shared": "3.4.15" + }, + "peerDependencies": { + "vue": "3.4.15" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", + "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.2.tgz", + "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.7.2", + "@vueuse/shared": "10.7.2", + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.7.2.tgz", + "integrity": "sha512-+u3RLPFedjASs5EKPc69Ge49WNgqeMfSxFn+qrQTzblPXZg6+EFzhjarS5edj2qAf6xQ93f95TUxRwKStXj/sQ==", + "dev": true, + "dependencies": { + "@vueuse/core": "10.7.2", + "@vueuse/shared": "10.7.2", + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "*", + "axios": "*", + "change-case": "*", + "drauu": "*", + "focus-trap": "*", + "fuse.js": "*", + "idb-keyval": "*", + "jwt-decode": "*", + "nprogress": "*", + "qrcode": "*", + "sortablejs": "*", + "universal-cookie": "*" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.2.tgz", + "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.2.tgz", + "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.20.0.tgz", + "integrity": "sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g==", + "dev": true, + "dependencies": { + "@algolia/cache-browser-local-storage": "4.20.0", + "@algolia/cache-common": "4.20.0", + "@algolia/cache-in-memory": "4.20.0", + "@algolia/client-account": "4.20.0", + "@algolia/client-analytics": "4.20.0", + "@algolia/client-common": "4.20.0", + "@algolia/client-personalization": "4.20.0", + "@algolia/client-search": "4.20.0", + "@algolia/logger-common": "4.20.0", + "@algolia/logger-console": "4.20.0", + "@algolia/requester-browser-xhr": "4.20.0", + "@algolia/requester-common": "4.20.0", + "@algolia/requester-node-http": "4.20.0", + "@algolia/transporter": "4.20.0" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "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==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/minisearch": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", + "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.19.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.2.tgz", + "integrity": "sha512-UA9DX/OJwv6YwP9Vn7Ti/vF80XL+YA5H2l7BpCtUr3ya8LWHFzpiO5R+N7dN16ujpIxhekRFuOOF82bXX7K/lg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/rollup": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.11.0.tgz", + "integrity": "sha512-Uin2J8Bpm3xaZi9Y8QibSys6uJOFZ+REMrf42v20AA3FUDUrshKkMEP6liJbMAHCm71wO6ls4mwAf7a3gFVxLw==", + "dev": true, + "peer": true + }, + "node_modules/shikiji": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.10.2.tgz", + "integrity": "sha512-wtZg3T0vtYV2PnqusWQs3mDaJBdCPWxFDrBM/SE5LfrX92gjUvfEMlc+vJnoKY6Z/S44OWaCRzNIsdBRWcTAiw==", + "dev": true, + "dependencies": { + "shikiji-core": "0.10.2" + } + }, + "node_modules/shikiji-core": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/shikiji-core/-/shikiji-core-0.10.2.tgz", + "integrity": "sha512-9Of8HMlF96usXJHmCL3Gd0Fcf0EcyJUF9m8EoAKKd98mHXi0La2AZl1h6PegSFGtiYcBDK/fLuKbDa1l16r1fA==", + "dev": true + }, + "node_modules/shikiji-transformers": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/shikiji-transformers/-/shikiji-transformers-0.10.2.tgz", + "integrity": "sha512-7IVTwl1af205ywYEq5bOAYOTOFW4V1dVX1EablP0nWKErqZeD1o93VMytxmtJomqS+YwbB8doY8SE3MFMn0aPQ==", + "dev": true, + "dependencies": { + "shikiji": "0.10.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "node_modules/vite": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", + "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.0.0-rc.40", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.0.0-rc.40.tgz", + "integrity": "sha512-1x9PCrcsJwqhpccyTR93uD6jpiPDeRC98CBCAQLLBb44a3VSXYBPzhCahi+2kwAYylu49p0XhseMPVM4IVcWcw==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.5.2", + "@docsearch/js": "^3.5.2", + "@types/markdown-it": "^13.0.7", + "@vitejs/plugin-vue": "^5.0.3", + "@vue/devtools-api": "^6.5.1", + "@vueuse/core": "^10.7.2", + "@vueuse/integrations": "^10.7.2", + "focus-trap": "^7.5.4", + "mark.js": "8.11.1", + "minisearch": "^6.3.0", + "shikiji": "^0.10.0", + "shikiji-core": "^0.10.0", + "shikiji-transformers": "^0.10.0", + "vite": "^5.0.12", + "vue": "^3.4.15" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4.3.2", + "postcss": "^8.4.33" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz", + "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.4.15", + "@vue/compiler-sfc": "3.4.15", + "@vue/runtime-dom": "3.4.15", + "@vue/server-renderer": "3.4.15", + "@vue/shared": "3.4.15" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..b9217567 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "vitepress": "^1.0.0-rc.40" + }, + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + } +} \ No newline at end of file diff --git a/gateway.go b/gateway.go deleted file mode 100644 index 8ab933ea..00000000 --- a/gateway.go +++ /dev/null @@ -1,219 +0,0 @@ -package infrared - -import ( - "errors" - "log" - "net/http" - "sync" - - "github.com/haveachin/infrared/callback" - "github.com/haveachin/infrared/protocol/handshaking" - "github.com/pires/go-proxyproto" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - proxiesActive = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "infrared_proxies", - Help: "The total number of proxies running", - }) -) - -type Gateway struct { - ReceiveProxyProtocol bool - listeners sync.Map - Proxies sync.Map - closed chan bool - wg sync.WaitGroup -} - -func (gateway *Gateway) ListenAndServe(proxies []*Proxy) error { - if len(proxies) <= 0 { - return errors.New("no proxies in gateway") - } - - gateway.closed = make(chan bool, len(proxies)) - - for _, proxy := range proxies { - if err := gateway.RegisterProxy(proxy); err != nil { - gateway.Close() - return err - } - } - - log.Println("All proxies are online") - return nil -} - -func (gateway *Gateway) EnablePrometheus(bind string) error { - gateway.wg.Add(1) - - go func() { - defer gateway.wg.Done() - - http.Handle("/metrics", promhttp.Handler()) - http.ListenAndServe(bind, nil) - }() - - log.Println("Enabling Prometheus metrics endpoint on", bind) - return nil -} - -func (gateway *Gateway) KeepProcessActive() { - gateway.wg.Wait() -} - -// Close closes all listeners -func (gateway *Gateway) Close() { - gateway.listeners.Range(func(k, v interface{}) bool { - gateway.closed <- true - _ = v.(Listener).Close() - return false - }) -} - -func (gateway *Gateway) CloseProxy(proxyUID string) { - log.Println("Closing proxy with UID", proxyUID) - v, ok := gateway.Proxies.LoadAndDelete(proxyUID) - if !ok { - return - } - proxiesActive.Dec() - proxy := v.(*Proxy) - - closeListener := true - gateway.Proxies.Range(func(k, v interface{}) bool { - otherProxy := v.(*Proxy) - if proxy.ListenTo() == otherProxy.ListenTo() { - closeListener = false - return false - } - return true - }) - - if !closeListener { - return - } - - v, ok = gateway.listeners.Load(proxy.ListenTo()) - if !ok { - return - } - v.(Listener).Close() -} - -func (gateway *Gateway) RegisterProxy(proxy *Proxy) error { - // Register new Proxy - proxyUID := proxy.UID() - log.Println("Registering proxy with UID", proxyUID) - gateway.Proxies.Store(proxyUID, proxy) - proxiesActive.Inc() - - proxy.Config.removeCallback = func() { - gateway.CloseProxy(proxyUID) - } - - proxy.Config.changeCallback = func() { - if proxyUID == proxy.UID() { - return - } - gateway.CloseProxy(proxyUID) - if err := gateway.RegisterProxy(proxy); err != nil { - log.Println(err) - } - } - - playersConnected.WithLabelValues(proxy.DomainName()) - - // Check if a gate is already listening to the Proxy address - addr := proxy.ListenTo() - if _, ok := gateway.listeners.Load(addr); ok { - return nil - } - - log.Println("Creating listener on", addr) - listener, err := Listen(addr) - if err != nil { - return err - } - gateway.listeners.Store(addr, listener) - - gateway.wg.Add(1) - go func() { - if err := gateway.listenAndServe(listener, addr); err != nil { - log.Printf("Failed to listen on %s; error: %s", proxy.ListenTo(), err) - } - }() - return nil -} - -func (gateway *Gateway) listenAndServe(listener Listener, addr string) error { - defer gateway.wg.Done() - - for { - conn, err := listener.Accept() - if err != nil { - // TODO: Refactor this; it feels hacky - if err.Error() == "use of closed network connection" { - log.Println("Closing listener on", addr) - gateway.listeners.Delete(addr) - return nil - } - - continue - } - - go func() { - log.Printf("[>] Incoming %s on listener %s", conn.RemoteAddr(), addr) - defer conn.Close() - if err := gateway.serve(conn, addr); err != nil { - log.Printf("[x] %s closed connection with %s; error: %s", conn.RemoteAddr(), addr, err) - return - } - log.Printf("[x] %s closed connection with %s", conn.RemoteAddr(), addr) - }() - } -} - -func (gateway *Gateway) serve(conn Conn, addr string) error { - connRemoteAddr := conn.RemoteAddr() - if gateway.ReceiveProxyProtocol { - header, err := proxyproto.Read(conn.Reader()) - if err != nil { - return err - } - connRemoteAddr = header.SourceAddr - } - - pk, err := conn.PeekPacket() - if err != nil { - return err - } - - hs, err := handshaking.UnmarshalServerBoundHandshake(pk) - if err != nil { - return err - } - - proxyUID := proxyUID(hs.ParseServerAddress(), addr) - - log.Printf("[i] %s requests proxy with UID %s", connRemoteAddr, proxyUID) - v, ok := gateway.Proxies.Load(proxyUID) - if !ok { - // Client send an invalid address/port; we don't have a v for that address - return errors.New("no proxy with uid " + proxyUID) - } - proxy := v.(*Proxy) - - if err := proxy.handleConn(conn, connRemoteAddr); err != nil { - proxy.CallbackLogger().LogEvent(callback.ErrorEvent{ - Error: err.Error(), - ProxyUID: proxyUID, - }) - return err - } - return nil -} diff --git a/gateway_test.go b/gateway_test.go deleted file mode 100644 index 99818de0..00000000 --- a/gateway_test.go +++ /dev/null @@ -1,635 +0,0 @@ -package infrared - -import ( - "encoding/json" - "fmt" - "net" - "strings" - "sync" - "testing" - - "github.com/haveachin/infrared/protocol" - "github.com/haveachin/infrared/protocol/handshaking" - "github.com/haveachin/infrared/protocol/status" - "github.com/pires/go-proxyproto" -) - -var serverDomain string = "infrared.gateway" - -type testError struct { - Error error - Message string -} - -func gatewayPort(portEnd int) int { - return 30000 + portEnd -} - -func gatewayAddr(portEnd int) string { - return portToAddr(gatewayPort(portEnd)) -} - -func serverPort(portEnd int) int { - return 20000 + portEnd -} - -func serverAddr(portEnd int) string { - return portToAddr(serverPort(portEnd)) -} - -func dialerPort(portEnd int) int { - return 10000 + portEnd -} - -func portToAddr(port int) string { - return fmt.Sprintf(":%d", port) -} - -func routeVersionName(index int) string { - return fmt.Sprintf("infrared.gateway-%d", index) -} - -func getIpFromAddr(addr net.Addr) string { - return strings.Split(addr.String(), ":")[0] -} - -func proxyConfigWithPortEnd(portEnd int) *ProxyConfig { - serverAddr := serverAddr(portEnd) - gatewayAddr := gatewayAddr(portEnd) - return createBasicProxyConfig(serverDomain, gatewayAddr, serverAddr) -} - -func createBasicProxyConfig(serverDomain, gatewayAddr, serverAddr string) *ProxyConfig { - return &ProxyConfig{ - DomainName: serverDomain, - ListenTo: gatewayAddr, - ProxyTo: serverAddr, - } -} - -func createProxyProtocolConfig(portEnd int, proxyproto bool) *ProxyConfig { - config := proxyConfigWithPortEnd(portEnd) - config.ProxyProtocol = proxyproto - return config -} - -func statusHandshakePort(portEnd int) protocol.Packet { - gatewayPort := gatewayPort(portEnd) - return serverHandshake(serverDomain, gatewayPort) -} - -func serverHandshake(domain string, port int) protocol.Packet { - hs := handshaking.ServerBoundHandshake{ - ProtocolVersion: 574, - ServerAddress: protocol.String(domain), - ServerPort: protocol.UnsignedShort(port), - NextState: 1, - } - return hs.Marshal() -} - -func configToProxies(config *ProxyConfig) []*Proxy { - proxyConfigs := make([]*ProxyConfig, 0) - proxyConfigs = append(proxyConfigs, config) - return configsToProxies(proxyConfigs) -} - -func configsToProxies(config []*ProxyConfig) []*Proxy { - var proxies []*Proxy - for _, c := range config { - proxy := &Proxy{Config: c} - proxies = append(proxies, proxy) - } - return proxies -} - -func sendHandshake(conn Conn, pk protocol.Packet) *testError { - if err := conn.WritePacket(pk); err != nil { - return &testError{err, "Can't write handshake"} - } - return nil -} - -func statusPKWithVersion(name string) StatusConfig { - samples := make([]PlayerSample, 0) - return StatusConfig{VersionName: name, ProtocolNumber: 754, - MaxPlayers: 20, PlayersOnline: 0, PlayerSamples: samples, MOTD: "Server MOTD"} -} - -func sendProxyProtocolHeader(rconn Conn) *testError { - header := createProxyProtocolHeader() - if _, err := header.WriteTo(rconn); err != nil { - return &testError{err, "Can't write proxy protocol header"} - } - return nil -} - -var serverVersionName = "Infrared-test-online" - -var onlineStatus = StatusConfig{ - VersionName: "Infrared 1.16.5 Online", - ProtocolNumber: 754, - MaxPlayers: 20, - MOTD: "Powered by Infrared", -} - -var offlineStatus = StatusConfig{ - VersionName: "Infrared 1.16.5 Offline", - ProtocolNumber: 754, - MaxPlayers: 20, - MOTD: "Powered by Infrared", -} - -type statusListenerConfig struct { - id int - addr string - status StatusConfig -} - -func statusListen(c statusListenerConfig, errorCh chan *testError) { - listener, err := Listen(c.addr) - if err != nil { - errorCh <- &testError{err, fmt.Sprintf("Can't listen to %v", c.addr)} - } - - go func() { - defer listener.Close() - for { - conn, err := listener.Accept() - if err != nil { - errorCh <- &testError{err, "Can't accept connection on listener"} - } - pk, err := c.status.StatusResponsePacket() - if err != nil { - errorCh <- &testError{err, "Can't create status response packet"} - } - go func() { - if err := conn.WritePacket(pk); err != nil { - errorCh <- &testError{err, "Can't write status response packet on connection"} - } - }() - } - }() -} - -type statusDialConfig struct { - pk protocol.Packet - gatewayAddr string - dialerPort int - sendProxyProtocolHeader bool - useProxyProtocol bool -} - -func statusDial(c statusDialConfig) (string, *testError) { - var conn Conn - var err error - if c.useProxyProtocol { - conn, err = createConnWithFakeIP(c.dialerPort, c.gatewayAddr) - } else { - conn, err = Dialer{}.Dial(c.gatewayAddr) - } - - if err != nil { - return "", &testError{err, "Can't make a connection with gateway"} - } - defer conn.Close() - - if c.sendProxyProtocolHeader { - if err := sendProxyProtocolHeader(conn); err != nil { - return "", err - } - } - - if err := sendHandshake(conn, c.pk); err != nil { - return "", err - } - - statusPk := status.ServerBoundRequest{}.Marshal() - if err := conn.WritePacket(statusPk); err != nil { - return "", &testError{err, "Can't write status request packet"} - } - - receivedPk, err := conn.ReadPacket() - if err != nil { - return "", &testError{err, "Can't read status reponse packet"} - } - - response, err := status.UnmarshalClientBoundResponse(receivedPk) - if err != nil { - return "", &testError{err, "Can't unmarshal status reponse packet"} - } - - res := &status.ResponseJSON{} - json.Unmarshal([]byte(response.JSONResponse), &res) - return res.Version.Name, nil -} - -func createConnWithFakeIP(dialerPort int, gatewayAddr string) (Conn, error) { - dialer := &net.Dialer{ - LocalAddr: &net.TCPAddr{ - IP: net.ParseIP("127.0.0.1"), - Port: dialerPort, - }, - } - netConn, err := dialer.Dial("tcp", gatewayAddr) - if err != nil { - return nil, err - } - return wrapConn(netConn), nil -} - -func createProxyProtocolHeader() proxyproto.Header { - return proxyproto.Header{ - Version: 2, - Command: proxyproto.PROXY, - TransportProtocol: proxyproto.TCPv4, - SourceAddr: &net.TCPAddr{ - IP: net.ParseIP("109.226.143.210"), - Port: 0, - }, - DestinationAddr: &net.TCPAddr{ - IP: net.ParseIP("210.223.216.109"), - Port: 0, - }, - } -} - -func proxyProtoListen(portEnd int) (string, *testError) { - listenAddr := serverAddr(portEnd) - listener, err := Listen(listenAddr) - if err != nil { - return "", &testError{err, fmt.Sprintf("Can't listen to %v", listenAddr)} - } - defer listener.Close() - - proxyListener := &proxyproto.Listener{Listener: listener.Listener} - defer proxyListener.Close() - - conn, err := proxyListener.Accept() - if err != nil { - return "", &testError{err, "Can't accept connection on listener"} - } - defer conn.Close() - return getIpFromAddr(conn.RemoteAddr()), nil -} - -func TestStatusRequest(t *testing.T) { - tt := []struct { - name string - portEnd int - onlineStatus StatusConfig - offlineStatus StatusConfig - activeServer bool - expectedVersion string - }{ - { - name: "ServerOnlineWithoutConfig", - portEnd: 570, - activeServer: true, - expectedVersion: serverVersionName, - }, - { - name: "ServerOfflineWithoutConfig", - portEnd: 571, - activeServer: false, - expectedVersion: "", - }, - { - name: "ServerOnlineWithConfig", - portEnd: 572, - onlineStatus: onlineStatus, - offlineStatus: offlineStatus, - activeServer: true, - expectedVersion: onlineStatus.VersionName, - }, - { - name: "ServerOfflineWithConfig", - portEnd: 573, - onlineStatus: onlineStatus, - offlineStatus: offlineStatus, - activeServer: false, - expectedVersion: offlineStatus.VersionName, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - wg := &sync.WaitGroup{} - errorCh := make(chan *testError) - resultCh := make(chan bool) - wg.Add(1) - go func(wg *sync.WaitGroup) { - config := proxyConfigWithPortEnd(tc.portEnd) - config.OnlineStatus = tc.onlineStatus - config.OfflineStatus = tc.offlineStatus - - gateway := Gateway{} - proxies := configToProxies(config) - if err := gateway.ListenAndServe(proxies); err != nil { - errorCh <- &testError{err, "Can't start gateway"} - } - wg.Done() - gateway.KeepProcessActive() - }(wg) - - if tc.activeServer { - wg.Add(1) - serverCfg := statusListenerConfig{} - serverCfg.status = statusPKWithVersion(serverVersionName) - serverCfg.addr = serverAddr(tc.portEnd) - go func() { - statusListen(serverCfg, errorCh) - wg.Done() - }() - } - - wg.Wait() - go func() { - pk := statusHandshakePort(tc.portEnd) - config := statusDialConfig{ - pk: pk, - gatewayAddr: gatewayAddr(tc.portEnd), - dialerPort: dialerPort(tc.portEnd), - } - receivedVersion, err := statusDial(config) - if err != nil { - errorCh <- err - return - } - - resultCh <- receivedVersion == tc.expectedVersion - }() - - select { - case err := <-errorCh: - t.Fatalf("Unexpected Error in test: %s\n%v", err.Message, err.Error) - case r := <-resultCh: - if !r { - t.Fail() - } - } - }) - } -} - -func TestProxyProtocol(t *testing.T) { - tt := []struct { - name string - proxyproto bool - receiveProxyproto bool - portEnd int - shouldMatch bool - expectingIp string - }{ - { - name: "ProxyProtocolOn", - proxyproto: true, - receiveProxyproto: false, - portEnd: 581, - shouldMatch: true, - expectingIp: "127.0.0.1", - }, - { - name: "ProxyProtocolOff", - proxyproto: false, - receiveProxyproto: false, - portEnd: 582, - shouldMatch: true, - expectingIp: "127.0.0.1", - }, - { - name: "ProxyProtocol Receive", - proxyproto: true, - receiveProxyproto: true, - portEnd: 583, - shouldMatch: true, - expectingIp: "109.226.143.210", - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - errorCh := make(chan *testError) - resultCh := make(chan bool) - wg := &sync.WaitGroup{} - - wg.Add(1) - go func(wg *sync.WaitGroup) { - config := createProxyProtocolConfig(tc.portEnd, tc.proxyproto) - gateway := Gateway{ - ReceiveProxyProtocol: tc.receiveProxyproto, - } - proxies := configToProxies(config) - if err := gateway.ListenAndServe(proxies); err != nil { - errorCh <- &testError{err, "Can't start gateway"} - } - wg.Done() - gateway.KeepProcessActive() - }(wg) - - go func() { - ip, err := proxyProtoListen(tc.portEnd) - if err != nil { - errorCh <- err - return - } - resultCh <- ip == tc.expectingIp - }() - wg.Wait() - go func() { - - pk := statusHandshakePort(tc.portEnd) - config := statusDialConfig{ - pk: pk, - gatewayAddr: gatewayAddr(tc.portEnd), - dialerPort: dialerPort(tc.portEnd), - useProxyProtocol: tc.proxyproto, - sendProxyProtocolHeader: tc.receiveProxyproto, - } - - _, err := statusDial(config) - if err != nil { - errorCh <- err - } - }() - - select { - case err := <-errorCh: - t.Fatalf("Unexpected Error in test: %s\n%v", err.Message, err.Error) - case r := <-resultCh: - if r != tc.shouldMatch { - t.Errorf("got: %v; want: %v", r, tc.shouldMatch) - } - } - - }) - } -} - -func TestRouting(t *testing.T) { - wg := &sync.WaitGroup{} - errorCh := make(chan *testError) - - basePort := 540 - routingConfig := make([]*ProxyConfig, 0) - serverConfigs := make([]statusListenerConfig, 0) - - servers := []struct { - id int - domain string - portEnd int - }{ - { - id: 0, - domain: "infrared", - portEnd: 530, - }, - { - id: 9, - domain: "infrared", - portEnd: 531, - }, - { - id: 1, - domain: "infrared-dash", - portEnd: 530, - }, - { - id: 2, - domain: ".dottedInfrared.", - portEnd: 530, - }, - } - - tt := []struct { - name string - expectedId int - requestDomain string - portEnd int - expectError bool - shouldMatch bool - }{ - { - name: "Single word domain", - expectedId: 0, - requestDomain: "infrared", - portEnd: 530, - expectError: false, - shouldMatch: true, - }, - { - name: "Single word domain but wrong id", - expectedId: 1, - requestDomain: "infrared", - portEnd: 530, - expectError: false, - shouldMatch: false, - }, - { - name: "duplicated domain but other port", - expectedId: 9, - requestDomain: "infrared", - portEnd: 531, - expectError: false, - shouldMatch: true, - }, - { - name: "Domain with a dash", - expectedId: 1, - requestDomain: "infrared-dash", - portEnd: 530, - expectError: false, - shouldMatch: true, - }, - { - name: "Domain with points at both ends", - expectedId: 2, - requestDomain: ".dottedInfrared.", - portEnd: 530, - expectError: true, - shouldMatch: false, - }, - } - - for i, server := range servers { - port := basePort + i - proxyC := &ProxyConfig{} - serverC := statusListenerConfig{} - - serverAddr := serverAddr(port) - proxyC.ListenTo = gatewayAddr(server.portEnd) - proxyC.ProxyTo = serverAddr - proxyC.DomainName = server.domain - routingConfig = append(routingConfig, proxyC) - - serverC.id = server.id - serverC.addr = serverAddr - serverC.status = statusPKWithVersion(routeVersionName(server.id)) - serverConfigs = append(serverConfigs, serverC) - } - - wg.Add(1) - go func() { - gateway := Gateway{} - proxies := configsToProxies(routingConfig) - if err := gateway.ListenAndServe(proxies); err != nil { - errorCh <- &testError{err, "Can't start gateway"} - } - wg.Done() - gateway.KeepProcessActive() - }() - - for _, c := range serverConfigs { - wg.Add(1) - go func(config statusListenerConfig) { - statusListen(config, errorCh) - wg.Done() - }(c) - } - - wg.Wait() - - select { - case err := <-errorCh: - t.Fatalf("Unexpected Error before tests: %s\n%v", err.Message, err.Error) - default: - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - resultCh := make(chan bool) - - go func() { - expectedName := routeVersionName(tc.expectedId) - pk := serverHandshake(tc.requestDomain, tc.portEnd) - config := statusDialConfig{ - pk: pk, - gatewayAddr: gatewayAddr(tc.portEnd), - dialerPort: dialerPort(tc.portEnd), - } - - receivedVersion, err := statusDial(config) - if err != nil { - errorCh <- err - return - } - resultCh <- receivedVersion == expectedName - }() - - select { - case err := <-errorCh: - if !tc.expectError { - t.Fatalf("Unexpected Error in test: %s\n%v", err.Message, err.Error) - } - case r := <-resultCh: - if r != tc.shouldMatch { - t.Fail() - } - } - }) - } -} - -func TestProxyBind(t *testing.T) { - // TODO: Figure out a way to test this -} diff --git a/go.mod b/go.mod index 24e9af24..13ef75d9 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,19 @@ module github.com/haveachin/infrared -go 1.16 +go 1.21 require ( - github.com/Microsoft/go-winio v0.4.16 // indirect - github.com/containerd/containerd v1.4.3 // indirect - github.com/docker/distribution v2.7.1+incompatible // indirect - github.com/docker/docker v20.10.3+incompatible - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/fsnotify/fsnotify v1.4.9 - github.com/go-chi/chi/v5 v5.0.6 - github.com/gofrs/uuid v4.0.0+incompatible - github.com/gogo/protobuf v1.3.2 // indirect - github.com/gorilla/mux v1.8.0 // indirect - github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.1 // indirect - github.com/pires/go-proxyproto v0.6.0 - github.com/prometheus/client_golang v1.10.0 - github.com/sirupsen/logrus v1.7.0 // indirect - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 - golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect - google.golang.org/grpc v1.35.0 // indirect - gotest.tools/v3 v3.0.3 // indirect + github.com/IGLOU-EU/go-wildcard v1.0.3 + github.com/cespare/xxhash/v2 v2.2.0 + github.com/google/uuid v1.6.0 + github.com/pires/go-proxyproto v0.7.0 + github.com/rs/zerolog v1.31.0 + github.com/spf13/pflag v1.0.5 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + golang.org/x/sys v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index c62679cd..b531d8ae 100644 --- a/go.sum +++ b/go.sum @@ -1,492 +1,29 @@ -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= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -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/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -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/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -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/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -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/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.3+incompatible h1:+HS4XO73J41FpA260ztGujJ+0WibrA2TPJEnWNSyGNE= -github.com/docker/docker v20.10.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -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.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi/v5 v5.0.6 h1:CHIMAkr36TRf/zYvOqNKklMDxEm9HuqdiK+syK+tYtw= -github.com/go-chi/chi/v5 v5.0.6/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -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/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -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-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/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 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -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.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 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/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/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 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -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 v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/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.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0/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-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -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/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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -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/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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -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-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -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-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/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= -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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -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/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pires/go-proxyproto v0.6.0 h1:cLJUPnuQdiNf7P/wbeOKmM1khVdaMgTFDLj8h9ZrVYk= -github.com/pires/go-proxyproto v0.6.0/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0= +github.com/IGLOU-EU/go-wildcard v1.0.3/go.mod h1:/qeV4QLmydCbwH0UMQJmXDryrFKJknWi/jjO8IiuQfY= +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= +github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg= -github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/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.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y= -github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -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/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -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/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -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.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -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/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -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/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= -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/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 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -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.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -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= -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-20190701094942-4def268fd1a4/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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -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-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/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-20190125091013-d26f9f9a57f3/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-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-20190813141303-74dc4d7220e7/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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -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/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/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-20181122145206-62eef0e2fa9b/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-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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/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-20200831180312-196b9ba8737a/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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/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-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -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-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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -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/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.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/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.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -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 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -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/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -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.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grafana/README.md b/grafana/README.md deleted file mode 100644 index e8fded26..00000000 --- a/grafana/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Grafana Dashboard -We provide a Grafana dashboard that visualizes the infrared prometheus exporters metrics. You can see all values per instance or globally across all instances. - -## Prerequisites - -* Infrared >= 1.1.0 with `-enable-prometheus` active -* Grafana >= 7.0.0 -* Prometheus >= 2.0.0 -* [Pie Chart plugin](https://grafana.com/grafana/plugins/grafana-piechart-panel/) - -A Prometheus data source needs to be [added](https://prometheus.io/docs/visualization/grafana/#using) before installing the dashboard. - -## Installing the Dashboard - -In the Grafana UI complete the following steps: - -1. Use the *New Dashboard* button and click *Import*. -2. Upload `dashboard.json` or copy and paste the contents of the file in the textbox and click *Load*. -3. You can change the name and folder and click *Import*. -4. The dashboard will appear. Under *Dashboard settings*, *Variables* and *instance* you can choose your Data source and click 'Update' to refresh the list of instances. - -![dashboard](./dashboard.png) - -## Graphs - -The dashboard comes with 2 rows with the following graphs: - -* Individual - * Total and average player count per instance. - * Amount of active proxies. - * Player distribution pie chart ([Pie Chart plugin](https://grafana.com/grafana/plugins/grafana-piechart-panel/) required). - * Player distribution graph. -* Global - * Total and average player count for all instances. - * Amount of active proxies on all instances. - * Player distribution pie chart ([Pie Chart plugin](https://grafana.com/grafana/plugins/grafana-piechart-panel/) required). - * Instance distribution pie chart ([Pie Chart plugin](https://grafana.com/grafana/plugins/grafana-piechart-panel/) required). - * Player distribution graph. \ No newline at end of file diff --git a/grafana/dashboard.json b/grafana/dashboard.json deleted file mode 100644 index de679a1e..00000000 --- a/grafana/dashboard.json +++ /dev/null @@ -1,741 +0,0 @@ -{ - "annotations": { - "list": [ - { - "$$hashKey": "object:1058", - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "limit": 100, - "name": "Annotations & Alerts", - "showIn": 0, - "type": "dashboard" - } - ] - }, - "description": "", - "editable": true, - "gnetId": 1860, - "graphTooltip": 0, - "id": 10, - "iteration": 1622370959805, - "links": [], - "panels": [ - { - "collapsed": false, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 367, - "panels": [], - "title": "Individual", - "type": "row" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "green", - "mode": "fixed" - }, - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 4, - "x": 0, - "y": 1 - }, - "id": 369, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "7.4.2", - "targets": [ - { - "expr": "sum(infrared_connected{instance=\"$instance\"})", - "interval": "", - "legendFormat": "Total", - "refId": "A" - }, - { - "expr": "sum(avg_over_time(infrared_connected{instance=\"$instance\"}[${__range_s}s]))", - "hide": false, - "interval": "", - "legendFormat": "Average", - "refId": "B" - } - ], - "title": "Players", - "type": "stat" - }, - { - "aliasColors": {}, - "breakPoint": "50%", - "cacheTimeout": null, - "combine": { - "label": "Others", - "threshold": 0 - }, - "datasource": null, - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fontSize": "80%", - "format": "short", - "gridPos": { - "h": 13, - "w": 5, - "x": 4, - "y": 1 - }, - "id": 370, - "interval": null, - "legend": { - "percentage": true, - "percentageDecimals": 1, - "show": true, - "values": true - }, - "legendType": "Under graph", - "links": [], - "nullPointMode": "connected", - "pieType": "pie", - "pluginVersion": "7.4.2", - "strokeWidth": 1, - "targets": [ - { - "expr": "sum by (host)(infrared_connected{instance=\"$instance\"})", - "interval": "", - "legendFormat": "{{host}}", - "refId": "A" - } - ], - "title": "Player Distribution", - "type": "grafana-piechart-panel", - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": null, - "fieldConfig": { - "defaults": { - "color": {}, - "custom": {}, - "thresholds": { - "mode": "absolute", - "steps": [] - } - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 13, - "w": 15, - "x": 9, - "y": 1 - }, - "hiddenSeries": false, - "id": 371, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "hideEmpty": false, - "max": true, - "min": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.2", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum by (host)(infrared_connected{instance=\"$instance\"})", - "interval": "", - "legendFormat": "{{host}}", - "refId": "A" - }, - { - "expr": "sum(infrared_connected)", - "hide": false, - "interval": "", - "legendFormat": "Total", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Players", - "tooltip": { - "shared": true, - "sort": 2, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:272", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "$$hashKey": "object:273", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 4, - "x": 0, - "y": 8 - }, - "id": 368, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "7.4.2", - "targets": [ - { - "expr": "sum(infrared_proxies{instance=\"$instance\"})", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Proxies", - "type": "stat" - }, - { - "collapsed": false, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 14 - }, - "id": 364, - "panels": [], - "title": "Global", - "type": "row" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 3, - "x": 0, - "y": 15 - }, - "id": 357, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "7.4.2", - "targets": [ - { - "expr": "sum(infrared_proxies)", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Total Proxies", - "type": "stat" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "fixedColor": "green", - "mode": "fixed" - }, - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 6, - "x": 3, - "y": 15 - }, - "id": 360, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "7.4.2", - "targets": [ - { - "expr": "sum(infrared_connected)", - "interval": "", - "legendFormat": "Total", - "refId": "A" - }, - { - "expr": "sum(avg_over_time(infrared_connected[${__range_s}s]))", - "hide": false, - "interval": "", - "legendFormat": "Average", - "refId": "B" - } - ], - "title": "Total Players", - "type": "stat" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": null, - "fieldConfig": { - "defaults": { - "color": {}, - "custom": {}, - "thresholds": { - "mode": "absolute", - "steps": [] - } - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 21, - "w": 15, - "x": 9, - "y": 15 - }, - "hiddenSeries": false, - "id": 359, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "hideEmpty": false, - "max": true, - "min": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.2", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum by (host)(infrared_connected)", - "interval": "", - "legendFormat": " {{host}}", - "refId": "A" - }, - { - "expr": "sum(infrared_connected)", - "hide": false, - "interval": "", - "legendFormat": "Total", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Total Players", - "tooltip": { - "shared": true, - "sort": 2, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:272", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "$$hashKey": "object:273", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "breakPoint": "50%", - "cacheTimeout": null, - "combine": { - "label": "Others", - "threshold": 0 - }, - "datasource": null, - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fontSize": "80%", - "format": "short", - "gridPos": { - "h": 14, - "w": 5, - "x": 0, - "y": 22 - }, - "id": 362, - "interval": null, - "legend": { - "percentage": true, - "percentageDecimals": 1, - "show": true, - "values": true - }, - "legendType": "Under graph", - "links": [], - "nullPointMode": "connected", - "pieType": "pie", - "pluginVersion": "7.4.2", - "strokeWidth": 1, - "targets": [ - { - "expr": "sum by (host)(infrared_connected)", - "interval": "", - "legendFormat": "{{host}}", - "refId": "A" - } - ], - "title": "Player Distribution", - "type": "grafana-piechart-panel", - "valueName": "current" - }, - { - "aliasColors": {}, - "breakPoint": "50%", - "cacheTimeout": null, - "combine": { - "label": "Others", - "threshold": 0 - }, - "datasource": null, - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fontSize": "80%", - "format": "short", - "gridPos": { - "h": 14, - "w": 4, - "x": 5, - "y": 22 - }, - "id": 365, - "interval": null, - "legend": { - "percentage": true, - "percentageDecimals": 1, - "show": true, - "values": true - }, - "legendType": "Under graph", - "links": [], - "nullPointMode": "connected", - "pieType": "pie", - "pluginVersion": "7.4.2", - "strokeWidth": 1, - "targets": [ - { - "expr": "sum by (instance)(infrared_connected)", - "interval": "", - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "title": "Instance Distribution", - "type": "grafana-piechart-panel", - "valueName": "current" - } - ], - "refresh": "5s", - "schemaVersion": 27, - "style": "dark", - "tags": [ - "linux" - ], - "templating": { - "list": [ - { - "allValue": null, - "current": { - "selected": false, - "text": "default", - "value": "default" - }, - "datasource": "default", - "definition": "label_values({job=\"infrared\"},instance)", - "description": null, - "error": null, - "hide": 0, - "includeAll": false, - "label": null, - "multi": false, - "name": "instance", - "query": { - "query": "label_values({job=\"infrared\"},instance)", - "refId": "StandardVariableQuery" - }, - "refresh": 0, - "regex": "", - "skipUrlSync": false, - "sort": 3, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - } - ] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "browser", - "title": "infrared", - "uid": "12312312", - "version": 3 -} \ No newline at end of file diff --git a/grafana/dashboard.png b/grafana/dashboard.png deleted file mode 100644 index 33d8892536c347f4c70816548e85259fb47f8e84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 345785 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

x-v+*jt8t2>p@Wae6>xUCjD`1!JR9Q&;KV=H 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�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
{5^fn9!W$5s_gDhdh%8-jtXf_ zGFLi9PQSQhUc7L4JtVQWVhTx;`DC{r(R!}9ZGqd4XXLb^h-K6hTuNAv=!wkD24ZZU zR{+$_aILRGxrhget;yrTJvWcFwp+9w**>6$U*($swNQX1zRh8lsG_*Bcve6oe(tTr`mnpm}T*_?CCr;-{9F3A^^lW`= z%6OQNZo%||v)QW{0#v-}6s}twQCI`t++NPOLiA18B8adm{^VD#*6g(SiLjnajaWqN zL4fE z%XimMZp)M@hxkoso?mcXsccf;jEyA{9Xe{QF|1e(W!jv$tn%@GmJ*_R zwooRlyKS)|_jux>Zx)CkGhr?U4~3eWgCP50gHG$P=}*gGV3yR46dWJGI09?{+NIu)nLzJ{i;lGN4`v$rQm*!FZUX!VxD_aj2!Hw|&)^V~6O&`o z#O<5@sRCHaH2y`%1BoIcPiU-IwWTkc7j+k_*yjZ`bZuM$pzo-y)U9-_CGooN!txib zr81rY;;ta~F~xVizXd9&+RlYhV3a5!s`qRKJm)$AaXUdW&zD@-<0d_DF<0R87z+7t zBoBrQsp09d`l4iLa?EUtfn#@j8e4Ij)Td_Ma&!qC-M#Y5_K4y)LP6+2gERbC?l9|Y zFef73G@-93H?jB8@f17NLEm`A?j+>!sobxqq;lK|d?IntAVcJtV{cl-Ptre;^We-> zIF%OuVRII7WW5;A##*=AUjoz2)JhH045Qz>Zzdw{ylON-ji<1;Ld(|>kY2_F(D`;4 zo%tX*;Zb4JAOdZfc|E*w;pgr-986h7_7JIUWtDxi#aU@AK^YG}5%dM#%ev+5j+&N= z-(f!2@h#Z4ns3PEJXu)4OTBxFCkW3Zr{ztb&Z3Wx%0x*D<~Ty)<2*fcTkV22aK#qB zTKp!aoBAT!7Sf`kopA;RM)So=1&If>YS^eOSRnVEsU?=#RIoSU z_v_o=x1z2ELk@lH1c#sXQ%a3m$aX)UY;09j%Rk1Flr+;8yjUlHVoKL+{blhTeBRH_ zKRl5x)R?8{&cPPqd0+0RVdg2w#Wev(`p!1)Q>u*%(q=2;9V_ZeUiLt@)`^-EJXUiQ zmJE3cFfe)8XINT~bjA-pd_0P5jhLZdbhXmhpcmf{>^06F-%`aMqV=29=H3DuT54g5 zg++NZa$FR+OsG|BUKigGS~mSP(lS0P&h*MuxIyAqT-ueDV0%5=W=|3Vr+i-R?N6EZ zVT!cJpcfoolhmO36u;Ac!BDl5eQDbY$42* zvr=hRdHVege+fs=X-kVYob)>{{K;|ikVsdO?LoiNF+IkX^$sCMZ5o-DH(&&T)mbc4 zomP_m%52DM?z<|wy9vd(-DJO)*~kL>z7RxnV=}TCghgY6$WxA9lsBsPU4EuhDCRDw zLlpcfr#K?yjs!5nqiS)N*+^}FuYHfdj~{n!um`z)Rw2`*G!<|^tHT>Ikiiak(QxU) zVnB|GD&*S`=%{7e+!Cm@9+Iodad&-4W(lu8FQX^;PFY+_iJ94v94Q*UHbZ0Xpt<64Fd4 zgK1grs2viS$Q)Q`i((P*7&QmAzQqIoe#ALB+b?3|kOq-oJ~8f+T_XyU$(}TNDfb;% ziuj)jm%p}*tut0T)}Jk*ERrlTF7j0~(@XxK=?pem9TjNF$k+S(-D=y>wLg5B%1~<# zL6$89By~`%g1S`@K6-rHjA8`I-f&}}48un(v4HvE@hnoEtT9&ezuOy?dwl3{`Nh2Z zc|CjHB<60l)k zH`EY{%iitjXw;>vNucLK6gi&JhB)Fsf7)Mk+|4f)+lu9VA_z4ig3l_20{jmUgl1>I zwP4yBxTbuf@6E#~P%M440rP2`O>0?UJ|~*M6}))y@S3%Z9N9j)rMA$)52{<%RNiRw zPCL!}^j|>j2ahz^nG1T{@IfiEluO{Z6vsw>BIB@uN)hLf^ps+8B7)y_-SpIPqEW77 zR%l2p{S~nxJB*|+#fG%sumkv@mI<7+FtKzq;93i<38Dn!{IvZS_WQRVtZOD@!Ze;9 z)-*+ejwpRj`tsf`vR(YvBMD=t>%Q)r_FQYiu4hM+G9}l_Fuk3AN5HL1vEZx2;LH>C znVSpmJq7phUl}H!V_}QMPug(dN+Qe19tA_M4AjS7`nps!_8@wBwS_V*e4`tg9zTtb zt|Is~Hz!0iOv~5VTcr%e418WRDN$-x@KjpQv4q>^)T5mDVu;u#l_cGB#yB9i{RZ-`dAS)pZg++9^8}oQh=-rv%y@#+^nCG8-DWe=}1) zwLrQ$7+F^Nc{BHU9bxshjr7l0(*`}sSY625Y5`zdp2#Ei+2hG&`tuor@oiW#>xlM4 z{kv%dPQA8Aj6gX$c`TNT{20b*!FQkFLIX0$(WMfZZrd*$l!*M5nMa2t#i$Y6)i;qG zI@BJL)$b0;BJ8d>VpHPbet$4t?i@c-=D96gs|Q)>XQB-ou?KkxmbNw)W*8FCb7=)e zJ&(&Md|53r@^W0glEMKY5Yl;8-3qb2Iu`seFSmKmPGY6$*34V0lNb50<4rrF<@^C6 zcqbaGsXypm$u~62+fnXoKy6=OSS7q9E)81F{3R3&7;$WSKQDM1fVB~T>aS#fh>$sw z6|N-e2QR%Ki?vLz^*sFtKH@bs2HEvjzN~4h5p-U}2+Q0s{C#0pu3v>{bp#R{*H_)E zW82?KW4Lq3f%VQ2M<~EOx`QFy3_&f56HWGW0ua`){f%K%+8!BA;>E})y4xSr`Dk~?W%Y>_%xGciKU}l7eHhT(;(@T6;Jg^F+SoZ07mu1Y z&7P}jSCBFXcYexgbnf%HMm1ijc9;CYU|H1-%(Nk$!S}rDSF{M!qjoJ~7$xb?{^b3k zfEA7P>oEcUJ^*v)E&8Z%BK}PAdXwA`s{C4Bt!F22c6U9<~fP9*vos+_{ z$}AM89NU9%a0H=)Bm0fJ4aRNH-Kp&{-?p59h3!~*9njg>x5otf;(uYtM>Vqg`i#1< z<|jze_bfH)9*2abaDSiZ@_m2yG`pfy}Hu2@Wv_r!7RhCmoOG_yrt9Lz4Fvu z1Ummm!K{-6S?5uCpCTjmq4c5mOi8s<^6Om0U3?xYqM5b4;g!J~?!rf&I<7HK5&~zG z?LD26@+PqY9J`p!u`MAmkpr@ytYMVBX`;hN;IDsYpU=D2lEkz8kN9AujMLcSotV-w ze@SG58C=BtFN9YEN1E=QKuZrzoSf!s4LJH~)mlN;)RELa`$73pH_7JF;agAwLAP0P`<(1J-7JoVYyaq6QGEo065P$Monksdj}`py*)VAXZ4+j7d;>P2hLKh_=b9Voww)!5qdtI) z4y74vKbmkOiAnm~n)2(@^@5!Sq4)|#{M7^GOlXr~iC<*gpba2R2&sk}eN8q^ku0<@ zv@etlF+C3ZPVD=F+YPQk_N)Q$bRH4MA6Czz29M|XJ@k&&rXFe(NGIe?NKb8zEebXr zZV(M$CZ}YB3un`;gJC^2xqI;7%JEOGwosETcaOjO>R4Yi&FXl6x?b15ZCE%az9A{V z`ywpyWrl*sEoTUU1gSuxTV(3B7_2WAtsGDV<&z`@C>pW(9(CvGmP~X5h3lT*% zAt?~m_qWiX3qhx#w@aC9b&5QM$hLDI z1<9vkT#p^QM@h#;6%8LNP83UU1j}qQ^o8gN&7(?LL6A!kSnv z6Okt!Mw553sMKA0-nHpg!w#6wF=UZShG>R>{L>)-5R>@|zF9z~>bd!FpyW|ZawN26 z51rn??`>AZx@UF|7s2qidyel*s$k)%M?K@q4aZQNmira8@7IK zy}pD|jzDU6@%!O*mTFrdX*JRq$-AZj5*Fm0bd`dV-v>+K1<@?OD z``M%GJ8Rlx$9)AnjOa>{mRF-0{DcB!#&Zxntk_cKQnxEAxh*pK?MiR3A=#$R^abmgHRHLZs_r>iXl=e2xtN`eW#f>Mz*|jVkix9 zk{AW)x?c1voL~=>1p|@w&SuHy`0$eqsiCsorCw60>H+3h_6DI%Lji$CyJdBHdmxhj zo>=%*;?9uL`&vYcN}L`VLiR1P%1$H;c8&sxb^WvM34*+5NwPnG-p42`Ojf=a8$g=) z-xV%*7-IH&V6HYbImBu7E_z+pe}P#=aCJ&vB_x}P_u-ENPjZS@AR;|zaON4$ht ziS3N(cjElJ7^hR#=|?GXX+5uJD_fdhgiP}?3S82pGaZSqO3lcB6lwD@5wloUkp1Wg z;%5WPuILK>xMiSNG(eob6HdpkRJv)vwXR!BVGdvT*FU5vyZos<_3&Ii%{T5#>+j*< z+lpXK+gz5Xyyilb-K0+g*Yd`eumUlp01=k=8F;NSXyXLgqtn$<1{Q4khe?}n&N&8_ z>ez=NZY4tO>FQ$4o2y|NvA^_p~*n-8p|;Cn8`4fG6k z`x2c!#vS`I>leKsd|(cFo%t~M^Sr*YhD{I15^$1P{5fLMi%{oa(r9{#PYBZf92nktINi(>>#0(1w;Lgw-6eQx_U2Ok<+gWT&LR$7cgOMXwQ|# ze5o;Bf1d4G~`1bn}; zqjqL$P`|(?6F&c+=wIH*{<=4$`I3{*Z=#0dRb%t0Q=?M@ySh8F~ zjF!QSEL~{*R>y&Q8l3j!H|*2qvWQvo$_WleZ2eo4%H{~l1!`?7P}Q$k87v7>A4V~6 zP@%*hn8WSe3mGZ3`zZg?cOJV7o~NhYDcp{N)}}lIFH|}uJ4yezF0XmVb~0YE)ddS3 z+(0txA8O>!jik1=yZO-(!(jS7zsP#XNb5@$9?bsp5oNUC(ZpF(9B_&x`GXvAYA-;M zXnr)E;Pcv0=X$`bwgc;H8cfQ2LI@wv28j)P_@v*ctl!FJnaVr9`uUO#_YP)Z5&y9A zpaqwhDJ$sfwVC#tPVrYK3TKIcu`I?JSkqr(yOj z7nBBSXG^|7dgo+ss!j#&Gvc4aa|?#Lgmj z>f42`A5s_vGj3Uo7IsAFqDM7Cl!lEY;nb>UCrhUQwZbvKFp?1HMN9r7$oZ@&1y5s| z0IS156X`iFEx*QP?t9g`HN8C$km-)SQ*n7Q!9zYiA7NK49@jeCdJ^UPw;~R0lr2sM zgJf(*cLdgEGCrydOx*&=2<}&D-DzcyGEcC|5~r;==>NHaH%ZYVouSduHLNoXgKddD zl>Gv2C3dY)`_8~#?8JnF`rwlwwbg&4hKo^p{GN-in@F8tFFk*B`Q3G0q8WOm2-5BN zV$R}^t~XJb@|QbaOJYeomWKZozXK*?4~JD%cQoZc-5cod_+{yQbSR@MHkuxu1S9*# zly=)pBvW2g*|0tq9{cWGtA*cLIR8+^N0K}=6?o|ujvYo*ZoYa{|dD;^pz($ygJsxc}{K0am0ZTs>){}!m=@jn{Xh*^odtplyzIZ20 z4D@V)y6rf5?_{KH^rvuiNA~3e(unXt)u|pI#VwY(IkuMc)`vu2{fV^rH#Ex*;)u5p zgv^ZXH8k*D3j`g_1~nMSw72E7m~Neg!S*6wV&n^w3(S6HN44fD*&?%@T}Mbo-IN{i z2+k4bW$)*vGJGRs(^#;7Ijpq zdH1qx;5HpFCRYuj^lOT~7wzXE2`0$M9J@cd@RxF+gM3ctEN z)z_0eyle9Sylc*~Ha4279p~x}2%LbWU|{7LS&_Jw=q<_%W17GYC;_7SMT+0^U`@y2 z-(SI@DWJ1WHAyVhQ4b>hQ=&ZbyE+2@?UhZAc?hvD8i;km$4yHljID%B65^%v{_cBk9 znXC;1k(65@;Xzf%JJjCm#C6?QA~@oUV5}Kin|!(NNF8%_eij%iw zi<~pnXyE@*$7*=vxPoo?0b%?LLVdy6GZFnPhIT|)&vP}omT#;-mR%fYt<&Ep{jjRS z1+EZiOVxzkiGpWr2zU6(H_o)kf}d+U8lz`0gIAoO7yL7%Yf|8()j;I0HDw_SA(TPa z_xr=z;qgouj6oL!Vv+=nWa!z>sd30p24|ZD9~LjnQ=_j^s|v(sWOJEorVc zH%Fk<^=Xp%I9*7r!E<(^{9=US6|*ac+rME~iw(r| z2_xLDy0Oy#QYAema(`djh#~fP_jcm(NWS}d|D?R8X872lHUYW-q}))uG5Ao!U$2|x9k#HmP^|suPu;|@5`Tm>z|tTXgpn}ln9(p8fZTVKO(ts zHWU>-Nd_FUO*^s40ZGf>q&~7hFIe(8&)VeZg zXwdc3$D|fhJ$fU%{7mYaDR=ZbCBvUW#EQ_C2+@*u&pZtQ^BqYqi>(3s8PO(hX+3*^ z#L8fxb%i~+Droc=54%IGjh-x(t90gQ#aNZeR!AGQ`@TYsr%K2cIFLp-l8by|QdCsLPw;Hbv%0?iw zc4sp6{vStY;n(Ebg>kx5kPZb2X+-G|5K$VDkcJ7;%|>_UPmykrZi$T;9V#guBPY$+ zsDX@l=ld@_pXWa3zR$U?@72FAtj~R+5NA|GFR=gNW09(-^5h*4r0+P!)pImZ;!rSN zWR}k%pGkc6;PZbY;GqA@cKp_$>D!^0|l zLf{>6;ACtQ?lBlJXJndzJ<}cbQo<5}Pc=kikNprR0Ubw#URiMm-T?D|59TsDW4Lw@ z|MArQ_7q);)`Q6s* zqV}R2;PXoI;lvXGw3Ftyl574~}7$YEAZ%hA2q4hz)JC!%(AAF+y>-FHhVd_>6FbD>(IPNxJG9bsNDI9qfJe3T#Z`=BNX?8SQ&`8+m`tFZ5mMs$UjZsC9e+S4HYO?YWW(cRzlJi$eyzdAaVXxc3$A`?Q3L7bdU8R0@yore?!b5lvM#IDLf4g z9S~lyDz(ty`@X~Cyod~#By}F7c@FdYyL$|^K3Wtvfp~nreG1BJRi>c6-y6=n!0%kp z&JyELy1z-hCB1^15@ke(FNBV-4_;CMD3XknRNelTqxcr+M%DKwUJ4h6BHkr$u8GC4 z)S)0+LcWGY<7T81vndrlj>_%SS>~iS$k=Z1@1SJmPz%u*zMak!e+$oKWfc&S?cXEG ziQaB!Lu<#M9?!LxNm6wJiC4#{J3aO#70oeatj2Y1B*yMuicW?1oF&?n==s^Nbxn_N z0K9$oKLsnRK9tNzvb%5n*F;1W()VCAWmP)Hb%faCk|lI#TK&Fs=Eq*z1eWVpNG?(B z27C%}svtSO9gJk$p>|0>imI{=&j>eQYV+6B&^u` zp@#bl;HPz+>L-bvEbgv8L>Y>HY56(XwA`<)_ovG$*8)JVSPy@n{$`a%PLYWC&&CS) zx=Y*m(p3ci7t9LrPUYVr3cmz(;I&fU6E|5Sq3S~`*WY(J8V#Nqu;7Tm#1r_GPB%8& zt3zEGqZ?QtKf5#5-IxY)s+m#)g_|3N06qa^#X)Y1FJf{&vumN<1LRuy(yJ}< zDwplIBXA~i)*^=LR^_fIoNi#AhpnEwE{q~;01lDhu~eVLWnyH2ScJEI$?ZxuFkz_G z?6v3d2uD!!Y<}~pze6FvrRX|^1OV{+^rson$;sSJbU5!^*9CKWZ{yP2TWI-IOXgHm zF2gq2iSd{jBy>=SX~DU85IK>8QwSd;jjWWp%NPuIUL zwIPS>ioXYyCqIGxcs_^A-;EVWjD%!z=8au`QzfvzuRd+65X4fnhHIEQ7z z_=kDgwJF>(J_l3DYpT+Q5h^q7oyX#gG4&dohjMq{97(`DYhEk?SI5CMXCi$tWY36O z;8DD$s4O-4&TSO*QUNEH&GUJE=fSfenKN&wdwu)Iip{I`rxBm*K|SS9YjpZxoV&|~ zUL%H`$%9x)?Fss3wtk)nkAA)L?jX_eLk+GW$&l9yFVdbdV1ED^8oHyVGOvGS#-hA^PZYFXXnE8 z=Enk)Pd3Y0dGJbvai~kP0TC~DJ|saix_hn)?qhIH?uYZ_7Q61-2Hg*G$Cc59-Gc_5 zu2!5iQCO)|{o{+Cd4p_ z%{2TzS%#q*37OVGOf+hPm6sOZnw(yRdfY3PHZHlI{hs~YxsH<45gFhiZb&g+kjIe& z$;Zd8I}EF@e?yBr^UF^b`xrTXf~cD>QF-@ar_Vw?so!u?Xdtx5<-37gcF43}P%K$c z*S^)sh41ciRW`F+2phxQso*feS4_p%_v)gR@1#&Eb#PxwSWSom)->th5)7W}{=Qk4 z#$ZJAQSL<4xm?yp@>yk5W?g;^5E*ZDG9fl*$8Fw?LNRKDdf76KW7!k-{AN z#*Ttf(Xc^^6AXXkVqlRVeBb;sR3|p+_x{tUQ~$dsb1CZX;e0!R`WSsD&z2HcB6$jX z#IHh4&#T+@tqPviPziRdc8*HXn$H|PYzw=^(A;?-zO>jZ8Ot2Knhm{s?4;yBqSKTG zypqnl>$lY^WFPzRH|Qob4fVwx5Cl`CPb^pZR8iiC=qt}$i+RDJh?`3(+6M&RxVZBR zxk)>b&9hs?$i;Ama~?zcbP#fG4de@X;-um2B5Z*p6;}l{fpk~suRpKgphypxP1?!E z%6yjYKm>ZIr^x?U=(Zi~^1O+D*F}-w5GFRMv(NtQ?pt{S9#*M4z~F@k_bdpmz(y+& zJXX57P-?}0ck>WC^vwGWn@%D;+p%dD2KVPU*F_>gFv5XY>XFOlo}o+cZndIs^nr+G zrC)*qjr%)hMeS9jXFEHV1n~x&Aa17p9*oX&(>zZXtUkMCyPeJdYPwsPgfYs}yO$S$ zax^2fU-Oi(R|rz-_Wy$$AgVd^Pa zN$RO4gdmPCGPfmn`C|;?qrCsObaY(6s^`X&{E+*{C+9!?u24TAt2&`C%aq)Z_fs}C z8}s;OxcKaTBpYLeF4=fy1h1E2~y)H-EdwfF#_6st{M+qWf6?@5W{T z#}|>Jne`s#Hi%Gk=Vmy;DkO*Es_K@YEM}-rJlfq0ftRzrCwgM5@5*GX*dy@MCE)^D;!RIqx6K&|W{2 zxS%>vwzz%EclThF-Fp(=IAl!Vb*K>TFP_y(0r#aY6tsWdjIQ z<;TIu#{*m z`P;$guG^SX@=4p+d{Ezju6%5Kno%GA~3NUFrK|>IyCPB=R=a6xUz5sO2L&bVh+zAj)4Jbe8XjOgP&V7FL^s#G@Ik zZ{c@wtM2XOW`+J7k}`jNTmnv+7k7@%nhq(!fSWeCVrUzYaAHU_EfruT)G~%L<(B*P z@pocL#eE+aUv%{1(d2#ab&njaGn*gNyy7{(%T0w6;ILV=-~Llx^hK(oaz%spqWI#g zGI#F@o~(f7Y!l=Uc-{Fxs^KmsQTDiO@z2+ulHmRh>A;cSep^{{9wSYxaHXDgG7v{6 zZGpT!+1k|ri?n~LuT4EvCL(QKb@U*&)OBUO#B~8M^?kpA5>+x8!+tYVfqwGNo%;Rz2D(P_6|L6qVk6H{oD7;+Wf|nB#l>8+eV~ z$fT}!6$CU)FM||Vv=7}XW|n82rw`J^eMc#e-aSm9Y*?VIc>GsJ26|(3VCz6vVKTE~ z;Yg{kbsXi~lJsPYkFfPU-Iga;`3E+<#(@3mH*~BE&nY3R9D^o~)h0nQ-A7)ADdOpA z9pxkp3w9~7HO`X7Ate;4rKc|{pZo7D#@N;#T4L3z>&?{T|3S?MX;PT4yUeY#6hvxZ z%-HC0JQBXFn{Gjjp|^7SWnY<*%&Gs2*=^=_Rf}BdNFffUNg2Gk*Da57StPwv5;JgM z21VPYGV1y%q$%fb=61j2e^&AKPXXX~7t`dr@aoH^04?h3n`M|Hl?MEJxHe5jjAN=t zISx)CSFNyt$Gq@(RJ{J}~}hE4Zjkm0e+B;mMz` zFtlEA5otB?A8%x}IzJcm9ZeLh_vFvfik&%jj>|8}G4a8 zklXBqf`(ogn}3H*?-t*$#i~*-vSJY3+$qNEm_Mb|z5(4uy)(a48G5l>q?fc41?d=h z2l8Do8#8YSQ(Z-L|Mn@MO%FcHVB(yakJ|dLPW-aQ{YZHK!_80y6LiVwI{5EiQ&!7> zh~Hsh=s;)YEnOkZBfX6!WVggy?6%h6Z6%Z&GKE$(n&JOUQM$2rGF`yAY~&=l7C?ggDtw%ygsZk?X<31bve5IMhZyn6a%qH|rN=O`u9qclHc zyKZtLNbc4O)D@Ro-(gU3e>lJEbNyG~Hesb;#!QVx0YER({t||g;uNnZy{!jQ8lB8QTXpza8y`53J?+P$R$27OFl{@YeZ{D z!}vb?_kr$Vj|d&d$^%K`uYa*fN?DJ!|G@|JW3n8(pLJwdppBDRA_m$kTjslp zWktw+d;HPb!|~L+JW~Cg_8YJxch##Mwl#HF!4cs`n*#;BMwnJzVfiskE%rF>Pj^A| zL*hFBQW0VXRJH4&jnPHs$2p`Ct2S8O3@z)}x(s|_!^S(NR(5bHqsE>RQ8sft=xn`~ zW5|i-`3Wd4VtlFpO*XunzS+G*U3~eIwvaeB- ze(^8ydyyjTtLDN{`Y1~SBg>S_Io-9y!otKA4*vz6@v&)9#UTMXWbvx3J7{e?>vCB$ zu0<}f9W#(Pxe>ct_zEG{;H;d5cQPjDHV96=Gp-1|KQFyHxLY~-nQXk7DCcfjptxJ5 zezF}baJz22HQMRc2TE0PC{y|?)(yEd3i27~LCq)E+&%cwak(XJQ7BeYxIh(W3W#ZRT{0bop4w=3S41OQp}D$soSe z?wr4#VBhP`)QLwVD9;imp|B4e9p-gkbTnZ3l4|amBi|`Td4$we=VA6x{ylU5-zTw822UlV+Jh=N z<-F1uvDdz9M$-eIbNDqk%TI>Vht6uf>6a$Il2rM&Ql|>Pwo|sW#ZGnXF-Do@9LUTt^o~6w5sd%d3ey;as-}LIMAtdfLynG*S6tUXmD&CO;fA)2fVz*k4YFW+3GA2n$3znA`_W}*0Et<2roXZ+}0 z@Q~PQ$K;cD`)7N_p9|aH3qfZgbQF(RlXved69 zwC0#d=v!&$uNG`cn8#~mDSV)--GG1ac>Hcw4)R~D^52?K43O<{=nn;Jo^Q~BLBh*f zfK8nVf;3`E4vTB^n0(ek=#AO(em{;(!l__P_yem8u$%k^!AsP_i97W-ZVER+Bpgmz-pe4WK5nkzg?weo3F-cqf)u^y!V(V64( z(Ql_vpyH{~FvS@ev0Huw;${e6S&<^Q?w^;I9%(9yhCP1K{N+JPmh|82vj5wmj{Yr% zMrsKDU&$8qaNLkvYa;7nDXvZJVLJp58lGnDdVTXdX3Vr!QF`FPOfr3L&QDSjqnQ|j z{efNAn{c$0RqiYFE+17G>A^G+YwYO`)!vyaTsE-&I&~Md%xxCz(@!^@p^u9&BTPi_ z_Nk1l&PRFP@5|x~1!kJJMnUhUxW;hlxwmXxj7?Sbzs_{YKNUd$VO8WO9AeJEz;;A7 z@6GWk59Frjf?pV9xwx^uY8PPeCL`Lft=R+uciy{w<<2vCr&E@9fq|P zT~?i3o_7rDJR}l0dMFMS#X7HsOLw|&7V`gabPbvEc>L|k5*v#&R&)Xz%%qLsNmpM7 z)H8lTJzsWHNR&}eIg^$D@7mNqyyJY=_vDM$Fr61RDL~7&LHi-t!sLYBK9%M=nlNzyf+`63hmVSos4tw4^H1aocpU!^C00xFOJ2n1 z#4Qt~Qm*gA(Cc%b=YDAQpd9CD@cT;|uUAiP9jGtX@n|0_(58up*-`Kt1-VIaQALZA zJ(&H_8mlAxL>fvg-fcaJrSdiYYLh;NXbsLPD%4uZ{4V&@X;Dl*^OAPV?98Y}Sbrvt z+#)d&lm5qq|Ec~%wiq+^l})DFvzg`)Q||IMUY!{A;HMhxu|^xvmw{nzYM(kpk$RIV zqo$O8H@zE{COBtr#50Y}7A_tN51;qrLDCs|+b~W&lhF>DpFWD%x}elO|6ChF{2M-B zjb^AG3HQH@s^k8ZRmVJRQIlWObHF*O-~qbo{_s`D1XgaylXS-9dZol zsou;}jSP@M!r3NUhzSf|s*ZZg)o+uf{O*{aPWlavK`19X|8!Cr!P$9JHk>&qF;b!r zfjXfMxm%EKT^s||b~dB9dnXlh&P614 zrW(1MN78%11Z~1`BWD=}@M2~qGJ&7nRB`_Hl-EUFGYrJLYbMwLfO;h8`0>RSV9I|Y5X(tYWmOcxhM zah7u`80zb~WS-^icl2?9x3G`GaN%5R3VzosUXdyu_ExL4<$!G@g25|xc1(i`u9D~L z%%1W5{8m{Fi)@I253b)C#Oh^H$F&Fd{=?Z5R7`Pwa`4x@7{#Rzg683*N)g9`A0g&t>P>-t zdY2gx94*GFYmfM}6?)7wSfuRxJI#a*#i7Ma`oNa&xeqdWeCujs%>$*L8U`y=TO`W{ z=@?w3YZbYZF+1o_QtNU`QgPnfJitOaey#P#yO7m?Fk$9oI>j3=TWM7f+>r9W;_udHdzukX=<_1WpqD?u(narf!x}M)JW3p#aXA$_5_~NX(+cRc=PtdQq76M9`!361{#W7>62=n|JUI^ zH=74M4#*8vdpFMGf8X~p>$TjSC~1w==+Mq^7M_Qk1>QEZ(*+?*f+qjF8+!tFc3WI? zelhwo(VyR%^w_Q}c>AdcttaFIpSo4`nEvM>P3nf2Z>D5+8t}qpI*GrCx}61$KM}X@ zoGxZ@6!-gEBvw7N>eKJgbriqZUn#)J`yL~vb30C$^q14lRoa5SND%yK2^?$d*8pXg z3Yrbe3~?CBs-9pox65kIS>R4+qBWT%u(s_WgGqo2Un=&-lncQd)jT^}V_o1*R&geXfVPo8X z{Ft_bu+99foHO{S<|mq>3T;@myfE8T#`IzNQqfvjbM5FloI`WgcX)QaZ+q#o_^!9^ zSS{dl3df}vIRItq>&oYY%|I}J{@)nk^Ai0>#05r0-P%;xhQuHcSneqzRHp zmY3CVMCh3SGb>eCB6`fi%<8fjyJ_BBhQ&?p&=bg7!)~o4fXrK*u9(o{d%SM~Ubgv5 z10g{&sqHqL{}_a+%`mJX~l;A?6{T8Rmcnvd66m1yq%@-;XxR^u}F z3g}3@Ba&$CVz>K9MO-sc721MRj`yWj`*{tp@!*lt@%=bc9&}txi{+0N{qAR<5L+3O z_lyR0St{BYExt@y&*oEEby&lj27*mz;!6oPL}I({M|sULo6mx}#NsGlf3?mfEkiS4 zty*iEqZWK3BUOk~wFOMPgQll2jvS)KBl;aWG{3V4h*%_tzZfYwdXz(p01fpl}`{!Jv=e#(w;R-iLRvS?GU( z0Vz2B8gJh#&}iM)vZr=0VfRZBk4#`^DiUi~LX}uB40dPOpYWQzmo#m>;3X-q3s~Yc zG8jivVcUK2xTL0Oft}i>2g5m10A5RF7lBf(SbFdPZx(u*E-g~`&K2Cc8CR@IrbW9R z$XWPvquY_OV%zxam&6lBpjdB$hbg%bH0 zZ^h@KQn=Pz1=SDpJ|Id9|7&=byq(}KW)6oQ-Mjgv3KIuaR{ zX1><6{_L2){?4xx7)*L64rQ3*X>Sin(6Et^Bj5PwtBDKwKC59h%`&j~OD8-cV?zJp zAb89I-~LNhWpXjU64*tdiGnr7YlkYCwgc1X@JP3NH%@aZhL?N5D)i+Of2zxA zQ!DlnN2|F=^~r&a!(pnZ`S+CWlyrdVFlA_5X?~ny@*78&yOC{wwjtB+D#@m)=m;QzZwON5K#DW9@un9ooU_Kw+_J6jo8A4 zjYejJLH#zM_4xJG3(of(XU2X;D>MdDAMkTsx_E&wPXo4~jg+gp07UoU&Z1B03*T!I zIoI-;t7X^B@y03*fb9C=`T9|m1#nHP!nLHk8RKSjtxS9rmJfuzs?c5isD0FqVUWF* zB6dk-2Q8XZxOeZHE&{;9b?3Kj1AS)o37K~gkpcxz*RE5{3)B&^d}9c1JTe+;!m3-) zC{lodYAE$-gziVpp}s655AuJVL|kJ|k;WrSE2$AZ0VG7)1-6;CcN-r83qRGBe=xQv zN~~JG>i^d)retT+KB~Oqr;>M5P1MmTqUn=y=y02-o5NZ;EmB3f`nB6u}C`*PNF`6ERNZ~Q7Snq zll{~&T3=Ue`@hWdzXo~Yj@QIx%c~=I`1$>KhH-e4mGL=6$+z;bDh<>wP^%Kin;>@S zdDziy%Tz6~lt|qiu6A;Sse^142i$@M#a|H3yZ}8@ynYdf;ETtRFVf5BEng-9fbCzk zlDj_485GD&wA|BJcI?z{HGyN5E+`CEXPzBt0WWD6eZRBQz+%WfDSgj;+h?!7csQlVKt1IONf;y2 z-;NbVs5!mfBPvYRh1!rhv9c@nVNFgg9&A@|U(M4Km;XOYp}cG_e#3s>e(+tCJp`Z) zQnwaHjz=PMEZu*i5u55Lb_y1IXHtqBu;*s+q#`}KIu2pJh?g#=(=_jR&1MD z$JA2k_N<113;poP3%VG`b*&ON=rSK&{q#0L^#lCwvl&qhAKc8vLJ36jl-M%w^}P8;Ai}u;AUwigNYytyh>isI_H3|)?(co#(T!WGs5#tpI^^C~-_4cFOu=G1i1rbc~ zyNWZp%*oT`{bY!XTY76xfQ9&|9Xn>g-kfcj0+|A1Xy$cEO*LL&MAil}ty!JD43hCw zAIom!vDaSa-~_(CcbcZkQg9;=Dns9UwBDW^vBYk|U3L0-`EHj)h6<*9y?uS33J@9E zAnSR(ND-Za@GG;O0T!f%$Fy4{!{HZAoJI}zNACG&Ep;)6i|^-n@*Bby z<`CZPlT(iUy=B!U^fuu2yC?K`Nh5r#)qW@aubz-MO#(Wq=A9n*`S5Ri=#RWDCKn%g zRmEP|hcDP>yX9_-pqCjyE>ZcWmXKp>kxkKqRNQRzW#^iM1-CC}2g;2!`+C_s+{*C% z?6&^Mgx;QHy~y8vjlkIOXDeSC%Y`GbR)55GHcC!^c>1VpsLn2UK_XNbK}59Fq#Ej) zVjY2%RSTHW`{uRn`h0s(fx#0FbKEfgaIjDN1pu@l!8)NK!Rz{03nJW&7eBhKBLB03 z`0$n&csmFekcyCOW^KHk9)axf?2zs$=QsP~&j*pTXm#Sf zkh*R`5iag67{FIr$kE)LW?zNteY#;W{#;-yd#R&Yy>JE4qT1R?BDuI6vhOlhz zJ>f*lj;Q`8V9>*2eg|XfP(Ca0Al7T&|LfQ9%()_AekC02-8HJetpjsR zT@x>`u#qHmNhv~H^`_D_Y4f%r66?&jyK({2NOT{Y+~p`JMG+=)Uv0%sJn!zlU(zU) zsgOXF?ye0{7Y0WT|2=EGYEErJ4P0jQu!Q6}mxaof243qb?Vqc01~5VCzsdm0Ce7wFn@D-c z-Wa9FRO@~J8WstshYM|uc{EZ#B%$z5;-wGH3f>^KPo z--cRGq%0hsyGufG!tp1EYf3?{U7(1~b-2UVqd<9c15jl9!L>&fqvRSc59UUv0+42b zQ?6@&Jt157aUH&~?58g1I!C#&?87J+j9l+)-9%qY^Ug@ESX5vxrOE0qz;h##*iKa- zj2`@9LEWn(=bG~AzD1lv(N0hvIQ_os2J_;dKX<5;Pp1zax=2D@buI{EO#tbd;-_*p z%hto}2t-yLrQKFQ&+l5#-sCk6(XjRWew5etm&UxlP5kyWU)|wyO=}mOR)HQD22X(m z1T<*3o@8QywM++Yf3@{%r`lC(M-^n)eb)N)zP8Lxt3=HBqv!>2EOdtX@}7Qt=Y90p z9#tyLR$d}(%eD8+VMW*an2Z7bz!55?GIk1qGj?DFV;N$SqiWQA@g$9$)S+*hoc|FY-G|&2?f1qe`&bu6 zmL5V2XGhB9*uEACH+Q#qSrJ*>)qO3Vbg07`^QnO;asYI^)-4)P!yv&J?PKy^Y55Nm zi#6XMyzye(khq}nI9}*pBMZ>wBQpFDqCG;HMvBKunJJIFKF(DD-j8-SbJ9!~fd zIZOS0kl8-Cr*dX;dS7(E#rfV>N~9ZZmp|+Zi`_StxScS*5WlOR7VSjH2;Tk*^xU*) zI6QAg7KV?y`7`XUqn?P|HBuS}f_fE7sRt3<=#jE*%+@^&yATTx_B@-q^=Q!O0l^+a zypE7!pn-MkBdpW`IxaOV;9Q^3T2UWchlUZ#u|XKy)&j96V_(&DMYa3YfI=4ZntEA1I1U0Ie^}L`mZ+;iLlPI1CDtuQwLv30ikzY9EZ0c4iB&Zy9tR5Zl z*j9rLWjkDYAoai(*kE@>B*j;9@k&8WKDK$oNokbb0c0z7E)gCx-vHxNrr#Iv!~WK6 z0}BO@bo`5nCPP*dnpRMH_f4$+ zVtIR5Q8)0?pHZ-TNH_`Kg}hfFo1xcHL_)Y~`4x6pG`DTUEaVa|4a6P)@(JT(`#GC5 zBUg|*F-rvFt}xLJr7~9NqF8ZXT-`paNR&nWIkJb{Ej^3_~e4@j@-xYM#&)*i&V>c0TeSMpSE zmluWx%J`Lq_XM^t0zA{grwMc!EQN7u{vAZ1DVyqCPcAv`^^e`UgBv)d3sm{(#F|@W zz5qSh|K+3Ia}UMFlSj|PiaH~L2asJSGecn)puat%;13k6iVs{}^H1oXDZHWjc(viZ z0Zf-Z^Z4*nghY^Tcg-+$cg;lu-*wn0IgIaf@(t=Z z*p}gnA+Hlyg`X4&Y|I<^28jGLjp8ET7lCiqvt*;|aVu&d5y-K#^-Qhb1C75?4l(_NO zN_0R<$|@qIEb-`TL2Zu{4#w?6_)#ik{uy@nps=7$9aVv_TR+Bg5{uHRUPovaBNHvcJNGO4ZGd${aopzwEA2 zU9CVaRaHIZR3fEA%RR6%zcZE_DtWaEIKIP>>sI3;k+)}T0b zo9B~ZR(JjnVa5jGWh-~2>uW(x`OBIxOD*WRQ5Wc!GEn8X{kDPoB-6~P$vVD+na=}` zTeESO>WrGEUWJp8GYAGGl_y?*sL&a!J9z)1BV`Kw0z3LTw36>dB`t#jIdz+D< z$;-^p>?FtB8+K9ifbje;1*90x=(fVp!4j}#qd|bziVghaq>JGOQB?-iKxUPl0(E~< zWf{(dFH^5K8q73r`b%w`k?W2)nstb4D7YMZs~?9_qtp5BAOc+Jr6`l1zz@k5I!ou% zD@{uh71lmg0q73s+4cV1)*d%-Bp(?PyYA|~B@mdzeW$G2G|zb-o{F{e=X5HpCh7on>Jh^BV?NW@{FLpLz=v_f{b1ZkwWnE4j`XSo0 zgBe1JpCK+Zn~q|U*n6g-jCh^rUFz!0+^6zpHl-X%O5+fQzM=8MjjckW%@TJFkDrsJ z9{!^NkPcUN^J$2rNRD%v)y<)U`S0%{^==l#yR9x^m{>$&VVgWz;=(F>E1l%)$bdPm zC6+|6JX<1Ih9z-f<>3XL)c0xQb%qyaRl(WaXdQuIRUyE&>57@-fnoCwl|b@7YgI51 zYtEE?7YvfiH>yZ6Y(5qNUeg|!uj~O9Jnt`n;AyZlf#%WqGHLF{LTlYEqkD*0hSr*w zf53Q5pxJ&ddt6zKOUiJ(KyZo1x`S1I_s0WVlV_e1`Q1MK?}e5=h6)D5Z<~C!-apq_ ze&gPdea(8<#d=(G3XWe3{rP8HzjUJFa@|@qT}du@uFmI9CHE+WuC35zfdy+$AW^Q( zBqQx0)*+F5N>6K<8UJQ>pX({3apT^$z_r4?^h;!eaf=V%$E;D|d?xsp_BXI(Nmd{7 zoD{vmsP#4`GZ@1VOWdz#4I=hVgfk`iqJSwnd8eHRX3sE0MCUpw-_-BR{b*g+pRL zfVkQrj`6l#_=47LO<8Z9huE}AnDim?f1$DCSM{oyhdd+H18*^u-wlMj-FOjuMmFAlBZl!E|9ChVYaom4>|ZY%<%t=w^1$mRAzWxp zy!DS>haPGWLW(SO#p#WXJnNr??blID=EfOQgh0oLAt~suT&J;bt)Q){WCwzJHb@xn z3rhX598Q&2tHegEORJQvjkdJp|CeqL;z=_H@!u@G9cUcc*#;&&A{qJtcW>J$!DK#` zd|B(`6>Wk;9w9dMVQIK&-CdrfhC+0Ndc-^yJgLY|U8qDB+5wDT-#yOLY8Z?z$A zE3Bw}wF*2cp|BbwGh%M=5@DV0`pP)o)fZ@NDfh>%AH)os`*b5E#V$gYEN84Y9_u{_ zg)rZHuczJ63o-)dkS2qG@4p;4?;5RM=yW;710{lP3%HvGph_!d#8je%9RdtQui-<# z*iU9kLCaOE@tNDTtRhnj(BDqDNi17Kl00_O`@eav9Bii6nn6MS*8+YXV|Q0aql6~t zp;)X0|JSh2;J0|yrlX?_e~86@my=azeym(XzM{t%`w!E|B9 z5xE4u8`VK57>M@JAqzwJ$4o+0yy|4hD0)UUW zNyx$4ql(zL$2fNXZ23acW+?SHpeESwEb0Ven>H$bxX$=!dHSRK+lh3()R!J74)Ulw zJoyu}i`?8+?tN|&MpA*g!788)Tfa{*9N`-!e@(82&wv`bSbQv=Ekc>iXHrG33>0*> z@Doz{k#Ed$Hl|Est>s_+ZkMv&8LF^3Gvt!?f;tVp2ea>-T3=#9hfC)BN!%~bpBhYA zN|MdonAzN=Q7=}t17^0ftxOHkhq~1*PHNa@QW?1NjWS z1>ebP@4=(u@CNmzr@^~@1H&6J)T|!&vHL5ZxK5L@SekGcPN@Ll|1{NEC$6#of{&Z} z?t<^27lQd$g4kydg}g?WThtu&wF!UJ=SWzkvj~+ZP>nY6HR>d^eqc<_y^mqtB4(J{ zss&7+0Kq| zuGuPf&fStf>!1xTJ&;PT`%s-8Xy5?qh8F(+h)8IFZL5Q?{IKQ^+!hyLJH|~d zJAsoi#usXJ?}xuuz&vR0>}z@;?BKbni;bu_1v!hm8cFEDUa8_Bmxl5powldeoE$QKLL9Ol6a}(ALi3#aV;y-XTT9Js zBk>C+x7WL6v^O7P^pvC{DpyQGQuztap}Ofdz=dr#gXDMwE#rga0w{-X=y|}#JAmfD z5=MW__5o+aF_k%`i;v`1T~fiqBz_a;WW+hsdR}+FS`3?*LX19CGKZda_|anEUGESnS3I-#TqgXO*JRT)UUc1vp;i_o6(rJ_;P|Hr*+3S&dTIDii zq-4DuqvCaXQIKRNu7hoY1K%Xq3BT>-+yVe@Dji0F??koQHGQ#LTZUk7fJaEO5q4Y7 zi1W__JS9&A-$a>~C3tU~Uk03L7-ZqH-%z>z}K zL$xa1R*AU{m=nx<`fh1ehSy_Ln~P19Yl)<{y5akJS*eUm^={(?z)iA=z9#9^=1Raa ziG6e6pfo?Z1`C|ZWC+iPZ$T=K>Byb9AP2>E()Vk!LXL6!ke|^J-H57ZW5y|dJG3e6OMZKNFJ%^ij_&xQQ9@$XB&9et=bVlq~QG(8>87?X&?3e-n zXmZ5dD6(=apB%ek!H;@&p{jUv`goqUAJu0$E5#UN)?iyyGMg;vdCg7nC`pgl^wy9` zqE;hRgxrtzr`NRJ578zQ3d>BA-k}6W@mFl7INn=Jh^;3ILXZ2hUstO2559GNkiZfb zTbHa`Yw&F^;m)o9qaW8;Xjsz=SEm*nc~osCf8s&T8xXWw{=v}sd=ajDZ?seBx*87n zmzvhUH1NKciLF}VInJ)Q(p0Tvk)Jdwoh$a#aG#bB?B1{qI~TUuOf)=BVmlaG(3C@M zQP=rxS4bWyw)d~q$O(;ST`&Alqp*&qEqgShgg_n^s$@woC(>P34=b-2+8#|EFL`i*G-W%59*?5}Pf?skLNgY<|)k;>v6O=~c4miJD6E%bSh> z86Uf-%dmN|4NZPxQ)TrJ3qss-t}?$#U~4e+_OJ&9w}6+R0C_Oh&-GZ)G=ev!y^ARE zz^+7_hDi2kB-XNpiT$&@LNK;WlBjSLk?&QmBsxgPVaJmW4jCAedVW#Q;yP8MXj1${ z26w{Y6qb_pOk(*xu@2oJQo#(5=jE#ezVu{QRy5ck;nV(`#ML`W(L+$1ZPafz1iud4>M$T#CFD$GJ;UO7fjdO0%(I#mk9Mh!mKbf{g5!mwGp{NJ1$Pt7lM;CHt{y?X0RT;NAumEt#q8D` zF-6z0BzHPJgpD7iHK8FA@YNP)^E>8hA+&qZjUDBQHDYb#G$%wfl(AywBRCnKrp@&% z4_>SPi1NLMoSj!Fi68VrIwU}%jV0E&Ms#_Vo%cFCy-XN zOH$YAzN=|;Ew3=l7RIxgRn@(cj;6vlR)ED-th*0oo~gtIXy-NlwwCuoNt=s>!PcA( zoh$_jC%$GaN4s!6Hq;vsyygvP7RidtIyh+=_Kwg*WNN{52fyRB5fBQeIE@Y+`y!Zp z>#|`H3qpxi7>!T}K)u^s<(64x%-)6Nkq#p1}4x8R5+gXLy zO~S1mv6%~ItDl+jxS0olSTcMevkJv&O?H-JX7>KHlrfYPPknjG4u*sRioDiFeD59% zKNtpyzU2*Rcx198H1v_nMP$RqOtm_M?qF_YXeJ>0E7#Ae_N?Lj0SH{vtYpPs48_cPSbg6>PzlJw}?mQoJZgu84zvGiq|>*|I^N{KR+uZch;&J>;Q!O@)Axx z5)boMv5O}cX8)W0A5B!y^hZDO0KM#x$t%CU$`-f^7kw$)lODN*hpEgnMr%47f1V4c zwU8Sq^dVa;Dn)s-2R>O{PM^Np^r|Y&Npi}by7zxaj0f-UAh8cw4=)T!-ZIvu2aUV* zW@;`s%~WqMUm$v4rdJj0Xe$tza=o3b+9XC#@L{5aGIk^Z~D*9tm8ZsJs-L62?489A=TnP5#w ze!scaX&&Et0_zSmLX>gJB7Rs4J5P?LG(L1&&BHP~DI}aD7PT*^i|_YZaEv(FTBKB4 zeUII#(?*}#7*98~G&?j?4Nu!4oRXwuEp&_n6|UUgzUgr{fL}Zs@f@!EEH7959+B%& zY%?c?f4iRkuHQzTy!ZOxG|Y*vj`jr$PwnaEq82r?eCYS$rKH!Jr`r>FSDEGf8V^|W z8&y6p(*%t7m+7;4>L<66VDnk6U!GhRoiP-5zrC*ecZS56jrND4JhxF?3DiA^-V#~y z%TdDZG7bcu0SysL@?$#g>e~4K@${8JZFOC^IK|zqh2jMFA}tgsP~07YwRmuc;?4`j z-7UDgLyCK#Sdaq6-R-8|y>ox$S0*Pj=j^@DTI(?|Z#>qq7il0Q{>tpoP0?u@mqgeh z-sXAP8qAyBeEx?8_jeti-zgk+rx9WsF#DjzsbanCWH8R+PQRH(iV?asXwQvSJNHnG zZw5~j+^qDVn4$kjxcP%KugkrCaYr^r3K(SipTgPKp~R9m!?@QQ$M?srkC#lHXMR}j zxrt&ow)uSp(+;(BkxZ*8pP%gF$;tmri*k`5Vk&SFm56qmgL0Wb;tg}b55p|7ICaBb z_S4qh^G!F(P77O#Xz##U;&>EcSix+GE!kAo^V=)S_)oM#lw-heK_(>r4#OhBoO$r4 z(DtX4Qb;gu%dT&r<1+XTA}9?yX&$KEvq3J5$9Da@^M3f|(DgicS?d|bfaWAqzLMw< z|CRECl8>(aZAKXz$lM}SuIBj8tj-c~m6}(T0B<=5(VjK4N`{S#-;5gr3BX{ zHgVp%n!D0%Jjd|_5-CoChgqM$RUUAqmnmbJs^Fron54*T{B+_#9g1ozd?PauR7?Ou z2fRT>Y4?-3>K|IpPWXpoU~4LLa&ko9@mzMm?~^~+v+)-}vL$CD+9lt+cW>^Cx(9mS z7__onr@awrly${qR)BkBnHt_aZnkdDo3&ouU}#gF4jd5qE_q(7+co&J z3L28=1}vy2pFj?X{FW?Y3A4L*L>YA*d2escC*&oL8r)DFMD0<%$-~%{ zDZ|DV4imQ*U_{~DY$f1ZYr5}4LbPR{V_7r!AKW8$$O7!Wvi0cJy^N2_P_MVfmXhD!(G0hl&;9a$0}cgN>sco3Y(qN!DHi8ytHH1iLA0s{KCF z7&p>a7RA@KO_AYt*LAjC3awIGWv=Owu%z<)U%dV>MxWbOU1`9Hk7lL=y2_t) zFveAvlnIJ`sofkTtR;9zcS|XqPX3=1yF3_w9x5oV6LHiL}tfcg{rhtZ9c+ zl>i2=;{gLr@f-sJcw7Sj75WPE;ar(-`BXHy0kZoC;2T2sxXc@qO7w{;)W5m6EC&hy`5f}i*uTO*?%Q;_rFMmo=aziFc|nCrPQO% z%;FtK8}(PE#d7cbJD(%tg}bBh|0Lw_>R|6m9&(7lPGdN(yU*l5m)dvNcS_M`SXMRL zbPz6W3|lwP(R^?G@GnW_obD#~0X{Ij={PP6GNkhma=?wEN zN~gmh3&ID(C%6fYw{9b3M0ls7W=`*;yWcP4-k(39v*a&gZf}%-yW$_TbG+DKQ?O{5 zhcL`Lq^Q~ixCaUxxk*Y?8Fygjvs11=y9&DSYhDd%-kQ609lY9HcW}n(!0Yyv+lZXB z8FE*{eZ%}}E={woQE}OgeFn%@)LQroRR~FF%&?Sl-eDKaxRzWJ;M{znfgkb`{Cp7+ zO}3p{O#kwaABarXzJk?`6cJjLrY_~c0*{R+Opqn_K_2l1v5SKNJ`5koQ1Y0@( zGb3@-{9VMq!CvGBR)PfQ8%7C9Vr_++^m7+XhQ_)!wm`1VJy-7zFMmTtx}V-PirG(c zAwLSazb6eXZAtN#`n0WaYd)YfCKvA4-#A!bglR!pM#4-)yWEe~6}tt0cm=D+vN3aq z82!9Ec+3SdSrG@YD~{O(na73G#qE|k(yd;(fKS}mtIen1#Mi0ojNO7(Yk~Q#A7>48 zz=ZPZv;dn`3ogH_jXKZA*2p^}u1XQsMpd0+a+}%Jal{ysNAs4=e(xTJf{m)pv6?#nGw*Wk+#ykO#px-Y&ljqJR=6zy80n`Y!VCa9)|8Vjp06Md?^AWQlsmHQL05hVe~^0uv2Lcto$fwzc=A* z5zD48k{G8Q(=077J_iqX_h_^`x$d`=iftxLN^j_j|7lF|7?9i^lBx6uMhB2K5Du$L zru~-}2~QU^^%;wTOhg8xIAt6*eNN2L=(|2<$}wJ!;WMuIpRa4%3f{mETWyD8uWDEENIf49~gM?KI2lKIXzu4JEX z`elT#hl|VD+|1_$enyiU&iC>P#89s>$1GtsAShcb8%&f{JDFF>{}T==JU#zfxhGD%!^qRF5gUev!G=235Y{X+d>q-d*_vh&ttzy$Nz zrlxZCBBPszQ45#ceL`s09ZTc_${A(7?_Az4dE zVF@mcCQGfLu#K9+6gu%FjsWe@JnY{&pxnDH8?sQEIBrqrb-CfTu~xHues*Qm`Jsbm z0aX*7oxQ7V0#i!i4#9trF`8%FApJk05E7inO*J^NsI9Ao-TWU-JO3;%yg#p$T2?nl z$VJ{V5*_d_d*kCSmu|?VNm{r|54hiJYGUZ9W^c*BdnykTkJi770a4rn%#$iZ$ES(eaC2_ ziSJ=ba`xtp2GkqX91gJ?qqhg3gJ=Qa+gQ=8R*=9G>Naq`QHJKxTpe30=Vpbo(PI$4 z8lhW*t6|(P&zro|b)QJ$G;5xrWxa^D+8b~e|IJGKuq`eFygq_M9W$7~DCplXaJX*! zJl9Wl^T%Pw!0=4V}MM^o`Ns9O=pgasTJFvn{xWCvCXP_-~XU6Qc4Q zf#Wr5`UQIdmV}pY!2qi!8o6!|}itl;)%Zknu?E>BC+P80{GrVLW<~-u*iie-` zThCB^Bfi_JSf`a3G$GrbWEO;&H!6JVIhE3VhE_SoZRX11^(CoE&}trMoIMH+dCaM; zd^3$N-?@7vkOs|jCcE3H_?O{rf-~@QBz&nIR$bn^oAi;!fo@B`S0THBt~maK!f_^K z(b5RI$7Tvc(9u+4f8NgsAcb)n(9M z^86eI7AIlftmzx4E^?~B^ldGid0xuQpzx0Vq3aGi{V>T_cka05DoTTD4I9Tgh~Ppk zO^4J};0TWkD%`(IMkDh_faFXBpJUZf@vBkTv5@l#)D2`nthfSP4l?{J&ImWVFzCSW zKl+2*jd>}fC@vJSd$9$e)&*Q1xeAX77cCyh>m zY54Z%LKl}J2OeG{f}pC_tQRfaobUy>XSvHAiHU6g{(^6Jye9T9RA-Yb%UhQsVV3M- z80Jtl;K$|B?gQVC0j(z2A0(1~K8fr9dw%^< zF0w$+Lkb3^uWH+0oas**+BKrOi6nRe|SKEGZzdhHoT(4wp70a7r}nB^B$PGx6bS#0+K484COWsEGdiXu;O9 zG;(*h_%?1WkJ8#fT(c(*Rz}%Tr5`5{R(xV2WNxdqk5K-2(2 znKD@3t0!yt`TK9cF6EFwGm`WbK?sVs|F7W+V*@N^i~mS_L;oP@a|HquN)3wUm@9cB zDkQbmI0EZdQTaXO2aB-2dH8b#GE~G4buvNesNiotl27g)VxN8?N#lH56km?OIw^0_ zfIC48rLOm2!)>`|Q|{o8B}f&n*Av^O99s2HBOnNT94%5aHuT?eGIW08t`uC47ldOm zC^6JW!-=wWFB6PvsdA8Lk|?ZPE=9N@=#Klj#Lw(QwWuLwenY?k<5Af)vc3Fn*07*7 zgtMq=o5}>GZY?Yunls{gy60{vSxDPt&r`_<98z*ND8LvK8Nu#}CDlx9m2|)vR$Z6j z{)^)uUvBOYMCuV|^id=Q0vnG1M*xmtS|M4jM8I zW4qUr$86OcZ0lLX-zyq^?LYqlVdBDHJqQ9;M%%%Ij$KLW9F#PZU7`1?Jy?th?nXW% zVM+|7q0wxhoHrGZ&-#Tj2*wFTblIr+@t@6^a=cqmAx3S3a8BPllc)3c;*c-dQ={st4OaiJceL{YBQ~Rk{TM1e zx(H-6+SsOhKM%vc_ix|muK#Gjr)>ojv8w?BZ-gCSl~9R?lsc0+tX@!e83ypOZt`Y= z?f^KK1gNt5*(TX+`0+MBkC<3$K(kaV2Jy&FIS31J+*k|W=;N(JRoo_AE7f(w zv4&%)oQnfmJ*X1kh4J#_jB^ukrvr%T^1SjiY3Af_UM)s3f&@*$V4uG|5h zk%@;}o0#re3Xmbpyfk)9YweZ;@7S&PXAe)Qh>fXM-IYmx1Omq&8hv{ICwu(I>@NJK z`b2$vXWYjH#66C|4>xOis+L?Exy?({~8&ZA^KT@q5 zH#_d(eFqYS_)=+3jSRvhV;KDYGfHro!^;N#lIhZIyo*Qu}x zbf2R|B*UkdfE)W`!&}#fkjS}4)aGBbzL#}(ZZ^M!Lv`s z_A_m_DJ0~E>Cdip@!Fwk$PPcDU}aQXE0#x>Ha}wsy$iiQ%4z5VNJz7}=ZPnzblrX- z;CsTVZR$M6bXN_y*XSb=<!fjg&TEu_>7UG`{%~Bx06rVd<5PnsSRy)_-tEf(Xv? zJ9r590)9Kw0iG1#)B1pAN7U!c93Z8ZnLV(O3$@ywnz1 zp6TJcmNw}D6IP(~3paPM3Ke2Eja0Ql$ez9BT5o88z)Zb38}KN2eP0uvy7}iYb>o+! z4Yyel8RS$ew5x>$vP{&`(U_%2H2CL}znIsOZl}{9Vd|F6w?2viW(zXy7qcsgntHa= zgX46f+@K;nbVrjPa~--aqM<47#D*+k{rS=QW5$3E!OtAFr|YqEOn!M*6v)Cv4g$j> zv11t+?Sbw%2xQZdh8G{j{{c91M*6|00kK=Ve6ky|*X_4-g>aV3LRz~vf{&ec^&_qc zT+H$?J*{x4PxsW0@#^Wv8t3Cp;NB5{c;kO&{d5avI%@D{tX{WA5hNZn71(XZ-~7AZ z7_eX0t2xo-bBu=kM(|q$BKgRYbXvLhc9oB8%H>aycZIT!#J+3`CZn5i$&LygC zxm_>iGFqGg=P(OL#_GEm!ZLI12EA^fo8tzyJXR58Eiabu|IG{mTRfA8I|1ZjF7XsW zm+>@Fws2bh7sKM_(^|81V^zV*Pk-FkI*D!7lXeGoy;y)PoN`_If`U z79RQV=z<`ew6QCi1|8>93{E?_VP<1)ij4woCQYX`2U2(2F41ZgIj^r>dtcx~{j!~7 z2`w0ll`m%tr3;C&xm|;N1&>f&6S}6C`WIPv)4nO36<1ux(o9a!`+KFF$KsYCjaN9- zz*K-K?Lfa_f79DcPU=PK;NdCsJp!1wh&g63R6%4w!e19>u8Nr)vdYAO8WY zVQS$4&yISt2rvIDNdDQ*Yy$`oh0&AH0&#hY;PnUH$n)ng$n=6K7#S zWalF zlVuNnV+mlVa==9ko8M_dt9&u60rqYD?)m*{?cNu-*mfY@0B7j=92Kv$h^prD;)7Z9_xa!+7{c4-S_ z6OSq(MTQ*f#WHhJ#6~NVM|nlhOQTld(CD+M@16K50!w zb8``A*seAaWcheM0ZVT4VGeFI0>eYq<~i5&BciWh5Af`NzuE$0S?zlLcGRhm?$?}* z47V7L;mo7aKjA$QHLwJJ-Bqs{i$Xkd%17I5oz=nG;G&XdS<`)vF<1=oL2-h-6gvG@ zW;hF1LNi;zFxo10;g~b`?Th`h%F12Kt{u)c-ys++5j?&OdhsEg#dm&q-ue}JB% znIn}6`F)2lr`o8Th_wg$ds|^SJXuk!+dInHe-A_y=oW;ctsAx#Mp{sj>fh4+S)Hgv@ig3pI=Yc=USR94BdSP~Zij zkiQXOUvQ1UcA?{Y3$<#VRpYx$;Wm~^aux)+hPKewRnxy(qiw(S5WuO(o1Z!szY25i z#CJ!LmZ#lIf9X*6-;Dx!K3voZUrxWfA&t&xW-(4*iK3F!4~vGC4}9mEH-(`G^i{t; zQ=p4Iu)kcb0<+H5ag=@C32uDPth4>6&DoysDYL3)RXnW-+`9u1R}T_tpO1Cz1oDX& z7-{*#uniy0#NGWitf?RFq_Q3gnqE^anRb29z|qQnT`5EMyBBF4+%X>Ab`SjD(#liLawSlB^AN9z;mkkAity&Vvh!R_#v(EaUl z%@^u8hWEdhNY!>Uzi{U1QI(Je9>S?16tnveCG{cBQ(Ar}8z=W3A35dI z?~H%l%SNlz-ye8)sA*QSFj{w6lx#}?iIPGwH4B}YC56-L`j__Tu+DeK-9KpIhiJl; z=Quvu*X;gx0&ow0IIaj6swLyJ2MUVplM(!2B2z^;`481HC@@Mr51{$wXUGt0)n{ah zbbmI%r|u(;!0)lU&Y959exzGQ^^wmOs=CUnN&&8CYwON)8nJhe_D!u@J!7>HQcISJ zV1s2vSk-_Td<PF@zF15x<6LKOdgB$=8?l` zKN+QqpGnPfWAxfpc-wmSS4I4BwnHQ8Mk*u0)8o0U;kl_eXLk+lR5vWDY7x+fOO*mF zJyzsdq3REoiv4RLZ2p`?l zedy82JkyfHr!fa|GiQ=_lT7mp2Gx&z9EcznHS|Az*UNJxgmGWU{wJs&VRxzQCF48k z{uPCQp!?^qnsPQUP2-LCt#V32ueJxm0y4s`zk~h>rV>c9WR5ujG z;)ZYT8E;jXg8pMXra}AXwZN%`Hy-r%{a%pI!FlTIJUz?L#{X?vZ1eRWoXiYcFCt{R za7;7JkRXJ% zKYAB(i-tg>-43p|3%IQ&u*?To4~8?)m#H}rOeFd;mFGd<(^p^qqx+V9xFpU8ZWe-8 zbH=xi8%y-mD*aLYMQ`ULD|oIsXF6Em1hzKHXRh&`4P&P>4s$uF96-H=n!~knajP|$ z*0ho+J?->&Q|pDUGgk+*cEU1WZ zCJ#nUq8$B!P)tj1rDDj-z_wc-ceV<<&01`SwLrPboeX+UaGPR(ewEkF%RSEXaS*ql zEb#!jznaG3_Io@K_?<_xm^1a#eHz{wbe)^Dj2;(^DAKmaWf$$nX zXj5?U%}m2%-QYOhh!95~hR#B-(D0xKz|2Y3%e9zn@l*uU^??d>A=S?lh^A3)s zs6Rh%+!DDxFvC;rRKQanCm$$7%sUpOvS57yTAmt)y1 z%y0nVURleZ$8>WkI|eFXB5sWKeSVih?$e4aYX>SmV6E4My-|7}?HJ%=uu^NU;YS0J z&7R*$KBP`xHAA^=^~jIGe8d;-BWGHTViqV0fh_U#b45E)LWJ!$^llsTIHbX8i2W4tfT36M7UI;N)1lLo@;-Sy&c57Sy{N_1rfm-@t<5f& z%AjHoRXmmlrB#69FH!<{1PKGR0OT*h@4MJodbm6!u2Hs4)d#dM;lJ=+=EQU)V76Pc_qSW!& zM#aeWvtMuMdd3?IFj_=d#w7CNVA2sP(vB$J$AaytJb`Zae_G`|I~zyGC&UM1WHv8u zR5Aff#xJcni(<2Zx7{{6HE*>u|3db|v4`s(waj71W3^)`mP@voywq(vB$L9f+;P84 zDSaGEXIt2Zmdb57GEM1up&wm+v1hdAVXRCCkk4m4qP30mQ8({+Vkj+vH3IBTTl1e< zP2t8Zi`5KH(B=WB)!_wUn?km%qFJux5rx+kLFuY19I(^DZty3rlUu);7>IStzqxN+ z86XVZpO5mEmYZ_%RDzI;P8^Z!SeRN4yq(n!*Ee^A-LmGIse@qlnz81Z?TiQ-t!_&< zT6_;>t^%veycBTFz5cItCyR1Y83tn12I4@Me_j6ank#9kd5`kA$uAauMBN>Y=Q-Qk z>-6dHrs`BIUn+ybhnlhDg>414@o!&aMc6rU9mQ$@$JwX%?T^=JzO*MFHLpARz}`-O z+84<6-c2QT^}($|z4D*^i{H)@X!`Zt=sV(JXX8<^tu31dY)PKmlUnYOM0^$n;6B(g zZjOdg`kcAg6UTS12aTBf>RL9Uppa?fu0Ise^mO=Q`0J9(rYSy#bZ+0GPp?C50u+?n zC?5xa_DpZ2e$>~;n3p<;AWcm=3_C*0hqk$1fTixG744XSiHV!iRRWGg;kICO94T}2 z!e@*nN3gT+l@YrB$|bXJ==|l^?p+aQCE)#=HP+pW-*VTXDtjT*tI(G2|KfQfXerSj z3Z})O6p_E(%;xz+)O1W7OEDv{sRZplCQt1iDi7ZE`G3uA8dR2_imRaMePbRX6fq@h zAD`i2($rD}G0^^zPvsD|F@8H2_bK`3Vqi@@z%H z$Tb1Wab#AFW(y$yr=9=$maNv)?K(bDxG&`q6Ujbs%M?4-xwE?G>W7K9l=X&bDAXS4 z(m~TUAlhtx@e^a$w$EBx59D;vtiKTG0l80k<663eNZ#HEG>g1f@DS|k7PBS@7--#; z@crG58AKqY@1J45SN35&kr#W#!j{8(TjT@d?bp>kZjOxVxLoCkxNVU6c%096Dj8h^ zQ5KK$n9(%vD|!gk!BNuqAjCS?Mnzh16+0T{J+nUR5$0HrBYI#8(%nc*Ev zaWF7D%wm%X{Hr?kWY@-DZ*RlHUBej^szksq3oN77LNZrH=}ushRR)>=VsrS6j%>ff zGl-UcR`Jf^%Ekd(dwFL9g}@0 zr*`I_PsNYuzCuDV&pI`|Ho-`Dh{?LDq7*m{ymaB3U$c-}Ako98-Eg5{;@Axzu;F;7 z`x~N^+vEw?fZ&?!9-y((83N5HBLclT}F0N1fYWBH?cHAUV1 zP1);~`R9xdlhZsm!63z02;j1(MWbCq!k@&`7m?prXiiy==e@1u=>+apXnK|B?i}e+ z_jgqHbk!gBkW}+S3`<=fPlUsmH>Mr(#lof&Y}B2~B+b+&QB7e6+IJr+)x%(wT9Lt; z4_4cn548wTD!rg=CWC$SYNOC{IC@_qiaY0#V;C2#xr}JD!c?eO_uwQcsJh=#t^Wg7 ztq}=Fe&=(LwCPG_7j$bONzQJQHpk8{(HXcOAoqR$b397Z+zOv@vA8?Rw4)NtX z1;?8qh0jf=e2(BDm_s?-D)E~VQLYJmvWBm%Pyw_rv$s>53ULpbwjTY9q|Xruugn;? zkVJFgl*QaWtk8n~JL=NUFz;Rai5!!<#DhYnGn z4(;qpZ}4t==0k|8EBiz|pf+QlIscH04@O5a_>%LDM{h1jV6)wt4w~W(<6Z7w%?$XF z>mM)dGobtM_k-oc&m<-Z0q6HzY<%*pt5asyvkU086t6$eIrHEcGEs#&N z{O*SEs;)|5_CwE(o}gp%*gMFv+ua(8fbi4S(A}Jyfbi*wsG+Ng_=~>*Ag+5=L2@UI zQTUWL*&i;9Z*L-%Gp^Sy%`KUo>RD8HefEn6{hEk&`l}S}w7M1z`k~OH+V$c*rryP6 z6432cuDo|xWSe5T4%86vv$nTi&IEYwY-@{&b&=3b-~zkspPXtO3jAWn4Pf zbe(|jY2cQuoEZ~-Qa+_6UiM@K2ajLRZ(1Z@h|g$?6+1l!jAeC=tKTn*hp@mw*ZwxJ#AT0m~B#lFlobc0Jmx z&4yJd=nlql8gO+e-by>+N|jzvuwYHCi*9QKYV8ZrDK*>=DngaqK$C1hx<<%6$h&t( zLGBX^Q|s0l`SpLo)}M%JuCN%c*x~F7_{`SYk{p7T>Eh1@+nQ6@IQK*X%D zA6AH+6KG&l-8OW`fWvVq+s7uk{@^!Ztt)5uWi+anIT~0S&e$uaSdG@T2-W&-ICtz7 z)AK_}Y0bV2f@`F~&}9UMmLHW&6H>2r&?^!XZs_>@^9FPfO*Fv^xWN{lb_NfpHsPc01Fl2zj)v6|Wo3$7D) zQa=5tDzWO#fApC2TL{VT-tdmHt+CU{XYRhP?})M#H&4%AKYz^VxIN+YICb~;I?aHE z-UY<n+YvDzF?LY8CTt$NJe@A~A1R&3#9sRyO7GCvZHB8(?kM-4FN+l7HLQI1&Xa{4 zJBsW#Jj~WKzBbU{1^LwEllLhw`J^cUOP4NQmu~;`B%N&}vhpPj(pX-PM$5QF2fwfgbW*tBaeV0`%?v z6NNz2-@7K8(s}GDKf@~rUv%CB@-tXWfo2p+An|h;mO_kmIX-KeT7)TCO#N$@uZ} z!%fe`lDim1I6d{;1hqdnZ8&Zl=$T%P%QYuNN3hTQ*}I6r?>INImCzu9PGdDNvkI~D z{2GsA#e*<337^>=3A8mru?qN#u(Zg9ATNTMg}7lb_P3%h8^`GUfSb2kvrSy`b{>_!QU=>^Ae_O;~&ND9a^__E{EPrjKp2>5+Y6- zNw_v$=u0RoQ~F*jSjLb(_D`_Ing6$d_MX(7@3^J^@Hum{^m62G(}`I5>8HmWypk09 zrdl$(cmM8KZ3Ti_Q^T&?kWeG$`~kc9^E^~;bGxKwqoyTC!{ws%_F68Ond#lZ&lV@n zTyxc?xUNn5Y7b7ki4KuXHX5J)((ml=P1-^l?{te88QJ={yTZv_lRN)Q$Up*rQBhG= zN76%6Nk73csHcDmC*&knqI6^DByS|KfI~<`(YOerXsU zemjte$=ro?wZ2YoM2#$Cj?k?Z`RShWmeY(9v z+M1tD;KB~r{yh?7)LJB(-&)6mC#5xv;L=R-tESDONT%4v(N@7%E+adxeR?B*ucNK) zc$duXkA{fB-|x?R4*fNcpSU?@2!NSiNmjC!NV|Ng(l>RBfLZd(e;gb00mQRu4WfY&vO_d9Zm-$jxuL8w`N5VN3IwhlblRc zsABj}UH`6dc?v;>38qom%AZ%*e^=Kwj!xcL4dZ4GvE2ltEDd?jwNuFCaPaA+kT!7~ z7x*fTzA$F`NGA5+f8E)1!!no=p6TT*S;Ux+-h#Ji3$!l z#67cFCESDh7nJx&`Ouk!}CVk4xYiURT$ zY`D)86g)c{N;4ERn|o~4E zjBpnFi=x+M!S?L@5;0Af>oN6W=m+A)MPX-3{Ky0b-PbD!AKmIxcQ1m|y4AI97>u*{ z?VqVHXn~h%P^8{DI>-2rD#%s=F3;9dyH^(sEdv7Lh)vi$cdh%bPuEaQ0p<(@4fMy}jpO*}l^R^vKblSET&{31QW8rv)l;)kj}63*_I(Wl=WIJS-#iY>>D~3S-A_k9~XY1=rWu++> zsQc)miMS~Qs!#_Eb`~)+vhLT%SlHXiPx^Q(BX5QdjJ2BA8ElNzMC(r-ZtSP8#G0uf9a!PK6275V^KR0w!BA<$L#1AU2Y4f$nqTfwSx z>99_j<3_nP$b4GH${38eacGXv`X4z0r=%gy$GjHYn%FCuOYeg4GVK2*_?2FT2~!L0 zzW|sg~}VUC&zQLso-fYv#w;pi;&fmg(yb%lgKLmeBd&gA^Byi zAHnHf&3KG?+8}yA{&6e0Sg41K8zHOZReTm3La$MGef#GBt*%!pf4a^!qqE6}pat)& z^;>sxK0-AkvQU+b?wB4cG7bd0vm5E>O%43(5vo@L1b)=+#L|ae3WnesoSXhjT)F@P0F*7p?XaD@_ z;S6O%Jk7Fbhw6DuT+%0d2}PJ?H!9o)Oi0U{BHZB5D3O zZtvIZpp{yz(bTwL4!EMX1?R#ZiZixU+m3_F=JR+atJ!s{jslyrH;%0O)0-3*l59yV z@A-4CBtu>ssW3z`uBKFi6Q;JFQn(0ir_2O~2oPQxect9Qk%vvU@(VQmq4saa)jnL= z*dU>z58!-2a2fz{ZC#}*COGYeWNb~FF1=5aR$(~UP_Y?Z=*(ygG8yx6vOI1164x11 z6wTNW%ZK3WCq#wdt`3VF%_7EfV&cG2>GVP++Le1dX(-X1R(K)rkgGu|yAdf=9dRs} z-Dg5i&!&?@E<$N5=iVB@JqUrnlkr-YjV^fptYm{U^ z7nG6=o+RZq)>;|)_5GlVFQy;&-AUIp-%?t~s&Dt?9o=B>$k8t1LlirzxM79S<~LQ9 zD9qJVNmCpwBHw5)Q!~uMKN*Pjo5V2^24K0ou%mdzg=)|q{*p(}Jbpk_==kM=Q5IBz zcQb6Sq8#kglwpILOool1k)E330P#To34+a$aR(TUNW|o_aOFBgL!N(fn(jHW*4B?$ z!*7~(hJfRbN4mcujxCOTz1`yY-X|PkxKyZkKMU`B3-C9pWi)NjuqTzjY{!>HgfRwM(0d5sB&vHEvMPV4aE}-eO6C)dMQfHi5>@{7@uMte7Qg$i0Mue%og9eAa@%i9 zS3QZ_qzHxVRt<=1ler4N&#o3fNewCAZvHX4?ey;R0{yW-v6t(EBPFx_W}M3fI=>f6 zpa7+Pe*8d7BZ=m{UZ`--FUt|zm&*M>Mb`W0^sEmQ2%OdGjV7f-j8)2&0V~%5{P?%G zbi!hfUd8(_Ap!Ayhs~t_ZbTLRGt;i^;K3d*vf(wjXM z|GFCYUC8;dC_%DRot(c;&ZFN`p@{H1V>wuM9L$)!xDyVcMGbEo%Qoz-_tVq>WXIC6 zW-baBY9TO81BqRhLjM|AecTSkPZYJ9$6N`IOyR7j<-M895 zZi05?HJ-~Tj_eJ3^%U1v!>t0h1DB3-T6=bzlrG7du2 z!0Q}hHyd2&yz5`1ECPJ|z_T+a0f7(VrPxaeIW`?kMlY&kR$YSP;sE3s(D&(%9q;vQ zMr4XDD^U+`eP?*5^eC_o{lo9of|$LsFqgWotPjW#=T`t`VjyvY#S0Dx0JyYiyaq zIXg$QA6ZVJhZy7vX5mZE=@?i4oE|y#3p`#VHa0sV&vCjSxu3d!ksdk;^cj_xUyQyo z)WWxHZ(vbmZsOn@lPfti8sgn)U(}d&nTSrZZr7y+>sGJsQ(Sn()pNKMmzGBIDt5Sg zwmn-G?eKIih4Jz7R?DYe((k}KyV3DBRm)wDQH~C5i~Fp5p~mSIlM}Cm3x!mZuSGiv zoT(bWt^Zvjx;_E^#nqv`HIVW~Z1iaTVX{oeDEu0o170JplKnB##JF8L6-<4U@s zsm$ADbIleJ6Qim@ZPBic&E8`%E?+Wsy;4 z_Vl0%`BK~)u7X-vFDy6)qluiLt){&H>H^7s$2tJ=PwYtgRAlWRpY#6W*d1r^JFd5y zERY+tJ`>@RV@~Bk)DgE@^~XyJZJ3Yl^oTPo^^>zDO(dda`zr71=XMU7`}(LJR-h>5 zxz`C$^dgIX4zOj^46G*EcYffNiwV=bC)}U3X@Byy=DBoUu7BFuA5xm^AUF1xy?jsAWynqKb-0z9^x4v&ITD*W_UBUA+~?mG5tpfa zuE3SlJspu6_ZGIZvN<%gJ?!@o!@%9ysrp(|l9eLa&JWS%aX;^ziYXM^aH3t)Yp%~i zx;F8@X%r6e@Z-4K)*F8LcIB`YFQy%xgMFSpDZ6Ao_0dD-tIm<5v$$o1xovwT5|g*< zmbWx6yZJl8x;#OUT~qWXHm{Mgifd`Kc~m0jBKQ-tr15Xq4*#Evk!0o?gFywYFqMX) zXur>uW&&%Ri}B=(Y_rF}ftd)Tq{jxao3Tsn44LtsFCK*Z7ip|>JDsD?TGgIaVyDr( zKOy2GQx|FE?o4Q@aYh7>$;Rm^N`Dr4!;n?9Ke~@GCk5>nmJ`r zEd}Ta|7>ejC;#yswdEC=au%iJQ7YybietZJ^~!Q{c9ykKkE%DaxsK!giJA}pKR(Mx zm!x!c!Fue^9+UOsYaxpt`ZBrWYEAUob09O~f7*FlI-xHTk z3N?J09|TWjr}MVx$X65bKo-BsO%|@2{;8+zWS2hOFGJ0Ge`4DR%mkgE{V<3XFcMOGr5Q!UzR-bns?jhb;qGf$8c=#~%-Eh_P6k-!aqha>#Ejztg&}i-b-XiM)>4 z_olGT0!Zs#uAH0+_JO6zO=f&at{`Pza_ar9G+;|~J=v{_J7^U&+t#}_hQ+#Tv2$mw zL;!i0P(9NjwRccq2p)0EOyRic!vX{r^A|I^|<=vKW!k>SXbAYQ{3pjkoVbC*!!fQ9Qj_!gppCf z1TTB~IGZm7kA}c{5^qq}8TYU4A1t;B%u}VjMkg@Z3tf(|_ zVZ(L02{W|>S^QwnkQZAj75r>1S)}IIOX|}e%ocWb)4L%Li$IRsW8KS%6ASxgZeZ0e zw$7h+MH|=6C*zglbK=Ih2-c=9#7VAp*jFrxrcpLL97VPj^b&I>gc$A_e)3GudGs%^ zN&cAieA~H{`#Ew98Hnp;bg1A}Jm#iNd`tAbz}M2{ux&1RJ7x7{{z@@{)bNdqVJdNI zrXLzNa_^|YuIx_hQo^ZTfQ!8oyvqhSa4LV()re<(X3y87K?^GPq>-@iGsuN(ye99a8eZv~!Zm@_Czh#q`%s+mkuc z)qt&EEGTa#S7s+>Y|CFUSp5G`_m)v{ZCke}79_z5Zoz}Q1_=(qg1ZL^?ykYz-Q7L7 zYXu5-f=l6ryWYy#dw=(wd%t_%`}JCT<4-jfu-dA*<`}(?(Z`$C(O)lfnsU5(#zjWy8g3IE4{5O%E`rVC7-f%c*}UAHPnC>jM;X+Y-8%~ z<1bhTF871UslgZG*|G|Cd|jDXmkcFU$}^0^IQ9+c)n0jfo&{r=0(BU7rm==;Vo-E0n9C=39bLRn8-oX*+P@3 zFR{ZQ(}}f`2$n@=NR~;9mP!#;R!36@v4gm%N+@9AE`Z#Cn-UD5xpHuGn639o&uYMsB>-YXa?~ z42L%~l3aL>)@J!uJNduBZ1lQ`n!70>Og6lK!ldkig+b*Gb(2|KYQbnCSi#1u6)0Px zI5K44%>bk}OB)7%_Hm~MDkY}PDVkPiJG5iG4!Z1oY|uH#AB`V;`c@dhW`|)yF(O+G z>~RlY;TG3ayZxD$EPWiah2@lQr+bX^O9EAygU_TLH-qR3|2xYfq(3k2qzw4^$Eu&G zJ#}4We&nD~D=EGH*1M6eN-uZDCZRZ{g_Lj7(pbyN>GhHD>RC#r7paL1y8>f$H=1>k z4*JRZUx5Dtn9uNAx&wTQK5idoOfn=d@OLz9#l|JbfE8E=AK3mtvyVSvKY9h%^XTE!IsGhZ~pjk7I)bE3R#W`3|() zsPqEY;!C~oZCoycmgW$&&JDA7S@x&n(aSIUWF{DuitQ`B1ja*zko z2l(s~{7G1IUc-fOroadJ!c=vnsk)Xul@)<*btc24y{VbUb_s8&9|rSK&s}I1#WX!a zozx}g=zkShcethOF)PHKkgNYNz*v^gj6{EzB-SP@jRVlE8{ZzR)AC`Y3rX?+ra44Q zz?x&^#!7hhKB!bV822$qB0?;VDWE-@pe|>a;ImPP6efajgH(zfr>DjAAW?V_*0OK@ z;z`0_3p^`vf&@HZfQ$Mj1ulwRNvtgbZ;dk8)vJ?ndeE&tzM~=)H(_OJ;0}Mwu?Ej_ zE1yRaVdw>hxnR*}ke7POLelETN13`P0&zn%2i!;h`Diytq9RzGZzZm2BeYlgohqPX zc#G!)j)E0EALx!^7^$cUdb6CT)w%5WK+%Vu)Et6Z#SN znBowbxTj{1?1-~Hyrm%he$U*VC{~Cqz90CZ1KE{^9`MV_xf=^06-NjALW?vUZJf<> z)8@V}-uBV3j(FJEB6!i)Bv(Z*MrtHwddo|@KstAEg6OfrE4n~F^9ZwGj0M2s)03oW z3Xkh5rWg8kS0*_v;$a&bLR0EAeNu%PDk8Mt*=p0vlj}OTMwr;^^dlU;j>r-vu))G= znov)J*oHT{C}XJ9G);{flt*M=eta&b33alMjO~@C(zR(3sh3i(woGJmRBSSXFrZJd z3!GY=>??!z`pIehrZkY4wue^7Z)4QDqp}n8_8UHA((kcqCdc!;QV-I6s-BzOq{6S| z;AtnLrjuw@O8}TkCr5Rx#kHsMtB;V9niiW``!3M3yIUvXcR;XErH)a`;c*hEABmV| zlGZ-T22@H;>rexJ!JUbnQ5qRdG=7vpUr5KXUsI}<9B`p_wU2|q9rDI6@DO`XDbYmI zWpOIEbh7^V5wE(Uyo096nQSY3^1($1!>({P`wlZ{JXU<7mx-GqHMwf0w$(_To*wDQ zM6Ah2_8qkF9bN^!1RzKj16btf@}s0XgstXcg>pf3my5E7hN#!To=I)I!J(B{*z-PK zJ6f+27T~T{a2(;F?Qg4GryJ@NYOP09!rPY(hoYWo<9Y0z~$&-eaZ1;u&o zwpGBMrm`Tor~8-L}pkzTZ7N4 zcR8)qazwx^XKV78;cLnOtM4|AhW^s`agnL4b?0jFH0~_}#3DnWbqC#yP!yR&rRdhphZeD+?_9$JLceWYfTyj21p zK1U)x7i&igv+4}kC_9l+SMPnv@TNF^OX>#6CDu>cltfD`5P_JLua^5*{z(Z}9awvS zt4QZm#Wn)@lzK~t+QmoF^ddyWVY&0RPr)3=9-YfxP!OS0SiyP$;|0d<`s^m&YTc{U6_x#YC6F1dk2a7MZ!U|Fz0wvh-1n`X^?tUiMuyPc=Ixf>NHrIc(6Iiml-WX zf{YX3O#UE@To;+yx0LM0-o&kLf~P2LkX1@vlBkBm!^9V8hk5JD6;~~%+7yFa7gHjv zLlbeHIX$-JIA||&4Eaqqs(Z~ggCMgsk@n6-n5A$_D1V*TmX=IHTqgZ0ab2AH8K}W_ z#E6y)844E@wg)?*qNU-x8oyxL2zUX0O0ET9Rt4xRZInyqtZt5@;4XNq>;lL zuYvP^O(GeiK~_X z!JMM7>gVTdr5R-7atO-P7!*xtkQO}3acw>74TrJn&}r3>RiYX%q-tl}u%RzTZm^m0 z#)dMXScK_vWy5a@QZB^F16^t8|D5Qk3{mD5g#lk1iG*mdLp4`xxc8ULWn-)?18fu1_a@X6;EOoeI&$4)leUlB z-jZ2%v=Z_-`dLxV*KuNjXw@F;IbD z!lF>Za3w=Rn~w~M3E9q87Bu}he+(fo={qs*9pY{L&BKzr17SzM0^hr@AL6NHFUK=( zbtLEd^X_>=ROR>m!K0L36Bn0+=UNYFuKgu_L=mmN)Izw1C3#BuG0s3xcz*PwJc*u zwc$sFk%{l^QH=cLk(n>QX{D#i2UDq*VKCR^IZ-D_MM$Q%tc}E0jkUDV=2SR4Dhnz{ z8!N~JG*q--q&e{OqVRS+#g#R*E2c?Ct$o;~!uf7*O(XpVM4t#?K6)UV>${qg%dYYn6H1?k@6;8vb1o zTNtzfIo;8h2cDaKzh1XmN8Hi$dp41YY~f~-&Z*NnGB?}tqr0OR(ZjD|f1Aozh)lAp za8fE#rvc`1W5e!WLj)Am=%{7SeNoYHXvgQdWDD-bk6wS3HaSZP4W;{;@cM)j5JBJO za{9)#0K;`x{dUtN)VRc@z;lPR}qej2ibYHuZsOWt*>4j3Tl zF;Vt_N5?noZ|=$gf&`E!oTjJmV*#elQQ|mvzwREA`68XkAZEHBgr{Z}a-Qy=56LCMaoq&eBd7L)$Kc7Pd@6CM| zjgS?LmOBu6s~%}T{EHyP;^0OgS(Yr`LoC9Ijs|ZwkrIQ8R~KG!r!a3+kjM?H>e zG%=FkQcTAgVG{=Mi#TvIFK9SMhmP=(0RFktND8FWWE86QA{l=bl6qHzz?+4$=qGL= zgd|7iA`N^@e-sPfEcrp)_EE%pP9!aw57L#kn96+8!uBE- zg=B#?Wb>((mJf7nFcIrHA?rT(_}NJvX9g+N=DzE&lLyys+rG>c99NZmYE9<}IfJ6Q zZ57^Mt2g&X)5Gop-DEQ__KnUt*iV)Mq)`hJHfXIbF|)bF_1_`p9+DjFO$kQ}vcup}AL ziUNvn_i{saC@sA;$|QCjg(Oj$(@$1E@5_|GUyp)xpLdY6kH~WiHPG$Rc6C`qJju(;@=E5UJ&>; zm%-XBwYF%1*`Ffv`Xze%cl=qmitC=Ai7{|-5zMmWiMO?q=-lbKM+Hba6x>nvXNW^k zUxK@y;rk3&45k+qQ(woeINCdTJ#M{Kh%2l94X1^LxS(@1wGXMDW4BPUIc%OvUoMQ=;j21HAXQ4V9 z2GhJ=Z1NskKqtW_eMn-+kX;F`2dN73!}F>PJl5ua%n5IL=J@XNsqOl>D*C{#^Bf5D z)_`cysr&Fn%YKt*91J?7F{la5c}JHiXDL{Sm6uKj$Cv^JyY`5(0 ze!eMF`(%X4KAQ(|pi+*q^<1*bWR*a#c4BE$cAr-e6hFU!Vug)v2j`J5-s=uaCa?Dw z;@^4NF>7~TV0fqftJrB~UtVGw?bl)9H6Hl6@}Pk3mmdvg@{ks{c65CFu!mlhIkJ)9 zEjm)#uPd2mi&i7dlfd$$B!z_KjEHOE=&W@cWuGwS1z#00xGRr-Rgh!nIU%RDT6J?y z1|w#LSWI^W!dFLK+`MVuS52cpf%3D4D|?cy~UwXnEw z15Hs6zX|#7?!o?XxWr3GceUj?^a4r16?MJxnXbZ%DHAP7I+EFv+TpR)=U$~kEnttO zF_P_?kOyy`gWf2=Ajv1Y|ALt`43Hb{N$S!7qDa~U)0C=NAFo~#cRhIpDGBZkaK&b4 zSI@$aYYHw9c6Y3VB*DS^K?2{HDWJ8MgJBJ?1n$Qt`L56f^}&x5$wk@<$?Xijtt2}S zZnoA8@|n%#jH}Ta>;YX6C@9Jr?uVl>Gy80vM{#^X1zMQYDmm6)+`Cz=wmp9sO-U-w z-C;h@8oax0>Lw+0Js$cUwVYI3*wNv=v9(n$OP}yGEm6U!9drG$`XcUIm7#mW^KScP zV#ga9LB;fCIvL2cjoaLGy9VLbz5)ckv^@p24)?>RFzK|EdsTCHl}HvjbUv?G-TwmD z{55%}t`l#KC&!Xw28<{5Dxa>fM!g^9Oz>!142>z%vEt(6N5M2X7p-pR{LQ3_g7w2w z*GFAS!Ev1L3AsCoQ^s>79h5MebG$|*1(^c&YLAVtbY0gAc6{5dBT^%`JvsjOu?57Z zhc{LT0m8NciC1b>+M;`v{X#U+`l|miwyDT$deQQLy-!#y4lX<=ZWsI#qg_85g(%7S zo4lH(^_9N0+N_uj{o+-hbWr)2?M9)K=-W{b8S>gq@K&$u+YZ3X93gmaRx3_LD zzzS_P6p`lp=GcrlLVt4dLcg|cmm_Z5qLsS;#Z1Sf+mSmPp2o2&%`fB*v$C?1xZ5;B zcwe~#kmL`GaBz3E=`Q!$9|D%-TM1z{=RMgIj_3;mOI+7|%%^{*U3f{iKbV+{+fjfh za-e^KY1+MZnIG_E)GLESLJ+#NI+f(A@N9m1xuH5H#vy;bcK_ywS<3sckaLKK4U9@g zixKWXORy>Qq8stnm=N?nADc%<_A#PFJ$}VN@Od~dzsSo6K(Q#Ye)-Z!!OICNL@F~_ z0W_XGNPE}f+V_TEKnO%RW&ReGiz1*wr z`e@=YX}9THgEc7VX-i(vSA}c1=_w&5F&EmTJ?)|laXDtd`lIhk@P+#1MQe%mC#s%S z&xPj2$7fdyi+fjgP(PSvd7!R=?rv0j{=UHPYKDUC5o->$qSg}R=HXil z)Q-8hyuWjwjATsVB#cD$2zqV`~n*zjC#StXo>dRVaqJ|d?!j3wXD$8F(SEj z18Z{s$Um)rkUXLJbUArix}z#>Mc=jJ69@^Rs}Cq{sLG8HY*sB->+c96-3*8k3hMUt zQ!?$BjN0U))T!|*p706)e8|gR4_P(8mjbyzo|md1K`{FBnRQg+)hgLKITJN}Gp9xQ zm08=`9S9^?Ny!2wt0MC4NRG`8Kf|CuCPbOVQk)}ryzK|ILZ)a9F7xJf($t-448G)q zhJjoSUawpQk16RV%R%+_F*grkI6eVKjHnz$@z>X&%~fV&`wjEAMp*2L4l<6J>R}L& zA>zBASced!e9K!sJ8lAdfNoVBI%!P9>q`ZyJtMBVI)~MYn0v~}=^Z+WkCWdta|uO> z>s^fyy2lN}Nyj4zXtx1^?3;UgAqg}uYD2X2;T2vxP8I4k{?Gv=>LZd}-!zBW61vrx z=`ZMWXaKUGGhA+IyMImG7s-xaw65I&P+wI~Am~C%%Io94=u?=%Sa1{v>apH`08yOd z?UZ}*@?Q_C+X-^Kml4t_MePOeb^m!0+u+P^gb;w{}3m%(}NXj?! z+Y05GP2|cgKc4OP2#+`bjcbuNUOW$TDn|DHP^8D&_*_WGbmiB%UCb?`0VPYKsQz$Q zK*qK%7yi+{%1n|_nmcNj546-5whC`v02Q5AeTq>%%>mq25ws%Ffx#o<(L z8>(>9Abk(M9(5I;#1ck^xv04tpGZyN=A!01c|FNd_u2`qAo z>QZm_%gY+%242W9_2hKABS-efbz};;>g$r5hljzXzYrOGd`WyBL1vt|crQD6Ao$0* zlwvZDD~7h~{V0D?5&$3q*9bxaCacUy`MIu;+}zy4O}LFOADr)uKxSJ*e257-z8KDj z^8r@zmako%sh{>xirN`yAwV`!>2WfcGSwo@Zo+3KpVPOpWJ@O(mqg{Bq%e>~?*_VV zPE04@9cDi9L0k$8X>*>S%Q;H2bzk!d&-WSu{)^a33kx?UZuQk353k*Lw@2NDIs4A!4|(y+yhIJ7@xK_3qV{Qr_1LB(yBe7^_taw({dDepj~q+4#qY$eXqE_&?iUZsD9UI9l^PBvMFYEyr@X^3#)uoHJcf*UkMy0mk}W2symKwZYrv;Y6V2*gRB>-E7Gi6&xrg=pZaitO=g>_mF zUo($1a_7#an`P+}_J!bIna??o1SE+5F_kVBC><$CBPfQVJaQ)bf98n}4bTQO*IECh zY=3=@@IXk^e_R?t`v2XP_a5el6T(IKkDIpxDm=Uu!(t|g5QO@7OUt~TY&+aMK7o#s zmn(QXjyRT;L%xxL16mIwWp|>lp|TH@Z=Hh zJMIg{eP?cWH#@Q_f39?RAZTN+Hjvv-2Eb)EPuW-R$~ryyrW&07*|IxwRyQJ!gs{j* zO|kr~-1erOB?-gsLim+TKGDq!v|PoDUDeyd*E*asU;W^jto+QQG=oo#7_@tq5%(5v zvb5=}>~ZQC;J-c5Px~(A$v1q6oP>)C1PYAhI%0!JsReICm<`bg5So)!4Z5!qQe$!k zynbC*NIpy%*Q$Pp4&XMJu=AgfjZB>6Q1pRc9Hg7^XzO`+#w^s*?z^^OwUuBU3+*0g z(+@{z%18IKuHdu|QRJn84fP zLs65LoBwA;A^(T@o7LE6#iBGt@PzhZ*mWqM7H(GzhyP{KYD+yOk$o<40E30b-UxvD$rcioW z^M@;GLPW$_l~gBlnB!lZnKO)#Rpn{Sf)!$&V6q5L^e{p&B2WL2tE*a=qZ1H1x3_UI zt8%Bx1vy{RwszfFsH1b%UzOIE5$m!W@y)xhhj~cZZ)(Vh4cJPhe}Jx-#Ima1y+(5y z3bkWw%D8@Bo~ZM|y7i5jeYowPP$%XQ_=0@&cHQH|;s>_95J^C>xTBjXHBxc{A;x(E zlAe3z>A<$)#z9(~8WZ_1h>dx}T1Y;a{jYA_yUy5`^^13S6d>=!MsNIh69Y?Zd$-|Je zy7CcP?e*cy8TUO}D`TZdWTq*Y%jnsYt{HC{D*h1) z{>|tjukL`qLOUa!B93J~Ywc)$6BF;JHVzzz=ivom2sR9T5D-DeH4c`$IdV{u>_loR zH2+4a7tGj;0VN;7N3$!Q#m$&2eUCK^D99_DhgQ{359l$FlrxBuM;i;`WFc*Vlk8Xs zzaw4fak!74R^MUs5qJm802;;r zhSy0Z`jeK%SJGovbJ1vv*hC$#jn3{Sxv^=iHj^LbF)50rMmrd2DQY11P*WyOcD*m5 zfp+pkmw48}MTo)o87WHZdZFsxK*iUTrGq|<#0-sxaIVycwdzzm2vV{AH_M+cb&ynP ztASo=R1Cehp&%D^@@l# zXEF9XRxL?)AP3$izK-f>3_h(dX@~%$>i69x=^Ay?E)Q5X%t0rjtdV*Xp0!oBz-8fsF_X5jb8=^;{R6*r2W78vZ%6O*(0WK*=GJ}#fJE}}z`Hgmm{yLuo%8Tb zbBII_+{S$r>6fgU7bxWH!Q&n}5pjn?n?l?>ljtZJn~nbVF%BhE$S`=dlg^8D zAIkQX5HZ#CDflO;%ZHnA?BLuc#Qnv$5&_s@-PWHE{dHL+p$L>=?*t}svZkjNi3jn|+9hu}kZ59ENbI^87vp@aV2_K}Cg&<(28bno53e5nwDnW(K0bKz#7jTC zAI)fl{)wV3T+z^nlMPh@rQfrQ)b7a7p~kB38*%^RmwK?YA9$y^b`TrT;1^{i_}I%k z3Cp4{5uCvM)28fjZxA*~d};RV^H5n-?O-@$4<9Jq1xdpm6O95W-3q3&s_GE;b$&5h zjUxF5KV)knU`aFicKyeKcalU0)meic?}8K53TX;Ow#i1$ex!2O&@F*dLMs-Yv7L2F z)a)!4;m1$>1oSG85f%s*9c(6Yf zL{IrI{+X{-HqPLNc8Z)J3{FuRC-*vVDhz%pp#G|7!v!@85eWV*nuV5{XJv;hCJ|yd zUtq?QGm)LdthRKd_O5IDbNVGUk7{*PLd%M&Ox5lZVI(>8NB=WjlbHWp{||fn|0Pw7 zs`1U$1y`g0m6qkk32d#N@4DgYTEwSUKMjvMo?XBDtQ4;a2J$jtU?ho46d=W6sLytF z+~2DQFJ+eAKrS-zM`P=$h^78keF=LMXpH!KR7S#oc&#Up&O->qzZ)G5-v9ARKGBfT zQj83%9wbfsWBIRk%ZQ`*7YJ5sw0g_ky_o>Qa(pV_bKZf*mE4Wj80j+ViG|)2OTI_O zi`zpLNTUu!IJ#d-y;qz#A@3~MWH43nj8SQ7!s;>@=}hi>j>nmYd|nYEU$EBql%!pI zA27}O`oF}ERdkBr$dj1EP5Sfc{#(QSe8*D3eRqCUN{3(`{S!sM!Q9c@z{-!a*+jBZ9~;)8@C$qvMmTd;n0|z^rSebI$i2iQ z77F<6UPC%pw(vS(xB^;|TGGaU!{V2D(S|y2;9CaGM(jtQl>qNg-2;);5|V3yMT^ZI z7|B)`kUfUO;V85OTM;vS(4Qe|G%8GuKU2h69md=*i_}_)#Ik8^m+2d=Coj1f{eBDT zOo)H`GxNOYx2o{BWbk2Z#fUILw-uUN=QG> zOi+l)ji@wqVd$gU#4st;NIHRS@aD9~KKwP$U@n4(RXdi5ynFNlfuWYt3vdDtNT$&_ zK~)yaTFs)wDPJ>rb8pt!mTL{RI2b+^-VjKBXz@Uhv9F#O~BNUa=NfnD(7{(WRrTi#=T6o%Vj^6PBc->?HW zoO?tG7g96uiHhJGXZ}n@L!!ZHeVRURdL-8k;GK8C*+RMdoi2WG_}jlJiAq~40mbAD zOoS?UVpHcQK0G&Rqe7`kAW~SUAg!mCu|y6I1YB?Vxo|j>paVgkb_27BG0mub!$NAM zVSmR`aWeR@`h%@X>YQb=9+?gkP9)v<*!g5Mk>Zo+`q^(C75!O?>%TRe`xjyaIFmG)MBUDM?NTqkTTXE0BJj9a_A_HCrKR z%W;i5xK3u_62_W86N&An1+SZC-EbVu*a{}!SU zG9O)!L&v#4&|kTIBe*FIn2h?+hRb6UOH3s7@6kbj%9 z{B@~!zr4WW4Z`L~{YmoJG&cD8wBT-I%$;Y!oFV%FKQzH|&@D{g@Z{#}?CC23d@4}^ z7&$8VdJ&po;z+m!TjHIpn>B>3tjmLG{_?O4v!M~HoDJI;LAU1&(pn=cIh{K%EobKjn>NXbG*E!0Ycp|#&)#@7j z0%1nx>h1J_fh~A-vjpbfV%%Iic-O>J(hG~}|BV-22L89a=q)kphkT_<9U=fpdK7rZ zyY_l5?(VZ3ol-GQ>dlnZ>{B(G1T!ZE9XWtK5XfuaUE=arl*CZfRE$0Kyyz$Rs|*4W zVm2ua~(Egus@vj*H5sCjV*!h3^8;-6Qf0xE~5vkA!44Eo_iJQ=W)n1*-q%GPmXxJxu z|Lj{KZQ97Z*CM83kDu3i>wSl23nO|bsN>;`X*F!2rfIzYW&u|}*cXLRBR9;3HJ?yzZdLpRu4oF6pODgsc8~T!&0Lj0l!uJo)GnmKHQ$gkkHNS-^!iw##G|Ge z>`%kP`=TuxD|`Sgn@FLx!|ng>xqp$|bjTphWQeOh)_(^SqTcto{?VHndi8z^d*Q6_$VdNJqK||)yUM){-atG``ehv6GmX?JyGSRqD9~J z&mT6G#$68el40lTo}^9G${U|*p>^h*?lqB?aVeOJ+3ClH;rIsxxyQpos-ChxfXJToLWe+$F=j)m$z6EHFS4x!8-DOS4`*()Y# z+nykIxHcUag72aDI{JFB+QMUv+NYqAcWwjK9PMprvH8yWLNjn8J2YaMnM`-@o$}G| z!wH?rP<Oqzj z)mu~n-9{~(HJeW>t5S(?KFsYW-tIvJdU+6g^ymMXmZ(@jlr%Aul>fc2DI_mG0&};&oUzrwXh-s6R9}Vw83g#WejsB}A1d)gA2>r|2 zSNo7HR1PW&`!NHqY8!rqa){o)&k~mI!atbWi?s(y7G_8OAok!WUDR_*g>#EAv#(f?ZQS3N z&KJ?`VDfmjmPHX#evo65RN=6~&sUe%&!89a{QfEaH8Qx^6hXz?6BQeB5VzC=n^`A26Yi{VCPCo7$F}wWzM-4#M&FcZoRxebs8B@&3Vl$R`~R zf&OdH4=y+@Cdyyt;SvQLb>)I^%Ktkn`lj~i5G^A{H8ctTds$of$VhzXo;1$qCX`~e ziapa7?N31kP$IH@0Q1-i{B>3aW7e!vNV-fpJ?NuTLX%k$+QDwLB6HaC8&Y1Ghaacz zA@PLy4sZ#bu$NUxaKS(DweeeR;C5y$mHEMdH_xBIxOpWKcnZ_`ZMP8XNEvZZX>X zowgS?puUxoXXHaw*yEXa+yQ_9xsButw7#`eL`#jn#vDRN>9o1&q5mU+(be>FYsb*l*Yab%o%2(C zmpXAev~_N4pnXHd|8Kme`ju<9K-c`mOp?D58lo)v^O&sOVgZn=9bPS(rljhL<*n_;AU z(-A=!b0;T}daG@@;uKip9qSXUzSF9g=0k&|LIXdWT|a2m9VC&5{aSYS9lRv zD;7!*O#GTqoD@2%M?K}Gg_uOIF2qgBi<$pM&rIOzYyUzG_rS2b!e-E_u$xCO;r{zZ zIDujtB?QRcF&_Ug*ftCa{+ko^{U42~mm_8^ZtKe$Ec1k(L(x6cU!7y~w*(PwnS5bT zbjEl};r&R{8n9#9PI@Z8=fJF*8XRsfZ%i;u0#9qnjzQI9U^p z?lKh13Q~{9j5=#RaOWxc27=yjik7=%9wtj~UobHgzZ(pMqBU(sq}WUxruhG=Fz|LSaL)(TB$ur9*a*_Zzx);5x%1f%5j5?M0A@V#a0520p%itT zcG}|^b>xO(H3u<0uM_(AYI4wgxx&u$sJ0&ZUQPIt^Bpm;hFcA!PCHCFiwMTlU4c0T z#Lt5kx3ZCV{4lQ>u4{@7+%hOXtG9z9y1ODs3;cHaPq#PRDayHR=`U!GOjOp1xKrg(y71<_GKLgC`CkfuUQ&>$Ev~=$^Xpm~+ z29yX^A#?8@2$$M>KtH(bN73r*3P-&Zuxg-ogKYx|j`EEanC)%7K4o_m!1sxu4QIYB zlgzl=;wneh39`K?E} z)7+TH8Vyc;6l(R)3olIgJlCrc8lh} zSkp@1;$|hwy9AkL<1Os~yKCj6zq8+w=m!Ob&quSwcTUbHIrr)3g8)nWp$Zz)YNTEo z*iH;w6tTcEwDltGfyn-&bwDqXoX)~`&HBdLKT;0f?u4`D@&7YxOguu4!0*_r8XoN+PSDX+ZGcpWGQ`}S)!Vw^Zc zivQ(5Dg*g(spM<*>5MiJP|WAabCu^~77=T%YQapUG;6=9%4My*sURPJ!?rTyeHo@b7K3rc9PSLRo=08i9v$;XOA>PF^- z1Y8;Phans!TuenwBgpd)FHwCD7{`7oE~?%BIQDs=g_yC@*Z6V5B7a8<)>SB1aN>4z zIq&sW!Aex2qy0OfV1@%thoOW~G-1DQfsq07Nhn5h1R=9A%GJRv`p|p!9g4t6PJ(>H zsBm@M#r79)srDI8>tEwt`qgFQoB6nDs%KuCm5}HmhC>$M=?G7l+~(Q zwDo7~0Qf9o;GQ!ekqD^n@ON;}_Y%0#qD+r!Q7KI!D*5`2eRA)b<+T$9VU{uo#hgdY zJG*ARTZ7#eNu&;m9Vd*i?8bKEXtn+9I*@>;+W{IUqE~dUGhi?zYXA8Lo^_m!Uw4mR zZOlXTyHeY2qHFEhpk&gp8d0&?&tD{mPOBb&SHXTo)H?XS?1PTZGiq{h z`BHPw_G!d~_bDxh{Gxv=vrlM*g~xY$yIvSC+-3BFyYQi$0jEiyI|}5ON9DUC;E3w!h*2* zRr2i=aBn@51zHj*Bw%=tS}|z1V?PR#R8S}D!r}xPtjBoP>uZ}9B&4Sd&Z4lZg@Kap zg}ef&CrbP`4mjOfwV{ zAdIAnqKe^?^U&Tw&huxdy0^1037Lgnah|Q5h^HkYwF@{nEGLkB{~WR?80iLeu)j*= z++WJ~pytCB9^G4bVpdDAQe+MF<-dtyr7e(*J6nK*aJjj%Cw?^D-5m9hCQ;Kg1>|dx?TlVoi{titKoZXF8z+{wP&Kbn{qBp!`)og`Z&G?KXdiTyMgz;eI!`d-};pXWQ7# zb{86uE2i=mKd+1(sk%Tg2r!_qx#r4{xzdsM{ef*ok~vsArFYWYvbXG+2v2e%0GL&f zd(^Js#2nmCWcDPo-!|@eB~*t;k{}c$bEdnc!-G@ZKllA6gR-~`0VcTLqy^9Q7ek9+ z-tKi8RLKL@hKs@nCnFn<>|P_1ViEd~SB11os$Iv})R6O>J`OJ&Csp?49Nhb=0f)$E zozW7!Vq>8?2BWs?l0NIi)EJrp9xR~{Pk^4^tpOhV@ck=MF%9H!9s^YXrIYo>Q?4-& zTHRA{u{D&a+5SV=dB}u6uxF4Z48($hv3++N=(w z^!WoHvMv0AqrPFYuL82GfFEG*1RK_+KULZq{q_25M<;2m9&aSp9G|h94C(R90}YWt z;*Vt#!oXf|fWhi5HFb68M|i$yzjpXdk{o3X?`pz$W>EBxWZ&DC?B0v+xolb2Fo(I< zK>r5uR;_-6HcKdyE2RGVsP4^iy?%5z5WCfrDsl2i5+5618_DGE z(6Acspu1(#-du#k?Ys2TI9-2kn8x2IeF1V_c22j@8f{&{=V&`GVuD(Gg zIX=NA`Z{7E_^<}L_Z(`OYLbjaf0p1Sj6%BbRtD?VEye_Iu#(U`%+tpM7Y6jssC|=(9tS|YnHxWr2VsH`YNr9_mTn0Oz`a`13xG9v;T|w#S7TgE(}UZ$qRUJi zW&Lgzv*$nd4|4&Zfg$uu`YbV+!~XgW0c}LN7oZBp1?U zUDwwN6+%^yuG0s;;;H@^s|h^_6f${_>wOM-$uDMxW!in_H?gG zM6PN&!S6p%Xm}p3+`TygTfcTFvB1E!-SV54Sdw22CD(5Y>grCO*jDBipTO8)e!tjY zEjUC*nBntQubjwU0b&u30KY8r?H z`Oc2{?pdB`Imp=B!U-dE-@<5c(PNuWZ$k)34^bQ;(?q$1EyHvfFE95Z>+0^FjJmdP z)RdOos}DzAo;i0_`%6_Sv6uxAPSQHitTxfGADk3KTv0#Kect9{%EuR?w(+sAYQ~YkY#N#^Qak3t;3*bFI0wp-7dYqlRg@1hAr|nchN$1~;I=6)#Ff{Vg1G1o! z+|UA`*P}HZ8hy#{Zi_7{Xe^ocNG)z=`($v~N*lOQe64kKnj(m!kc*>o6VY2zFxN;CMBv*#DE;#^DogR0<@irYN_@zsI%^pJS!I&|*5TQocMR1xP3k%HIj|hjK zHY1QnUa`cFm7iJ4D10FZpkANu#Xr?RbSJh@6qtao|8gWxP{q`3zcoN`*Ts1Pf_s&$hp2R$aq>I!Lp0_ z9yI~oEjezm46?Y z{T4dngJj~+5>-uCrQ)fAxV$bCQYIgA^Sq{h+au*cA0C7_&jhJ*mKSNljiX`#=6I+>sUXs$HqN}RKWqxaG+-x|i3wcVZ_xp+%867Us$#UlAAE*86omn* zGnG}W=$vWy0e6cqsfR}2(?X&qqisyJ%Jf6p@*02Kgo({;ybB*-JgMEXQJ*SdR_B4r zVl*i%6HY{co9mX~-(&%kKL1>py#FmPAxh@jpKOaPgU;u_yv#!IjZBO*%Ee8tO53JH-8U@-t*;&=i zSANZ}h3jeARhKmGR8z7%UW}El6d-brG#b1AIAbjtM7Pg1`Y;dIXz8$R4Vj6CilKi2S6JSC z&jPW{=QpPyL89P$L{x~t?UT{$6Hhkwza6hCawps`(m{fRtg+q+Lw4Sox|GlUy|HZmCB5)Thi`&kr&VPoT<<={5FEpJQ{QpL%zt2E0Z%4zi)kjtCndf2m zfZ|N)kZ*ziS=8asMN#ae=76<2iKM60!)=G~tg6;{1jAsZ6hM7BA(f}{OLOP9@l)-hfhC9BU@U?BY!6=+tEsS~pBdb{Fk=9PAoX!6;t z+@+5ge((~O25P=GxT1r+_AOjpH`PAptuIgDZ&(h!ErR-_RYJ=rl+WlIiRi!|Cz|eZ zIh>vf9KJO!5!D9m7R#J5x-eON!Xx1dK6T*8X;*DJ>1V!d?v-x3^P=zanWh-yi*^#7 z4)S)y&l@4Wu&T`lCwK2^zwC}sE-P&tI~y|DxH+jf%TsyIxWfwF>%AC$;UY?>ycC?P zMr3LV;qW!P-~70vi*WRY4z6a8fVG)hn%cTo{U5}xh}BY zKRNCaEZ@IC7M2>*!01UZ(!yFb@@hZv?M$iHjd}6H4nyNU70?{Z`d35GP8-F-ve9)i zoulWsMKR@bXj@5s`l8>H+ih%?x~(s&3$2Dv;qgHMBnCrls2wEtbf@g<@-7{hN4Hw- zeNxO}Y}{I&i9cXe?-U?4+?;?+&7IMnTD)B5LR5$vvYJ;rNkhmv&25QzKRF4epqSWW zG2yR+;IX86i(}IF9pFEE9I;zMQE%Q^qWusGlOF$iZxfz!Yf@pnGT>;nK z=w3n#S%Uc^+d_tH#O&t+3tyTR@_9-FkRfn;c7da?HdW$x)8cC*qtiOFa6X0i#%0S% z(UmnC;gCGUd_i8!%bh;)<-LeUWNkHIZ?`hftbf@0%*KclGC@4CR+7&x^|{OygL9pi z=a6YcPJz3lv2-?V18$Q28oiZyLFNU;9Kyr^{m~5@Z5E)80Mh!mdw@Z9r|n$A-%hrE z0o4hYGt}wDlL&}5ksv=-6|qg?Z?~J@rQ{jT52J7qDLUp|!#@y@6ED1cA2fg->@PBp zR}5|k_GI@VnpH@Cp}OS54JUf};Ta{bN;--J;n zxv+@j_wB07cP~SS^o}<4aB{%GIA%YV=&Gp%3EcDRX?wne56*=ovr~cL1dJX~vXrc8G`%j4P;9pBkA<*8b<=ZNNN0Kw?V+ zT3J1>^aG{x5L8;E!>&BgunTpxW(cGD^Das~%z1giwQY5@kTM$tL8l!_AI}al zUn1M?79Oc(G71O+gdy~&lV6N<;Elj|jT zA6Pcu%~5wvS^<KXyG`~S^L=UZ#Vp>3biJvkNQIvxrbFx3P%`^`7dS41RggqPN!AhoW|vm; zomSosu-M6g!LR)KDa)zg*dKXlgEgm|BCdmu(tYpNSyXK?U0}3$l_;@jF>|S1+YY|z zK)@j5jl!bfqRf|>VpuEs)VGbBY^k*V#lg~Kuk!--bS_Mm8?%NRLsmTwazDRw>;6!2 zH*qvjS8hV2OplypV)?TjAwwQeV1pp?d+=qum4k2~PWV}u+1Le~I}W&-&4y0Z)BR!0 z!`GG`N&015_R!g0$nuKEzJK$|*%X)JlN<2*R~RBa;e(zlOd8;yg!yvF%0@HJmJ)wCtdCDMcCtlE$&fAphYC>?_%c%--m+Tl?E=%QESb8Rw9z?%Z1Zf z#2=PfxpA;1llf_fVe{M$$o5I9tENx8f7SV!gJMwKsGJbrISnQ4!|c=`Fm>i&$$EL@t{C73_Nf> zc@^fE9Ds^Aq|Zo^@MICy`8BG!2=M$GQsEOJ>Z!laRM!!k^8%4#9D$V;T<+SM%21E8 z0*YPCzST26reQVacSX}1Mm?zwHJ%usgc9;aJlfeU7r2^|y0GSwqlyw?#%6q#n<-;O zu72d#v;A+Xtm%JIWfRB#Qe|Jx;A{9`&5t-d54(7L0bW(Eo2x$0r}RX>845LhXywdb zv=?l+f#qkzYcJc{i)}1786A^7}rD;3dOw zU`7Hwv21H`OoOSeg7Lbr?X@HJX0gjS-2CYkIVQFyDh6fx=N2b{&3qCi#Af6BEmYeW zgnrq?<>i;|by^yga*10VB+17sg0l4`pzxKHjaM6;7Y?S|ADog~mJuqcz8((W$0?&- zS`vVa9tUxoJ}{i=YRa~CXB;h_^FOu0SzK^KxE3raE$p7V=@!ku1xuuBs=c2(=~*Kd zv=CfuP5zRrzeF*FE%SMexni#}RlK|ZKa+t<-rZa+Jw&`j)1}>)Gdwc2>-qH_pHsiE z7F|6(@`$)j|L)2ExOYq1B5tGJl-Hsd+bMqT+gD z`9d{5DqdhR7Q~5z4oyjZt&FDCmqf%HtGlaK^2CSGnr`fSRvl_MbOVJa3n{eel*0=f z1gIU{65MsS4!LHOt=PzsGSxAKo^47+sAkCOEuU%2_zL;)w|GDgevXHs&(Z?1Cpvv> znf)|B;qzu5g`Am+rA4>g=Rr3bqaCp!oOg&$+}=yJwoh)Bb6-iI8Hf^5<>xTmY)>h0 zmVPBkvsCUtwWRprn>*O~aR-rH}5=RpmWyFm1+U?3WU9G@_FNMr!Ccp{oMx8>jOHN6+ zwZ?Z#a4?XYE=l}m`;nYB%eA)1jOz6h!^crt%>o6o75YaW_+ zoi#f{OFV1#G}oUZ*2Xt1CqoYBY6}5?bs)uO)HeP0g>IC!fpD>6o&QY*huK+$3KELD%eb60)eCI&&h`PDvAp|5 zH18!8&KYc68GM&x3(d5eCn8+cr28dKY`x)Y3&T?MLrBv)&JDmx@iqgvFO zOX#;Gt1Et?g(mt2IO?oO-9DcgJ0@%@H$<@o-q7V5ox8r-K!LzY<4 zX0{gyh3o7D$C zEK(JGarbm#**qA)UN0k_dSoY?aK0%RHA~&^qn=o_Hnmg<2yZQOL2rcWU+mo%o&1@< zw8i>|?gWz$P+V*ubL6r;=nXIk(kB8thuL;5g@w^E{N5b84|rD?E4rUmHJa_E&3$Ox z9Rz7f-zRt57>)GKj5`D2*UGtyB+3nvuOj3i_Z&Tg*XZ=)>g1`RaoK=Hb!JAey&tXj zttnHZsXNA`zX>c@jZJA428?N>X4m>)TbAYKQ@A5g`|}@v@WO39jKo8v=m}2)w&}BK z7lgi5u9ZC0LM_?db@b>~7oK!4d_()tk!_Cl%04nrbRmR&aT|Ti;T_Z@r7*~-e{iq8 zs8Z%D!w{Z{HHVj`CU?HiNl;_7;jKu}h0JM}8T&y{O*J3TkDuI$=b(q%fC;`Qmw29s zU$(7*HW$-AD4J}sD{NllGsg2r)DFV2AQ7+ZtxOFl=`cQ#W^>UkdW=2A*GCgJtQ_LJ z$n#~$yI}Ip#3gc1xkugi{j-q}Y--h#`efs(j%6d93nYjWG^b+wxJ|qG@y^zwK?v)Q z63#1#$aG64jV6vg~>$h4E)^M_D-Ou~*bXw)u&}l6Ci? z@(!}x-(H_v>(^~DrpdruR57qi^~&htxi0`+cY)yX*#YV9p((NQj;7xaG$Mc11g;L> zvph-YoE6G+39%yrsDTGQT``jj;04U$G~`?uw9AQ}As!c3H$o6PCI)$`mSp85)iAx? zyET-=-M!Crs$VaJ?I43HbscMWJ%*0)L4vaW zp3O@_;@Y=&uq(XqJv0|it~B)Qu}#WLTD6US<#BTqb+Om~tiqpHbkU!~lVtjD%kGZ=m@*(VBu_kPl{QxV+#<;qc zrP_%8J@X1UEA>j-c^z~2g(sm>@JHyy0i=3vra};5T=`0I7$<`&6zql7Rri%hrR2d1 zQ`Wn>7%_di0x5ie>w66fHF!*=WRYT5PMRQrU=ou8piy?uDeV=evCkCq@$vB>e+BSD z`gKJ{tG@)!-NxE&W~rl^(1}IQv6ym}-$~L)rQ0*?!|xOEHo2t| z-wTNdD=?hFSmH@H4JJ_3EHp14uAUqk-6_;q+tfT^Gu|U$!u0#`sLVOtA{y0|Q1~1ao(RPz|96Z;^lC{RL83DG6tJ z?%|2l%z`r{w+-9MnXjY=R)AR4d*3E<9*aisMHYC5V&;wJ&dv< z*Ks8#e}>0G0hn-df6(zbf*0{yq9gNqAaTIj*|BQT@w98Rb|SyLYX&7ZsUKXlqm^ow z!>nykmSUS`lDXWuDgQUtJ>%7 z$4b8@?+XcqLlyEFn7)zP9p_y*6TO>>Ni4WFQFjiw^5q?duy94_GZ2 z>OEuxe;h3)*kj@!%6=^hV%yj6yA(gBk4s2|Fs`qk5%f(sl_~Hd`l#V8G*dq<)$Ze#l!{_M6J3ADJLtp=KLmmVLC;8E79FK zs%xvHO*bH0l?FzcCXPPN0d$MWG_|&C!0C<9q3OWJZ0|{hR?~YN=m}R-jllp*$w5XS zyC!>A{DI-6cXyfx#G6Ni%7DJVBc0KAyyZ5ui4$Ed!Q#t>@c`&~wZC4x5{^5$I>JD_ z-rlBBg=-%>8sj#ks*^t_*4vlr3^ti!?O;ns=v!xzQHik>Q33X0ONwQ=(Az1+OL9imqx;-d~Q)U0& zVISJRSMOm~m-EU__$ODTqzw@6>v)-y#CfMDTWL#qy)eKTyP4J9;5lr~PB{3z7;6q~ zjnXQl?|B_ci$1skn9~*;{jfjal)Ia~T^kc27HsYvkH_^^Fhohw9~d zO7y;1DR^BlOZ#Y3Sax8~XVNUA;H3QRHQd*QIdve1LCb##R|$rHQ1gF4 zaNGrsf8IgQ1*uum@{Ka*6%LK77h)P1;pF}u|L?ocCd0i&T1*#3!)lVQWhHzlP6;H^ zq;Ov+ctk_#VJ`}*t3{cw5#OsA;`H9NGz^ogk|o4f>SQnPJo79#UWT^ z`l6Fho2xP|RpN>SrYcWVQt{6Bv3c=Cfj8#lD4ozkICL;M&otQzokI-VqfIt`+JjTEjByC`(Q= zOE$$Qc1sRQS!-xx-$gIJZt_<-;B|Zd{$lJYst8gdooBWiS13!BtK@w~ei>32zE&(Jf zKEf)mzVs}QIt>QV$Kc4Mdvr-)7OS+bY`<=4 zV(_njK;)U4Hq|&bM&NE6-!gb%dlR94KIkbgFra{inlaS-XZ*LI?_~TG-dtQKi6-xX zpzV}1(g>bag!>LL)7D@i6ySD@jn-tu*%+QwQhfKB8f7ANIHXjz!MbOT)AXLyr`4ON z1!pE;$U-D2?AcYI4jJ^0#ua`@h|~At2_UYMmpm-gD0(H{WXgDjL*Nf_OVw!?a4i8d zDL5jDeGXrt$G0kHi#2~9kZOQ*fK>_)S&7m1ng*qREpL0!z-@lN_yod|dpQ8j&N`PlwEPtf(aLkwS&TS@xc8 zv>xHgkAC@R&q<5D2)fl}SH}fma>W;G%e|h{&YWmU4MH*n(6^qY$uT5FQsf)o>==8h zf?SYCuwrcVoLt7P;#Q6j(maQ9=Xab4J;(++}T+rv%cC7g4 zm3CJrwte@UK3z%Qn@!9ZlF&5o6Mkp%P1dpI1^HqVY?=!T2hOE#ed+~+s9XSVR(>4o z^CPN>>TfaBPMmPuruG)Oo24>L0W)wRIp(fN`nM#9c0k1J^W_CQ`pmL1`I*+jB^|Pa zL8>lPZ)wmKhv@D8GeAvhWQBA6iM6oy4W^%=lft{Z87or^Adh?`pMU* z;7oHVPwg;UWJ9336^`N!6^hmk5yrKN>PsbD895Doau0=_EE0uDM5HNqeWXS z%7MlWx9g6R&?&g3ljv2fA78U!zoA}JJtNYREmk|dGf3Q5c9PZoFkT|x*&a*u`b4**w=D970;@Yb(s1Ep%mh6H%V(wi)-lB`0xd3ipiT zH1t)k;~G7d#PE*iwxXbl)bF>j?8_dzNYFt*&+5NL{DPe}ApnJP$pSynG|O z-&LYoGvcsnWnQ=P`b?^(N`a#qEUx$v5`eutH%}m^Sn-3#vW||q(fp-BK zy#b4k?l7<(oRNWxOS9NJe)O}S{uWdUuG}q>Wm-Gr-FwUrxvAbd@wR^>kLbl{fx@NTSyrG}J`t)^3J!$vMAJesbTOp54;Zr4VlecO# z%X4^Yhl4qfcrNINOI1l@Z5O}L!7b+bce$sDbz>k3nLshp9%~({Vv^}23>7y(kcB5t zg?R^c-Iey2-*5w8u9TY82`wM)kxtV_z))P4qbezWof2e}@0(UPHR5;4X4A)MG&qb| z(-}H_4hz)JQFy|NhtvKyM$iVI-jFZbCrg+0_0=Ar*+}Xp+qSVqn7;;N$j&i-Yot#z znF+a&M4BLyM9&;|exw#s<%!5}5$PlGJgMl_J>i%7bR*&y#S}tEIJ!T8@2@CDxT`~Qb+_Q2~ zv0qM5L$$Kudgr)n4X?cJlv0-jIKOcW>D_<-$+n_bvbiG*K%(`DW}MO=PjDXcj> zmAC`@vGjrSDr<@l$vXw3@ic($@5h=Rckaqz3S=S++ju>tuj;ww9^y-PdW>G?}FhJ4w-}r_+f6-m;uC;E3 zAo!?AbS8j%(SY;a8C|hQKSqVB_+^4fY+SyhpFu!^cRv~>YwzU=kz;LfM)Kq6F`IM$ zM}FBNK{S3nTdjqUqzPmgve(Xry$hQqKNP8wL$c!_krI-x0Ump9+U~6pEfj*Q_k8{X zrEyAs(yg|C^e#Ss^e($Ps{FC+x#yjo3xX>vpiZZ@-5BVJz_aF6&}GFp8S=#=#>$Ky zEs_iD^Z-Nm{ASv(c4F0N<2kxOWO=-yrPERjkNLLt+{gFGnotQdI6^8lvmt;s+fcmCMA&I>I0j-|%3lj_LW3EWJ#UdvIx%UcHF$#+kC zSepZE?qtpC6vZmg)xuAuhbUKhEX0ZzSL(D!ampB~eqlVSJ=rWT1^z%hA68Deoh*KJ zctK6Z{om^kUAvcgj}iQ6*T?)ROylY5tAJweeR)Fh6>G15j&2Xllzrc5;zEr+l96ob z&Q(B?30P5D0!94-j?N`K8tlwNDD|UNQcCREQ8vmRv3W5+DNBE%mqbKamm^be9rPzm zl&_-TCn=O21K|yKQ9ySmF_fAH*Dl>*v)AL2XjHlZuvz<-lkq@?&9i0ROZfT3V|reP z?^kNu%pAPC0bok^{v^GzN87XNKyI*G{#EVwmK0)Y#Ho}rtOtZgt)}+1oZ5!En-yr) zK6{2p>DSm9AQe1$V#KzM^ifO~6^!a?^3KUSn39P!#9Pn{5$2h4D~Ca>OwmF3owS8o zo2P~creu<&G-~rgb?>b1rWtwe!k$<(@X8yt*3m_sBPI>yPWEwFZ)*v^8=d9Jw(sz$ z5WZ~hV*M*8`z6M%ahq^0uOe_*WAF!$>km&dxsZ7z*PfD0qIHARM)GG?%ummVb_VFP zT6i<~^*OREhSeu;2tv2lZdOs^(<+?9ukoFr z{Gm1qUD?HTC|KaTLmGfI+_ZoR+b=SRI{c7VqE@=(%5{nUlJ~b7WYfFG-DD~cs*v&{P*i~io-9+)AqtH2{sDVbqY#$TG$z{eol>~58kbLQGhG&SMjSrxRgKne_i~B5YnU2kIf&dn0HZlxaXy(JN#f?au^jw!8qut+vdR zO_#!6E!mngpUd%YyohlVpG1i3jM3c_YYK}IQc)l(8W_Pu))cC0GQ4&p{0+P207E9O zkfXkHV{tp~j@Z!UVC$1|*+&P}ZGakQ&MaSF^W>;?=n{-t;!ATB$1=1TKJ0e+jJA19 zPmi|8Dx( z`B@f*6!gnIb2UYcIyu9v$?PpG83|HlsUq2GF$&@2u6^d-q-Dc$4n)4&mB@G!_Q4(1 zc(c|M$1fsTQzCTJbLym{TBrtD>|8c)Z4L3fIQQmcGj`=fYpta`h4?zr zYlaVo*@&#iLdW5d)SuivqA%a9DbRM6jF$j2CW~Z?6!v|$kYQ0S_Qg{(599E;r#mf8 z34!8ym+#fkUh1SHyaut*ikUIPhXW7W!VEu^%O_fX>f9D^O8L0xcWtA1grRzEp28zA ziuWmLF~*wNNc{6suIV^}s>Q`n8_`g8i0A==-x5@G@mjEoe8K7Ba*8uc&5rmp+)1~{ z64W zZCcWGI$I+h%=uaQAHyWvkr+7z1+_->(x*!o>*4S)S0wHTy$<+gy_YB~R8c+szl_-$ zbX0M#!k_ihR2=3TNUJ9%c)-=YiAB{Tq$eqF5+gjsPAwSUZ6P5LfYWbn@e$=3iHVGi z)@qC*{0|89RqL0I+XdU4BmS?%?RWc#4~6+J@|_vv3UZD6U4chF7wC_}v7!9(WYP#N zfLSxq#`65**3Cr83-618&jgpABZT+!^cxtkwM)@o8YB$oIx2r;^%t`G{kOq{fFG`S z@_0a_^S|pr!RiAP7s6No5M&<`w0|L5*_9;-qKN#Ok!Kd7hkRSk{u8zRVn1OhRb-V> zSRJ8f4Lbh;rBrt`o(#d2dcZGVYHxi0X+-uBM*IACfu{`^`IF$1q>T@lW%RFWV^U&z z3&#(PYDVrshkpg1ZE1T_Lq{(5QzQ`-^{yUPgLOZ7Q-U{+^eLwvFkq#>)Jgx_h8=$s=1Xk_ z!$%$1LiBr{_khij-K$#R#v<%%)COEmEom*{c4Tr;G1*FW)=r{fC##gb50VE-#)^Bb5@v|Wd(VaDIsTciB>mYFAvee!!d k2=||@$N#@ulEh&|cBBkMg3r81f1d5tOJ(U&N#mgZ1Cro{K>z>% diff --git a/pkg/infrared/config/file.go b/pkg/infrared/config/file.go new file mode 100644 index 00000000..874e949a --- /dev/null +++ b/pkg/infrared/config/file.go @@ -0,0 +1,147 @@ +package config + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + ir "github.com/haveachin/infrared/pkg/infrared" + "gopkg.in/yaml.v3" +) + +type FileType string + +const ( + YAML FileType = "yaml" +) + +type decoder interface { + Decode(io.Reader, any) error +} + +type decoderFunc func(io.Reader, any) error + +func (fn decoderFunc) Decode(r io.Reader, v any) error { + return fn(r, v) +} + +func newYamlDecoder() decoder { + return decoderFunc(func(r io.Reader, v any) error { + return yaml.NewDecoder(r).Decode(v) + }) +} + +// FileProvider reads a config file and returns a populated infrared.Config struct. +type FileProvider struct { + ConfigPath string + // Must be a directory + ProxiesPath string + // Types of the file + // Defaults to YAML + Type FileType +} + +func (p FileProvider) Config() (ir.Config, error) { + var dcr decoder + switch p.Type { + case YAML: + fallthrough + default: + dcr = newYamlDecoder() + } + + return p.readAndUnmashalConfig(dcr) +} + +func (p FileProvider) readAndUnmashalConfig(dcr decoder) (ir.Config, error) { + path, err := filepath.EvalSymlinks(p.ConfigPath) + if err != nil { + return ir.Config{}, err + } + + f, err := os.Open(path) + if err != nil { + return ir.Config{}, err + } + defer f.Close() + + var cfg ir.Config + if err = dcr.Decode(f, &cfg); err != nil { + return ir.Config{}, fmt.Errorf("failed to decode file %q: %w", p.ConfigPath, err) + } + + srvCfgs, err := loadServerConfigs(dcr, p.ProxiesPath) + if err != nil { + return ir.Config{}, err + } + cfg.ServerConfigs = srvCfgs + + return cfg, nil +} + +func loadServerConfigs(dcr decoder, path string) ([]ir.ServerConfig, error) { + path, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, err + } + + paths := make([]string, 0) + if err = filepath.WalkDir(path, walkServerDirFunc(&paths)); err != nil { + return nil, err + } + + return readAndUnmashalServerConfigs(dcr, paths) +} + +func walkServerDirFunc(paths *[]string) fs.WalkDirFunc { + return func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if d.Type()&os.ModeSymlink == os.ModeSymlink { + path, err = filepath.EvalSymlinks(path) + if err != nil { + return err + } + } + + *paths = append(*paths, path) + return nil + } +} + +func readAndUnmashalServerConfigs(dcr decoder, paths []string) ([]ir.ServerConfig, error) { + cfgs := make([]ir.ServerConfig, 0) + for _, path := range paths { + cfg, err := readAndUnmashalServerConfig(dcr, path) + if err != nil { + return nil, err + } + cfgs = append(cfgs, cfg) + } + + return cfgs, nil +} + +func readAndUnmashalServerConfig(dcr decoder, path string) (ir.ServerConfig, error) { + f, err := os.Open(path) + if err != nil { + return ir.ServerConfig{}, err + } + defer f.Close() + + cfg := ir.ServerConfig{} + if err = dcr.Decode(f, &cfg); err != nil && !errors.Is(err, io.EOF) { + return ir.ServerConfig{}, err + } + + return cfg, nil +} diff --git a/pkg/infrared/conn.go b/pkg/infrared/conn.go new file mode 100644 index 00000000..b58ab2e2 --- /dev/null +++ b/pkg/infrared/conn.go @@ -0,0 +1,102 @@ +package infrared + +import ( + "bufio" + "io" + "net" + "sync" + "time" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/handshaking" + "github.com/haveachin/infrared/pkg/infrared/protocol/login" +) + +var connPool = sync.Pool{ + New: func() any { + return &Conn{ + readPks: [2]protocol.Packet{}, + } + }, +} + +type Conn struct { + net.Conn + + r *bufio.Reader + w io.Writer + timeout time.Duration + readPks [2]protocol.Packet + handshake handshaking.ServerBoundHandshake + loginStart login.ServerBoundLoginStart + reqDomain ServerDomain +} + +func newConn(c net.Conn) *Conn { + if c == nil { + panic("c cannot be nil") + } + + conn, ok := connPool.Get().(*Conn) + if !ok { + panic("connPool contains other implementations of net.Conn") + } + + conn.Conn = c + conn.r = bufio.NewReader(c) + conn.w = c + conn.reqDomain = "" + conn.timeout = time.Second * 10 + return conn +} + +func (c *Conn) Read(b []byte) (int, error) { + if err := c.SetReadDeadline(time.Now().Add(c.timeout)); err != nil { + return 0, err + } + return c.r.Read(b) +} + +func (c *Conn) Write(b []byte) (int, error) { + if err := c.SetWriteDeadline(time.Now().Add(c.timeout)); err != nil { + return 0, err + } + return c.w.Write(b) +} + +func (c *Conn) ReadPacket(pk *protocol.Packet) error { + _, err := pk.ReadFrom(c.r) + return err +} + +func (c *Conn) ReadPackets(pks ...*protocol.Packet) error { + for i := 0; i < len(pks); i++ { + if err := c.ReadPacket(pks[i]); err != nil { + return err + } + } + return nil +} + +func (c *Conn) WritePacket(pk protocol.Packet) error { + _, err := pk.WriteTo(c.w) + return err +} + +func (c *Conn) WritePackets(pks ...protocol.Packet) error { + for _, pk := range pks { + if err := c.WritePacket(pk); err != nil { + return err + } + } + return nil +} + +func (c *Conn) ForceClose() error { + if conn, ok := c.Conn.(*net.TCPConn); ok { + if err := conn.SetLinger(0); err != nil { + return err + } + } + return c.Close() +} diff --git a/pkg/infrared/filter.go b/pkg/infrared/filter.go new file mode 100644 index 00000000..d78cffad --- /dev/null +++ b/pkg/infrared/filter.go @@ -0,0 +1,65 @@ +package infrared + +import ( + "net" +) + +type Filterer interface { + Filter(c net.Conn) error +} + +type FilterFunc func(c net.Conn) error + +func (f FilterFunc) Filter(c net.Conn) error { + return f(c) +} + +type ( + FilterID string +) + +type FilterConfigFunc func(cfg *FiltersConfig) + +func WithFilterConfig(c FiltersConfig) FilterConfigFunc { + return func(cfg *FiltersConfig) { + *cfg = c + } +} + +type FiltersConfig struct { + RateLimiter *RateLimiterConfig `yaml:"rateLimiter"` +} + +type Filter struct { + cfg FiltersConfig + filterers []Filterer +} + +func NewFilter(fns ...FilterConfigFunc) Filter { + var cfg FiltersConfig + for _, fn := range fns { + fn(&cfg) + } + + filterers := make([]Filterer, 0) + + if cfg.RateLimiter != nil { + cfg := cfg.RateLimiter + f := RateLimitByIP(cfg.RequestLimit, cfg.WindowLength) + filterers = append(filterers, f) + } + + return Filter{ + cfg: cfg, + filterers: filterers, + } +} + +func (f Filter) Filter(c net.Conn) error { + for _, f := range f.filterers { + if err := f.Filter(c); err != nil { + return err + } + } + return nil +} diff --git a/pkg/infrared/infrared.go b/pkg/infrared/infrared.go new file mode 100644 index 00000000..cb6adbba --- /dev/null +++ b/pkg/infrared/infrared.go @@ -0,0 +1,317 @@ +package infrared + +import ( + "errors" + "io" + "net" + "strings" + "sync" + "time" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/pires/go-proxyproto" + "github.com/rs/zerolog" +) + +type Config struct { + BindAddr string `yaml:"bind"` + ServerConfigs []ServerConfig `yaml:"servers"` + FiltersConfig FiltersConfig `yaml:"filters"` + KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` +} + +type ConfigFunc func(cfg *Config) + +func WithBindAddr(bindAddr string) ConfigFunc { + return func(cfg *Config) { + cfg.BindAddr = bindAddr + } +} + +func AddServerConfig(fns ...ServerConfigFunc) ConfigFunc { + return func(cfg *Config) { + var sCfg ServerConfig + for _, fn := range fns { + fn(&sCfg) + } + cfg.ServerConfigs = append(cfg.ServerConfigs, sCfg) + } +} + +func WithKeepAliveTimeout(d time.Duration) ConfigFunc { + return func(cfg *Config) { + cfg.KeepAliveTimeout = d + } +} + +func DefaultConfig() Config { + return Config{ + BindAddr: ":25565", + KeepAliveTimeout: 30 * time.Second, + FiltersConfig: FiltersConfig{ + RateLimiter: &RateLimiterConfig{ + RequestLimit: 10, + WindowLength: time.Second, + }, + }, + } +} + +type ConfigProvider interface { + Config() (Config, error) +} + +func MustConfig(fn func() (Config, error)) Config { + cfg, err := fn() + if err != nil { + panic(err) + } + + return cfg +} + +type Infrared struct { + Logger zerolog.Logger + + cfg Config + + l net.Listener + sg *ServerGateway + filter Filter + bufPool sync.Pool + conns map[net.Addr]*Conn +} + +func New(fns ...ConfigFunc) *Infrared { + cfg := DefaultConfig() + for _, fn := range fns { + fn(&cfg) + } + + return NewWithConfig(cfg) +} + +func NewWithConfigProvider(prv ConfigProvider) *Infrared { + cfg := MustConfig(prv.Config) + return NewWithConfig(cfg) +} + +func NewWithConfig(cfg Config) *Infrared { + return &Infrared{ + cfg: cfg, + bufPool: sync.Pool{ + New: func() any { + b := make([]byte, 1<<15) + return &b + }, + }, + conns: make(map[net.Addr]*Conn), + } +} + +func (ir *Infrared) init() error { + ir.Logger.Info(). + Str("bind", ir.cfg.BindAddr). + Msg("Starting listener") + + l, err := net.Listen("tcp", ir.cfg.BindAddr) + if err != nil { + return err + } + ir.l = l + + srvs := make([]*Server, 0) + for _, sCfg := range ir.cfg.ServerConfigs { + srv, err := NewServer(WithServerConfig(sCfg)) + if err != nil { + return err + } + srvs = append(srvs, srv) + } + + ir.filter = NewFilter(WithFilterConfig(ir.cfg.FiltersConfig)) + sg, err := NewServerGateway(srvs, nil) + if err != nil { + return err + } + ir.sg = sg + + return nil +} + +func (ir *Infrared) ListenAndServe() error { + if err := ir.init(); err != nil { + return err + } + + for { + c, err := ir.l.Accept() + if errors.Is(err, net.ErrClosed) { + return err + } else if err != nil { + ir.Logger.Debug(). + Err(err). + Msg("Error accepting new connection") + + continue + } + + go ir.handleNewConn(c) + } +} + +func (ir *Infrared) handleNewConn(c net.Conn) { + if err := ir.filter.Filter(c); err != nil { + ir.Logger.Debug(). + Err(err). + Msg("Filtered connection") + return + } + + conn := newConn(c) + defer func() { + conn.ForceClose() + connPool.Put(conn) + }() + + if err := ir.handleConn(conn); err != nil { + ir.Logger.Debug(). + Err(err). + Msg("Error while handling connection") + } +} + +func (ir *Infrared) handleConn(c *Conn) error { + if err := c.ReadPackets(&c.readPks[0], &c.readPks[1]); err != nil { + return err + } + + if err := c.handshake.Unmarshal(c.readPks[0]); err != nil { + return err + } + + reqDomain := c.handshake.ParseServerAddress() + if strings.Contains(reqDomain, ":") { + host, _, err := net.SplitHostPort(reqDomain) + if err != nil { + return err + } + reqDomain = host + } + c.reqDomain = ServerDomain(reqDomain) + + resp, err := ir.sg.RequestServer(ServerRequest{ + Domain: c.reqDomain, + IsLogin: c.handshake.IsLoginRequest(), + ProtocolVersion: protocol.Version(c.handshake.ProtocolVersion), + ReadPackets: c.readPks, + }) + if err != nil { + return err + } + + if c.handshake.IsStatusRequest() { + return handleStatus(c, resp) + } + + return ir.handleLogin(c, resp) +} + +func handleStatus(c *Conn, resp ServerResponse) error { + if err := c.WritePacket(resp.StatusResponse); err != nil { + return err + } + + pingPk := c.readPks[0] + if err := c.ReadPacket(&pingPk); err != nil { + return err + } + + if err := c.WritePacket(pingPk); err != nil { + return err + } + + return nil +} + +func (ir *Infrared) handleLogin(c *Conn, resp ServerResponse) error { + hsVersion := protocol.Version(c.handshake.ProtocolVersion) + if err := c.loginStart.Unmarshal(c.readPks[1], hsVersion); err != nil { + return err + } + + c.timeout = ir.cfg.KeepAliveTimeout + + return ir.handlePipe(c, resp) +} + +func (ir *Infrared) handlePipe(c *Conn, resp ServerResponse) error { + rc := resp.ServerConn + defer rc.Close() + + if resp.SendProxyProtocol { + if err := writeProxyProtocolHeader(c.RemoteAddr(), rc); err != nil { + return err + } + } + + if err := rc.WritePackets(c.readPks[0], c.readPks[1]); err != nil { + return err + } + + rcClosedChan := make(chan struct{}) + cClosedChan := make(chan struct{}) + + c.timeout = ir.cfg.KeepAliveTimeout + rc.timeout = ir.cfg.KeepAliveTimeout + ir.conns[c.RemoteAddr()] = c + + go ir.copy(rc, c, cClosedChan) + go ir.copy(c, rc, rcClosedChan) + + var waitChan chan struct{} + select { + case <-cClosedChan: + rc.Close() + waitChan = rcClosedChan + case <-rcClosedChan: + c.ForceClose() + waitChan = cClosedChan + } + <-waitChan + delete(ir.conns, c.RemoteAddr()) + + return nil +} + +func (ir *Infrared) copy(dst io.WriteCloser, src io.ReadCloser, srcClosedChan chan struct{}) { + _, _ = io.Copy(dst, src) + srcClosedChan <- struct{}{} +} + +func writeProxyProtocolHeader(addr net.Addr, rc net.Conn) error { + rcAddr := rc.RemoteAddr() + tcpAddr, ok := rcAddr.(*net.TCPAddr) + if !ok { + panic("not a tcp connection") + } + + tp := proxyproto.TCPv4 + if tcpAddr.IP.To4() == nil { + tp = proxyproto.TCPv6 + } + + header := &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: tp, + SourceAddr: addr, + DestinationAddr: rcAddr, + } + + if _, err := header.WriteTo(rc); err != nil { + return err + } + + return nil +} diff --git a/pkg/infrared/infrared_test.go b/pkg/infrared/infrared_test.go new file mode 100644 index 00000000..34f204d3 --- /dev/null +++ b/pkg/infrared/infrared_test.go @@ -0,0 +1,81 @@ +package infrared + +import ( + "bufio" + "net" + "testing" + + "github.com/pires/go-proxyproto" +) + +type TestConn struct { + net.Conn +} + +func (c *TestConn) RemoteAddr() net.Addr { + return &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 25565, + } +} + +func TestInfrared_handlePipe_ProxyProtocol(t *testing.T) { + rcIn, rcOut := net.Pipe() + _, cOut := net.Pipe() + + c := TestConn{Conn: cOut} + rc := TestConn{Conn: rcIn} + + srv := New() + + go func() { + resp := ServerResponse{ + ServerConn: newConn(&rc), + SendProxyProtocol: true, + } + + _ = srv.handlePipe(newConn(&c), resp) + }() + + r := bufio.NewReader(rcOut) + header, err := proxyproto.Read(r) + if err != nil { + t.Fatalf("Unexpected error reading proxy protocol header: %v", err) + } + + if header.Command != proxyproto.PROXY { + t.Fatalf("Unexpected proxy protocol command: %v", header.Command) + } + + if header.TransportProtocol != proxyproto.TCPv4 { + t.Fatalf("Unexpected proxy protocol transport protocol: %v", header.TransportProtocol) + } + + if header.Version != 2 { + t.Fatalf("Unexpected proxy protocol version: %v", header.Version) + } +} + +func TestInfrared_handlePipe_NoProxyProtocol(t *testing.T) { + rcIn, rcOut := net.Pipe() + _, cOut := net.Pipe() + + c := TestConn{Conn: cOut} + rc := TestConn{Conn: rcIn} + + srv := New() + + go func() { + resp := ServerResponse{ + ServerConn: newConn(&rc), + SendProxyProtocol: false, + } + + _ = srv.handlePipe(newConn(&c), resp) + }() + + r := bufio.NewReader(rcOut) + if _, err := proxyproto.Read(r); err == nil { + t.Fatal("Expected error reading proxy protocol header, but got nothing") + } +} diff --git a/protocol/errors.go b/pkg/infrared/protocol/errors.go similarity index 100% rename from protocol/errors.go rename to pkg/infrared/protocol/errors.go diff --git a/pkg/infrared/protocol/handshaking/serverbound_handshake.go b/pkg/infrared/protocol/handshaking/serverbound_handshake.go new file mode 100644 index 00000000..58975cdc --- /dev/null +++ b/pkg/infrared/protocol/handshaking/serverbound_handshake.go @@ -0,0 +1,140 @@ +package handshaking + +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/haveachin/infrared/pkg/infrared/protocol" +) + +const ( + ServerBoundHandshakeID int32 = 0x00 + + StateStatusServerBoundHandshake = protocol.Byte(1) + StateLoginServerBoundHandshake = protocol.Byte(2) + + SeparatorForge = "\x00" + SeparatorRealIP = "///" +) + +type ServerBoundHandshake struct { + ProtocolVersion protocol.VarInt + ServerAddress protocol.String + ServerPort protocol.UnsignedShort + NextState protocol.Byte +} + +func (pk ServerBoundHandshake) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ServerBoundHandshakeID, + pk.ProtocolVersion, + pk.ServerAddress, + pk.ServerPort, + pk.NextState, + ) +} + +func (pk *ServerBoundHandshake) Unmarshal(packet protocol.Packet) error { + if packet.ID != ServerBoundHandshakeID { + return protocol.ErrInvalidPacketID + } + + return packet.Decode( + &pk.ProtocolVersion, + &pk.ServerAddress, + &pk.ServerPort, + &pk.NextState, + ) +} + +func (pk *ServerBoundHandshake) SetServerAddress(addr string) { + oldAddr := pk.ParseServerAddress() + newAddr := strings.Replace(string(pk.ServerAddress), oldAddr, addr, 1) + pk.ServerAddress = protocol.String(newAddr) +} + +func (pk ServerBoundHandshake) IsStatusRequest() bool { + return pk.NextState == StateStatusServerBoundHandshake +} + +func (pk ServerBoundHandshake) IsLoginRequest() bool { + return pk.NextState == StateLoginServerBoundHandshake +} + +func (pk ServerBoundHandshake) IsForgeAddress() bool { + addr := string(pk.ServerAddress) + return len(strings.Split(addr, SeparatorForge)) > 1 +} + +func (pk ServerBoundHandshake) IsRealIPAddress() bool { + addr := string(pk.ServerAddress) + return len(strings.Split(addr, SeparatorRealIP)) > 1 +} + +func (pk ServerBoundHandshake) ParseServerAddress() string { + addr := string(pk.ServerAddress) + if i := strings.Index(addr, SeparatorForge); i != -1 { + addr = addr[:i] + } + if i := strings.Index(addr, SeparatorRealIP); i != -1 { + addr = addr[:i] + } + // Resolves an issue with some proxies + addr = strings.Trim(addr, ".") + return addr +} + +func parseTCPAddr(addr string) (net.Addr, error) { + ipStr, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, err + } + + return &net.TCPAddr{ + IP: net.ParseIP(ipStr), + Port: port, + }, nil +} + +func (pk ServerBoundHandshake) ParseRealIP() (net.Addr, time.Time, []byte, error) { + payload := strings.Split(string(pk.ServerAddress), SeparatorRealIP) + if len(payload) < 4 { + return nil, time.Time{}, nil, errors.New("invalid payload") + } + + addr, err := parseTCPAddr(payload[1]) + if err != nil { + return nil, time.Time{}, nil, err + } + + timeStamp, err := time.Parse(time.UnixDate, payload[2]) + if err != nil { + return nil, time.Time{}, nil, err + } + + return addr, timeStamp, []byte(payload[3]), nil +} + +func (pk *ServerBoundHandshake) UpgradeToRealIP(clientAddr net.Addr, timestamp time.Time) { + addr := string(pk.ServerAddress) + addrWithForge := strings.SplitN(addr, SeparatorForge, 3) + + if len(addrWithForge) > 0 { + addr = fmt.Sprintf("%s///%s///%d", addrWithForge[0], clientAddr.String(), timestamp.Unix()) + } + + if len(addrWithForge) > 1 { + addr = fmt.Sprintf("%s\x00%s\x00", addr, addrWithForge[1]) + } + + pk.ServerAddress = protocol.String(addr) +} diff --git a/protocol/handshaking/serverbound_handshake_test.go b/pkg/infrared/protocol/handshaking/serverbound_handshake_test.go similarity index 53% rename from protocol/handshaking/serverbound_handshake_test.go rename to pkg/infrared/protocol/handshaking/serverbound_handshake_test.go index 729cb3e9..2185a695 100644 --- a/protocol/handshaking/serverbound_handshake_test.go +++ b/pkg/infrared/protocol/handshaking/serverbound_handshake_test.go @@ -1,50 +1,63 @@ -package handshaking +package handshaking_test import ( "bytes" - "github.com/haveachin/infrared/protocol" "net" "strconv" "strings" "testing" "time" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/handshaking" ) func TestServerBoundHandshake_Marshal(t *testing.T) { tt := []struct { - packet ServerBoundHandshake + packet handshaking.ServerBoundHandshake marshaledPacket protocol.Packet }{ { - packet: ServerBoundHandshake{ + packet: handshaking.ServerBoundHandshake{ ProtocolVersion: 578, - ServerAddress: "spook.space", + ServerAddress: "example.com", ServerPort: 25565, - NextState: ServerBoundHandshakeStatusState, + NextState: handshaking.StateStatusServerBoundHandshake, }, marshaledPacket: protocol.Packet{ - ID: 0x00, - Data: []byte{0xC2, 0x04, 0x0B, 0x73, 0x70, 0x6F, 0x6F, 0x6B, 0x2E, 0x73, 0x70, 0x61, 0x63, 0x65, 0x63, 0xDD, 0x01}, + ID: 0x00, + Data: []byte{ + 0xC2, 0x04, // ProtoVerion + 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // Server Address + 0x63, 0xDD, // Server Port + 0x01, // Next State + }, }, }, { - packet: ServerBoundHandshake{ + packet: handshaking.ServerBoundHandshake{ ProtocolVersion: 578, ServerAddress: "example.com", ServerPort: 1337, - NextState: ServerBoundHandshakeStatusState, + NextState: handshaking.StateStatusServerBoundHandshake, }, marshaledPacket: protocol.Packet{ - ID: 0x00, - Data: []byte{0xC2, 0x04, 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x05, 0x39, 0x01}, + ID: 0x00, + Data: []byte{ + 0xC2, 0x04, // ProtoVerion + 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // Server Address + 0x05, 0x39, // Server Port + 0x01, // Next State + }, }, }, } for _, tc := range tt { - pk := tc.packet.Marshal() + var pk protocol.Packet + _ = tc.packet.Marshal(&pk) - if pk.ID != ServerBoundHandshakePacketID { + if pk.ID != handshaking.ServerBoundHandshakeID { t.Error("invalid packet id") } @@ -57,38 +70,47 @@ func TestServerBoundHandshake_Marshal(t *testing.T) { func TestUnmarshalServerBoundHandshake(t *testing.T) { tt := []struct { packet protocol.Packet - unmarshalledPacket ServerBoundHandshake + unmarshalledPacket handshaking.ServerBoundHandshake }{ { packet: protocol.Packet{ ID: 0x00, - // ProtoVer. | Server Address |Serv. Port | Nxt State - Data: []byte{0xC2, 0x04, 0x0B, 0x73, 0x70, 0x6F, 0x6F, 0x6B, 0x2E, 0x73, 0x70, 0x61, 0x63, 0x65, 0x63, 0xDD, 0x01}, + Data: []byte{ + 0xC2, 0x04, // ProtoVerion + 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // Server Address + 0x63, 0xDD, // Server Port + 0x01, // Next State + }, }, - unmarshalledPacket: ServerBoundHandshake{ + unmarshalledPacket: handshaking.ServerBoundHandshake{ ProtocolVersion: 578, - ServerAddress: "spook.space", + ServerAddress: "example.com", ServerPort: 25565, - NextState: ServerBoundHandshakeStatusState, + NextState: handshaking.StateStatusServerBoundHandshake, }, }, { packet: protocol.Packet{ ID: 0x00, - // ProtoVer. | Server Address |Serv. Port | Nxt State - Data: []byte{0xC2, 0x04, 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x05, 0x39, 0x01}, + Data: []byte{ + 0xC2, 0x04, // ProtoVerion + 0x0B, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, // Server Address + 0x05, 0x39, // Server Port + 0x01, // Next State + }, }, - unmarshalledPacket: ServerBoundHandshake{ + unmarshalledPacket: handshaking.ServerBoundHandshake{ ProtocolVersion: 578, ServerAddress: "example.com", ServerPort: 1337, - NextState: ServerBoundHandshakeStatusState, + NextState: handshaking.StateStatusServerBoundHandshake, }, }, } + var actual handshaking.ServerBoundHandshake for _, tc := range tt { - actual, err := UnmarshalServerBoundHandshake(tc.packet) + err := actual.Unmarshal(tc.packet) if err != nil { t.Error(err) } @@ -106,18 +128,18 @@ func TestUnmarshalServerBoundHandshake(t *testing.T) { func TestServerBoundHandshake_IsStatusRequest(t *testing.T) { tt := []struct { - handshake ServerBoundHandshake + handshake handshaking.ServerBoundHandshake result bool }{ { - handshake: ServerBoundHandshake{ - NextState: ServerBoundHandshakeStatusState, + handshake: handshaking.ServerBoundHandshake{ + NextState: handshaking.StateStatusServerBoundHandshake, }, result: true, }, { - handshake: ServerBoundHandshake{ - NextState: ServerBoundHandshakeLoginState, + handshake: handshaking.ServerBoundHandshake{ + NextState: handshaking.StateLoginServerBoundHandshake, }, result: false, }, @@ -132,18 +154,18 @@ func TestServerBoundHandshake_IsStatusRequest(t *testing.T) { func TestServerBoundHandshake_IsLoginRequest(t *testing.T) { tt := []struct { - handshake ServerBoundHandshake + handshake handshaking.ServerBoundHandshake result bool }{ { - handshake: ServerBoundHandshake{ - NextState: ServerBoundHandshakeStatusState, + handshake: handshaking.ServerBoundHandshake{ + NextState: handshaking.StateStatusServerBoundHandshake, }, result: false, }, { - handshake: ServerBoundHandshake{ - NextState: ServerBoundHandshakeLoginState, + handshake: handshaking.ServerBoundHandshake{ + NextState: handshaking.StateLoginServerBoundHandshake, }, result: true, }, @@ -162,19 +184,19 @@ func TestServerBoundHandshake_IsForgeAddress(t *testing.T) { result bool }{ { - addr: ForgeSeparator, + addr: handshaking.SeparatorForge, result: true, }, { - addr: "example.com:1234" + ForgeSeparator, + addr: "example.com:1234" + handshaking.SeparatorForge, result: true, }, { - addr: "example.com" + ForgeSeparator + "some data", + addr: "example.com" + handshaking.SeparatorForge + "some data", result: true, }, { - addr: "example.com" + ForgeSeparator + "some data" + RealIPSeparator + "more", + addr: "example.com" + handshaking.SeparatorForge + "some data" + handshaking.SeparatorRealIP + "more", result: true, }, { @@ -188,7 +210,7 @@ func TestServerBoundHandshake_IsForgeAddress(t *testing.T) { } for _, tc := range tt { - hs := ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} + hs := handshaking.ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} if hs.IsForgeAddress() != tc.result { t.Errorf("%s: got: %v; want: %v", tc.addr, !tc.result, tc.result) } @@ -201,19 +223,19 @@ func TestServerBoundHandshake_IsRealIPAddress(t *testing.T) { result bool }{ { - addr: RealIPSeparator, + addr: handshaking.SeparatorRealIP, result: true, }, { - addr: "example.com:25565" + RealIPSeparator, + addr: "example.com:25565" + handshaking.SeparatorRealIP, result: true, }, { - addr: "example.com:1337" + RealIPSeparator + "some data", + addr: "example.com:1337" + handshaking.SeparatorRealIP + "some data", result: true, }, { - addr: "example.com" + ForgeSeparator + "some data" + RealIPSeparator + "more", + addr: "example.com" + handshaking.SeparatorForge + "some data" + handshaking.SeparatorRealIP + "more", result: true, }, { @@ -227,7 +249,7 @@ func TestServerBoundHandshake_IsRealIPAddress(t *testing.T) { } for _, tc := range tt { - hs := ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} + hs := handshaking.ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} if hs.IsRealIPAddress() != tc.result { t.Errorf("%s: got: %v; want: %v", tc.addr, !tc.result, tc.result) } @@ -248,33 +270,33 @@ func TestServerBoundHandshake_ParseServerAddress(t *testing.T) { expectedAddr: "example.com:25565", }, { - addr: ForgeSeparator, + addr: handshaking.SeparatorForge, expectedAddr: "", }, { - addr: RealIPSeparator, + addr: handshaking.SeparatorRealIP, expectedAddr: "", }, { - addr: "example.com" + ForgeSeparator, + addr: "example.com" + handshaking.SeparatorForge, expectedAddr: "example.com", }, { - addr: "example.com" + ForgeSeparator + "some data", + addr: "example.com" + handshaking.SeparatorForge + "some data", expectedAddr: "example.com", }, { - addr: "example.com:25565" + RealIPSeparator + "some data", + addr: "example.com:25565" + handshaking.SeparatorRealIP + "some data", expectedAddr: "example.com:25565", }, { - addr: "example.com:1234" + ForgeSeparator + "some data" + RealIPSeparator + "more", + addr: "example.com:1234" + handshaking.SeparatorForge + "some data" + handshaking.SeparatorRealIP + "more", expectedAddr: "example.com:1234", }, } for _, tc := range tt { - hs := ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} + hs := handshaking.ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} if hs.ParseServerAddress() != tc.expectedAddr { t.Errorf("got: %v; want: %v", hs.ParseServerAddress(), tc.expectedAddr) } @@ -322,20 +344,24 @@ func TestServerBoundHandshake_UpgradeToRealIP(t *testing.T) { } for _, tc := range tt { - hs := ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} + hs := handshaking.ServerBoundHandshake{ServerAddress: protocol.String(tc.addr)} hs.UpgradeToRealIP(&tc.clientAddr, tc.timestamp) if hs.ParseServerAddress() != tc.addr { t.Errorf("got: %v; want: %v", hs.ParseServerAddress(), tc.addr) } - realIpSegments := strings.Split(string(hs.ServerAddress), RealIPSeparator) + realIPSegments := strings.Split(string(hs.ServerAddress), handshaking.SeparatorRealIP) + if len(realIPSegments) < 3 { + t.Error("no real ip to test") + return + } - if realIpSegments[1] != tc.clientAddr.String() { - t.Errorf("got: %v; want: %v", realIpSegments[1], tc.addr) + if realIPSegments[1] != tc.clientAddr.String() { + t.Errorf("got: %v; want: %v", realIPSegments[1], tc.addr) } - unixTimestamp, err := strconv.ParseInt(realIpSegments[2], 10, 64) + unixTimestamp, err := strconv.ParseInt(realIPSegments[2], 10, 64) if err != nil { t.Error(err) } @@ -347,17 +373,18 @@ func TestServerBoundHandshake_UpgradeToRealIP(t *testing.T) { } func BenchmarkHandshakingServerBoundHandshake_Marshal(b *testing.B) { - isHandshakePk := ServerBoundHandshake{ + hsPk := handshaking.ServerBoundHandshake{ ProtocolVersion: 578, - ServerAddress: "spook.space", + ServerAddress: "example.com", ServerPort: 25565, NextState: 1, } - pk := isHandshakePk.Marshal() + var pk protocol.Packet + _ = hsPk.Marshal(&pk) for n := 0; n < b.N; n++ { - if _, err := UnmarshalServerBoundHandshake(pk); err != nil { + if err := hsPk.Unmarshal(pk); err != nil { b.Error(err) } } diff --git a/pkg/infrared/protocol/login/clientbound_disconnect.go b/pkg/infrared/protocol/login/clientbound_disconnect.go new file mode 100644 index 00000000..daeb0403 --- /dev/null +++ b/pkg/infrared/protocol/login/clientbound_disconnect.go @@ -0,0 +1,16 @@ +package login + +import "github.com/haveachin/infrared/pkg/infrared/protocol" + +const ClientBoundDisconnectID int32 = 0x00 + +type ClientBoundDisconnect struct { + Reason protocol.Chat +} + +func (pk ClientBoundDisconnect) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ClientBoundDisconnectID, + pk.Reason, + ) +} diff --git a/protocol/login/clientbound_disconnect_test.go b/pkg/infrared/protocol/login/clientbound_disconnect_test.go similarity index 65% rename from protocol/login/clientbound_disconnect_test.go rename to pkg/infrared/protocol/login/clientbound_disconnect_test.go index e118c2b7..c8a3608d 100644 --- a/protocol/login/clientbound_disconnect_test.go +++ b/pkg/infrared/protocol/login/clientbound_disconnect_test.go @@ -1,18 +1,20 @@ -package login +package login_test import ( "bytes" - "github.com/haveachin/infrared/protocol" "testing" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/login" ) func TestClientBoundDisconnect_Marshal(t *testing.T) { tt := []struct { - packet ClientBoundDisconnect + packet login.ClientBoundDisconnect marshaledPacket protocol.Packet }{ { - packet: ClientBoundDisconnect{ + packet: login.ClientBoundDisconnect{ Reason: protocol.Chat(""), }, marshaledPacket: protocol.Packet{ @@ -21,7 +23,7 @@ func TestClientBoundDisconnect_Marshal(t *testing.T) { }, }, { - packet: ClientBoundDisconnect{ + packet: login.ClientBoundDisconnect{ Reason: protocol.Chat("Hello, World!"), }, marshaledPacket: protocol.Packet{ @@ -31,10 +33,11 @@ func TestClientBoundDisconnect_Marshal(t *testing.T) { }, } + var pk protocol.Packet for _, tc := range tt { - pk := tc.packet.Marshal() + _ = tc.packet.Marshal(&pk) - if pk.ID != ClientBoundDisconnectPacketID { + if pk.ID != login.ClientBoundDisconnectID { t.Error("invalid packet id") } diff --git a/pkg/infrared/protocol/login/clientbound_encryptionrequest.go b/pkg/infrared/protocol/login/clientbound_encryptionrequest.go new file mode 100644 index 00000000..3c431ce1 --- /dev/null +++ b/pkg/infrared/protocol/login/clientbound_encryptionrequest.go @@ -0,0 +1,32 @@ +package login + +import "github.com/haveachin/infrared/pkg/infrared/protocol" + +const ClientBoundEncryptionRequestID int32 = 0x01 + +type ClientBoundEncryptionRequest struct { + ServerID protocol.String + PublicKey protocol.ByteArray + VerifyToken protocol.ByteArray +} + +func (pk ClientBoundEncryptionRequest) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ClientBoundEncryptionRequestID, + pk.ServerID, + pk.PublicKey, + pk.VerifyToken, + ) +} + +func (pk ClientBoundEncryptionRequest) Unmarshal(packet protocol.Packet) error { + if packet.ID != ClientBoundEncryptionRequestID { + return protocol.ErrInvalidPacketID + } + + return packet.Decode( + &pk.ServerID, + &pk.PublicKey, + &pk.VerifyToken, + ) +} diff --git a/pkg/infrared/protocol/login/serverbound_encryptionresponse.go b/pkg/infrared/protocol/login/serverbound_encryptionresponse.go new file mode 100644 index 00000000..96cf08a9 --- /dev/null +++ b/pkg/infrared/protocol/login/serverbound_encryptionresponse.go @@ -0,0 +1,29 @@ +package login + +import "github.com/haveachin/infrared/pkg/infrared/protocol" + +const ServerBoundEncryptionResponseID int32 = 0x01 + +type ServerBoundEncryptionResponse struct { + SharedSecret protocol.ByteArray + VerifyToken protocol.ByteArray +} + +func (pk ServerBoundEncryptionResponse) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ServerBoundEncryptionResponseID, + pk.SharedSecret, + pk.VerifyToken, + ) +} + +func (pk *ServerBoundEncryptionResponse) Unmarshal(packet protocol.Packet) error { + if packet.ID != ServerBoundEncryptionResponseID { + return protocol.ErrInvalidPacketID + } + + return packet.Decode( + &pk.SharedSecret, + &pk.VerifyToken, + ) +} diff --git a/pkg/infrared/protocol/login/serverbound_loginstart.go b/pkg/infrared/protocol/login/serverbound_loginstart.go new file mode 100644 index 00000000..2f9ad5a4 --- /dev/null +++ b/pkg/infrared/protocol/login/serverbound_loginstart.go @@ -0,0 +1,95 @@ +package login + +import ( + "bytes" + + "github.com/haveachin/infrared/pkg/infrared/protocol" +) + +const ServerBoundLoginStartID int32 = 0x00 + +type ServerBoundLoginStart struct { + Name protocol.String + + // Added in 1.19; removed in 1.19.3 + HasSignature protocol.Boolean + Timestamp protocol.Long + PublicKey protocol.ByteArray + Signature protocol.ByteArray + + // Added in 1.19 + HasPlayerUUID protocol.Boolean // removed in 1.20.2 + PlayerUUID protocol.UUID +} + +func (pk ServerBoundLoginStart) Marshal(packet *protocol.Packet, version protocol.Version) error { + fields := make([]protocol.FieldEncoder, 0, 7) + fields = append(fields, pk.Name) + + switch { + case version >= protocol.Version1_19 && + version < protocol.Version1_19_3: + fields = append(fields, pk.HasSignature) + if pk.HasSignature { + fields = append(fields, pk.Timestamp, pk.PublicKey, pk.Signature) + } + fallthrough + case version >= protocol.Version1_19_3 && + version < protocol.Version1_20_2: + fields = append(fields, pk.HasPlayerUUID) + if pk.HasPlayerUUID { + fields = append(fields, pk.PlayerUUID) + } + case version >= protocol.Version1_20_2: + fields = append(fields, pk.PlayerUUID) + } + + return packet.Encode( + ServerBoundLoginStartID, + fields..., + ) +} + +//nolint:gocognit +func (pk *ServerBoundLoginStart) Unmarshal(packet protocol.Packet, version protocol.Version) error { + if packet.ID != ServerBoundLoginStartID { + return protocol.ErrInvalidPacketID + } + + r := bytes.NewReader(packet.Data) + if err := protocol.ScanFields(r, &pk.Name); err != nil { + return err + } + + switch { + case version >= protocol.Version1_19 && + version < protocol.Version1_19_3: + if err := protocol.ScanFields(r, &pk.HasSignature); err != nil { + return err + } + + if pk.HasSignature { + if err := protocol.ScanFields(r, &pk.Timestamp, &pk.PublicKey, &pk.Signature); err != nil { + return err + } + } + fallthrough + case version >= protocol.Version1_19_3 && + version < protocol.Version1_20_2: + if err := protocol.ScanFields(r, &pk.HasPlayerUUID); err != nil { + return err + } + + if pk.HasPlayerUUID { + if err := protocol.ScanFields(r, &pk.PlayerUUID); err != nil { + return err + } + } + case version >= protocol.Version1_20_2: + if err := protocol.ScanFields(r, &pk.PlayerUUID); err != nil { + return err + } + } + + return nil +} diff --git a/protocol/login/serverbound_loginstart_test.go b/pkg/infrared/protocol/login/serverbound_loginstart_test.go similarity index 56% rename from protocol/login/serverbound_loginstart_test.go rename to pkg/infrared/protocol/login/serverbound_loginstart_test.go index 546f304b..afb55d3d 100644 --- a/protocol/login/serverbound_loginstart_test.go +++ b/pkg/infrared/protocol/login/serverbound_loginstart_test.go @@ -1,21 +1,25 @@ -package login +package login_test import ( - "github.com/haveachin/infrared/protocol" "testing" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/login" ) func TestUnmarshalServerBoundLoginStart(t *testing.T) { tt := []struct { packet protocol.Packet - unmarshalledPacket ServerLoginStart + version protocol.Version + unmarshalledPacket login.ServerBoundLoginStart }{ { packet: protocol.Packet{ ID: 0x00, Data: []byte{0x00}, }, - unmarshalledPacket: ServerLoginStart{ + version: protocol.Version1_18_2, + unmarshalledPacket: login.ServerBoundLoginStart{ Name: protocol.String(""), }, }, @@ -24,15 +28,16 @@ func TestUnmarshalServerBoundLoginStart(t *testing.T) { ID: 0x00, Data: []byte{0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}, }, - unmarshalledPacket: ServerLoginStart{ + version: protocol.Version1_18_2, + unmarshalledPacket: login.ServerBoundLoginStart{ Name: protocol.String("Hello, World!"), }, }, } + var loginStart login.ServerBoundLoginStart for _, tc := range tt { - loginStart, err := UnmarshalServerBoundLoginStart(tc.packet) - if err != nil { + if err := loginStart.Unmarshal(tc.packet, tc.version); err != nil { t.Error(err) } diff --git a/pkg/infrared/protocol/packet.go b/pkg/infrared/protocol/packet.go new file mode 100644 index 00000000..79f16cd7 --- /dev/null +++ b/pkg/infrared/protocol/packet.go @@ -0,0 +1,118 @@ +package protocol + +import ( + "bytes" + "fmt" + "io" +) + +const MaxDataLength = 0x200000 + +type Packet struct { + ID int32 + Data []byte +} + +func (pk Packet) Decode(fields ...FieldDecoder) error { + r := bytes.NewReader(pk.Data) + return ScanFields(r, fields...) +} + +func ScanFields(r io.Reader, fields ...FieldDecoder) error { + for i, v := range fields { + _, err := v.ReadFrom(r) + if err != nil { + return fmt.Errorf("scanning packet field[%d] error: %w", i, err) + } + } + return nil +} + +func (pk *Packet) Encode(id int32, fields ...FieldEncoder) error { + buf := bytes.NewBuffer(pk.Data[:0]) + for _, f := range fields { + if _, err := f.WriteTo(buf); err != nil { + return err + } + } + pk.ID = id + pk.Data = buf.Bytes() + return nil +} + +func (pk Packet) WriteTo(w io.Writer) (int64, error) { + pkLen := VarInt(VarInt(pk.ID).Len() + len(pk.Data)) + nLen, err := pkLen.WriteTo(w) + if err != nil { + return nLen, err + } + n := nLen + + nID, err := VarInt(pk.ID).WriteTo(w) + n += nID + if err != nil { + return n, err + } + + if len(pk.Data) > 0 { + nData, err := w.Write(pk.Data) + n += int64(nData) + if err != nil { + return n, err + } + } + + return n, err +} + +func (pk *Packet) ReadFrom(r io.Reader) (int64, error) { + var pkLen VarInt + n, err := pkLen.ReadFrom(r) + if err != nil { + return n, err + } + + var pkID VarInt + nID, err := pkID.ReadFrom(r) + n += nID + if err != nil { + return n, err + } + pk.ID = int32(pkID) + + lengthOfData := int(pkLen) - int(nID) + if lengthOfData < 0 || lengthOfData > MaxDataLength { + return n, fmt.Errorf("invalid packet data length of %d", lengthOfData) + } + + if cap(pk.Data) < lengthOfData { + pk.Data = make([]byte, lengthOfData) + } else { + pk.Data = pk.Data[:lengthOfData] + } + + nData, err := io.ReadFull(r, pk.Data) + n += int64(nData) + if err != nil { + return n, err + } + + return n, nil +} + +type Builder struct { + buf bytes.Buffer +} + +func (p *Builder) WriteField(fields ...FieldEncoder) { + for _, f := range fields { + _, err := f.WriteTo(&p.buf) + if err != nil { + panic(err) + } + } +} + +func (p *Builder) Packet(id int32) Packet { + return Packet{ID: id, Data: p.buf.Bytes()} +} diff --git a/pkg/infrared/protocol/peeker.go b/pkg/infrared/protocol/peeker.go new file mode 100644 index 00000000..ac253553 --- /dev/null +++ b/pkg/infrared/protocol/peeker.go @@ -0,0 +1,40 @@ +package protocol + +import "io" + +type PeekReader interface { + Peek(n int) ([]byte, error) + io.Reader +} + +type BytePeeker struct { + PeekReader + Cursor int +} + +func (p *BytePeeker) Read(b []byte) (int, error) { + buf, err := p.Peek(len(b) + p.Cursor) + if err != nil { + return 0, err + } + + for i := 0; i < len(b); i++ { + b[i] = buf[i+p.Cursor] + } + + p.Cursor += len(b) + + return len(b), nil +} + +func (p *BytePeeker) ReadByte() (byte, error) { + buf, err := p.Peek(1 + p.Cursor) + if err != nil { + return 0x00, err + } + + b := buf[p.Cursor] + p.Cursor++ + + return b, nil +} diff --git a/protocol/peeker_test.go b/pkg/infrared/protocol/peeker_test.go similarity index 79% rename from protocol/peeker_test.go rename to pkg/infrared/protocol/peeker_test.go index aeffd542..05e82d08 100644 --- a/protocol/peeker_test.go +++ b/pkg/infrared/protocol/peeker_test.go @@ -1,35 +1,38 @@ -package protocol +package protocol_test import ( "bufio" "bytes" + "errors" "io" "testing" + + "github.com/haveachin/infrared/pkg/infrared/protocol" ) func TestBytePeeker_ReadByte(t *testing.T) { tt := []struct { - peeker bytePeeker + peeker protocol.BytePeeker data []byte expectedByte byte }{ { - peeker: bytePeeker{ - cursor: 0, + peeker: protocol.BytePeeker{ + Cursor: 0, }, data: []byte{0x00, 0x01, 0x02, 0x03}, expectedByte: 0x00, }, { - peeker: bytePeeker{ - cursor: 1, + peeker: protocol.BytePeeker{ + Cursor: 1, }, data: []byte{0x00, 0x01, 0x02, 0x03}, expectedByte: 0x01, }, { - peeker: bytePeeker{ - cursor: 3, + peeker: protocol.BytePeeker{ + Cursor: 3, }, data: []byte{0x00, 0x01, 0x02, 0x03}, expectedByte: 0x03, @@ -44,7 +47,7 @@ func TestBytePeeker_ReadByte(t *testing.T) { // Act b, err := tc.peeker.ReadByte() - if err != nil && err != io.EOF { + if err != nil && errors.Is(err, io.EOF) { t.Error(err) } @@ -61,30 +64,30 @@ func TestBytePeeker_ReadByte(t *testing.T) { func TestBytePeeker_Read(t *testing.T) { tt := []struct { - peeker bytePeeker + peeker protocol.BytePeeker data []byte expectedData []byte expectedN int }{ { - peeker: bytePeeker{ - cursor: 0, + peeker: protocol.BytePeeker{ + Cursor: 0, }, data: []byte{0x00, 0x01, 0x02, 0x03}, expectedData: []byte{0x00, 0x01, 0x02, 0x03}, expectedN: 4, }, { - peeker: bytePeeker{ - cursor: 1, + peeker: protocol.BytePeeker{ + Cursor: 1, }, data: []byte{0x00, 0x01, 0x02, 0x03}, expectedData: []byte{0x01, 0x02, 0x03}, expectedN: 3, }, { - peeker: bytePeeker{ - cursor: 3, + peeker: protocol.BytePeeker{ + Cursor: 3, }, data: []byte{0x00, 0x01, 0x02, 0x03}, expectedData: []byte{0x03}, @@ -101,7 +104,7 @@ func TestBytePeeker_Read(t *testing.T) { // Act n, err := tc.peeker.Read(resultData) - if err != nil && err != io.EOF { + if err != nil && errors.Is(err, io.EOF) { t.Error(err) } diff --git a/pkg/infrared/protocol/play/clientbound_disconnect.go b/pkg/infrared/protocol/play/clientbound_disconnect.go new file mode 100644 index 00000000..210964b9 --- /dev/null +++ b/pkg/infrared/protocol/play/clientbound_disconnect.go @@ -0,0 +1,16 @@ +package play + +import "github.com/haveachin/infrared/pkg/infrared/protocol" + +const ClientBoundDisconnectID int32 = 0x17 + +type ClientBoundDisconnect struct { + Reason protocol.Chat +} + +func (pk ClientBoundDisconnect) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ClientBoundDisconnectID, + pk.Reason, + ) +} diff --git a/pkg/infrared/protocol/status/clientbound_response.go b/pkg/infrared/protocol/status/clientbound_response.go new file mode 100644 index 00000000..a3c223ce --- /dev/null +++ b/pkg/infrared/protocol/status/clientbound_response.go @@ -0,0 +1,98 @@ +package status + +import "github.com/haveachin/infrared/pkg/infrared/protocol" + +const ( + ClientBoundResponseID int32 = 0x00 +) + +type ClientBoundResponse struct { + JSONResponse protocol.String +} + +func (pk ClientBoundResponse) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ClientBoundResponseID, + pk.JSONResponse, + ) +} + +func (pk *ClientBoundResponse) Unmarshal(packet protocol.Packet) error { + if packet.ID != ClientBoundResponseID { + return protocol.ErrInvalidPacketID + } + + return packet.Decode( + &pk.JSONResponse, + ) +} + +type ResponseJSON struct { + Version VersionJSON `json:"version"` + Players PlayersJSON `json:"players"` + // This has to be any to support the new chat style system + Description any `json:"description"` + Favicon string `json:"favicon,omitempty"` + // Added since 1.19 + PreviewsChat bool `json:"previewsChat"` + // Added since 1.19.1 + EnforcesSecureChat bool `json:"enforcesSecureChat"` + // FMLModInfo should be set if the client is expecting a FML server + // to response. This is necessary for the client to recognise the + // server as a valid Forge server. + FMLModInfo *FMLModInfoJSON `json:"modinfo,omitempty"` + // FML2ForgeData should be set if the client is expecting a FML2 server + // to response. This is necessary for the client to recognise the + // server as a valid Forge server. + FML2ForgeData *FML2ForgeDataJSON `json:"forgeData,omitempty"` +} + +type VersionJSON struct { + Name string `json:"name"` + Protocol int `json:"protocol"` +} + +type PlayersJSON struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []PlayerSampleJSON `json:"sample,omitempty"` +} + +type PlayerSampleJSON struct { + Name string `json:"name"` + ID string `json:"id"` +} + +type DescriptionJSON struct { + Text string `json:"text"` +} + +// FMLModInfoJSON is a part of the FML Server List Ping. +type FMLModInfoJSON struct { + LoaderType string `json:"type"` + ModList []FMLModJSON `json:"modList"` +} + +type FMLModJSON struct { + ID string `json:"modid"` + Version string `json:"version"` +} + +// FML2ForgeDataJSON is a part of the FML2 Server List Ping. +type FML2ForgeDataJSON struct { + Channels []FML2ChannelsJSON `json:"channels"` + Mods []FML2ModJSON `json:"mods"` + FMLNetworkVersion int `json:"fmlNetworkVersion"` + D string `json:"d"` +} + +type FML2ChannelsJSON struct { + Res string `json:"res"` + Version string `json:"version"` + Required bool `json:"required"` +} + +type FML2ModJSON struct { + ID string `json:"modId"` + Marker string `json:"modmarker"` +} diff --git a/protocol/status/clientbound_response_test.go b/pkg/infrared/protocol/status/clientbound_response_test.go similarity index 69% rename from protocol/status/clientbound_response_test.go rename to pkg/infrared/protocol/status/clientbound_response_test.go index e824e9f7..6f2d7b87 100644 --- a/protocol/status/clientbound_response_test.go +++ b/pkg/infrared/protocol/status/clientbound_response_test.go @@ -1,18 +1,20 @@ -package status +package status_test import ( "bytes" - "github.com/haveachin/infrared/protocol" "testing" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/status" ) func TestClientBoundResponse_Marshal(t *testing.T) { tt := []struct { - packet ClientBoundResponse + packet status.ClientBoundResponse marshaledPacket protocol.Packet }{ { - packet: ClientBoundResponse{ + packet: status.ClientBoundResponse{ JSONResponse: protocol.String(""), }, marshaledPacket: protocol.Packet{ @@ -21,7 +23,7 @@ func TestClientBoundResponse_Marshal(t *testing.T) { }, }, { - packet: ClientBoundResponse{ + packet: status.ClientBoundResponse{ JSONResponse: protocol.String("Hello, World!"), }, marshaledPacket: protocol.Packet{ @@ -31,10 +33,11 @@ func TestClientBoundResponse_Marshal(t *testing.T) { }, } + var pk protocol.Packet for _, tc := range tt { - pk := tc.packet.Marshal() + _ = tc.packet.Marshal(&pk) - if pk.ID != ClientBoundResponsePacketID { + if pk.ID != status.ClientBoundResponseID { t.Error("invalid packet id") } @@ -47,14 +50,14 @@ func TestClientBoundResponse_Marshal(t *testing.T) { func TestUnmarshalClientBoundResponse(t *testing.T) { tt := []struct { packet protocol.Packet - unmarshalledPacket ClientBoundResponse + unmarshalledPacket status.ClientBoundResponse }{ { packet: protocol.Packet{ ID: 0x00, Data: []byte{0x00}, }, - unmarshalledPacket: ClientBoundResponse{ + unmarshalledPacket: status.ClientBoundResponse{ JSONResponse: "", }, }, @@ -63,15 +66,15 @@ func TestUnmarshalClientBoundResponse(t *testing.T) { ID: 0x00, Data: []byte{0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}, }, - unmarshalledPacket: ClientBoundResponse{ + unmarshalledPacket: status.ClientBoundResponse{ JSONResponse: protocol.String("Hello, World!"), }, }, } + var actual status.ClientBoundResponse for _, tc := range tt { - actual, err := UnmarshalClientBoundResponse(tc.packet) - if err != nil { + if err := actual.Unmarshal(tc.packet); err != nil { t.Error(err) } @@ -81,5 +84,4 @@ func TestUnmarshalClientBoundResponse(t *testing.T) { t.Errorf("got: %v, want: %v", actual, expected) } } - } diff --git a/pkg/infrared/protocol/status/serverbound_request.go b/pkg/infrared/protocol/status/serverbound_request.go new file mode 100644 index 00000000..3b5eb315 --- /dev/null +++ b/pkg/infrared/protocol/status/serverbound_request.go @@ -0,0 +1,13 @@ +package status + +import "github.com/haveachin/infrared/pkg/infrared/protocol" + +const ServerBoundRequestID int32 = 0x00 + +type ServerBoundRequest struct{} + +func (pk ServerBoundRequest) Marshal(packet *protocol.Packet) error { + return packet.Encode( + ServerBoundRequestID, + ) +} diff --git a/pkg/infrared/protocol/status/serverbound_request_test.go b/pkg/infrared/protocol/status/serverbound_request_test.go new file mode 100644 index 00000000..eb6851b8 --- /dev/null +++ b/pkg/infrared/protocol/status/serverbound_request_test.go @@ -0,0 +1,32 @@ +package status_test + +import ( + "testing" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/status" +) + +func TestServerBoundRequest_Marshal(t *testing.T) { + tt := []struct { + packet status.ServerBoundRequest + marshaledPacket protocol.Packet + }{ + { + packet: status.ServerBoundRequest{}, + marshaledPacket: protocol.Packet{ + ID: 0x00, + Data: []byte{}, + }, + }, + } + + var pk protocol.Packet + for _, tc := range tt { + _ = tc.packet.Marshal(&pk) + + if pk.ID != status.ServerBoundRequestID { + t.Error("invalid packet id") + } + } +} diff --git a/pkg/infrared/protocol/types.go b/pkg/infrared/protocol/types.go new file mode 100644 index 00000000..4c0eb6c7 --- /dev/null +++ b/pkg/infrared/protocol/types.go @@ -0,0 +1,265 @@ +package protocol + +import ( + "errors" + "io" + + "github.com/google/uuid" +) + +// A Field is both FieldEncoder and FieldDecoder. +type Field interface { + FieldEncoder + FieldDecoder +} + +// A FieldEncoder can be encoded as minecraft protocol used. +type FieldEncoder io.WriterTo + +// A FieldDecoder can Decode from minecraft protocol. +type FieldDecoder io.ReaderFrom + +type ( + // Boolean of True is encoded as 0x01, false as 0x00. + Boolean bool + // Byte is signed 8-bit integer, two's complement. + Byte int8 + // UnsignedShort is unsigned 16-bit integer. + UnsignedShort uint16 + // Long is signed 64-bit integer, two's complement. + Long int64 + // String is sequence of Unicode scalar values. + String string + + // Chat is encoded as a String with max length of 32767. + Chat = String + + // VarInt is variable-length data encoding a two's complement signed 32-bit integer. + VarInt int32 + + // UUID encoded as an unsigned 128-bit integer. + UUID uuid.UUID + + // ByteArray is []byte with prefix VarInt as length. + ByteArray []byte +) + +const ( + MaxVarIntLen = 5 + MaxVarLongLen = 10 +) + +func (b Boolean) WriteTo(w io.Writer) (int64, error) { + var v byte + if b { + v = 0x01 + } else { + v = 0x00 + } + nn, err := w.Write([]byte{v}) + return int64(nn), err +} + +func (b *Boolean) ReadFrom(r io.Reader) (int64, error) { + n, v, err := readByte(r) + if err != nil { + return n, err + } + + *b = v != 0 + return n, nil +} + +func (s String) WriteTo(w io.Writer) (int64, error) { + byteStr := []byte(s) + n1, err := VarInt(len(byteStr)).WriteTo(w) + if err != nil { + return n1, err + } + n2, err := w.Write(byteStr) + return n1 + int64(n2), err +} + +func (s *String) ReadFrom(r io.Reader) (int64, error) { + var l VarInt // String length + + nn, err := l.ReadFrom(r) + if err != nil { + return nn, err + } + n := nn + + bs := make([]byte, l) + if _, err := io.ReadFull(r, bs); err != nil { + return n, err + } + n += int64(l) + + *s = String(bs) + return n, nil +} + +// readByte read one byte from io.Reader. +func readByte(r io.Reader) (int64, byte, error) { + if r, ok := r.(io.ByteReader); ok { + v, err := r.ReadByte() + return 1, v, err + } + var v [1]byte + n, err := r.Read(v[:]) + return int64(n), v[0], err +} + +func (b Byte) WriteTo(w io.Writer) (int64, error) { + nn, err := w.Write([]byte{byte(b)}) + return int64(nn), err +} + +func (b *Byte) ReadFrom(r io.Reader) (int64, error) { + n, v, err := readByte(r) + if err != nil { + return n, err + } + *b = Byte(v) + return n, nil +} + +func (us UnsignedShort) WriteTo(w io.Writer) (int64, error) { + n := uint16(us) + byteLen := uint16(8) + nn, err := w.Write([]byte{byte(n >> byteLen), byte(n)}) + return int64(nn), err +} + +func (us *UnsignedShort) ReadFrom(r io.Reader) (int64, error) { + var bs [2]byte + nn, err := io.ReadFull(r, bs[:]) + if err != nil { + return int64(nn), err + } + n := int64(nn) + + *us = UnsignedShort(int16(bs[0])<<8 | int16(bs[1])) + return n, nil +} + +func (l Long) WriteTo(w io.Writer) (int64, error) { + n := uint64(l) + nn, err := w.Write([]byte{ + byte(n >> 56), byte(n >> 48), byte(n >> 40), byte(n >> 32), + byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n), + }) + return int64(nn), err +} + +func (l *Long) ReadFrom(r io.Reader) (int64, error) { + var bs [8]byte + nn, err := io.ReadFull(r, bs[:]) + if err != nil { + return int64(nn), err + } + n := int64(nn) + + *l = Long(int64(bs[0])<<56 | int64(bs[1])<<48 | int64(bs[2])<<40 | int64(bs[3])<<32 | + int64(bs[4])<<24 | int64(bs[5])<<16 | int64(bs[6])<<8 | int64(bs[7])) + return n, nil +} + +func (v VarInt) WriteTo(w io.Writer) (int64, error) { + var vi [MaxVarIntLen]byte + n := v.WriteToBytes(vi[:]) + n, err := w.Write(vi[:n]) + return int64(n), err +} + +// WriteToBytes encodes the VarInt into buf and returns the number of bytes written. +// If the buffer is too small, WriteToBytes will panic. +func (v VarInt) WriteToBytes(buf []byte) int { + num := uint32(v) + i := 0 + for { + b := num & 0x7F + num >>= 7 + if num != 0 { + b |= 0x80 + } + buf[i] = byte(b) + i++ + if num == 0 { + break + } + } + return i +} + +func (v *VarInt) ReadFrom(r io.Reader) (int64, error) { + var vi uint32 + var num, n int64 + for sec := byte(0x80); sec&0x80 != 0; num++ { + if num > MaxVarIntLen { + return 0, errors.New("VarInt is too big") + } + + var err error + n, sec, err = readByte(r) + if err != nil { + return n, err + } + + vi |= uint32(sec&0x7F) << uint32(7*num) + } + *v = VarInt(vi) + return n, nil +} + +// Len returns the number of bytes required to encode the VarInt. +func (v VarInt) Len() int { + switch { + case v < 0: + return MaxVarIntLen + case v < 1<<(7*1): + return 1 + case v < 1<<(7*2): + return 2 + case v < 1<<(7*3): + return 3 + case v < 1<<(7*4): + return 4 + default: + return 5 + } +} + +func (b ByteArray) WriteTo(w io.Writer) (int64, error) { + n1, err := VarInt(len(b)).WriteTo(w) + if err != nil { + return n1, err + } + n2, err := w.Write(b) + return n1 + int64(n2), err +} + +func (b *ByteArray) ReadFrom(r io.Reader) (int64, error) { + var length VarInt + n1, err := length.ReadFrom(r) + if err != nil { + return n1, err + } + if cap(*b) < int(length) { + *b = make(ByteArray, length) + } else { + *b = (*b)[:length] + } + n2, err := io.ReadFull(r, *b) + return n1 + int64(n2), err +} + +func (u UUID) WriteTo(w io.Writer) (int64, error) { + nn, err := w.Write(u[:]) + return int64(nn), err +} + +func (u *UUID) ReadFrom(r io.Reader) (int64, error) { + nn, err := io.ReadFull(r, (*u)[:]) + return int64(nn), err +} diff --git a/pkg/infrared/protocol/types_test.go b/pkg/infrared/protocol/types_test.go new file mode 100644 index 00000000..347efe89 --- /dev/null +++ b/pkg/infrared/protocol/types_test.go @@ -0,0 +1,174 @@ +package protocol_test + +import ( + "bytes" + "testing" + + "github.com/haveachin/infrared/pkg/infrared/protocol" +) + +var varInts = []protocol.VarInt{0, 1, 2, 127, 128, 255, 2147483647, -1, -2147483648} + +var packedVarInts = [][]byte{ + {0x00}, + {0x01}, + {0x02}, + {0x7f}, + {0x80, 0x01}, + {0xff, 0x01}, + {0xff, 0xff, 0xff, 0xff, 0x07}, + {0xff, 0xff, 0xff, 0xff, 0x0f}, + {0x80, 0x80, 0x80, 0x80, 0x08}, +} + +func TestVarInt_WriteTo(t *testing.T) { + var buf bytes.Buffer + for i, v := range varInts { + buf.Reset() + if n, err := v.WriteTo(&buf); err != nil { + t.Fatalf("Write to bytes.Buffer should never fail: %v", err) + } else if n != int64(buf.Len()) { + t.Errorf("Number of byte returned by WriteTo should equal to buffer.Len()") + } + if p := buf.Bytes(); !bytes.Equal(p, packedVarInts[i]) { + t.Errorf("pack int %d should be \"% x\", get \"% x\"", v, packedVarInts[i], p) + } + } +} + +func TestVarInt_ReadFrom(t *testing.T) { + for i, v := range packedVarInts { + var vi protocol.VarInt + if _, err := vi.ReadFrom(bytes.NewReader(v)); err != nil { + t.Errorf("unpack \"% x\" error: %v", v, err) + } + if vi != varInts[i] { + t.Errorf("unpack \"% x\" should be %d, get %d", v, varInts[i], vi) + } + } +} + +func TestVarInt_ReadFrom_tooLongData(t *testing.T) { + var vi protocol.VarInt + data := []byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01} + if _, err := vi.ReadFrom(bytes.NewReader(data)); err != nil { + t.Logf("unpack \"% x\" error: %v", data, err) + } else { + t.Errorf("unpack \"% x\" should be error, get %d", data, vi) + } +} + +func FuzzVarInt_Len(f *testing.F) { + for _, v := range varInts { + f.Add(int32(v)) + } + var buf bytes.Buffer + f.Fuzz(func(t *testing.T, v int32) { + defer buf.Reset() + if _, err := protocol.VarInt(v).WriteTo(&buf); err != nil { + t.Fatal(err) + } + if a, b := buf.Len(), protocol.VarInt(v).Len(); a != b { + t.Errorf("VarInt(%d) Length calculation error: calculated to be %d, actually %d", v, b, a) + } + }) +} + +func TestUnsignedShort_ReadFrom(t *testing.T) { + tt := []struct { + name string + expected protocol.UnsignedShort + bb []byte + }{ + { + name: "zero", + expected: 0, + bb: []byte{0x00, 0x00}, + }, + { + name: "255", + expected: 255, + bb: []byte{0x00, 0xff}, + }, + { + name: "65280", + expected: 65280, + bb: []byte{0xff, 0x00}, + }, + { + name: "3840", + expected: 3840, + bb: []byte{0x0f, 0x00}, + }, + { + name: "65535", + expected: 65535, + bb: []byte{0xff, 0xff}, + }, + } + + for _, tc := range tt { + var actual protocol.UnsignedShort + buf := bytes.NewBuffer(tc.bb) + t.Run(tc.name, func(t *testing.T) { + if n, err := actual.ReadFrom(buf); err != nil { + t.Error(err) + } else if n != 2 { + t.Errorf("n != 2") + } + + if actual != tc.expected { + t.Errorf("want %d; got %d", tc.expected, actual) + } + }) + } +} + +func TestUnsignedShort_WriteTo(t *testing.T) { + tt := []struct { + name string + us protocol.UnsignedShort + expected []byte + }{ + { + name: "zero", + us: 0, + expected: []byte{0x00, 0x00}, + }, + { + name: "255", + us: 255, + expected: []byte{0x00, 0xff}, + }, + { + name: "65280", + us: 65280, + expected: []byte{0xff, 0x00}, + }, + { + name: "3840", + us: 3840, + expected: []byte{0x0f, 0x00}, + }, + { + name: "65535", + us: 65535, + expected: []byte{0xff, 0xff}, + }, + } + + for _, tc := range tt { + var actual bytes.Buffer + t.Run(tc.name, func(t *testing.T) { + if n, err := tc.us.WriteTo(&actual); err != nil { + t.Error(err) + } else if n != 2 { + t.Errorf("n != 2") + } + + if !bytes.Equal(actual.Bytes(), tc.expected) { + t.Errorf("want %d; got %d", tc.expected, actual.Bytes()) + } + }) + } +} diff --git a/pkg/infrared/protocol/versions.go b/pkg/infrared/protocol/versions.go new file mode 100644 index 00000000..3a6f82f2 --- /dev/null +++ b/pkg/infrared/protocol/versions.go @@ -0,0 +1,31 @@ +package protocol + +import "strconv" + +type Version int32 + +const ( + Version1_18_2 Version = 758 + Version1_19 Version = 759 + Version1_19_3 Version = 761 + Version1_20_2 Version = 764 +) + +func (v Version) Name() string { + switch v { + case Version1_18_2: + return "1.18.2" + case Version1_19: + return "1.19" + case Version1_19_3: + return "1.19.3" + case Version1_20_2: + return "1.20.2" + default: + return strconv.Itoa(int(v)) + } +} + +func (v Version) ProtocolNumber() int32 { + return int32(v) +} diff --git a/pkg/infrared/rate_limiter.go b/pkg/infrared/rate_limiter.go new file mode 100644 index 00000000..e0402693 --- /dev/null +++ b/pkg/infrared/rate_limiter.go @@ -0,0 +1,245 @@ +package infrared + +import ( + "errors" + "math" + "net" + "strconv" + "strings" + "sync" + "time" + + "github.com/cespare/xxhash/v2" +) + +type RateLimiterConfig struct { + RequestLimit int `yaml:"requestLimit"` + WindowLength time.Duration `yaml:"windowLength"` +} + +func RateLimit(requestLimit int, windowLength time.Duration, options ...RateLimiterOption) Filterer { + return newRateLimiter(requestLimit, windowLength, options...).Filterer() +} + +func RateLimitByIP(requestLimit int, windowLength time.Duration) Filterer { + return RateLimit(requestLimit, windowLength, WithKeyFuncs(KeyByIP)) +} + +func KeyByIP(c net.Conn) string { + rAddr := c.RemoteAddr().String() + ip, _, err := net.SplitHostPort(rAddr) + if err != nil { + ip = rAddr + } + return canonicalizeIP(ip) +} + +func WithKeyFuncs(keyFuncs ...RateLimiterKeyFunc) RateLimiterOption { + return func(rl *rateLimiter) { + if len(keyFuncs) > 0 { + rl.keyFn = composedKeyFunc(keyFuncs...) + } + } +} + +func WithKeyByIP() RateLimiterOption { + return WithKeyFuncs(KeyByIP) +} + +func composedKeyFunc(keyFuncs ...RateLimiterKeyFunc) RateLimiterKeyFunc { + return func(c net.Conn) string { + var key strings.Builder + for i := 0; i < len(keyFuncs); i++ { + k := keyFuncs[i](c) + key.WriteString(k) + } + return key.String() + } +} + +type RateLimiterKeyFunc func(c net.Conn) string +type RateLimiterOption func(rl *rateLimiter) + +// canonicalizeIP returns a form of ip suitable for comparison to other IPs. +// For IPv4 addresses, this is simply the whole string. +// For IPv6 addresses, this is the /64 prefix. +// https://github.com/didip/tollbooth/blob/v6.1.2/libstring/libstring.go#L57-L102 +func canonicalizeIP(ip string) string { + isIPv6 := false + // This is how net.ParseIP decides if an address is IPv6 + // https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/net/ip.go;l=704 + for i := 0; !isIPv6 && i < len(ip); i++ { + switch ip[i] { + case '.': + // IPv4 + return ip + case ':': + // IPv6 + isIPv6 = true + } + } + if !isIPv6 { + // Not an IP address at all + return ip + } + + // By default, the string representation of a net.IPNet (masked IP address) is just + // "full_address/mask_bits". But using that will result in different addresses with + // the same /64 prefix comparing differently. So we need to zero out the last 64 bits + // so that all IPs in the same prefix will be the same. + // + // Note: When 1.18 is the minimum Go version, this can be written more cleanly like: + // netip.PrefixFrom(netip.MustParseAddr(ipv6), 64).Masked().Addr().String() + // (With appropriate error checking.) + + ipv6 := net.ParseIP(ip) + if ipv6 == nil { + return ip + } + + const bytesToZero = (128 - 64) / 8 + for i := len(ipv6) - bytesToZero; i < len(ipv6); i++ { + ipv6[i] = 0 + } + + // Note that this doesn't have the "/64" suffix customary with a CIDR representation, + // but those three bytes add nothing for us. + return ipv6.String() +} + +func newRateLimiter(requestLimit int, windowLength time.Duration, options ...RateLimiterOption) *rateLimiter { + rl := &rateLimiter{ + requestLimit: requestLimit, + windowLength: windowLength, + limitCounter: localCounter{ + counters: make(map[uint64]*count), + windowLength: windowLength, + }, + } + + for _, opt := range options { + opt(rl) + } + + if rl.keyFn == nil { + rl.keyFn = func(c net.Conn) string { + return "*" + } + } + + if rl.onRequestLimit == nil { + rl.onRequestLimit = func(c net.Conn) { + c.Close() + } + } + + return rl +} + +type rateLimiter struct { + requestLimit int + windowLength time.Duration + keyFn RateLimiterKeyFunc + limitCounter localCounter + onRequestLimit func(c net.Conn) +} + +func (r *rateLimiter) Status(key string) (bool, float64) { + t := time.Now().UTC() + currentWindow := t.Truncate(r.windowLength) + previousWindow := currentWindow.Add(-r.windowLength) + + currCount, prevCount := r.limitCounter.Get(key, currentWindow, previousWindow) + + diff := t.Sub(currentWindow) + rate := float64(prevCount)*(float64(r.windowLength)-float64(diff))/float64(r.windowLength) + float64(currCount) + return rate > float64(r.requestLimit), rate +} + +var ErrRateLimitReached = errors.New("rate limit reached") + +func (r *rateLimiter) Filterer() Filterer { + return FilterFunc(func(c net.Conn) error { + key := r.keyFn(c) + currentWindow := time.Now().UTC().Truncate(r.windowLength) + + _, rate := r.Status(key) + nrate := int(math.Round(rate)) + + if nrate >= r.requestLimit { + r.onRequestLimit(c) + return ErrRateLimitReached + } + + r.limitCounter.Inc(key, currentWindow) + return nil + }) +} + +type localCounter struct { + counters map[uint64]*count + windowLength time.Duration + lastEvict time.Time + mu sync.Mutex +} + +type count struct { + value int + updatedAt time.Time +} + +func (c *localCounter) Inc(key string, currentWindow time.Time) { + c.evict() + + c.mu.Lock() + defer c.mu.Unlock() + + hkey := limitCounterKey(key, currentWindow) + + v, ok := c.counters[hkey] + if !ok { + v = &count{} + c.counters[hkey] = v + } + v.value++ + v.updatedAt = time.Now() +} + +func (c *localCounter) Get(key string, currentWindow, previousWindow time.Time) (int, int) { + c.mu.Lock() + defer c.mu.Unlock() + + curr, ok := c.counters[limitCounterKey(key, currentWindow)] + if !ok { + curr = &count{value: 0, updatedAt: time.Now()} + } + prev, ok := c.counters[limitCounterKey(key, previousWindow)] + if !ok { + prev = &count{value: 0, updatedAt: time.Now()} + } + + return curr.value, prev.value +} + +func (c *localCounter) evict() { + c.mu.Lock() + defer c.mu.Unlock() + + if time.Since(c.lastEvict) < c.windowLength { + return + } + c.lastEvict = time.Now() + + for k, v := range c.counters { + if time.Since(v.updatedAt) >= c.windowLength { + delete(c.counters, k) + } + } +} + +func limitCounterKey(key string, window time.Time) uint64 { + h := xxhash.New() + _, _ = h.WriteString(key) + _, _ = h.WriteString(strconv.FormatInt(window.Unix(), 10)) + return h.Sum64() +} diff --git a/pkg/infrared/server.go b/pkg/infrared/server.go new file mode 100644 index 00000000..b7d7aaa3 --- /dev/null +++ b/pkg/infrared/server.go @@ -0,0 +1,315 @@ +package infrared + +import ( + "encoding/json" + "errors" + "net" + "strings" + "sync" + "time" + + "github.com/IGLOU-EU/go-wildcard" + "github.com/cespare/xxhash/v2" + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/status" +) + +type ( + ServerID string + ServerAddress string + ServerDomain string +) + +type ServerConfigFunc func(cfg *ServerConfig) + +func WithServerConfig(c ServerConfig) ServerConfigFunc { + return func(cfg *ServerConfig) { + *cfg = c + } +} + +func WithServerDomains(sd ...ServerDomain) ServerConfigFunc { + return func(cfg *ServerConfig) { + cfg.Domains = sd + } +} + +func WithServerAddresses(addr ...ServerAddress) ServerConfigFunc { + return func(cfg *ServerConfig) { + cfg.Addresses = addr + } +} + +type ServerConfig struct { + Domains []ServerDomain `yaml:"domains"` + Addresses []ServerAddress `yaml:"addresses"` + SendProxyProtocol bool `yaml:"sendProxyProtocol"` +} + +type Server struct { + cfg ServerConfig +} + +func NewServer(fns ...ServerConfigFunc) (*Server, error) { + var cfg ServerConfig + for _, fn := range fns { + fn(&cfg) + } + + if len(cfg.Addresses) == 0 { + return nil, errors.New("no addresses") + } + + return &Server{ + cfg: cfg, + }, nil +} + +func (s Server) Dial() (*Conn, error) { + c, err := net.Dial("tcp", string(s.cfg.Addresses[0])) + if err != nil { + return nil, err + } + + conn := newConn(c) + conn.timeout = time.Second * 10 + return conn, nil +} + +type ServerRequest struct { + Domain ServerDomain + IsLogin bool + ProtocolVersion protocol.Version + ReadPackets [2]protocol.Packet +} + +type ServerResponse struct { + ServerConn *Conn + StatusResponse protocol.Packet + SendProxyProtocol bool +} + +type ServerRequester interface { + RequestServer(ServerRequest) (ServerResponse, error) +} + +type ServerGateway struct { + responder ServerRequestResponder + servers map[ServerDomain]*Server +} + +func NewServerGateway(servers []*Server, responder ServerRequestResponder) (*ServerGateway, error) { + if len(servers) == 0 { + return nil, errors.New("server gateway: no servers to route to") + } + + srvs := make(map[ServerDomain]*Server) + for _, srv := range servers { + for _, d := range srv.cfg.Domains { + dStr := string(d) + dStr = strings.ToLower(dStr) + dmn := ServerDomain(dStr) + srvs[dmn] = srv + } + } + + if responder == nil { + responder = DialServerResponder{ + respProvs: make(map[*Server]StatusResponseProvider), + } + } + + return &ServerGateway{ + servers: srvs, + responder: responder, + }, nil +} + +func (sg *ServerGateway) findServer(domain ServerDomain) *Server { + dm := string(domain) + dm = strings.ToLower(dm) + for d, srv := range sg.servers { + if wildcard.Match(string(d), dm) { + return srv + } + } + return nil +} + +func (sg *ServerGateway) RequestServer(req ServerRequest) (ServerResponse, error) { + srv := sg.findServer(req.Domain) + if srv == nil { + return ServerResponse{}, errors.New("server not found") + } + + return sg.responder.RespondeToServerRequest(req, srv) +} + +type ServerRequestResponder interface { + RespondeToServerRequest(ServerRequest, *Server) (ServerResponse, error) +} + +type DialServerResponder struct { + respProvs map[*Server]StatusResponseProvider +} + +func (r DialServerResponder) RespondeToServerRequest(req ServerRequest, srv *Server) (ServerResponse, error) { + if req.IsLogin { + return r.respondeToLoginRequest(req, srv) + } + + return r.respondeToStatusRequest(req, srv) +} + +func (r DialServerResponder) respondeToLoginRequest(_ ServerRequest, srv *Server) (ServerResponse, error) { + rc, err := srv.Dial() + if err != nil { + return ServerResponse{}, err + } + + return ServerResponse{ + ServerConn: rc, + SendProxyProtocol: srv.cfg.SendProxyProtocol, + }, nil +} + +func (r DialServerResponder) respondeToStatusRequest(req ServerRequest, srv *Server) (ServerResponse, error) { + respProv, ok := r.respProvs[srv] + if !ok { + respProv = &statusResponseProvider{ + server: srv, + cacheTTL: 30 * time.Second, + statusHash: make(map[protocol.Version]uint64), + statusResponseCache: make(map[uint64]*statusCacheEntry), + } + r.respProvs[srv] = respProv + } + + _, pk, err := respProv.StatusResponse(req.ProtocolVersion, req.ReadPackets) + if err != nil { + return ServerResponse{}, err + } + + return ServerResponse{ + StatusResponse: pk, + }, nil +} + +type StatusResponseProvider interface { + StatusResponse(protocol.Version, [2]protocol.Packet) (status.ResponseJSON, protocol.Packet, error) +} + +type statusCacheEntry struct { + expiresAt time.Time + responseJSON status.ResponseJSON + responsePk protocol.Packet +} + +func (e statusCacheEntry) isExpired() bool { + return e.expiresAt.Before(time.Now()) +} + +type statusResponseProvider struct { + server *Server + + mu sync.Mutex + cacheTTL time.Duration + statusHash map[protocol.Version]uint64 + statusResponseCache map[uint64]*statusCacheEntry +} + +func (s *statusResponseProvider) requestNewStatusResponseJSON( + readPks [2]protocol.Packet, +) (status.ResponseJSON, protocol.Packet, error) { + rc, err := s.server.Dial() + if err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + + if err := rc.WritePackets(readPks[0], readPks[1]); err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + + var pk protocol.Packet + if err := rc.ReadPacket(&pk); err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + rc.Close() + + var respPk status.ClientBoundResponse + if err := respPk.Unmarshal(pk); err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + + var respJSON status.ResponseJSON + if err := json.Unmarshal([]byte(respPk.JSONResponse), &respJSON); err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + + return respJSON, pk, nil +} + +func (s *statusResponseProvider) StatusResponse( + protVer protocol.Version, + readPks [2]protocol.Packet, +) (status.ResponseJSON, protocol.Packet, error) { + if s.cacheTTL <= 0 { + return s.requestNewStatusResponseJSON(readPks) + } + + // Prunes all expired status reponses + s.prune() + + s.mu.Lock() + defer s.mu.Unlock() + + hash, okHash := s.statusHash[protVer] + entry, okCache := s.statusResponseCache[hash] + if !okHash || !okCache { + return s.cacheResponse(protVer, readPks) + } + + return entry.responseJSON, entry.responsePk, nil +} + +func (s *statusResponseProvider) cacheResponse( + protVer protocol.Version, + readPks [2]protocol.Packet, +) (status.ResponseJSON, protocol.Packet, error) { + newStatusResp, pk, err := s.requestNewStatusResponseJSON(readPks) + if err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + + hash := xxhash.New().Sum64() + s.statusHash[protVer] = hash + + s.statusResponseCache[hash] = &statusCacheEntry{ + expiresAt: time.Now().Add(s.cacheTTL), + responseJSON: newStatusResp, + responsePk: pk, + } + + return newStatusResp, pk, nil +} + +func (s *statusResponseProvider) prune() { + s.mu.Lock() + defer s.mu.Unlock() + + expiredHashes := []uint64{} + for hash, entry := range s.statusResponseCache { + if entry.isExpired() { + delete(s.statusResponseCache, hash) + expiredHashes = append(expiredHashes, hash) + } + } + + for protVer, hash := range s.statusHash { + for _, expiredHash := range expiredHashes { + if hash == expiredHash { + delete(s.statusHash, protVer) + } + } + } +} diff --git a/process/docker.go b/process/docker.go deleted file mode 100644 index c2ed8593..00000000 --- a/process/docker.go +++ /dev/null @@ -1,90 +0,0 @@ -package process - -import ( - "fmt" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type docker struct { - client *client.Client - containerName string -} - -// NewDocker create a new docker process that manages a container -func NewDocker(containerName string) (Process, error) { - cli, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - return nil, err - } - - return docker{ - client: cli, - containerName: fmt.Sprintf("/%s", containerName), - }, nil -} - -func (proc docker) Start() error { - containerID, err := proc.resolveContainerName() - if err != nil { - return err - } - - ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) - defer cancel() - - return proc.client.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) -} - -func (proc docker) Stop() error { - containerID, err := proc.resolveContainerName() - if err != nil { - return err - } - - ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) - defer cancel() - - return proc.client.ContainerStop(ctx, containerID, nil) -} - -func (proc docker) IsRunning() (bool, error) { - containerID, err := proc.resolveContainerName() - if err != nil { - return false, err - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - info, err := proc.client.ContainerInspect(ctx, containerID) - if err != nil { - return false, err - } - - return info.State.Running, nil -} - -func (proc docker) resolveContainerName() (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) - defer cancel() - - containers, err := proc.client.ContainerList(ctx, types.ContainerListOptions{All: true}) - if err != nil { - return "", err - } - - for _, container := range containers { - for _, name := range container.Names { - if name != proc.containerName { - continue - } - return container.ID, nil - } - } - - return "", fmt.Errorf("container with name \"%s\" not found", proc.containerName) -} diff --git a/process/portainer.go b/process/portainer.go deleted file mode 100644 index 6f46dd58..00000000 --- a/process/portainer.go +++ /dev/null @@ -1,148 +0,0 @@ -package process - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "github.com/docker/docker/client" - "github.com/docker/docker/errdefs" - "io/ioutil" - "net/http" -) - -const ( - contentType = "application/json" - authenticationEndpoint = "http://%s/api/auth" - dockerEndpoint = "tcp://%s/api/endpoints/%s/docker" -) - -type portainer struct { - docker docker - address string - username string - password string - header map[string]string -} - -// NewPortainer creates a new portainer process that manages a docker container -func NewPortainer(containerName, address, endpointID, username, password string) (Process, error) { - baseURL := fmt.Sprintf(dockerEndpoint, address, endpointID) - header := map[string]string{} - cli, err := client.NewClientWithOpts( - client.WithHost(baseURL), - client.WithScheme("http"), - client.WithAPIVersionNegotiation(), - client.WithHTTPHeaders(header), - ) - if err != nil { - return nil, err - } - - return portainer{ - docker: docker{ - client: cli, - containerName: "/" + containerName, - }, - address: address, - username: username, - password: password, - header: header, - }, nil -} - -func (portainer portainer) Start() error { - err := portainer.docker.Start() - if err == nil { - return nil - } - - if !isUnauthorized(err) { - return err - } - - if err := portainer.authenticate(); err != nil { - return fmt.Errorf("could not authorize; %s", err) - } - - return portainer.docker.Start() -} - -func (portainer portainer) Stop() error { - err := portainer.docker.Stop() - if err == nil { - return nil - } - - if !isUnauthorized(err) { - return err - } - - if err := portainer.authenticate(); err != nil { - return fmt.Errorf("could not authorize; %s", err) - } - - return portainer.docker.Stop() -} - -func (portainer portainer) IsRunning() (bool, error) { - isRunning, err := portainer.docker.IsRunning() - if err == nil { - return isRunning, nil - } - - if !isUnauthorized(err) { - return false, err - } - - if err := portainer.authenticate(); err != nil { - return false, fmt.Errorf("could not authorize; %s", err) - } - - return portainer.docker.IsRunning() -} - -func isUnauthorized(err error) bool { - return errdefs.GetHTTPErrorStatusCode(err) == http.StatusUnauthorized -} - -func (portainer *portainer) authenticate() error { - var credentials = struct { - Username string `json:"Username"` - Password string `json:"Password"` - }{ - Username: portainer.username, - Password: portainer.password, - } - - bodyJSON, err := json.Marshal(credentials) - if err != nil { - return err - } - - url := fmt.Sprintf(authenticationEndpoint, portainer.address) - response, err := http.Post(url, contentType, bytes.NewBuffer(bodyJSON)) - if err != nil { - return err - } - - if response.StatusCode != http.StatusOK { - return errors.New(http.StatusText(response.StatusCode)) - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - var jwtResponse = struct { - JWT string `json:"jwt"` - }{} - - if err := json.Unmarshal(data, &jwtResponse); err != nil { - return err - } - - portainer.header["Authorization"] = fmt.Sprintf("Bearer %s", jwtResponse.JWT) - return nil -} diff --git a/process/process.go b/process/process.go deleted file mode 100644 index 14ca6d75..00000000 --- a/process/process.go +++ /dev/null @@ -1,14 +0,0 @@ -package process - -import ( - "time" -) - -const contextTimeout = 10 * time.Second - -// Process is an arbitrary process that can be started or stopped -type Process interface { - Start() error - Stop() error - IsRunning() (bool, error) -} diff --git a/protocol/handshaking/serverbound_handshake.go b/protocol/handshaking/serverbound_handshake.go deleted file mode 100644 index f78935f0..00000000 --- a/protocol/handshaking/serverbound_handshake.go +++ /dev/null @@ -1,99 +0,0 @@ -package handshaking - -import ( - "fmt" - "github.com/haveachin/infrared/protocol" - "net" - "strings" - "time" -) - -const ( - ServerBoundHandshakePacketID byte = 0x00 - - ServerBoundHandshakeStatusState = protocol.Byte(1) - ServerBoundHandshakeLoginState = protocol.Byte(2) - - ForgeSeparator = "\x00" - RealIPSeparator = "///" -) - -type ServerBoundHandshake struct { - ProtocolVersion protocol.VarInt - ServerAddress protocol.String - ServerPort protocol.UnsignedShort - NextState protocol.Byte -} - -func (pk ServerBoundHandshake) Marshal() protocol.Packet { - return protocol.MarshalPacket( - ServerBoundHandshakePacketID, - pk.ProtocolVersion, - pk.ServerAddress, - pk.ServerPort, - pk.NextState, - ) -} - -func UnmarshalServerBoundHandshake(packet protocol.Packet) (ServerBoundHandshake, error) { - var pk ServerBoundHandshake - - if packet.ID != ServerBoundHandshakePacketID { - return pk, protocol.ErrInvalidPacketID - } - - if err := packet.Scan( - &pk.ProtocolVersion, - &pk.ServerAddress, - &pk.ServerPort, - &pk.NextState, - ); err != nil { - return pk, err - } - - return pk, nil -} - -func (pk ServerBoundHandshake) IsStatusRequest() bool { - return pk.NextState == ServerBoundHandshakeStatusState -} - -func (pk ServerBoundHandshake) IsLoginRequest() bool { - return pk.NextState == ServerBoundHandshakeLoginState -} - -func (pk ServerBoundHandshake) IsForgeAddress() bool { - addr := string(pk.ServerAddress) - return len(strings.Split(addr, ForgeSeparator)) > 1 -} - -func (pk ServerBoundHandshake) IsRealIPAddress() bool { - addr := string(pk.ServerAddress) - return len(strings.Split(addr, RealIPSeparator)) > 1 -} - -func (pk ServerBoundHandshake) ParseServerAddress() string { - addr := string(pk.ServerAddress) - addr = strings.Split(addr, ForgeSeparator)[0] - addr = strings.Split(addr, RealIPSeparator)[0] - // Resolves an issue with some proxies - addr = strings.Trim(addr, ".") - return addr -} - -func (pk *ServerBoundHandshake) UpgradeToRealIP(clientAddr net.Addr, timestamp time.Time) { - if pk.IsRealIPAddress() { - return - } - - addr := string(pk.ServerAddress) - addrWithForge := strings.SplitN(addr, ForgeSeparator, 3) - - addr = fmt.Sprintf("%s///%s///%d", addrWithForge[0], clientAddr.String(), timestamp.Unix()) - - if len(addrWithForge) > 1 { - addr = fmt.Sprintf("%s\x00%s\x00", addr, addrWithForge[1]) - } - - pk.ServerAddress = protocol.String(addr) -} diff --git a/protocol/login/clientbound_disconnect.go b/protocol/login/clientbound_disconnect.go deleted file mode 100644 index 9d75e0df..00000000 --- a/protocol/login/clientbound_disconnect.go +++ /dev/null @@ -1,18 +0,0 @@ -package login - -import ( - "github.com/haveachin/infrared/protocol" -) - -const ClientBoundDisconnectPacketID byte = 0x00 - -type ClientBoundDisconnect struct { - Reason protocol.Chat -} - -func (pk ClientBoundDisconnect) Marshal() protocol.Packet { - return protocol.MarshalPacket( - ClientBoundDisconnectPacketID, - pk.Reason, - ) -} diff --git a/protocol/login/serverbound_loginstart.go b/protocol/login/serverbound_loginstart.go deleted file mode 100644 index eaafdc3a..00000000 --- a/protocol/login/serverbound_loginstart.go +++ /dev/null @@ -1,25 +0,0 @@ -package login - -import ( - "github.com/haveachin/infrared/protocol" -) - -const ServerBoundLoginStartPacketID byte = 0x00 - -type ServerLoginStart struct { - Name protocol.String -} - -func UnmarshalServerBoundLoginStart(packet protocol.Packet) (ServerLoginStart, error) { - var pk ServerLoginStart - - if packet.ID != ServerBoundLoginStartPacketID { - return pk, protocol.ErrInvalidPacketID - } - - if err := packet.Scan(&pk.Name); err != nil { - return pk, err - } - - return pk, nil -} diff --git a/protocol/packet.go b/protocol/packet.go deleted file mode 100644 index 6c39e8f9..00000000 --- a/protocol/packet.go +++ /dev/null @@ -1,93 +0,0 @@ -package protocol - -import ( - "bytes" - "fmt" - "io" -) - -// Packet is the raw representation of message that is send between the client and the server -type Packet struct { - ID byte - Data []byte -} - -// Scan decodes and copies the Packet data into the fields -func (pk Packet) Scan(fields ...FieldDecoder) error { - return ScanFields(bytes.NewReader(pk.Data), fields...) -} - -// Marshal encodes the packet and all it's fields -func (pk *Packet) Marshal() ([]byte, error) { - var packedData []byte - data := []byte{pk.ID} - data = append(data, pk.Data...) - - packedData = append(packedData, VarInt(int32(len(data))).Encode()...) - - return append(packedData, data...), nil -} - -// ScanFields decodes a byte stream into fields -func ScanFields(r DecodeReader, fields ...FieldDecoder) error { - for _, field := range fields { - if err := field.Decode(r); err != nil { - return err - } - } - return nil -} - -// MarshalPacket transforms an ID and Fields into a Packet -func MarshalPacket(ID byte, fields ...FieldEncoder) Packet { - var pkt Packet - pkt.ID = ID - - for _, v := range fields { - pkt.Data = append(pkt.Data, v.Encode()...) - } - - return pkt -} - -// ReadPacketBytes decodes a byte stream and cuts the first Packet as a byte array out -func ReadPacketBytes(r DecodeReader) ([]byte, error) { - var packetLength VarInt - if err := packetLength.Decode(r); err != nil { - return nil, err - } - - if packetLength < 1 { - return nil, fmt.Errorf("packet length too short") - } - - data := make([]byte, packetLength) - if _, err := io.ReadFull(r, data); err != nil { - return nil, fmt.Errorf("reading the content of the packet failed: %v", err) - } - - return data, nil -} - -// ReadPacket decodes and decompresses a byte stream and cuts the first Packet out -func ReadPacket(r DecodeReader) (Packet, error) { - data, err := ReadPacketBytes(r) - if err != nil { - return Packet{}, err - } - - return Packet{ - ID: data[0], - Data: data[1:], - }, nil -} - -// PeekPacket decodes and decompresses a byte stream and peeks the first Packet -func PeekPacket(p PeekReader) (Packet, error) { - r := bytePeeker{ - PeekReader: p, - cursor: 0, - } - - return ReadPacket(&r) -} diff --git a/protocol/packet_test.go b/protocol/packet_test.go deleted file mode 100644 index a506a681..00000000 --- a/protocol/packet_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package protocol - -import ( - "bufio" - "bytes" - "testing" -) - -func TestPacket_Marshal(t *testing.T) { - tt := []struct { - packet Packet - expected []byte - }{ - { - packet: Packet{ - ID: 0x00, - Data: []byte{0x00, 0xf2}, - }, - expected: []byte{0x03, 0x00, 0x00, 0xf2}, - }, - { - packet: Packet{ - ID: 0x0f, - Data: []byte{0x00, 0xf2, 0x03, 0x50}, - }, - expected: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, - }, - } - - for _, tc := range tt { - actual, err := tc.packet.Marshal() - if err != nil { - t.Error(err) - } - - if !bytes.Equal(actual, tc.expected) { - t.Errorf("got: %v; want: %v", actual, tc.expected) - } - } -} - -func TestPacket_Scan(t *testing.T) { - // Arrange - packet := Packet{ - ID: 0x00, - Data: []byte{0x00, 0xf2}, - } - - var booleanField Boolean - var byteField Byte - - // Act - err := packet.Scan( - &booleanField, - &byteField, - ) - - // Assert - if err != nil { - t.Error(err) - } - - if booleanField != false { - t.Error("got: true; want: false") - } - - if !bytes.Equal(byteField.Encode(), []byte{0xf2}) { - t.Errorf("got: %x; want: %x", byteField.Encode(), 0xf2) - } -} - -func TestScanFields(t *testing.T) { - // Arrange - packet := Packet{ - ID: 0x00, - Data: []byte{0x00, 0xf2}, - } - - var booleanField Boolean - var byteField Byte - - // Act - err := ScanFields( - bytes.NewReader(packet.Data), - &booleanField, - &byteField, - ) - - // Assert - if err != nil { - t.Error(err) - } - - if booleanField != false { - t.Error("got: true; want: false") - } - - if !bytes.Equal(byteField.Encode(), []byte{0xf2}) { - t.Errorf("got: %x; want: %x", byteField.Encode(), 0xf2) - } -} - -func TestMarshalPacket(t *testing.T) { - // Arrange - packetId := byte(0x00) - booleanField := Boolean(false) - byteField := Byte(0x0f) - packetData := []byte{0x00, 0x0f} - - // Act - packet := MarshalPacket(packetId, booleanField, byteField) - - // Assert - if packet.ID != packetId { - t.Errorf("packet id: got: %v; want: %v", packet.ID, packetId) - } - - if !bytes.Equal(packet.Data, packetData) { - t.Errorf("got: %v; want: %v", packet.Data, packetData) - } -} - -func TestReadPacketBytes(t *testing.T) { - tt := []struct { - data []byte - packetBytes []byte - }{ - { - data: []byte{0x03, 0x00, 0x00, 0xf2, 0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, - packetBytes: []byte{0x00, 0x00, 0xf2}, - }, - { - data: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50, 0x30, 0x01, 0xef, 0xaa}, - packetBytes: []byte{0x0f, 0x00, 0xf2, 0x03, 0x50}, - }, - } - - for _, tc := range tt { - readBytes, err := ReadPacketBytes(bytes.NewReader(tc.data)) - if err != nil { - t.Error(err) - } - - if !bytes.Equal(readBytes, tc.packetBytes) { - t.Errorf("got: %v; want: %v", readBytes, tc.packetBytes) - } - } -} - -func TestReadPacket(t *testing.T) { - tt := []struct { - data []byte - packet Packet - dataAfterRead []byte - }{ - { - data: []byte{0x03, 0x00, 0x00, 0xf2, 0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, - packet: Packet{ - ID: 0x00, - Data: []byte{0x00, 0xf2}, - }, - dataAfterRead: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, - }, - { - data: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50, 0x30, 0x01, 0xef, 0xaa}, - packet: Packet{ - ID: 0x0f, - Data: []byte{0x00, 0xf2, 0x03, 0x50}, - }, - dataAfterRead: []byte{0x30, 0x01, 0xef, 0xaa}, - }, - } - - for _, tc := range tt { - buf := bytes.NewBuffer(tc.data) - pk, err := ReadPacket(buf) - if err != nil { - t.Error(err) - } - - if pk.ID != tc.packet.ID { - t.Errorf("packet ID: got: %v; want: %v", pk.ID, tc.packet.ID) - } - - if !bytes.Equal(pk.Data, tc.packet.Data) { - t.Errorf("packet data: got: %v; want: %v", pk.Data, tc.packet.Data) - } - - if !bytes.Equal(buf.Bytes(), tc.dataAfterRead) { - t.Errorf("data after read: got: %v; want: %v", tc.data, tc.dataAfterRead) - } - } -} - -func TestPeekPacket(t *testing.T) { - tt := []struct { - data []byte - packet Packet - }{ - { - data: []byte{0x03, 0x00, 0x00, 0xf2, 0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50}, - packet: Packet{ - ID: 0x00, - Data: []byte{0x00, 0xf2}, - }, - }, - { - data: []byte{0x05, 0x0f, 0x00, 0xf2, 0x03, 0x50, 0x30, 0x01, 0xef, 0xaa}, - packet: Packet{ - ID: 0x0f, - Data: []byte{0x00, 0xf2, 0x03, 0x50}, - }, - }, - } - - for _, tc := range tt { - dataCopy := make([]byte, len(tc.data)) - copy(dataCopy, tc.data) - - pk, err := PeekPacket(bufio.NewReader(bytes.NewReader(dataCopy))) - if err != nil { - t.Error(err) - } - - if pk.ID != tc.packet.ID { - t.Errorf("packet ID: got: %v; want: %v", pk.ID, tc.packet.ID) - } - - if !bytes.Equal(pk.Data, tc.packet.Data) { - t.Errorf("packet data: got: %v; want: %v", pk.Data, tc.packet.Data) - } - - if !bytes.Equal(dataCopy, tc.data) { - t.Errorf("data after read: got: %v; want: %v", dataCopy, tc.data) - } - } -} diff --git a/protocol/peeker.go b/protocol/peeker.go deleted file mode 100644 index 62760c10..00000000 --- a/protocol/peeker.go +++ /dev/null @@ -1,40 +0,0 @@ -package protocol - -import "io" - -type PeekReader interface { - Peek(n int) ([]byte, error) - io.Reader -} - -type bytePeeker struct { - PeekReader - cursor int -} - -func (peeker *bytePeeker) Read(b []byte) (int, error) { - buf, err := peeker.Peek(len(b) + peeker.cursor) - if err != nil { - return 0, err - } - - for i := 0; i < len(b); i++ { - b[i] = buf[i+peeker.cursor] - } - - peeker.cursor += len(b) - - return len(b), nil -} - -func (peeker *bytePeeker) ReadByte() (byte, error) { - buf, err := peeker.Peek(1 + peeker.cursor) - if err != nil { - return 0x00, err - } - - b := buf[peeker.cursor] - peeker.cursor++ - - return b, nil -} diff --git a/protocol/status/clientbound_response.go b/protocol/status/clientbound_response.go deleted file mode 100644 index 588eff0d..00000000 --- a/protocol/status/clientbound_response.go +++ /dev/null @@ -1,61 +0,0 @@ -package status - -import ( - "github.com/haveachin/infrared/protocol" -) - -const ClientBoundResponsePacketID byte = 0x00 - -type ClientBoundResponse struct { - JSONResponse protocol.String -} - -func (pk ClientBoundResponse) Marshal() protocol.Packet { - return protocol.MarshalPacket( - ClientBoundResponsePacketID, - pk.JSONResponse, - ) -} - -func UnmarshalClientBoundResponse(packet protocol.Packet) (ClientBoundResponse, error) { - var pk ClientBoundResponse - - if packet.ID != ClientBoundResponsePacketID { - return pk, protocol.ErrInvalidPacketID - } - - if err := packet.Scan( - &pk.JSONResponse, - ); err != nil { - return pk, err - } - - return pk, nil -} - -type ResponseJSON struct { - Version VersionJSON `json:"version"` - Players PlayersJSON `json:"players"` - Description DescriptionJSON `json:"description"` - Favicon string `json:"favicon"` -} - -type VersionJSON struct { - Name string `json:"name"` - Protocol int `json:"protocol"` -} - -type PlayersJSON struct { - Max int `json:"max"` - Online int `json:"online"` - Sample []PlayerSampleJSON `json:"sample"` -} - -type PlayerSampleJSON struct { - Name string `json:"name"` - ID string `json:"id"` -} - -type DescriptionJSON struct { - Text string `json:"text"` -} diff --git a/protocol/status/serverbound_request.go b/protocol/status/serverbound_request.go deleted file mode 100644 index 54b0c2c7..00000000 --- a/protocol/status/serverbound_request.go +++ /dev/null @@ -1,15 +0,0 @@ -package status - -import ( - "github.com/haveachin/infrared/protocol" -) - -const ServerBoundRequestPacketID byte = 0x00 - -type ServerBoundRequest struct{} - -func (pk ServerBoundRequest) Marshal() protocol.Packet { - return protocol.MarshalPacket( - ServerBoundRequestPacketID, - ) -} diff --git a/protocol/status/serverbound_request_test.go b/protocol/status/serverbound_request_test.go deleted file mode 100644 index 274ea487..00000000 --- a/protocol/status/serverbound_request_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package status - -import ( - "github.com/haveachin/infrared/protocol" - "testing" -) - -func TestServerBoundRequest_Marshal(t *testing.T) { - tt := []struct { - packet ServerBoundRequest - marshaledPacket protocol.Packet - }{ - { - packet: ServerBoundRequest{}, - marshaledPacket: protocol.Packet{ - ID: 0x00, - Data: []byte{}, - }, - }, - } - - for _, tc := range tt { - pk := tc.packet.Marshal() - - if pk.ID != ServerBoundRequestPacketID { - t.Error("invalid packet id") - } - } -} diff --git a/protocol/types.go b/protocol/types.go deleted file mode 100644 index 2f3066ac..00000000 --- a/protocol/types.go +++ /dev/null @@ -1,256 +0,0 @@ -package protocol - -import ( - "bytes" - "errors" - "github.com/gofrs/uuid" - "io" -) - -// A Field is both FieldEncoder and FieldDecoder -type Field interface { - FieldEncoder - FieldDecoder -} - -// A FieldEncoder can be encode as minecraft protocol used. -type FieldEncoder interface { - Encode() []byte -} - -// A FieldDecoder can Decode from minecraft protocol -type FieldDecoder interface { - Decode(r DecodeReader) error -} - -//DecodeReader is both io.Reader and io.ByteReader -type DecodeReader interface { - io.ByteReader - io.Reader -} - -type ( - // Boolean of True is encoded as 0x01, false as 0x00. - Boolean bool - // Byte is signed 8-bit integer, two's complement - Byte int8 - // UnsignedShort is unsigned 16-bit integer - UnsignedShort uint16 - // Long is signed 64-bit integer, two's complement - Long int64 - // String is sequence of Unicode scalar values - String string - - // Chat is encoded as a String with max length of 32767. - Chat = String - // Identifier is encoded as a String with max length of 32767. - Identifier = String - - // VarInt is variable-length data encoding a two's complement signed 32-bit integer - VarInt int32 - - // UUID encoded as an unsigned 128-bit integer - UUID uuid.UUID - - // ByteArray is []byte with prefix VarInt as length - ByteArray []byte - - // OptionalByteArray is []byte without prefix VarInt as length - OptionalByteArray []byte -) - -// ReadNBytes read N bytes from bytes.Reader -func ReadNBytes(r DecodeReader, n int) ([]byte, error) { - bb := make([]byte, n) - var err error - for i := 0; i < n; i++ { - bb[i], err = r.ReadByte() - if err != nil { - return nil, err - } - } - return bb, nil -} - -// Encode a Boolean -func (b Boolean) Encode() []byte { - if b { - return []byte{0x01} - } - return []byte{0x00} -} - -// Decode a Boolean -func (b *Boolean) Decode(r DecodeReader) error { - v, err := r.ReadByte() - if err != nil { - return err - } - - *b = v != 0 - return nil -} - -// Encode a String -func (s String) Encode() []byte { - byteString := []byte(s) - var bb []byte - bb = append(bb, VarInt(len(byteString)).Encode()...) // len - bb = append(bb, byteString...) // data - return bb -} - -// Decode a String -func (s *String) Decode(r DecodeReader) error { - var l VarInt // String length - if err := l.Decode(r); err != nil { - return err - } - - bb, err := ReadNBytes(r, int(l)) - if err != nil { - return err - } - - *s = String(bb) - return nil -} - -// Encode a Byte -func (b Byte) Encode() []byte { - return []byte{byte(b)} -} - -// Decode a Byte -func (b *Byte) Decode(r DecodeReader) error { - v, err := r.ReadByte() - if err != nil { - return err - } - *b = Byte(v) - return nil -} - -// Encode a Unsigned Short -func (us UnsignedShort) Encode() []byte { - n := uint16(us) - return []byte{ - byte(n >> 8), - byte(n), - } -} - -// Decode a UnsignedShort -func (us *UnsignedShort) Decode(r DecodeReader) error { - bb, err := ReadNBytes(r, 2) - if err != nil { - return err - } - - *us = UnsignedShort(int16(bb[0])<<8 | int16(bb[1])) - return nil -} - -// Encode a Long -func (l Long) Encode() []byte { - n := uint64(l) - return []byte{ - byte(n >> 56), byte(n >> 48), byte(n >> 40), byte(n >> 32), - byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n), - } -} - -// Decode a Long -func (l *Long) Decode(r DecodeReader) error { - bb, err := ReadNBytes(r, 8) - if err != nil { - return err - } - - *l = Long(int64(bb[0])<<56 | int64(bb[1])<<48 | int64(bb[2])<<40 | int64(bb[3])<<32 | - int64(bb[4])<<24 | int64(bb[5])<<16 | int64(bb[6])<<8 | int64(bb[7])) - return nil -} - -// Encode a VarInt -func (v VarInt) Encode() []byte { - num := uint32(v) - var bb []byte - for { - b := num & 0x7F - num >>= 7 - if num != 0 { - b |= 0x80 - } - bb = append(bb, byte(b)) - if num == 0 { - break - } - } - return bb -} - -// Decode a VarInt -func (v *VarInt) Decode(r DecodeReader) error { - var n uint32 - for i := 0; ; i++ { - sec, err := r.ReadByte() - if err != nil { - return err - } - - n |= uint32(sec&0x7F) << uint32(7*i) - - if i >= 5 { - return errors.New("VarInt is too big") - } else if sec&0x80 == 0 { - break - } - } - - *v = VarInt(n) - return nil -} - -// Encode a ByteArray -func (b ByteArray) Encode() []byte { - return append(VarInt(len(b)).Encode(), b...) -} - -// Decode a ByteArray -func (b *ByteArray) Decode(r DecodeReader) error { - var length VarInt - if err := length.Decode(r); err != nil { - return err - } - *b = make([]byte, length) - _, err := r.Read(*b) - return err -} - -// Encode a UUID -func (u UUID) Encode() []byte { - return u[:] -} - -// Decode a UUID -func (u *UUID) Decode(r DecodeReader) error { - _, err := io.ReadFull(r, (*u)[:]) - return err -} - -// Encode a OptionalByteArray -func (b OptionalByteArray) Encode() []byte { - return b -} - -// Decode a OptionalByteArray -func (b *OptionalByteArray) Decode(r DecodeReader) error { - buf := bytes.NewBuffer([]byte{}) - _, err := buf.ReadFrom(r) - if err != nil { - return err - } - *b = buf.Bytes() - return nil -} diff --git a/protocol/types_test.go b/protocol/types_test.go deleted file mode 100644 index 06562a78..00000000 --- a/protocol/types_test.go +++ /dev/null @@ -1,415 +0,0 @@ -package protocol - -import ( - "bytes" - "github.com/gofrs/uuid" - "io" - "testing" -) - -func TestReadNBytes(t *testing.T) { - tt := [][]byte{ - {0x00, 0x01, 0x02, 0x03}, - {0x03, 0x01, 0x02, 0x02}, - } - - for _, tc := range tt { - bb, err := ReadNBytes(bytes.NewBuffer(tc), len(tc)) - if err != nil { - t.Errorf("reading bytes: %s", err) - } - - if !bytes.Equal(bb, tc) { - t.Errorf("got %v; want: %v", bb, tc) - } - } -} - -var booleanTestTable = []struct { - decoded Boolean - encoded []byte -}{ - { - decoded: Boolean(false), - encoded: []byte{0x00}, - }, - { - decoded: Boolean(true), - encoded: []byte{0x01}, - }, -} - -func TestBoolean_Encode(t *testing.T) { - for _, tc := range booleanTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestBoolean_Decode(t *testing.T) { - for _, tc := range booleanTestTable { - var actualDecoded Boolean - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - t.Errorf("decoding: %s", err) - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var varIntTestTable = []struct { - decoded VarInt - encoded []byte -}{ - { - decoded: VarInt(0), - encoded: []byte{0x00}, - }, - { - decoded: VarInt(1), - encoded: []byte{0x01}, - }, - { - decoded: VarInt(2), - encoded: []byte{0x02}, - }, - { - decoded: VarInt(127), - encoded: []byte{0x7f}, - }, - { - decoded: VarInt(128), - encoded: []byte{0x80, 0x01}, - }, - { - decoded: VarInt(255), - encoded: []byte{0xff, 0x01}, - }, - { - decoded: VarInt(2097151), - encoded: []byte{0xff, 0xff, 0x7f}, - }, - { - decoded: VarInt(2147483647), - encoded: []byte{0xff, 0xff, 0xff, 0xff, 0x07}, - }, - { - decoded: VarInt(-1), - encoded: []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, - }, - { - decoded: VarInt(-2147483648), - encoded: []byte{0x80, 0x80, 0x80, 0x80, 0x08}, - }, -} - -func TestVarInt_Encode(t *testing.T) { - for _, tc := range varIntTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestVarInt_Decode(t *testing.T) { - for _, tc := range varIntTestTable { - var actualDecoded VarInt - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - t.Errorf("decoding: %s", err) - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var stringTestTable = []struct { - decoded String - encoded []byte -}{ - { - decoded: String(""), - encoded: []byte{0x00}, - }, - { - decoded: String("Hello, World!"), - encoded: []byte{0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}, - }, - { - decoded: String("Minecraft"), - encoded: []byte{0x09, 0x4d, 0x69, 0x6e, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74}, - }, - { - decoded: String("♥"), - encoded: []byte{0x03, 0xe2, 0x99, 0xa5}, - }, -} - -func TestString_Encode(t *testing.T) { - for _, tc := range stringTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestString_Decode(t *testing.T) { - for _, tc := range stringTestTable { - var actualDecoded String - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - t.Errorf("decoding: %s", err) - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var byteTestTable = []struct { - decoded Byte - encoded []byte -}{ - { - decoded: Byte(0x00), - encoded: []byte{0x00}, - }, - { - decoded: Byte(0x0f), - encoded: []byte{0x0f}, - }, -} - -func TestByte_Encode(t *testing.T) { - for _, tc := range byteTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestByte_Decode(t *testing.T) { - for _, tc := range byteTestTable { - var actualDecoded Byte - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - t.Errorf("decoding: %s", err) - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var unsignedShortTestTable = []struct { - decoded UnsignedShort - encoded []byte -}{ - { - decoded: UnsignedShort(0), - encoded: []byte{0x00, 0x00}, - }, - { - decoded: UnsignedShort(15), - encoded: []byte{0x00, 0x0f}, - }, - { - decoded: UnsignedShort(16), - encoded: []byte{0x00, 0x10}, - }, - { - decoded: UnsignedShort(255), - encoded: []byte{0x00, 0xff}, - }, - { - decoded: UnsignedShort(256), - encoded: []byte{0x01, 0x00}, - }, - { - decoded: UnsignedShort(65535), - encoded: []byte{0xff, 0xff}, - }, -} - -func TestUnsignedShort_Encode(t *testing.T) { - for _, tc := range unsignedShortTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestUnsignedShort_Decode(t *testing.T) { - for _, tc := range unsignedShortTestTable { - var actualDecoded UnsignedShort - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - t.Errorf("decoding: %s", err) - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var longTestTable = []struct { - decoded Long - encoded []byte -}{ - { - decoded: Long(-9223372036854775808), - encoded: []byte{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - }, - { - decoded: Long(0), - encoded: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - }, - { - decoded: Long(15), - encoded: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f}, - }, - { - decoded: Long(16), - encoded: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10}, - }, - { - decoded: Long(9223372036854775807), - encoded: []byte{0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, - }, -} - -func TestLong_Encode(t *testing.T) { - for _, tc := range longTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestLong_Decode(t *testing.T) { - for _, tc := range longTestTable { - var actualDecoded Long - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - t.Errorf("decoding: %s", err) - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var byteArrayTestTable = []struct { - decoded ByteArray - encoded []byte -}{ - { - decoded: ByteArray([]byte{}), - encoded: []byte{0x00}, - }, - { - decoded: ByteArray([]byte{0x00}), - encoded: []byte{0x01, 0x00}, - }, -} - -func TestByteArray_Encode(t *testing.T) { - for _, tc := range byteArrayTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestByteArray_Decode(t *testing.T) { - for _, tc := range byteArrayTestTable { - actualDecoded := ByteArray([]byte{}) - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - if err != io.EOF { - t.Errorf("decoding: %s", err) - } - } - - if !bytes.Equal(actualDecoded, tc.decoded) { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var optionalByteArrayTestTable = []struct { - decoded OptionalByteArray - encoded []byte -}{ - { - decoded: OptionalByteArray([]byte{}), - encoded: []byte{}, - }, - { - decoded: OptionalByteArray([]byte{0x00}), - encoded: []byte{0x00}, - }, -} - -func TestOptionalByteArray_Encode(t *testing.T) { - for _, tc := range optionalByteArrayTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestOptionalByteArray_Decode(t *testing.T) { - for _, tc := range optionalByteArrayTestTable { - actualDecoded := OptionalByteArray([]byte{}) - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - if err != io.EOF { - t.Errorf("decoding: %s", err) - } - } - - if !bytes.Equal(actualDecoded, tc.decoded) { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} - -var uuidTestTable = []struct { - decoded UUID - encoded []byte -}{ - { - decoded: UUID(uuid.UUID{}), - encoded: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - }, - { - decoded: UUID(uuid.UUID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}), - encoded: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, - }, -} - -func TestUUID_Encode(t *testing.T) { - for _, tc := range uuidTestTable { - if !bytes.Equal(tc.decoded.Encode(), tc.encoded) { - t.Errorf("encoding: got: %v; want: %v", tc.decoded.Encode(), tc.encoded) - } - } -} - -func TestUUID_Decode(t *testing.T) { - for _, tc := range uuidTestTable { - var actualDecoded UUID - if err := actualDecoded.Decode(bytes.NewReader(tc.encoded)); err != nil { - if err != io.EOF { - t.Errorf("decoding: %s", err) - } - } - - if actualDecoded != tc.decoded { - t.Errorf("decoding: got %v; want: %v", actualDecoded, tc.decoded) - } - } -} diff --git a/proxy.go b/proxy.go deleted file mode 100644 index f8cc9fd2..00000000 --- a/proxy.go +++ /dev/null @@ -1,448 +0,0 @@ -package infrared - -import ( - "fmt" - "log" - "net" - "strings" - "sync" - "time" - - "github.com/haveachin/infrared/callback" - "github.com/haveachin/infrared/process" - "github.com/haveachin/infrared/protocol" - "github.com/haveachin/infrared/protocol/handshaking" - "github.com/haveachin/infrared/protocol/login" - "github.com/pires/go-proxyproto" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - playersConnected = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "infrared_connected", - Help: "The total number of connected players", - }, []string{"host"}) -) - -func proxyUID(domain, addr string) string { - return fmt.Sprintf("%s@%s", strings.ToLower(domain), addr) -} - -type Proxy struct { - Config *ProxyConfig - - cancelTimeoutFunc func() - players map[Conn]string - mu sync.Mutex -} - -func (proxy *Proxy) Process() process.Process { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - if proxy.Config.process != nil { - return proxy.Config.process - } - - if proxy.Config.Docker.IsPortainer() { - portainer, err := process.NewPortainer( - proxy.Config.Docker.ContainerName, - proxy.Config.Docker.Portainer.Address, - proxy.Config.Docker.Portainer.EndpointID, - proxy.Config.Docker.Portainer.Username, - proxy.Config.Docker.Portainer.Password, - ) - if err != nil { - log.Println("Failed to create a Portainer process; error:", err) - return nil - } - proxy.Config.process = portainer - return portainer - } - - if proxy.Config.Docker.IsDocker() { - docker, err := process.NewDocker(proxy.Config.Docker.ContainerName) - if err != nil { - log.Println("Failed to create a Docker process; error:", err) - return nil - } - proxy.Config.process = docker - return docker - } - - return nil -} - -func (proxy *Proxy) DomainName() string { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.DomainName -} - -func (proxy *Proxy) ListenTo() string { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.ListenTo -} - -func (proxy *Proxy) ProxyTo() string { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.ProxyTo -} - -func (proxy *Proxy) Dialer() (*Dialer, error) { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.Dialer() -} - -func (proxy *Proxy) DisconnectMessage() string { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.DisconnectMessage -} - -func (proxy *Proxy) IsOnlineStatusConfigured() bool { - proxy.Config.Lock() - defer proxy.Config.Unlock() - return proxy.Config.OnlineStatus.ProtocolNumber != 0 -} - -func (proxy *Proxy) OnlineStatusPacket() (protocol.Packet, error) { - proxy.Config.Lock() - defer proxy.Config.Unlock() - return proxy.Config.OnlineStatus.StatusResponsePacket() -} - -func (proxy *Proxy) OfflineStatusPacket() (protocol.Packet, error) { - proxy.Config.Lock() - defer proxy.Config.Unlock() - return proxy.Config.OfflineStatus.StatusResponsePacket() -} - -func (proxy *Proxy) Timeout() time.Duration { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return time.Millisecond * time.Duration(proxy.Config.Timeout) -} - -func (proxy *Proxy) DockerTimeout() time.Duration { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return time.Millisecond * time.Duration(proxy.Config.Docker.Timeout) -} - -func (proxy *Proxy) SpoofForcedHost() string { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.SpoofForcedHost -} - -func (proxy *Proxy) ProxyProtocol() bool { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.ProxyProtocol -} - -func (proxy *Proxy) RealIP() bool { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return proxy.Config.RealIP -} - -func (proxy *Proxy) CallbackLogger() callback.Logger { - proxy.Config.RLock() - defer proxy.Config.RUnlock() - return callback.Logger{ - URL: proxy.Config.CallbackServer.URL, - Events: proxy.Config.CallbackServer.Events, - } -} - -func (proxy *Proxy) UID() string { - return proxyUID(proxy.DomainName(), proxy.ListenTo()) -} - -func (proxy *Proxy) addPlayer(conn Conn, username string) { - proxy.mu.Lock() - defer proxy.mu.Unlock() - if proxy.players == nil { - proxy.players = map[Conn]string{} - } - proxy.players[conn] = username -} - -func (proxy *Proxy) removePlayer(conn Conn) int { - proxy.mu.Lock() - defer proxy.mu.Unlock() - if proxy.players == nil { - proxy.players = map[Conn]string{} - return 0 - } - delete(proxy.players, conn) - return len(proxy.players) -} - -func (proxy *Proxy) logEvent(event callback.Event) { - if _, err := proxy.CallbackLogger().LogEvent(event); err != nil { - log.Println("[w] Failed callback logging; error:", err) - } -} - -func (proxy *Proxy) handleConn(conn Conn, connRemoteAddr net.Addr) error { - pk, err := conn.ReadPacket() - if err != nil { - return err - } - - hs, err := handshaking.UnmarshalServerBoundHandshake(pk) - if err != nil { - return err - } - - proxyDomain := proxy.DomainName() - proxyTo := proxy.ProxyTo() - proxyUID := proxy.UID() - - dialer, err := proxy.Dialer() - if err != nil { - return err - } - - rconn, err := dialer.Dial(proxyTo) - if err != nil { - log.Printf("[i] %s did not respond to ping; is the target offline?", proxyTo) - if hs.IsStatusRequest() { - return proxy.handleStatusRequest(conn, false) - } - if err := proxy.startProcessIfNotRunning(); err != nil { - return err - } - proxy.timeoutProcess() - return proxy.handleLoginRequest(conn) - } - defer rconn.Close() - - if hs.IsStatusRequest() && proxy.IsOnlineStatusConfigured() { - return proxy.handleStatusRequest(conn, true) - } - - spoofForcedHost := proxy.SpoofForcedHost() - if spoofForcedHost != "" { - hs.ServerAddress = protocol.String(spoofForcedHost) - pk = hs.Marshal() - } - - if proxy.ProxyProtocol() { - header := &proxyproto.Header{ - Version: 2, - Command: proxyproto.PROXY, - TransportProtocol: proxyproto.TCPv4, - SourceAddr: connRemoteAddr, - DestinationAddr: rconn.RemoteAddr(), - } - - if _, err = header.WriteTo(rconn); err != nil { - return err - } - } - - if proxy.RealIP() { - hs.UpgradeToRealIP(connRemoteAddr, time.Now()) - pk = hs.Marshal() - } - - if err := rconn.WritePacket(pk); err != nil { - return err - } - - var username string - connected := false - if hs.IsLoginRequest() { - proxy.cancelProcessTimeout() - username, err = proxy.sniffUsername(conn, rconn, connRemoteAddr) - if err != nil { - return err - } - proxy.addPlayer(conn, username) - proxy.logEvent(callback.PlayerJoinEvent{ - Username: username, - RemoteAddress: connRemoteAddr.String(), - TargetAddress: proxyTo, - ProxyUID: proxyUID, - }) - playersConnected.With(prometheus.Labels{"host": proxyDomain}).Inc() - connected = true - } - - go pipe(rconn, conn) - pipe(conn, rconn) - - if connected { - proxy.logEvent(callback.PlayerLeaveEvent{ - Username: username, - RemoteAddress: connRemoteAddr.String(), - TargetAddress: proxyTo, - ProxyUID: proxyUID, - }) - playersConnected.With(prometheus.Labels{"host": proxyDomain}).Dec() - } - - remainingPlayers := proxy.removePlayer(conn) - if remainingPlayers <= 0 { - proxy.timeoutProcess() - } - return nil -} - -func pipe(src, dst Conn) { - buffer := make([]byte, 0xffff) - - for { - n, err := src.Read(buffer) - if err != nil { - return - } - - data := buffer[:n] - - _, err = dst.Write(data) - if err != nil { - return - } - } -} - -func (proxy *Proxy) startProcessIfNotRunning() error { - if proxy.Process() == nil { - return nil - } - - running, err := proxy.Process().IsRunning() - if err != nil { - return err - } - - if running { - return nil - } - - log.Println("[i] Starting container for", proxy.UID()) - proxy.logEvent(callback.ContainerStartEvent{ProxyUID: proxy.UID()}) - return proxy.Process().Start() -} - -func (proxy *Proxy) timeoutProcess() { - if proxy.Process() == nil { - return - } - - if proxy.DockerTimeout() <= 0 { - return - } - - proxy.cancelProcessTimeout() - - log.Printf("[i] Starting container timeout %s on %s", proxy.DockerTimeout(), proxy.UID()) - timer := time.AfterFunc(proxy.DockerTimeout(), func() { - log.Println("[i] Stopping container on", proxy.UID()) - proxy.logEvent(callback.ContainerStopEvent{ProxyUID: proxy.UID()}) - if err := proxy.Process().Stop(); err != nil { - log.Printf("[w] Failed to stop the container for %s; error: %s", proxy.UID(), err) - } - }) - - proxy.cancelTimeoutFunc = func() { - if timer.Stop() { - log.Println("[i] Timout stopped for", proxy.UID()) - } - } -} - -func (proxy *Proxy) cancelProcessTimeout() { - if proxy.cancelTimeoutFunc == nil { - return - } - - proxy.cancelTimeoutFunc() - proxy.cancelTimeoutFunc = nil -} - -func (proxy *Proxy) sniffUsername(conn, rconn Conn, connRemoteAddr net.Addr) (string, error) { - pk, err := conn.ReadPacket() - if err != nil { - return "", err - } - rconn.WritePacket(pk) - - ls, err := login.UnmarshalServerBoundLoginStart(pk) - if err != nil { - return "", err - } - log.Printf("[i] %s with username %s connects through %s", connRemoteAddr, ls.Name, proxy.UID()) - return string(ls.Name), nil -} - -func (proxy *Proxy) handleLoginRequest(conn Conn) error { - packet, err := conn.ReadPacket() - if err != nil { - return err - } - - loginStart, err := login.UnmarshalServerBoundLoginStart(packet) - if err != nil { - return err - } - - message := proxy.DisconnectMessage() - templates := map[string]string{ - "username": string(loginStart.Name), - "now": time.Now().Format(time.RFC822), - "remoteAddress": conn.LocalAddr().String(), - "localAddress": conn.LocalAddr().String(), - "domain": proxy.DomainName(), - "proxyTo": proxy.ProxyTo(), - "listenTo": proxy.ListenTo(), - } - - for key, value := range templates { - message = strings.Replace(message, fmt.Sprintf("{{%s}}", key), value, -1) - } - - return conn.WritePacket(login.ClientBoundDisconnect{ - Reason: protocol.Chat(fmt.Sprintf("{\"text\":\"%s\"}", message)), - }.Marshal()) -} - -func (proxy *Proxy) handleStatusRequest(conn Conn, online bool) error { - // Read the request packet and send status response back - _, err := conn.ReadPacket() - if err != nil { - return err - } - - var responsePk protocol.Packet - if online { - responsePk, err = proxy.OnlineStatusPacket() - if err != nil { - return err - } - } else { - responsePk, err = proxy.OfflineStatusPacket() - if err != nil { - return err - } - } - - if err := conn.WritePacket(responsePk); err != nil { - return err - } - - pingPk, err := conn.ReadPacket() - if err != nil { - return err - } - - return conn.WritePacket(pingPk) -} diff --git a/tools/dos/main.go b/tools/dos/main.go new file mode 100644 index 00000000..30866eca --- /dev/null +++ b/tools/dos/main.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "crypto/rand" + "flag" + "log" + "net" + "os" + "runtime" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/handshaking" + "github.com/haveachin/infrared/pkg/infrared/protocol/login" + "github.com/pires/go-proxyproto" +) + +var ( + handshakePayload []byte + loginStartPayload []byte +) + +func initPayload() { + var pk protocol.Packet + _ = handshaking.ServerBoundHandshake{ + ProtocolVersion: 758, + ServerAddress: "localhost", + ServerPort: 25565, + NextState: handshaking.StateStatusServerBoundHandshake, + }.Marshal(&pk) + var buf bytes.Buffer + _, _ = pk.WriteTo(&buf) + handshakePayload = buf.Bytes() + + _ = login.ServerBoundLoginStart{ + Name: "Test", + HasSignature: false, + HasPlayerUUID: false, + }.Marshal(&pk, protocol.Version1_19) + buf.Reset() + _, _ = pk.WriteTo(&buf) + loginStartPayload = buf.Bytes() + + log.Println(len(handshakePayload) + len(loginStartPayload)) +} + +var ( + sendProxyProtocol = false +) + +func initFlags() { + flag.BoolVar(&sendProxyProtocol, "p", sendProxyProtocol, "sends a proxy protocol v2 header before its payload") + flag.Parse() +} + +func main() { + runtime.GOMAXPROCS(4) + + initFlags() + initPayload() + + targetAddr := "localhost:25565" + + if len(os.Args) < 2 { + log.Println("No target address specified") + log.Printf("Defaulting to %s\n", targetAddr) + } else { + targetAddr = os.Args[1] + } + + conn, err := net.Dial("tcp", targetAddr) + if err != nil { + log.Fatal(err) + } + _ = conn.Close() + + for i := 0; ; i++ { + if i > 0 && i%10 == 0 { + log.Printf("%d requests sent\n", i) + } + + go func() { + c, err := net.Dial("tcp", targetAddr) + if err != nil { + return + } + + if sendProxyProtocol { + if err = writeProxyProtocolHeader(randomAddr(), c); err != nil { + log.Printf("Write proxy protocol: %s", err) + } + } + + _, _ = c.Write(handshakePayload) + _, _ = c.Write(loginStartPayload) + _ = c.Close() + }() + // time.Sleep(time.Millisecond * 10) + } +} + +func randomAddr() net.Addr { + addrBytes := make([]byte, 6) + _, _ = rand.Read(addrBytes) + + return &net.TCPAddr{ + IP: net.IPv4(addrBytes[0], addrBytes[1], addrBytes[2], addrBytes[3]), + Port: int(addrBytes[4])*256 + int(addrBytes[5]), + } +} + +func writeProxyProtocolHeader(addr net.Addr, rc net.Conn) error { + tp := proxyproto.TCPv4 + addrTCP, ok := addr.(*net.TCPAddr) + if !ok { + panic("not a tcp connection") + } + + if addrTCP.IP.To4() == nil { + tp = proxyproto.TCPv6 + } + + header := &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: tp, + SourceAddr: addr, + DestinationAddr: rc.RemoteAddr(), + } + + if _, err := header.WriteTo(rc); err != nil { + return err + } + return nil +} diff --git a/tools/malpk/main.go b/tools/malpk/main.go new file mode 100644 index 00000000..08d8bf6c --- /dev/null +++ b/tools/malpk/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "bytes" + "log" + "net" + "sync" + + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/handshaking" +) + +var payload []byte + +func initPayload() { + buf := new(bytes.Buffer) + _, _ = protocol.VarInt(0x200000).WriteTo(buf) + _, _ = protocol.VarInt(handshaking.ServerBoundHandshakeID).WriteTo(buf) + _, _ = protocol.VarInt(protocol.Version1_20_2.ProtocolNumber()).WriteTo(buf) + _, _ = protocol.String("localhost").WriteTo(buf) + _, _ = protocol.UnsignedShort(25565).WriteTo(buf) + _, _ = protocol.Byte(2).WriteTo(buf) + payload = buf.Bytes() +} + +func main() { + initPayload() + + n := 100000 + + wg := sync.WaitGroup{} + wg.Add(n) + + for i := 0; i < n; i++ { + go func() { + c, err := net.Dial("tcp", "localhost:25565") + if err != nil { + log.Println(err) + return + } + + if _, err = c.Write(payload); err != nil { + log.Println(err) + } + _ = c.Close() + wg.Done() + }() + } + + wg.Wait() +}