From cca50968aba2ab9e8b9a0a688a751fdd028d7821 Mon Sep 17 00:00:00 2001 From: Hendrik Schlehlein Date: Fri, 29 Dec 2023 16:49:55 +0100 Subject: [PATCH] feat: add config file loading (#194) --- .gitignore | 8 +- Makefile | 7 +- README.md | 3 + cmd/infrared/config.go | 30 + cmd/infrared/main.go | 92 +- configs/config.yml | 19 + configs/embed.go | 6 + configs/proxy.yml | 16 + deployments/docker-compose.dev.yml | 2 +- docs/.vitepress/config.mts | 88 + docs/.vitepress/theme/custom.css | 24 + docs/.vitepress/theme/index.js | 5 + {assets => docs/assets}/agplv3_logo.svg | 0 {assets => docs/assets}/logo.svg | 0 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 | 19 + docs/config/index.md | 26 + docs/config/proxies.md | 24 + docs/contribute.md | 1 + docs/getting-started.md | 79 + docs/guide/forward-player-ips.md | 15 + docs/index.md | 30 + docs/package-lock.json | 1487 +++++++++++++++++ docs/package.json | 10 + go.mod | 2 + go.sum | 6 + pkg/infrared/config/file.go | 147 ++ pkg/infrared/conn.go | 3 +- pkg/infrared/infrared.go | 110 +- pkg/infrared/infrared_test.go | 40 +- .../protocol/login/serverbound_loginstart.go | 73 +- pkg/infrared/protocol/versions.go | 3 + pkg/infrared/server.go | 45 +- tools/dos/main.go | 2 +- 36 files changed, 2455 insertions(+), 106 deletions(-) create mode 100644 cmd/infrared/config.go create mode 100644 configs/config.yml create mode 100644 configs/embed.go create mode 100644 configs/proxy.yml create mode 100644 docs/.vitepress/config.mts create mode 100644 docs/.vitepress/theme/custom.css create mode 100644 docs/.vitepress/theme/index.js rename {assets => docs/assets}/agplv3_logo.svg (100%) rename {assets => docs/assets}/logo.svg (100%) 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/contribute.md create mode 100644 docs/getting-started.md create mode 100644 docs/guide/forward-player-ips.md create mode 100644 docs/index.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 pkg/infrared/config/file.go diff --git a/.gitignore b/.gitignore index 4a7acc58..2571254d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,15 @@ __debug_bin* # Local Dev Files .dev/ +cmd/infrared/config.yml # VSCode .vscode/ # IntelliJ -.idea/ \ No newline at end of file +.idea/ + +# Docs +docs/node_modules/ +docs/.vitepress/cache/ +docs/.vitepress/dist/ \ No newline at end of file diff --git a/Makefile b/Makefile index b279e020..32596f35 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +.PHONY: docs + test: go test -race -timeout 10s ./... @@ -16,4 +18,7 @@ dev: dos: CGO_ENABLED=0 go build -ldflags "-s -w" -o ./out/dos ./tools/dos - ./out/dos \ No newline at end of file + ./out/dos + +docs: + cd ./docs && npm run docs:dev diff --git a/README.md b/README.md index 044090a7..a5c5b207 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ Infrared works as a reverse proxy using a sub-/domains to connect clients to a s - [X] Reverse Proxy - [X] Wildcards Support - [X] Multi-Domain Support +- [X] Status Response Caching +- [ ] Proxy Protocol Support +- [ ] Ratelimiter ## Contributing diff --git a/cmd/infrared/config.go b/cmd/infrared/config.go new file mode 100644 index 00000000..54e78c90 --- /dev/null +++ b/cmd/infrared/config.go @@ -0,0 +1,30 @@ +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() + } + + if info.IsDir() { + return errors.New("ir.Config is a directory") + } + + return nil +} + +func createDefaultConfigFile() error { + bb := configs.DefaultInfraredConfig + return os.WriteFile(configPath, bb, 0664) +} diff --git a/cmd/infrared/main.go b/cmd/infrared/main.go index d183a86d..e02493e1 100644 --- a/cmd/infrared/main.go +++ b/cmd/infrared/main.go @@ -2,18 +2,92 @@ package main import ( "log" + "os" + "os/signal" + "syscall" ir "github.com/haveachin/infrared/pkg/infrared" + "github.com/haveachin/infrared/pkg/infrared/config" + "github.com/spf13/pflag" ) +const ( + envVarPrefix = "INFRARED" +) + +var ( + configPath = "config.yml" + workingDir = "." + proxiesDir = "./proxies" +) + +func envVarString(p *string, name string) { + key := envVarPrefix + "_" + name + v := os.Getenv(key) + if v == "" { + return + } + *p = v +} + +func initEnvVars() { + envVarString(&configPath, "CONFIG") + envVarString(&workingDir, "WORKING_DIR") + envVarString(&proxiesDir, "PROXIES_DIR") +} + +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.Parse() +} + +func init() { + initEnvVars() + initFlags() +} + func main() { - srv := ir.New( - ir.WithBindAddr(":25565"), - ir.AddServerConfig( - ir.WithServerDomains("*"), - ir.WithServerAddress(":25566"), - ), - ) - - log.Println(srv.ListenAndServe()) + log.Println("Starting Infrared") + + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + if err := os.Chdir(workingDir); err != nil { + return err + } + + if err := createConfigIfNotExist(); err != nil { + return err + } + + srv := ir.NewWithConfigProvider(config.FileProvider{ + ConfigPath: configPath, + ProxiesPath: proxiesDir, + }) + + errChan := make(chan error, 1) + go func() { + errChan <- srv.ListenAndServe() + }() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + log.Println("System is online") + + select { + case sig := <-sigChan: + log.Printf("Received %s", sig.String()) + case err := <-errChan: + if err != nil { + return err + } + } + + return nil } diff --git a/configs/config.yml b/configs/config.yml new file mode 100644 index 00000000..f9fbc90b --- /dev/null +++ b/configs/config.yml @@ -0,0 +1,19 @@ +# Infrared Config + +# Address that Infrared bind and listens to +# +bind: 0.0.0.0:25565 + +proxyProtocol: + # Receive proxy protocol + # + #receive: true + + # TODO + # + #trustedProxies: + # - "127.0.0.1" + +# Maximum duration between packets before the client gets timed out. +# +keepAliveTimeout: 30s 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/proxy.yml b/configs/proxy.yml new file mode 100644 index 00000000..709b46da --- /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: + - example.com: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/deployments/docker-compose.dev.yml b/deployments/docker-compose.dev.yml index 2377da30..9d2d88c3 100644 --- a/deployments/docker-compose.dev.yml +++ b/deployments/docker-compose.dev.yml @@ -11,7 +11,7 @@ services: - 25566:25565/tcp environment: EULA: "TRUE" - VERSION: "1.20.1" + VERSION: "1.20.4" TYPE: PAPER networks: - infrared diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..d4d4091d --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,88 @@ +import { defineConfig} from 'vitepress' + +// https://vitepress.dev/reference/site-config +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: { + // https://vitepress.dev/reference/default-theme-config + logo: '/assets/logo.svg', + + nav: [ + { + text: 'Guides', + items: [ + { text: 'Forward Player IPs', link: '/guide/forward-player-ips' }, + ] + }, + { + text: 'Config', + items: [ + { text: 'Global', link: '/config/' }, + { text: 'Proxy', link: '/config/proxy' }, + { 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' }, + ] + }, + ], + + 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: 'Guides', + items: [ + { text: 'Forward Player IPs', link: '/guide/forward-player-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/assets/agplv3_logo.svg b/docs/assets/agplv3_logo.svg similarity index 100% rename from assets/agplv3_logo.svg rename to docs/assets/agplv3_logo.svg diff --git a/assets/logo.svg b/docs/assets/logo.svg similarity index 100% rename from assets/logo.svg rename to docs/assets/logo.svg 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..ea36d2eb --- /dev/null +++ b/docs/config/cli-and-env-vars.md @@ -0,0 +1,19 @@ +# CLI & Env Vars + +## Config Path + +| Environment Variable | CLI Flag | Default | +|----------------------|--------------------|--------------| +| `INFRARED_CONFIG` | `--config` or `-c` | `config.yml` | + +## Working Directory + +| Environment Variable | CLI Flag | Default | +|------------------------|-------------------------|---------| +| `INFRARED_WORKING_DIR` | `--working-dir` or `-w` | `.` | + +## Proxies Path + +| Environment Variable | CLI Flag | Default | +|------------------------|-------------------------|-------------| +| `INFRARED_PROXIES_DIR` | `--proxies-dir` or `-p` | `./proxies` | \ No newline at end of file diff --git a/docs/config/index.md b/docs/config/index.md new file mode 100644 index 00000000..55909108 --- /dev/null +++ b/docs/config/index.md @@ -0,0 +1,26 @@ +# Config + +On fist start Infrared should generate a `config.yml` file and a `proxies` directory. +The default config file should look something like this: + +```yml [config.yml] +# Infrared Config + +# Address that Infrared bind and listens to +# +bind: 0.0.0.0:25565 + +proxyProtocol: + # Receive proxy protocol + # + #receive: true + + # TODO + # + #trustedProxies: + # - "127.0.0.1" + +# Maximum duration between packets before the client gets timed out. +# +keepAliveTimeout: 30s +``` diff --git a/docs/config/proxies.md b/docs/config/proxies.md new file mode 100644 index 00000000..b5167a67 --- /dev/null +++ b/docs/config/proxies.md @@ -0,0 +1,24 @@ +# 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) + +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: + - "*" + +addresses: + - example.com: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/docs/contribute.md b/docs/contribute.md new file mode 100644 index 00000000..8bd99fdd --- /dev/null +++ b/docs/contribute.md @@ -0,0 +1 @@ +# Contribute \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..98f0b48c --- /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:/infrared +``` diff --git a/docs/guide/forward-player-ips.md b/docs/guide/forward-player-ips.md new file mode 100644 index 00000000..809e986c --- /dev/null +++ b/docs/guide/forward-player-ips.md @@ -0,0 +1,15 @@ +# 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: +```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/index.md b/docs/index.md new file mode 100644 index 00000000..21ec7cae --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: Infrared + text: A Simple & Powerful Minecraft Proxy + tagline: + image: + src: /assets/logo.svg + alt: Infrared + actions: + - theme: brand + text: Getting Started + link: /getting-started + - theme: alt + text: Contribute + link: /contribute + +features: + - icon: 📖 + title: Free and Open Source + details: Free as in speech. + - icon: ⚡ + title: High Performance + details: Written with scale in mind. + - icon: 🪶 + title: Simple and Lightweight + details: Easy to use on any hardware. +--- \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..0e4d18c0 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,1487 @@ +{ + "name": "docs2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "vitepress": "^1.0.0-rc.28" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "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/android-arm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", + "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", + "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", + "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", + "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", + "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", + "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", + "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", + "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", + "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", + "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", + "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", + "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", + "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", + "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", + "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", + "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", + "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", + "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", + "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", + "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", + "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", + "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", + "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.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.0.tgz", + "integrity": "sha512-OINaBGY+Wc++U0rdr7BLuFClxcoWaVW3vQYqmQq6B3bqQ/2olkaoz+K8+af/Mmka/C2yN5j+L9scBkv4BtKsDA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.0.tgz", + "integrity": "sha512-UdMf1pOQc4ZmUA/NTmKhgJTBimbSKnhPS2zJqucqFyBRFPnPDtwA8MzrGNTjDeQbIAWfpJVAlxejw+/lQyBK/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.0.tgz", + "integrity": "sha512-L0/CA5p/idVKI+c9PcAPGorH6CwXn6+J0Ys7Gg1axCbTPgI8MeMlhA6fLM9fK+ssFhqogMHFC8HDvZuetOii7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.0.tgz", + "integrity": "sha512-QZCbVqU26mNlLn8zi/XDDquNmvcr4ON5FYAHQQsyhrHx8q+sQi/6xduoznYXwk/KmKIXG5dLfR0CvY+NAWpFYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.0.tgz", + "integrity": "sha512-VpSQ+xm93AeV33QbYslgf44wc5eJGYfYitlQzAi3OObu9iwrGXEnmu5S3ilkqE3Pr/FkgOiJKV/2p0ewf4Hrtg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.0.tgz", + "integrity": "sha512-OrEyIfpxSsMal44JpEVx9AEcGpdBQG1ZuWISAanaQTSMeStBW+oHWwOkoqR54bw3x8heP8gBOyoJiGg+fLY8qQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.0.tgz", + "integrity": "sha512-1H7wBbQuE6igQdxMSTjtFfD+DGAudcYWhp106z/9zBA8OQhsJRnemO4XGavdzHpGhRtRxbgmUGdO3YQgrWf2RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.0.tgz", + "integrity": "sha512-FVyFI13tXw5aE65sZdBpNjPVIi4Q5mARnL/39UIkxvSgRAIqCo5sCpCELk0JtXHGee2owZz5aNLbWNfBHzr71Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.0.tgz", + "integrity": "sha512-eBPYl2sLpH/o8qbSz6vPwWlDyThnQjJfcDOGFbNjmjb44XKC1F5dQfakOsADRVrXCNzM6ZsSIPDG5dc6HHLNFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.0.tgz", + "integrity": "sha512-xaOHIfLOZypoQ5U2I6rEaugS4IYtTgP030xzvrBf5js7p9WI9wik07iHmsKaej8Z83ZDxN5GyypfoyKV5O5TJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.0.tgz", + "integrity": "sha512-Al6quztQUrHwcOoU2TuFblUQ5L+/AmPBXFR6dUvyo4nRj2yQRK0WIUaGMF/uwKulvRcXkpHe3k9A8Vf93VDktA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.0.tgz", + "integrity": "sha512-8kdW+brNhI/NzJ4fxDufuJUjepzINqJKLGHuxyAtpPG9bMbn8P5mtaCcbOm0EzLJ+atg+kF9dwg8jpclkVqx5w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "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.6", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.6.tgz", + "integrity": "sha512-0VqpvusJn1/lwRegCxcHVdmLfF+wIsprsKMC9xW8UPcTxhFcQtoN/fBU1zMe8pH7D/RuueMh2CaBaNv+GrLqTw==", + "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": "4.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz", + "integrity": "sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz", + "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz", + "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz", + "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-ssr": "3.3.8", + "@vue/reactivity-transform": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.31", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz", + "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "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.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz", + "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==", + "dev": true, + "dependencies": { + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz", + "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.0", + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz", + "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.3.8", + "@vue/shared": "3.3.8" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz", + "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==", + "dev": true, + "dependencies": { + "@vue/runtime-core": "3.3.8", + "@vue/shared": "3.3.8", + "csstype": "^3.1.2" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz", + "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.3.8", + "@vue/shared": "3.3.8" + }, + "peerDependencies": { + "vue": "3.3.8" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz", + "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.6.1.tgz", + "integrity": "sha512-Pc26IJbqgC9VG1u6VY/xrXXfxD33hnvxBnKrLlA2LJlyHII+BSrRoTPJgGYq7qZOu61itITFUnm6QbacwZ4H8Q==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.6.1", + "@vueuse/shared": "10.6.1", + "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.6.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.6.1.tgz", + "integrity": "sha512-mPDupuofMJ4DPmtX/FfP1MajmWRzYDv8WSaTCo8LQ5kFznjWgmUQ16ApjYqgMquqffNY6+IRMdMgosLDRZOSZA==", + "dev": true, + "dependencies": { + "@vueuse/core": "10.6.1", + "@vueuse/shared": "10.6.1", + "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.6.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.6.1.tgz", + "integrity": "sha512-qhdwPI65Bgcj23e5lpGfQsxcy0bMjCAsUGoXkJ7DsoeDUdasbZ2DBa4dinFCOER3lF4gwUv+UD2AlA11zdzMFw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.6.1.tgz", + "integrity": "sha512-TECVDTIedFlL0NUfHWncf3zF9Gc4VfdxfQc8JFwoVZQmxpONhLxFrlm0eHQeidHj4rdTPL3KXJa0TZCk1wnc5Q==", + "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/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", + "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.5", + "@esbuild/android-arm64": "0.19.5", + "@esbuild/android-x64": "0.19.5", + "@esbuild/darwin-arm64": "0.19.5", + "@esbuild/darwin-x64": "0.19.5", + "@esbuild/freebsd-arm64": "0.19.5", + "@esbuild/freebsd-x64": "0.19.5", + "@esbuild/linux-arm": "0.19.5", + "@esbuild/linux-arm64": "0.19.5", + "@esbuild/linux-ia32": "0.19.5", + "@esbuild/linux-loong64": "0.19.5", + "@esbuild/linux-mips64el": "0.19.5", + "@esbuild/linux-ppc64": "0.19.5", + "@esbuild/linux-riscv64": "0.19.5", + "@esbuild/linux-s390x": "0.19.5", + "@esbuild/linux-x64": "0.19.5", + "@esbuild/netbsd-x64": "0.19.5", + "@esbuild/openbsd-x64": "0.19.5", + "@esbuild/sunos-x64": "0.19.5", + "@esbuild/win32-arm64": "0.19.5", + "@esbuild/win32-ia32": "0.19.5", + "@esbuild/win32-x64": "0.19.5" + } + }, + "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/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.2.0.tgz", + "integrity": "sha512-BECkorDF1TY2rGKt9XHdSeP9TP29yUbrAaCh/C03wpyf1vx3uYcP/+8XlMcpTkgoU0rBVnHMAOaP83Rc9Tm+TQ==", + "dev": true + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "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.5.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.0.tgz", + "integrity": "sha512-41xsWhzxqjMDASCxH5ibw1mXk+3c4TNI2UjKbLxe6iEzrSQnqOzmmK8/3mufCPbzHNJ2e04Fc1ddI35hHy+8zg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.5.0", + "@rollup/rollup-android-arm64": "4.5.0", + "@rollup/rollup-darwin-arm64": "4.5.0", + "@rollup/rollup-darwin-x64": "4.5.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.5.0", + "@rollup/rollup-linux-arm64-gnu": "4.5.0", + "@rollup/rollup-linux-arm64-musl": "4.5.0", + "@rollup/rollup-linux-x64-gnu": "4.5.0", + "@rollup/rollup-linux-x64-musl": "4.5.0", + "@rollup/rollup-win32-arm64-msvc": "4.5.0", + "@rollup/rollup-win32-ia32-msvc": "4.5.0", + "@rollup/rollup-win32-x64-msvc": "4.5.0", + "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/shiki": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.0.tgz", + "integrity": "sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.31", + "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.28", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.0.0-rc.28.tgz", + "integrity": "sha512-cB0DNhX1jgmyZxPSrH5E+YpgpSlLuDL4ec9UjeqTe/Si1+MEvIJRgifB0RjGfojKa+gkSo97nLO6WN+iFgtgXQ==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.5.2", + "@docsearch/js": "^3.5.2", + "@types/markdown-it": "^13.0.6", + "@vitejs/plugin-vue": "^4.5.0", + "@vue/devtools-api": "^6.5.1", + "@vueuse/core": "^10.6.1", + "@vueuse/integrations": "^10.6.1", + "focus-trap": "^7.5.4", + "mark.js": "8.11.1", + "minisearch": "^6.2.0", + "mrmime": "^1.0.1", + "shiki": "^0.14.5", + "vite": "^5.0.0", + "vue": "^3.3.8" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4.3.2", + "postcss": "^8.4.31" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, + "node_modules/vue": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz", + "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-sfc": "3.3.8", + "@vue/runtime-dom": "3.3.8", + "@vue/server-renderer": "3.3.8", + "@vue/shared": "3.3.8" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..4b777749 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "vitepress": "^1.0.0-rc.28" + }, + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index a6c9410e..1934a698 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,6 @@ require ( github.com/cespare/xxhash v1.1.0 github.com/google/uuid v1.3.0 github.com/pires/go-proxyproto v0.7.0 + github.com/spf13/pflag v1.0.5 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 0a88ca86..aef8b9eb 100644 --- a/go.sum +++ b/go.sum @@ -10,3 +10,9 @@ github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwy github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/infrared/config/file.go b/pkg/infrared/config/file.go new file mode 100644 index 00000000..700f11f9 --- /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 index a40fe538..02018fff 100644 --- a/pkg/infrared/conn.go +++ b/pkg/infrared/conn.go @@ -82,8 +82,7 @@ func (c *conn) WritePackets(pks ...protocol.Packet) error { } func (c *conn) ForceClose() error { - switch conn := c.Conn.(type) { - case *net.TCPConn: + if conn, ok := c.Conn.(*net.TCPConn); ok { if err := conn.SetLinger(0); err != nil { return err } diff --git a/pkg/infrared/infrared.go b/pkg/infrared/infrared.go index 87b697fa..fad65b3d 100644 --- a/pkg/infrared/infrared.go +++ b/pkg/infrared/infrared.go @@ -1,6 +1,7 @@ package infrared import ( + "errors" "io" "log" "net" @@ -13,8 +14,9 @@ import ( ) type Config struct { - BindAddr string - ServerConfigs []ServerConfig + BindAddr string `yaml:"bind"` + ServerConfigs []ServerConfig `yaml:"servers"` + KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` } type ConfigFunc func(cfg *Config) @@ -35,20 +37,56 @@ func AddServerConfig(fns ...ServerConfigFunc) ConfigFunc { } } +func WithKeepAliveTimeout(d time.Duration) ConfigFunc { + return func(cfg *Config) { + cfg.KeepAliveTimeout = d + } +} + +func DefaultConfig() Config { + return Config{ + BindAddr: ":25565", + KeepAliveTimeout: 30 * 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 { cfg Config l net.Listener srvs []*Server bufPool sync.Pool + conns map[net.Addr]*conn } func New(fns ...ConfigFunc) *Infrared { - var cfg Config + 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{ @@ -57,10 +95,12 @@ func New(fns ...ConfigFunc) *Infrared { return &b }, }, + conns: make(map[net.Addr]*conn), } } func (ir *Infrared) init() error { + log.Printf("Listening on %s", ir.cfg.BindAddr) l, err := net.Listen("tcp", ir.cfg.BindAddr) if err != nil { return err @@ -68,7 +108,11 @@ func (ir *Infrared) init() error { ir.l = l for _, sCfg := range ir.cfg.ServerConfigs { - srv := NewServer(WithServerConfig(sCfg)) + srv, err := NewServer(WithServerConfig(sCfg)) + if err != nil { + return err + } + ir.srvs = append(ir.srvs, srv) } @@ -81,6 +125,8 @@ func (ir *Infrared) ListenAndServe() error { } sgInChan := make(chan ServerRequest) + defer close(sgInChan) + sg := serverGateway{ Servers: ir.srvs, requestChan: sgInChan, @@ -93,9 +139,10 @@ func (ir *Infrared) ListenAndServe() error { func (ir *Infrared) listenAndServe(srvReqChan chan<- ServerRequest) error { for { c, err := ir.l.Accept() - if err != nil { - // TODO: Handle Listener closed - log.Println(err) + if errors.Is(err, net.ErrClosed) { + return nil + } else if err != nil { + log.Printf("Error accepting new conn: %s", err) continue } @@ -144,20 +191,20 @@ func (ir *Infrared) handleConn(c *conn) error { ReadPks: c.readPks, ResponseChan: respChan, } - + resp := <-respChan if resp.Err != nil { return resp.Err } if c.handshake.IsStatusRequest() { - return ir.handleStatus(c, resp) + return handleStatus(c, resp) } return ir.handleLogin(c, resp) } -func (ir *Infrared) handleStatus(c *conn, resp ServerRequestResponse) error { +func handleStatus(c *conn, resp ServerRequestResponse) error { if err := c.WritePacket(resp.StatusResponse); err != nil { return err } @@ -180,7 +227,7 @@ func (ir *Infrared) handleLogin(c *conn, resp ServerRequestResponse) error { return err } - c.timeout = time.Second * 30 + c.timeout = ir.cfg.KeepAliveTimeout return ir.handlePipe(c, resp) } @@ -189,16 +236,8 @@ func (ir *Infrared) handlePipe(c *conn, resp ServerRequestResponse) error { rc := resp.ServerConn defer rc.ForceClose() - if resp.UseProxyProtocol { - header := &proxyproto.Header{ - Version: 2, - Command: proxyproto.PROXY, - TransportProtocol: proxyproto.TCPv4, - SourceAddr: c.RemoteAddr(), - DestinationAddr: rc.RemoteAddr(), - } - - if _, err := header.WriteTo(rc); err != nil { + if resp.SendProxyProtocol { + if err := writeProxyProtocolHeader(c.RemoteAddr(), rc); err != nil { return err } } @@ -210,6 +249,10 @@ func (ir *Infrared) handlePipe(c *conn, resp ServerRequestResponse) error { 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) @@ -223,6 +266,7 @@ func (ir *Infrared) handlePipe(c *conn, resp ServerRequestResponse) error { waitChan = cClosedChan } <-waitChan + delete(ir.conns, c.RemoteAddr()) return nil } @@ -234,3 +278,27 @@ func (ir *Infrared) copy(dst io.WriteCloser, src io.ReadCloser, srcClosedChan ch io.CopyBuffer(dst, src, *b) srcClosedChan <- struct{}{} } + +func writeProxyProtocolHeader(addr net.Addr, rc net.Conn) error { + rcAddr := rc.RemoteAddr() + tcpAddr := rcAddr.(*net.TCPAddr) + + 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 index 7c9e807c..6efbdb2e 100644 --- a/pkg/infrared/infrared_test.go +++ b/pkg/infrared/infrared_test.go @@ -15,21 +15,20 @@ import ( "github.com/pires/go-proxyproto" ) -type mockServerRequestResponder struct { -} +type mockServerRequestResponder struct{} func (r mockServerRequestResponder) RespondeToServerRequest(req ServerRequest, srv *Server) { req.ResponseChan <- ServerRequestResponse{} } func BenchmarkHandleConn_Status(b *testing.B) { - var hsPk protocol.Packet + var hsStatusPk protocol.Packet handshaking.ServerBoundHandshake{ ProtocolVersion: 1337, ServerAddress: "localhost", ServerPort: 25565, NextState: handshaking.StateStatusServerBoundHandshake, - }.Marshal(&hsPk) + }.Marshal(&hsStatusPk) var statusPk protocol.Packet status.ServerBoundRequest{}.Marshal(&statusPk) var pingPk protocol.Packet @@ -40,9 +39,9 @@ func BenchmarkHandleConn_Status(b *testing.B) { pks []protocol.Packet }{ { - name: "normal_handshake", + name: "status_handshake", pks: []protocol.Packet{ - hsPk, + hsStatusPk, statusPk, pingPk, }, @@ -50,16 +49,22 @@ func BenchmarkHandleConn_Status(b *testing.B) { } for _, tc := range tt { + in, out := net.Pipe() sgInChan := make(chan ServerRequest) + srv, err := NewServer(func(cfg *ServerConfig) { + *cfg = ServerConfig{ + Domains: []ServerDomain{ + "localhost", + }, + } + }) + if err != nil { + b.Error(err) + } + sg := serverGateway{ Servers: []*Server{ - NewServer(func(cfg *ServerConfig) { - *cfg = ServerConfig{ - Domains: []ServerDomain{ - "localhost", - }, - } - }), + srv, }, requestChan: sgInChan, responder: mockServerRequestResponder{}, @@ -69,7 +74,6 @@ func BenchmarkHandleConn_Status(b *testing.B) { b.Error(err) } }() - in, out := net.Pipe() c := newConn(out) c.srvReqChan = sgInChan @@ -145,8 +149,8 @@ func TestProxyProtocolhandlePipe(t *testing.T) { testConn := ProxyProtocolTesterConn{c: serverConnIn} reqResponse := ServerRequestResponse{ - ServerConn: newConn(&testConn), - UseProxyProtocol: true, + ServerConn: newConn(&testConn), + SendProxyProtocol: true, } go ir.handlePipe(newConn(&clientConn), reqResponse) @@ -183,8 +187,8 @@ func TestNoProxyProtocolhandlePipe(t *testing.T) { testConn := ProxyProtocolTesterConn{c: serverConnIn} reqResponse := ServerRequestResponse{ - ServerConn: newConn(&testConn), - UseProxyProtocol: false, + ServerConn: newConn(&testConn), + SendProxyProtocol: false, } go ir.handlePipe(newConn(&clientConn), reqResponse) diff --git a/pkg/infrared/protocol/login/serverbound_loginstart.go b/pkg/infrared/protocol/login/serverbound_loginstart.go index 5f69d756..f296972c 100644 --- a/pkg/infrared/protocol/login/serverbound_loginstart.go +++ b/pkg/infrared/protocol/login/serverbound_loginstart.go @@ -13,35 +13,38 @@ const ( ) type ServerBoundLoginStart struct { - Name protocol.String - HasPublicKey protocol.Boolean + Name protocol.String // Added in 1.19; removed in 1.19.3 - Timestamp protocol.Long - PublicKey protocol.ByteArray - Signature protocol.ByteArray + HasSignature protocol.Boolean + Timestamp protocol.Long + PublicKey protocol.ByteArray + Signature protocol.ByteArray // Added in 1.19 - HasPlayerUUID protocol.Boolean + HasPlayerUUID protocol.Boolean // removed in 1.20.2 PlayerUUID protocol.UUID } func (pk ServerBoundLoginStart) Marshal(packet *protocol.Packet, version protocol.Version) { - if version < protocol.Version_1_19 { - packet.Encode( - IDServerBoundLoginStart, - pk.Name, - ) - return - } - fields := make([]protocol.FieldEncoder, 0, 7) - fields = append(fields, pk.Name, pk.HasPublicKey) - if pk.HasPublicKey { - fields = append(fields, pk.Timestamp, pk.PublicKey, pk.Signature) - } - fields = append(fields, pk.HasPlayerUUID) - if pk.HasPlayerUUID { + fields = append(fields, pk.Name) + + switch { + case version >= protocol.Version_1_19 && + version < protocol.Version_1_19_3: + fields = append(fields, pk.HasSignature) + if pk.HasSignature { + fields = append(fields, pk.Timestamp, pk.PublicKey, pk.Signature) + } + fallthrough + case version >= protocol.Version_1_19_3 && + version < protocol.Version_1_20_2: + fields = append(fields, pk.HasPlayerUUID) + if pk.HasPlayerUUID { + fields = append(fields, pk.PlayerUUID) + } + case version >= protocol.Version_1_20_2: fields = append(fields, pk.PlayerUUID) } @@ -61,27 +64,31 @@ func (pk *ServerBoundLoginStart) Unmarshal(packet protocol.Packet, version proto return err } - if version < protocol.Version_1_19 { - return nil - } - - if version < protocol.Version_1_19_3 { - if err := protocol.ScanFields(r, &pk.HasPublicKey); err != nil { + switch { + case version >= protocol.Version_1_19 && + version < protocol.Version_1_19_3: + if err := protocol.ScanFields(r, &pk.HasSignature); err != nil { return err } - if pk.HasPublicKey { + if pk.HasSignature { if err := protocol.ScanFields(r, &pk.Timestamp, &pk.PublicKey, &pk.Signature); err != nil { return err } } - } - - if err := protocol.ScanFields(r, &pk.HasPlayerUUID); err != nil { - return err - } + fallthrough + case version >= protocol.Version_1_19_3 && + version < protocol.Version_1_20_2: + if err := protocol.ScanFields(r, &pk.HasPlayerUUID); err != nil { + return err + } - if pk.HasPlayerUUID { + if pk.HasPlayerUUID { + if err := protocol.ScanFields(r, &pk.PlayerUUID); err != nil { + return err + } + } + case version >= protocol.Version_1_20_2: if err := protocol.ScanFields(r, &pk.PlayerUUID); err != nil { return err } diff --git a/pkg/infrared/protocol/versions.go b/pkg/infrared/protocol/versions.go index f964c681..cd7e08bb 100644 --- a/pkg/infrared/protocol/versions.go +++ b/pkg/infrared/protocol/versions.go @@ -8,6 +8,7 @@ const ( Version_1_18_2 Version = 758 Version_1_19 Version = 759 Version_1_19_3 Version = 761 + Version_1_20_2 Version = 764 ) func (v Version) Name() string { @@ -18,6 +19,8 @@ func (v Version) Name() string { return "1.19" case Version_1_19_3: return "1.19.3" + case Version_1_20_2: + return "1.20.2" default: return strconv.Itoa(int(v)) } diff --git a/pkg/infrared/server.go b/pkg/infrared/server.go index 222c8738..a8f3e0dd 100644 --- a/pkg/infrared/server.go +++ b/pkg/infrared/server.go @@ -15,6 +15,7 @@ import ( ) type ( + ServerID string ServerAddress string ServerDomain string ) @@ -27,50 +28,55 @@ func WithServerConfig(c ServerConfig) ServerConfigFunc { } } -func WithServerDomains(dd ...ServerDomain) ServerConfigFunc { +func WithServerDomains(sd ...ServerDomain) ServerConfigFunc { return func(cfg *ServerConfig) { - cfg.Domains = dd + cfg.Domains = sd } } -func WithServerAddress(addr ServerAddress) ServerConfigFunc { +func WithServerAddresses(addr ...ServerAddress) ServerConfigFunc { return func(cfg *ServerConfig) { - cfg.Address = addr + cfg.Addresses = addr } } type ServerConfig struct { - Domains []ServerDomain `mapstructure:"domains"` - Address ServerAddress `mapstructure:"address"` + Domains []ServerDomain `yaml:"domains"` + Addresses []ServerAddress `yaml:"addresses"` + SendProxyProtocol bool `yaml:"sendProxyProtocol"` } type Server struct { - cfg ServerConfig - statusResponseJSONProvider StatusResponseProvider + cfg ServerConfig + statusRespProv StatusResponseProvider } -func NewServer(fns ...ServerConfigFunc) *Server { +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") + } + srv := &Server{ cfg: cfg, } - srv.statusResponseJSONProvider = &statusResponseProvider{ + srv.statusRespProv = &statusResponseProvider{ server: srv, cacheTTL: 30 * time.Second, statusHash: make(map[protocol.Version]uint64), statusResponseCache: make(map[uint64]*statusCacheEntry), } - return srv + return srv, nil } func (s Server) Dial() (*conn, error) { - c, err := net.Dial("tcp", string(s.cfg.Address)) + c, err := net.Dial("tcp", string(s.cfg.Addresses[0])) if err != nil { return nil, err } @@ -89,10 +95,10 @@ type ServerRequest struct { } type ServerRequestResponse struct { - ServerConn *conn - StatusResponse protocol.Packet - UseProxyProtocol bool - Err error + ServerConn *conn + StatusResponse protocol.Packet + SendProxyProtocol bool + Err error } type serverGateway struct { @@ -167,13 +173,14 @@ func (r DialServerRequestResponder) RespondeToServerRequest(req ServerRequest, s rc, err := srv.Dial() req.ResponseChan <- ServerRequestResponse{ - ServerConn: rc, - Err: err, + ServerConn: rc, + Err: err, + SendProxyProtocol: srv.cfg.SendProxyProtocol, } return } - _, pk, err := srv.statusResponseJSONProvider.StatusResponse(req.ProtocolVersion, req.ReadPks) + _, pk, err := srv.statusRespProv.StatusResponse(req.ProtocolVersion, req.ReadPks) req.ResponseChan <- ServerRequestResponse{ StatusResponse: pk, Err: err, diff --git a/tools/dos/main.go b/tools/dos/main.go index 3266512a..a229ce3f 100644 --- a/tools/dos/main.go +++ b/tools/dos/main.go @@ -34,7 +34,7 @@ func initPayload() { login.ServerBoundLoginStart{ Name: "Test", - HasPublicKey: false, + HasSignature: false, HasPlayerUUID: false, }.Marshal(&pk, protocol.Version_1_19) buf.Reset()