diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5e6cec4..9abd7c6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,14 +13,16 @@ }, // run arguments passed to docker - "runArgs": ["--security-opt", "label=disable"], + "runArgs": [ + "--security-opt", "label=disable" + ], "containerEnv": { - // extensions to preload before other extensions + // extensions to preload before other extensions "PRELOAD_EXTENSIONS": "arrterian.nix-env-selector" }, - // disable command overriding and updating remote user ID + // disable command overriding and updating remote user ID "overrideCommand": false, "userEnvProbe": "loginShell", "updateRemoteUserUID": false, @@ -29,14 +31,18 @@ "onCreateCommand": "nix-shell --command 'echo done building nix dev environment'", // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [8000], + "forwardPorts": [ + 3000 + ], "customizations": { "vscode": { - "extensions": ["arrterian.nix-env-selector"] + "extensions": [ + "arrterian.nix-env-selector" + ] } } // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "go version", -} +} \ No newline at end of file diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a8c4cd9..6c2496d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -32,12 +32,23 @@ jobs: username: imterah password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}} - - name: Build Docker image + - name: Build Docker images run: | - docker build . --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME + docker build ./backend --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME + docker build -t ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME -f MigrationDockerfile . - - name: Upload Docker image + docker build ./sshfrontend --tag ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME + + - name: Upload all Docker images run: | docker tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME ghcr.io/imterah/hermes:latest docker push ghcr.io/imterah/hermes:$GITHUB_REF_NAME docker push ghcr.io/imterah/hermes:latest + + docker tag ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME ghcr.io/imterah/hermes-backend-migration:latest + docker push ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME + docker push ghcr.io/imterah/hermes-backend-migration:latest + + docker tag ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME ghcr.io/imterah/hermes-lom:latest + docker push ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME + docker push ghcr.io/imterah/hermes-lom:latest diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..39e61fd --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +[core] + hooksPath = .githooks/ \ No newline at end of file diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..ec1c700 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +shopt -s globstar +set -e + +ROOT="$(git rev-parse --show-toplevel)" + +pushd $ROOT/api +npx eslint src +popd + +pushd $ROOT/lom +npx eslint src +popd + +# Formatting step +$ROOT/api/node_modules/.bin/prettier --ignore-unknown --write $ROOT/{api,lom}/{eslint.config.js,src/**/*.ts} +git update-index --again +exit 0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d5920a3..f82cd86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,15 @@ # Go artifacts -backend/api/api backend/sshbackend/sshbackend backend/dummybackend/dummybackend -backend/sshappbackend/local-code/remote-bin -backend/sshappbackend/local-code/sshappbackend backend/externalbackendlauncher/externalbackendlauncher -frontend/frontend +backend/api/api # Backup artifacts *.json.gz +# LOM +sshfrontend/keys + # Output out diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..57562cf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "always", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c1fa49f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +## [v1.1.2](https://github.com/imterah/nextnet/tree/v1.1.2) (2024-09-29) + +## [v1.1.1](https://github.com/imterah/nextnet/tree/v1.1.1) (2024-09-29) + +## [v1.1.0](https://github.com/imterah/nextnet/tree/v1.1.0) (2024-09-22) + +**Fixed bugs:** + +- Desktop app fails to build on macOS w/ `nix-shell` [\#1](https://github.com/imterah/nextnet/issues/1) + +**Merged pull requests:** + +- chore\(deps\): bump find-my-way from 8.1.0 to 8.2.2 in /api [\#17](https://github.com/imterah/nextnet/pull/17) +- chore\(deps\): bump axios from 1.6.8 to 1.7.4 in /lom [\#16](https://github.com/imterah/nextnet/pull/16) +- chore\(deps\): bump micromatch from 4.0.5 to 4.0.8 in /lom [\#15](https://github.com/imterah/nextnet/pull/15) +- chore\(deps\): bump braces from 3.0.2 to 3.0.3 in /lom [\#13](https://github.com/imterah/nextnet/pull/13) +- chore\(deps-dev\): bump braces from 3.0.2 to 3.0.3 in /api [\#11](https://github.com/imterah/nextnet/pull/11) +- chore\(deps\): bump ws from 8.17.0 to 8.17.1 in /api [\#10](https://github.com/imterah/nextnet/pull/10) + +## [v1.0.1](https://github.com/imterah/nextnet/tree/v1.0.1) (2024-05-18) + +**Merged pull requests:** + +- Adds public key authentication [\#6](https://github.com/imterah/nextnet/pull/6) +- Add support for eslint [\#5](https://github.com/imterah/nextnet/pull/5) + +## [v1.0.0](https://github.com/imterah/nextnet/tree/v1.0.0) (2024-05-10) + +## [v0.1.1](https://github.com/imterah/nextnet/tree/v0.1.1) (2024-05-05) + +## [v0.1.0](https://github.com/imterah/nextnet/tree/v0.1.0) (2024-05-05) + +**Implemented enhancements:** + +- \(potentially\) Migrate nix shell to nix flake [\#2](https://github.com/imterah/nextnet/issues/2) + +**Closed issues:** + +- add precommit hooks [\#3](https://github.com/imterah/nextnet/issues/3) + +**Merged pull requests:** + +- Reimplements PassyFire as a possible backend [\#4](https://github.com/imterah/nextnet/pull/4) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ae9c525..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM golang:latest AS build -WORKDIR /build -COPY . /build -RUN cd backend; bash build.sh -FROM busybox:stable-glibc AS run -WORKDIR /app -COPY --from=build /build/backend/backends.prod.json /app/backends.json -COPY --from=build /build/backend/api/api /app/hermes -COPY --from=build /build/backend/sshbackend/sshbackend /app/sshbackend -COPY --from=build /build/backend/sshappbackend/local-code/sshappbackend /app/sshappbackend -ENTRYPOINT ["/app/hermes", "--backends-path", "/app/backends.json"] diff --git a/LICENSE b/LICENSE index a085e23..8914588 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Tera +Copyright (c) 2024, Greyson Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MigrationDockerfile b/MigrationDockerfile new file mode 100644 index 0000000..1b71a21 --- /dev/null +++ b/MigrationDockerfile @@ -0,0 +1,25 @@ +FROM golang:latest AS build +WORKDIR /build +COPY backend /build +RUN cd api; go build . +FROM node:22.11.0-bookworm AS run +LABEL org.opencontainers.image.source="https://git.terah.dev/imterah/nextnet" +COPY migration-entrypoint.sh /app/entrypoint.sh +COPY backend-legacy/src /app/legacy/src +COPY backend-legacy/prisma /app/legacy/prisma +COPY backend-legacy/tsconfig.json /app/legacy/ +COPY backend-legacy/package.json /app/legacy/ +COPY backend-legacy/package-lock.json /app/legacy/ +WORKDIR /app/legacy +RUN apt update +RUN apt install postgresql -y +RUN npm install --save-dev +RUN npm run build +RUN rm out/**/*.ts out/**/*.map +RUN rm -rf src +RUN npm prune --production +WORKDIR /app/modern +COPY --from=build /build/api/api /app/modern/hermes +RUN echo "{}" >> /app/modern/backends.json +WORKDIR /app +ENTRYPOINT sh entrypoint.sh diff --git a/README.md b/README.md index 323855d..01d5885 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ 1. Copy and change the default password (or username & db name too) from the template file `prod-docker.env`: ```bash - sed -e "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" -e "s/JWT_SECRET=hermes/JWT_SECRET=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" prod-docker.env > .env + sed "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" prod-docker.env > .env ``` 2. Build the docker stack: `docker compose --env-file .env up -d` diff --git a/apiclient/apiclient.go b/apiclient/apiclient.go deleted file mode 100644 index 8f23a80..0000000 --- a/apiclient/apiclient.go +++ /dev/null @@ -1,21 +0,0 @@ -package apiclient - -import "git.terah.dev/imterah/hermes/apiclient/users" - -type HermesAPIClient struct { - URL string -} - -/// Users - -func (api *HermesAPIClient) UserGetRefreshToken(username *string, email *string, password string) (string, error) { - return users.GetRefreshToken(api.URL, username, email, password) -} - -func (api *HermesAPIClient) UserGetJWTFromToken(refreshToken string) (string, error) { - return users.GetJWTFromToken(api.URL, refreshToken) -} - -func (api *HermesAPIClient) UserCreate(fullName, username, email, password string, isBot bool) (string, error) { - return users.CreateUser(api.URL, fullName, username, email, password, isBot) -} diff --git a/apiclient/backendstructs/struct.go b/apiclient/backendstructs/struct.go deleted file mode 100644 index be4b757..0000000 --- a/apiclient/backendstructs/struct.go +++ /dev/null @@ -1,102 +0,0 @@ -package backendstructs - -type BackendCreationRequest struct { - Token string `validate:"required"` - Name string `validate:"required"` - Description *string `json:"description"` - Backend string `validate:"required"` - BackendParameters interface{} `json:"connectionDetails" validate:"required"` -} - -type BackendLookupRequest struct { - Token string `validate:"required"` - BackendID *uint `json:"id"` - Name *string `json:"name"` - Description *string `json:"description"` - Backend *string `json:"backend"` -} - -type BackendRemovalRequest struct { - Token string `validate:"required"` - BackendID uint `json:"id" validate:"required"` -} - -type ConnectionsRequest struct { - Token string `validate:"required" json:"token"` - Id uint `validate:"required" json:"id"` -} - -type ProxyCreationRequest struct { - Token string `validate:"required" json:"token"` - Name string `validate:"required" json:"name"` - Description *string `json:"description"` - Protocol string `validate:"required" json:"protocol"` - SourceIP string `validate:"required" json:"sourceIP"` - SourcePort uint16 `validate:"required" json:"sourcePort"` - DestinationPort uint16 `validate:"required" json:"destinationPort"` - ProviderID uint `validate:"required" json:"providerID"` - AutoStart *bool `json:"autoStart"` -} - -type ProxyLookupRequest struct { - Token string `validate:"required" json:"token"` - Id *uint `json:"id"` - Name *string `json:"name"` - Description *string `json:"description"` - Protocol *string `json:"protocol"` - SourceIP *string `json:"sourceIP"` - SourcePort *uint16 `json:"sourcePort"` - DestinationPort *uint16 `json:"destPort"` - ProviderID *uint `json:"providerID"` - AutoStart *bool `json:"autoStart"` -} - -type ProxyRemovalRequest struct { - Token string `validate:"required" json:"token"` - ID uint `validate:"required" json:"id"` -} - -type ProxyStartRequest struct { - Token string `validate:"required" json:"token"` - ID uint `validate:"required" json:"id"` -} - -type ProxyStopRequest struct { - Token string `validate:"required" json:"token"` - ID uint `validate:"required" json:"id"` -} - -type UserCreationRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Username string `json:"username" validate:"required"` - - ExistingUserToken string `json:"token"` - IsBot bool `json:"isBot"` -} - -type UserLoginRequest struct { - Username *string `json:"username"` - Email *string `json:"email"` - - Password string `json:"password" validate:"required"` -} - -type UserLookupRequest struct { - Token string `validate:"required"` - UID *uint `json:"id"` - Name *string `json:"name"` - Email *string `json:"email"` - Username *string `json:"username"` - IsBot *bool `json:"isServiceAccount"` -} - -type UserRefreshRequest struct { - Token string `json:"token" validate:"required"` -} - -type UserRemovalRequest struct { - Token string `json:"token" validate:"required"` - UID *uint `json:"uid"` -} diff --git a/apiclient/users/auth.go b/apiclient/users/auth.go deleted file mode 100644 index 91e7f67..0000000 --- a/apiclient/users/auth.go +++ /dev/null @@ -1,99 +0,0 @@ -package users - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "git.terah.dev/imterah/hermes/apiclient/backendstructs" -) - -type refreshTokenResponse struct { - Success bool `json:"success"` - RefreshToken string `json:"refreshToken"` -} - -type jwtTokenResponse struct { - Success bool `json:"success"` - JWT string `json:"token"` -} - -func GetRefreshToken(url string, username, email *string, password string) (string, error) { - body, err := json.Marshal(&backendstructs.UserLoginRequest{ - Username: username, - Email: email, - Password: password, - }) - - if err != nil { - return "", err - } - - res, err := http.Post(fmt.Sprintf("%s/api/v1/users/login", url), "application/json", bytes.NewBuffer(body)) - - if err != nil { - return "", err - } - - bodyContents, err := io.ReadAll(res.Body) - - if err != nil { - return "", fmt.Errorf("failed to read response body: %s", err.Error()) - } - - response := &refreshTokenResponse{} - - if err := json.Unmarshal(bodyContents, response); err != nil { - return "", err - } - - if !response.Success { - return "", fmt.Errorf("failed to get refresh token") - } - - if response.RefreshToken == "" { - return "", fmt.Errorf("refresh token is empty") - } - - return response.RefreshToken, nil -} - -func GetJWTFromToken(url, refreshToken string) (string, error) { - body, err := json.Marshal(&backendstructs.UserRefreshRequest{ - Token: refreshToken, - }) - - if err != nil { - return "", err - } - - res, err := http.Post(fmt.Sprintf("%s/api/v1/users/refresh", url), "application/json", bytes.NewBuffer(body)) - - if err != nil { - return "", err - } - - bodyContents, err := io.ReadAll(res.Body) - - if err != nil { - return "", fmt.Errorf("failed to read response body: %s", err.Error()) - } - - response := &jwtTokenResponse{} - - if err := json.Unmarshal(bodyContents, response); err != nil { - return "", err - } - - if !response.Success { - return "", fmt.Errorf("failed to get JWT token") - } - - if response.JWT == "" { - return "", fmt.Errorf("JWT token is empty") - } - - return response.JWT, nil -} diff --git a/apiclient/users/create.go b/apiclient/users/create.go deleted file mode 100644 index 6e03c58..0000000 --- a/apiclient/users/create.go +++ /dev/null @@ -1,63 +0,0 @@ -package users - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "git.terah.dev/imterah/hermes/apiclient/backendstructs" -) - -type createUserResponse struct { - Error string `json:"error"` - Success bool `json:"success"` - RefreshToken string `json:"refreshToken"` -} - -func CreateUser(url, fullName, username, email, password string, isBot bool) (string, error) { - body, err := json.Marshal(&backendstructs.UserCreationRequest{ - Username: username, - Name: fullName, - Email: email, - Password: password, - IsBot: isBot, - }) - - if err != nil { - return "", err - } - - res, err := http.Post(fmt.Sprintf("%s/api/v1/users/create", url), "application/json", bytes.NewBuffer(body)) - - if err != nil { - return "", err - } - - bodyContents, err := io.ReadAll(res.Body) - - if err != nil { - return "", fmt.Errorf("failed to read response body: %s", err.Error()) - } - - response := &createUserResponse{} - - if err := json.Unmarshal(bodyContents, response); err != nil { - return "", err - } - - if response.Error != "" { - return "", fmt.Errorf("error from server: %s", response.Error) - } - - if !response.Success { - return "", fmt.Errorf("failed to get refresh token") - } - - if response.RefreshToken == "" { - return "", fmt.Errorf("refresh token is empty") - } - - return response.RefreshToken, nil -} diff --git a/backend-legacy/Dockerfile b/backend-legacy/Dockerfile new file mode 100644 index 0000000..8a1df00 --- /dev/null +++ b/backend-legacy/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22.11.0-bookworm +LABEL org.opencontainers.image.source="https://github.com/greysoh/nextnet" +WORKDIR /app/ +COPY src /app/src +COPY prisma /app/prisma +COPY docker-entrypoint.sh /app/ +COPY tsconfig.json /app/ +COPY package.json /app/ +COPY package-lock.json /app/ +RUN npm install --save-dev +RUN npm run build +RUN rm out/**/*.ts out/**/*.map +RUN rm -rf src +RUN npm prune --production +ENTRYPOINT sh docker-entrypoint.sh diff --git a/backend-legacy/dev.env b/backend-legacy/dev.env new file mode 100644 index 0000000..750a474 --- /dev/null +++ b/backend-legacy/dev.env @@ -0,0 +1,7 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="postgresql://nextnet:nextnet@localhost:5432/nextnet?schema=nextnet" \ No newline at end of file diff --git a/backend-legacy/docker-entrypoint.sh b/backend-legacy/docker-entrypoint.sh new file mode 100644 index 0000000..b76e0e1 --- /dev/null +++ b/backend-legacy/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +export NODE_ENV="production" + +if [[ "$DATABASE_URL" == "" ]]; then + export DATABASE_URL="postgresql://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@nextnet-postgres:5432/$POSTGRES_DB?schema=nextnet" +fi + +echo "Welcome to NextNet." +echo "Running database migrations..." +npx prisma migrate deploy +echo "Starting application..." +npm start diff --git a/backend-legacy/eslint.config.js b/backend-legacy/eslint.config.js new file mode 100644 index 0000000..8cd9d68 --- /dev/null +++ b/backend-legacy/eslint.config.js @@ -0,0 +1,19 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + + { + languageOptions: { + globals: globals.node, + }, + + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off", + }, + }, +]; diff --git a/backend-legacy/package-lock.json b/backend-legacy/package-lock.json new file mode 100644 index 0000000..d43e4f7 --- /dev/null +++ b/backend-legacy/package-lock.json @@ -0,0 +1,3302 @@ +{ + "name": "nextnet", + "version": "1.1.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextnet", + "version": "1.1.2", + "license": "BSD-3-Clause", + "dependencies": { + "@fastify/websocket": "^11.0.1", + "@prisma/client": "^6.0.0", + "bcrypt": "^5.1.1", + "fastify": "^5.1.0", + "node-ssh": "^13.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.16.0", + "@types/bcrypt": "^5.0.2", + "@types/node": "^22.10.1", + "@types/ssh2": "^1.15.1", + "@types/ws": "^8.5.13", + "eslint": "^9.16.0", + "globals": "^15.12.0", + "nodemon": "^3.1.7", + "pino-pretty": "^13.0.0", + "prettier": "^3.4.1", + "prisma": "^5.22.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", + "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", + "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/websocket": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.0.1.tgz", + "integrity": "sha512-44yam5+t1I9v09hWBYO+ezV88+mb9Se2BjgERtzB/68+0mGeTfFkjBeDBe2y+ZdiPpeO2rhevhdnfrBm5mqH+Q==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.3", + "fastify-plugin": "^5.0.0", + "ws": "^8.16.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@prisma/client": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.1.tgz", + "integrity": "sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", + "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.16.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", + "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.1", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.3.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause" + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.1.0.tgz", + "integrity": "sha512-0SdUC5AoiSgMSc2Vxwv3WyKzyGMDJRAW/PgNsK1kZrnkO6MeqUIW9ovVg9F2UGIqtIcclYMyeJa4rK6OZc7Jxg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^4.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.1", + "secure-json-parse": "^2.7.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", + "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", + "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/light-my-request": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.3.0.tgz", + "integrity": "sha512-bWTAPJmeWQH5suJNYwG0f5cs0p6ho9e6f1Ppoxv5qMosY+s9Ir2+ZLvvHcgA7VTDop4zl/NCHhOVVqU+kd++Ow==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-ssh": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.0.tgz", + "integrity": "sha512-7vsKR2Bbs66th6IWCy/7SN4MSwlVt+G6QrHB631BjRUM8/LmvDugtYhi0uAmgvHS/+PVurfNBOmELf30rm0MZg==", + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "make-dir": "^3.1.0", + "sb-promise-queue": "^2.1.0", + "sb-scandir": "^3.1.0", + "shell-escape": "^0.2.0", + "ssh2": "^1.14.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", + "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", + "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", + "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sb-promise-queue": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.0.tgz", + "integrity": "sha512-zwq4YuP1FQFkGx2Q7GIkZYZ6PqWpV+bg0nIO1sJhWOyGyhqbj0MsTvK6lCFo5TQwX5pZr6SCQ75e8PCDCuNvkg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/sb-scandir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.0.tgz", + "integrity": "sha512-70BVm2xz9jn94zSQdpvYrEG101/UV9TVGcfWr9T5iob3QhCK4lYXeculfBqPGFv3XTeKgx4dpWyYIDeZUqo4kg==", + "license": "MIT", + "dependencies": { + "sb-promise-queue": "^2.1.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-escape": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", + "integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", + "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.16.0", + "@typescript-eslint/parser": "8.16.0", + "@typescript-eslint/utils": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend-legacy/package.json b/backend-legacy/package.json new file mode 100644 index 0000000..2f8dba9 --- /dev/null +++ b/backend-legacy/package.json @@ -0,0 +1,38 @@ +{ + "name": "nextnet", + "version": "1.1.2", + "description": "Yet another dashboard to manage portforwarding technologies", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "start": "cd out && node --enable-source-maps index.js", + "dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\"" + }, + "keywords": [], + "author": "greysoh", + "license": "BSD-3-Clause", + "devDependencies": { + "@eslint/js": "^9.16.0", + "@types/bcrypt": "^5.0.2", + "@types/node": "^22.10.1", + "@types/ssh2": "^1.15.1", + "@types/ws": "^8.5.13", + "eslint": "^9.16.0", + "globals": "^15.12.0", + "nodemon": "^3.1.7", + "pino-pretty": "^13.0.0", + "prettier": "^3.4.1", + "prisma": "^5.22.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0" + }, + "dependencies": { + "@fastify/websocket": "^11.0.1", + "@prisma/client": "^6.0.0", + "bcrypt": "^5.1.1", + "fastify": "^5.1.0", + "node-ssh": "^13.2.0" + } +} diff --git a/backend-legacy/prisma/migrations/20240421200334_init/migration.sql b/backend-legacy/prisma/migrations/20240421200334_init/migration.sql new file mode 100644 index 0000000..17e7104 --- /dev/null +++ b/backend-legacy/prisma/migrations/20240421200334_init/migration.sql @@ -0,0 +1,53 @@ +-- CreateTable +CREATE TABLE "DesinationProvider" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "backend" TEXT NOT NULL, + "connectionDetails" TEXT NOT NULL, + + CONSTRAINT "DesinationProvider_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ForwardRule" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "sourceIP" TEXT NOT NULL, + "sourcePort" INTEGER NOT NULL, + "destIP" TEXT NOT NULL, + "destPort" INTEGER NOT NULL, + "destProviderID" INTEGER NOT NULL, + "enabled" BOOLEAN NOT NULL, + + CONSTRAINT "ForwardRule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "id" SERIAL NOT NULL, + "permission" TEXT NOT NULL, + "has" BOOLEAN NOT NULL, + "userID" INTEGER NOT NULL, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "password" TEXT NOT NULL, + "rootToken" TEXT, + "isRootServiceAccount" BOOLEAN, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql b/backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql new file mode 100644 index 0000000..a673c64 --- /dev/null +++ b/backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `destIP` on the `ForwardRule` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ForwardRule" DROP COLUMN "destIP"; diff --git a/backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql b/backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql new file mode 100644 index 0000000..a0a108f --- /dev/null +++ b/backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `protocol` to the `ForwardRule` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "ForwardRule" ADD COLUMN "protocol" TEXT NOT NULL; diff --git a/backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql b/backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql new file mode 100644 index 0000000..5af7c52 --- /dev/null +++ b/backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "username" TEXT; diff --git a/backend-legacy/prisma/migrations/migration_lock.toml b/backend-legacy/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/backend-legacy/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/backend-legacy/prisma/schema.prisma b/backend-legacy/prisma/schema.prisma new file mode 100644 index 0000000..486f3e3 --- /dev/null +++ b/backend-legacy/prisma/schema.prisma @@ -0,0 +1,54 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model DesinationProvider { + id Int @id @default(autoincrement()) + + name String + description String? + backend String + connectionDetails String +} + +model ForwardRule { + id Int @id @default(autoincrement()) + + name String + description String? + protocol String + sourceIP String + sourcePort Int + destPort Int + destProviderID Int + enabled Boolean +} + +model Permission { + id Int @id @default(autoincrement()) + + permission String + has Boolean + user User @relation(fields: [userID], references: [id]) + userID Int +} + +model User { + id Int @id @default(autoincrement()) + + email String @unique + username String? // NOT optional in the API, but just for backwards compat + name String + password String // Will be hashed using bcrypt + rootToken String? + isRootServiceAccount Boolean? + permissions Permission[] +} \ No newline at end of file diff --git a/backend-legacy/src/tools/exportDBContents.ts b/backend-legacy/src/tools/exportDBContents.ts new file mode 100644 index 0000000..20c31bc --- /dev/null +++ b/backend-legacy/src/tools/exportDBContents.ts @@ -0,0 +1,52 @@ +import { createReadStream, createWriteStream } from "node:fs"; +import { Readable, pipeline } from "node:stream"; +import { createGzip } from "node:zlib"; +import process from "node:process"; + +import { PrismaClient } from "@prisma/client"; + +const gzip = createGzip(); + +if (process.argv.length <= 2) { + console.error( + "Missing arguments! Usage: node ./out/tools/exportDBContents.js exportPath.json.gz", + ); + process.exit(1); +} + +console.log("Initializing Database..."); +const prisma = new PrismaClient(); +console.log("Initialized Database."); + +console.log("Getting all destinationProviders..."); +const destinationProviders = await prisma.desinationProvider.findMany(); + +console.log("Getting all forwardRules..."); +const forwardRules = await prisma.forwardRule.findMany(); + +console.log("Getting all permissions..."); +const allPermissions = await prisma.permission.findMany(); + +console.log("Getting all users..."); +const users = await prisma.user.findMany(); + +const masterList = JSON.stringify({ + destinationProviders, + forwardRules, + allPermissions, + users, +}); + +const source = new Readable(); +source.push(masterList); +source.push(null); + +const destination = createWriteStream(process.argv[2]); + +pipeline(source, gzip, destination, err => { + if (err) { + console.error("Failed to compress JSON data:", err); + } else { + console.log("Sucesfully saved DB contents."); + } +}); diff --git a/backend-legacy/tsconfig.json b/backend-legacy/tsconfig.json new file mode 100644 index 0000000..d584b3b --- /dev/null +++ b/backend-legacy/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + + "outDir": "./out", + "rootDir": "./src", + + "strict": true, + "esModuleInterop": true, + "sourceMap": true, + + "declaration": true, + "declarationMap": true, + + "strictPropertyInitialization": false, + }, + + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..cd1c390 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:latest AS build +WORKDIR /build +COPY . /build +RUN bash build.sh +FROM busybox:stable AS run +WORKDIR /app +COPY --from=build /build/backends.prod.json /app/backends.json +COPY --from=build /build/api/api /app/hermes +COPY --from=build /build/sshbackend/sshbackend /app/sshbackend +ENTRYPOINT ["/app/hermes", "--backends-path", "/app/backends.json"] diff --git a/backend/api/backendruntime/core.go b/backend/api/backendruntime/core.go deleted file mode 100644 index eac5934..0000000 --- a/backend/api/backendruntime/core.go +++ /dev/null @@ -1,15 +0,0 @@ -package backendruntime - -import "os" - -var ( - AvailableBackends []*Backend - RunningBackends map[uint]*Runtime - TempDir string - shouldLog bool -) - -func init() { - RunningBackends = make(map[uint]*Runtime) - shouldLog = os.Getenv("HERMES_DEVELOPMENT_MODE") != "" || os.Getenv("HERMES_BACKEND_LOGGING_ENABLED") != "" || os.Getenv("HERMES_LOG_LEVEL") == "debug" -} diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index 8d5ca7a..0f871f8 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -6,47 +6,50 @@ import ( "net" "os" "os/exec" - "strings" - "sync" "time" - "git.terah.dev/imterah/hermes/backend/backendlauncher" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backendlauncher" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" ) -// TODO TODO TODO(imterah): -// This code is a mess. This NEEDS to be rearchitected and refactored to work better. Or at the very least, this code needs to be documented heavily. +var ( + AvailableBackends []*Backend + RunningBackends map[uint]*Runtime + TempDir string +) -func handleCommand(command interface{}, sock net.Conn, rtcChan chan interface{}) error { - bytes, err := commonbackend.Marshal(command) +func init() { + RunningBackends = make(map[uint]*Runtime) +} + +func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) { + bytes, err := commonbackend.Marshal(commandType, command) if err != nil { log.Warnf("Failed to marshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to marshal message: %s", err.Error()) - return fmt.Errorf("failed to marshal message: %s", err.Error()) + return } if _, err := sock.Write(bytes); err != nil { log.Warnf("Failed to write message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to write message: %s", err.Error()) - return fmt.Errorf("failed to write message: %s", err.Error()) + return } - data, err := commonbackend.Unmarshal(sock) + _, data, err := commonbackend.Unmarshal(sock) if err != nil { log.Warnf("Failed to unmarshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to unmarshal message: %s", err.Error()) - return fmt.Errorf("failed to unmarshal message: %s", err.Error()) + return } rtcChan <- data - - return nil } func (runtime *Runtime) goRoutineHandler() error { @@ -66,7 +69,7 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debugf("Acquired unix socket at: %s", sockPath) go func() { - log.Debug("Created new Goroutine for socket connection handling") + log.Debug("Creating new goroutine for socket connection handling") for { log.Debug("Waiting for Unix socket connections...") @@ -77,122 +80,56 @@ func (runtime *Runtime) goRoutineHandler() error { return } - log.Debug("Recieved connection. Attempting to figure out backend state...") + log.Debug("Recieved connection. Initializing...") - timeoutChannel := time.After(500 * time.Millisecond) + defer sock.Close() - select { - case <-timeoutChannel: - log.Debug("Timeout reached. Assuming backend is running.") - case hasRestarted, ok := <-runtime.processRestartNotification: - if !ok { - log.Warnf("Failed to get the process restart notification state!") - } - - if hasRestarted { - if runtime.OnCrashCallback == nil { - log.Warn("The backend has restarted for some reason, but we could not run the on crash callback as the callback is not set!") - } else { - log.Debug("We have restarted. Running the restart callback...") - runtime.OnCrashCallback(sock) - } - - log.Debug("Clearing caches...") - runtime.cleanUpPendingCommandProcessingJobs() - runtime.messageBufferLock = sync.Mutex{} - } else { - log.Debug("We have not restarted.") - } - } - - go func() { - log.Debug("Setting up Hermes keepalive Goroutine") - hasFailedBackendRunningCheckAlready := false - - for { - if !runtime.isRuntimeRunning { - return - } - - // Asking for the backend status seems to be a "good-enough" keepalive system. Plus, it provides useful telemetry. - // There isn't a ping command in the backend API, so we have to make do with what we have. - // - // To be safe here, we have to use the proper (yet annoying) facilities to prevent cross-talk, since we're in - // a goroutine, and can't talk directly. This actually has benefits, as the OuterLoop should exit on its own, if we - // encounter a critical error. - statusResponse, err := runtime.ProcessCommand(&commonbackend.BackendStatusRequest{}) - - if err != nil { - log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", err.Error()) - log.Debugf("Attempting to close socket...") - err := sock.Close() - - if err != nil { - log.Debugf("Failed to close socket: %s", err.Error()) - } - - continue - } - - switch responseMessage := statusResponse.(type) { - case *commonbackend.BackendStatusResponse: - if !responseMessage.IsRunning { - if hasFailedBackendRunningCheckAlready { - if responseMessage.Message != "" { - log.Warnf("Backend (in backend keepalive) is up but not active: %s", responseMessage.Message) - } else { - log.Warnf("Backend (in backend keepalive) is up but not active") - } - } - - hasFailedBackendRunningCheckAlready = true - } - default: - log.Errorf("Got illegal response type for backend (in backend keepalive): %T", responseMessage) - log.Debugf("Attempting to close socket...") - err := sock.Close() - - if err != nil { - log.Debugf("Failed to close socket: %s", err.Error()) - } - } - - time.Sleep(5 * time.Second) - } - }() - - OuterLoop: for { - _ = <-runtime.startProcessingNotification - runtime.isRuntimeCurrentlyProcessing = true + commandRaw := <-runtime.RuntimeCommands - for chanIndex, messageData := range runtime.messageBuffer { - if messageData == nil { - continue - } + log.Debug("Got message from server") - err := handleCommand(messageData.Message, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - - runtime.messageBuffer[chanIndex] = nil + switch command := commandRaw.(type) { + case *commonbackend.AddProxy: + handleCommand("addProxy", command, sock, runtime.RuntimeCommands) + case *commonbackend.BackendStatusRequest: + handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.BackendStatusResponse: + handleCommand("backendStatusResponse", command, sock, runtime.RuntimeCommands) + case *commonbackend.CheckClientParameters: + handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) + case *commonbackend.CheckParametersResponse: + handleCommand("checkParametersResponse", command, sock, runtime.RuntimeCommands) + case *commonbackend.CheckServerParameters: + handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyClientConnection: + handleCommand("proxyClientConnection", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyConnectionsRequest: + handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyConnectionsResponse: + handleCommand("proxyConnectionsResponse", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyInstanceResponse: + handleCommand("proxyInstanceResponse", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyInstanceRequest: + handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyStatusRequest: + handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyStatusResponse: + handleCommand("proxyStatusResponse", command, sock, runtime.RuntimeCommands) + case *commonbackend.RemoveProxy: + handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) + case *commonbackend.Start: + handleCommand("start", command, sock, runtime.RuntimeCommands) + case *commonbackend.Stop: + handleCommand("stop", command, sock, runtime.RuntimeCommands) + default: + log.Warnf("Recieved unknown command type from channel: %q", command) + runtime.RuntimeCommands <- fmt.Errorf("unknown command recieved") } - - runtime.isRuntimeCurrentlyProcessing = false } - - sock.Close() } }() - runtime.processRestartNotification <- false - for { log.Debug("Starting process...") @@ -224,14 +161,6 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debug("Sleeping 5 seconds, and then restarting process") time.Sleep(5 * time.Second) - - // NOTE(imterah): This could cause hangs if we're not careful. If the process dies so much that we can't keep up, it should deserve to be hung, really. - // There's probably a better way to do this, but this works. - // - // If this does turn out to be a problem, just increase the Goroutine buffer size. - runtime.processRestartNotification <- true - - log.Debug("Sent off notification.") } } @@ -240,11 +169,7 @@ func (runtime *Runtime) Start() error { return fmt.Errorf("runtime already running") } - runtime.messageBuffer = make([]*messageForBuf, 10) - runtime.messageBufferLock = sync.Mutex{} - - runtime.startProcessingNotification = make(chan bool) - runtime.processRestartNotification = make(chan bool, 1) + runtime.RuntimeCommands = make(chan interface{}) runtime.logger = &writeLogger{ Runtime: runtime, @@ -292,90 +217,6 @@ func (runtime *Runtime) Stop() error { return nil } -func (runtime *Runtime) ProcessCommand(command interface{}) (interface{}, error) { - schedulingAttempts := 0 - var commandChannel chan interface{} - -SchedulingLoop: - for { - if !runtime.isRuntimeRunning { - time.Sleep(10 * time.Millisecond) - } - - if schedulingAttempts > 50 { - return nil, fmt.Errorf("failed to schedule message transmission after 50 tries (REPORT THIS ISSUE)") - } - - runtime.messageBufferLock.Lock() - - // Attempt to find spot in buffer to schedule message transmission - for i, message := range runtime.messageBuffer { - if message != nil { - continue - } - - commandChannel = make(chan interface{}) - - runtime.messageBuffer[i] = &messageForBuf{ - Channel: commandChannel, - Message: command, - } - - runtime.messageBufferLock.Unlock() - break SchedulingLoop - } - - runtime.messageBufferLock.Unlock() - time.Sleep(100 * time.Millisecond) - - schedulingAttempts++ - } - - if !runtime.isRuntimeCurrentlyProcessing { - runtime.startProcessingNotification <- true - } - - // Fetch response and close Channel - response, ok := <-commandChannel - - if !ok { - return nil, fmt.Errorf("failed to read from command channel: recieved signal that is not OK") - } - - close(commandChannel) - - err, ok := response.(error) - - if ok { - return nil, err - } - - return response, nil -} - -func (runtime *Runtime) cleanUpPendingCommandProcessingJobs() { - for messageIndex, message := range runtime.messageBuffer { - if message == nil { - continue - } - - timeoutChannel := time.After(100 * time.Millisecond) - - select { - case <-timeoutChannel: - log.Warn("Message channel is likely running (timed out reading from it without an error)") - close(message.Channel) - case _, ok := <-message.Channel: - if ok { - log.Warn("Message channel is running, but should be stopped (since message is NOT nil!)") - close(message.Channel) - } - } - - runtime.messageBuffer[messageIndex] = nil - } -} - func NewBackend(path string) *Runtime { return &Runtime{ ProcessPath: path, diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index cd4b3b8..68f1316 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -4,9 +4,6 @@ import ( "net" "os/exec" "strings" - "sync" - - "github.com/charmbracelet/log" ) type Backend struct { @@ -14,28 +11,15 @@ type Backend struct { Path string `validate:"required"` } -type messageForBuf struct { - Channel chan interface{} - // TODO(imterah): could this be refactored to just be a []byte instead? Look into this - Message interface{} -} - type Runtime struct { - isRuntimeRunning bool - isRuntimeCurrentlyProcessing bool - startProcessingNotification chan bool - logger *writeLogger - currentProcess *exec.Cmd - currentListener net.Listener - processRestartNotification chan bool + isRuntimeRunning bool + logger *writeLogger + currentProcess *exec.Cmd + currentListener net.Listener - messageBufferLock sync.Mutex - messageBuffer []*messageForBuf - - ProcessPath string - Logs []string - - OnCrashCallback func(sock net.Conn) + ProcessPath string + Logs []string + RuntimeCommands chan interface{} } type writeLogger struct { @@ -44,17 +28,6 @@ type writeLogger struct { func (writer writeLogger) Write(p []byte) (n int, err error) { logSplit := strings.Split(string(p), "\n") - - if shouldLog { - for _, logLine := range logSplit { - if logLine == "" { - continue - } - - log.Debug("spawned backend logs: " + logLine) - } - } - writer.Runtime.Logs = append(writer.Runtime.Logs, logSplit...) return len(p), err diff --git a/backend/api/backup.go b/backend/api/backup.go new file mode 100644 index 0000000..1d4ebd8 --- /dev/null +++ b/backend/api/backup.go @@ -0,0 +1,293 @@ +package main + +import ( + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "git.terah.dev/imterah/hermes/api/dbcore" + "github.com/charmbracelet/log" + "github.com/go-playground/validator/v10" + "github.com/urfave/cli/v2" + "gorm.io/gorm" +) + +// Data structures +type BackupBackend struct { + ID uint `json:"id" validate:"required"` + + Name string `json:"name" validate:"required"` + Description *string `json:"description"` + Backend string `json:"backend" validate:"required"` + BackendParameters string `json:"connectionDetails" validate:"required"` +} + +type BackupProxy struct { + ID uint `json:"id" validate:"required"` + BackendID uint `json:"destProviderID" validate:"required"` + + Name string `json:"name" validate:"required"` + Description *string `json:"description"` + Protocol string `json:"protocol" validate:"required"` + SourceIP string `json:"sourceIP" validate:"required"` + SourcePort uint16 `json:"sourcePort" validate:"required"` + DestinationPort uint16 `json:"destPort" validate:"required"` + AutoStart bool `json:"enabled" validate:"required"` +} + +type BackupPermission struct { + ID uint `json:"id" validate:"required"` + + PermissionNode string `json:"permission" validate:"required"` + HasPermission bool `json:"has" validate:"required"` + UserID uint `json:"userID" validate:"required"` +} + +type BackupUser struct { + ID uint `json:"id" validate:"required"` + + Email string `json:"email" validate:"required"` + Username *string `json:"username"` + Name string `json:"name" validate:"required"` + Password string `json:"password" validate:"required"` + IsBot *bool `json:"isRootServiceAccount"` + + Token *string `json:"rootToken" validate:"required"` +} + +type BackupData struct { + Backends []*BackupBackend `json:"destinationProviders" validate:"required"` + Proxies []*BackupProxy `json:"forwardRules" validate:"required"` + Permissions []*BackupPermission `json:"allPermissions" validate:"required"` + Users []*BackupUser `json:"users" validate:"required"` +} + +// From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text +func stripAllAlphanumeric(s string) string { + var result strings.Builder + for i := 0; i < len(s); i++ { + b := s[i] + if ('a' <= b && b <= 'z') || + ('A' <= b && b <= 'Z') || + ('0' <= b && b <= '9') { + result.WriteByte(b) + } + } + return result.String() +} + +func backupRestoreEntrypoint(cCtx *cli.Context) error { + log.Info("Decompressing backup...") + + backupFile, err := os.Open(cCtx.String("backup-path")) + + if err != nil { + return fmt.Errorf("failed to open backup: %s", err.Error()) + } + + reader, err := gzip.NewReader(backupFile) + + if err != nil { + return fmt.Errorf("failed to initialize Gzip (compression) reader: %s", err.Error()) + } + + backupDataBytes, err := io.ReadAll(reader) + + if err != nil { + return fmt.Errorf("failed to read backup contents: %s", err.Error()) + } + + log.Info("Decompressed backup. Cleaning up...") + + err = reader.Close() + + if err != nil { + return fmt.Errorf("failed to close Gzip reader: %s", err.Error()) + } + + err = backupFile.Close() + + if err != nil { + return fmt.Errorf("failed to close backup: %s", err.Error()) + } + + log.Info("Parsing backup into internal structures...") + + backupData := &BackupData{} + + err = json.Unmarshal(backupDataBytes, backupData) + + if err != nil { + return fmt.Errorf("failed to parse backup: %s", err.Error()) + } + + if err := validator.New().Struct(backupData); err != nil { + return fmt.Errorf("failed to validate backup: %s", err.Error()) + } + + log.Info("Initializing database and opening it...") + + err = dbcore.InitializeDatabase(&gorm.Config{}) + + if err != nil { + log.Fatalf("Failed to initialize database: %s", err) + } + + log.Info("Running database migrations...") + + if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil { + return fmt.Errorf("Failed to run database migrations: %s", err) + } + + log.Info("Restoring database...") + bestEffortOwnerUIDFromBackup := -1 + + log.Info("Attempting to find user to use as owner of resources...") + + for _, user := range backupData.Users { + foundUser := false + failedAdministrationCheck := false + + for _, permission := range backupData.Permissions { + if permission.UserID != user.ID { + continue + } + + foundUser = true + + if !strings.HasPrefix(permission.PermissionNode, "routes.") && permission.PermissionNode != "permissions.see" && !permission.HasPermission { + log.Infof("User with email '%s' and ID of '%d' failed administration check (lacks all permissions required). Attempting to find better user", user.Email, user.ID) + failedAdministrationCheck = true + + break + } + } + + if !foundUser { + log.Warnf("User with email '%s' and ID of '%d' lacks any permissions!", user.Email, user.ID) + continue + } + + if failedAdministrationCheck { + continue + } + + log.Infof("Using user with email '%s', and ID of '%d'", user.Email, user.ID) + bestEffortOwnerUIDFromBackup = int(user.ID) + + break + } + + if bestEffortOwnerUIDFromBackup == -1 { + log.Warnf("Could not find Administrative level user to use as the owner of resources. Using user with email '%s', and ID of '%d'", backupData.Users[0].Email, backupData.Users[0].ID) + bestEffortOwnerUIDFromBackup = int(backupData.Users[0].ID) + } + + var bestEffortOwnerUID uint + + for _, user := range backupData.Users { + log.Infof("Migrating user with email '%s' and ID of '%d'", user.Email, user.ID) + tokens := make([]dbcore.Token, 0) + permissions := make([]dbcore.Permission, 0) + + if user.Token != nil { + tokens = append(tokens, dbcore.Token{ + Token: *user.Token, + DisableExpiry: true, + CreationIPAddr: "127.0.0.1", // We don't know the creation IP address... + }) + } + + for _, permission := range backupData.Permissions { + if permission.UserID != user.ID { + continue + } + + permissions = append(permissions, dbcore.Permission{ + PermissionNode: permission.PermissionNode, + HasPermission: permission.HasPermission, + }) + } + + username := "" + + if user.Username == nil { + username = strings.ToLower(stripAllAlphanumeric(user.Email)) + log.Warnf("User with ID of '%d' doesn't have a username. Derived username from email is '%s' (email is '%s')", user.ID, username, user.Email) + } else { + username = *user.Username + } + + userDatabase := &dbcore.User{ + Email: user.Email, + Username: username, + Name: user.Name, + Password: base64.StdEncoding.EncodeToString([]byte(user.Password)), + IsBot: user.IsBot, + + Tokens: tokens, + Permissions: permissions, + } + + if err := dbcore.DB.Create(userDatabase).Error; err != nil { + log.Errorf("Failed to create user: %s", err.Error()) + continue + } + + if uint(bestEffortOwnerUIDFromBackup) == user.ID { + bestEffortOwnerUID = userDatabase.ID + } + } + + for _, backend := range backupData.Backends { + log.Infof("Migrating backend ID '%d' with name '%s'", backend.ID, backend.Name) + + backendDatabase := &dbcore.Backend{ + UserID: bestEffortOwnerUID, + Name: backend.Name, + Description: backend.Description, + Backend: backend.Backend, + BackendParameters: base64.StdEncoding.EncodeToString([]byte(backend.BackendParameters)), + } + + if err := dbcore.DB.Create(backendDatabase).Error; err != nil { + log.Errorf("Failed to create backend: %s", err.Error()) + continue + } + + log.Infof("Migrating proxies for backend ID '%d'", backend.ID) + + for _, proxy := range backupData.Proxies { + if proxy.BackendID != backend.ID { + continue + } + + log.Infof("Migrating proxy ID '%d' with name '%s'", proxy.ID, proxy.Name) + + proxyDatabase := &dbcore.Proxy{ + BackendID: backendDatabase.ID, + UserID: bestEffortOwnerUID, + + Name: proxy.Name, + Description: proxy.Description, + Protocol: proxy.Protocol, + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestinationPort: proxy.DestinationPort, + AutoStart: proxy.AutoStart, + } + + if err := dbcore.DB.Create(proxyDatabase).Error; err != nil { + log.Errorf("Failed to create proxy: %s", err.Error()) + } + } + } + + log.Info("Successfully upgraded to Hermes from NextNet.") + + return nil +} diff --git a/backend/api/controllers/v1/backends/create.go b/backend/api/controllers/v1/backends/create.go index 314dc3e..2650fb5 100644 --- a/backend/api/controllers/v1/backends/create.go +++ b/backend/api/controllers/v1/backends/create.go @@ -6,13 +6,14 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type BackendCreationRequest struct { @@ -23,114 +24,132 @@ type BackendCreationRequest struct { BackendParameters interface{} `json:"connectionDetails" validate:"required"` } -func SetupCreateBackend(state *state.State) { - state.Engine.POST("/api/v1/backends/create", func(c *gin.Context) { - var req BackendCreationRequest +func CreateBackend(c *gin.Context) { + var req BackendCreationRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := state.JWT.GetUserFromJWT(req.Token) + user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "backends.add") { + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", + "error": err.Error(), }) return - } - - var backendParameters []byte - - switch parameters := req.BackendParameters.(type) { - case string: - backendParameters = []byte(parameters) - case map[string]interface{}: - backendParameters, err = json.Marshal(parameters) - - if err != nil { - log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to prepare parameters", - }) - - return - } - default: - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Invalid type for connectionDetails (recieved %T)", parameters), - }) - - return - } - - var backendRuntimeFilePath string - - for _, runtime := range backendruntime.AvailableBackends { - if runtime.Name == req.Backend { - backendRuntimeFilePath = runtime.Path - } - } - - if backendRuntimeFilePath == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Unsupported backend recieved", - }) - - return - } - - backend := backendruntime.NewBackend(backendRuntimeFilePath) - err = backend.Start() - - if err != nil { - log.Warnf("Failed to start backend: %s", err.Error()) + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to start backend", + "error": "Failed to parse token", }) return } + } - backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{ - Arguments: backendParameters, + if !permissions.UserHasPermission(user, "backends.add") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", }) + return + } + + var backendParameters []byte + + switch parameters := req.BackendParameters.(type) { + case string: + backendParameters = []byte(parameters) + case map[string]interface{}: + backendParameters, err = json.Marshal(parameters) + if err != nil { - log.Warnf("Failed to get response for backend: %s", err.Error()) + log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to prepare parameters", + }) + + return + } + default: + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid type for connectionDetails (recieved %T)", parameters), + }) + + return + } + + var backendRuntimeFilePath string + + for _, runtime := range backendruntime.AvailableBackends { + if runtime.Name == req.Backend { + backendRuntimeFilePath = runtime.Path + } + } + + if backendRuntimeFilePath == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Unsupported backend recieved", + }) + + return + } + + backend := backendruntime.NewBackend(backendRuntimeFilePath) + err = backend.Start() + + if err != nil { + log.Warnf("Failed to start backend: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to start backend", + }) + + return + } + + backend.RuntimeCommands <- &commonbackend.CheckServerParameters{ + Type: "checkServerParameters", + Arguments: backendParameters, + } + + backendParamCheckResponse := <-backend.RuntimeCommands + + switch responseMessage := backendParamCheckResponse.(type) { + case error: + log.Warnf("Failed to get response for backend: %s", responseMessage.Error()) + + err = backend.Stop() + + if err != nil { + log.Warnf("Failed to stop backend: %s", err.Error()) + } + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get status response from backend", + }) + + return + case *commonbackend.CheckParametersResponse: + if responseMessage.InResponseTo != "checkServerParameters" { + log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo) err = backend.Stop() @@ -145,126 +164,108 @@ func SetupCreateBackend(state *state.State) { return } - switch responseMessage := backendParamCheckResponse.(type) { - case *commonbackend.CheckParametersResponse: - if responseMessage.InResponseTo != "checkServerParameters" { - log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo) - - err = backend.Stop() - - if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) - } - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get status response from backend", - }) - - return - } - - if !responseMessage.IsValid { - err = backend.Stop() - - if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) - } - - var errorMessage string - - if responseMessage.Message == "" { - errorMessage = "Unkown error while trying to parse connectionDetails" - } else { - errorMessage = fmt.Sprintf("Invalid backend parameters: %s", responseMessage.Message) - } - - c.JSON(http.StatusBadRequest, gin.H{ - "error": errorMessage, - }) - - return - } - default: - log.Warnf("Got illegal response type for backend: %T", responseMessage) - } - - log.Info("Passed backend checks successfully") - - backendInDatabase := &db.Backend{ - UserID: user.ID, - Name: req.Name, - Description: req.Description, - Backend: req.Backend, - BackendParameters: base64.StdEncoding.EncodeToString(backendParameters), - } - - if result := state.DB.DB.Create(&backendInDatabase); result.Error != nil { - log.Warnf("Failed to create backend: %s", result.Error.Error()) - + if !responseMessage.IsValid { err = backend.Stop() if err != nil { log.Warnf("Failed to stop backend: %s", err.Error()) } - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add backend into database", + var errorMessage string + + if responseMessage.Message == "" { + errorMessage = "Unkown error while trying to parse connectionDetails" + } else { + errorMessage = fmt.Sprintf("Invalid backend parameters: %s", responseMessage.Message) + } + + c.JSON(http.StatusBadRequest, gin.H{ + "error": errorMessage, }) return } + default: + log.Warnf("Got illegal response type for backend: %T", responseMessage) + } - backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{ - Arguments: backendParameters, - }) + log.Info("Passed backend checks successfully") + + backendInDatabase := &dbcore.Backend{ + UserID: user.ID, + Name: req.Name, + Description: req.Description, + Backend: req.Backend, + BackendParameters: base64.StdEncoding.EncodeToString(backendParameters), + } + + if result := dbcore.DB.Create(&backendInDatabase); result.Error != nil { + log.Warnf("Failed to create backend: %s", result.Error.Error()) + + err = backend.Stop() if err != nil { - log.Warnf("Failed to get response for backend: %s", err.Error()) + log.Warnf("Failed to stop backend: %s", err.Error()) + } + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add backend into database", + }) + + return + } + + backend.RuntimeCommands <- &commonbackend.Start{ + Type: "start", + Arguments: backendParameters, + } + + backendStartResponse := <-backend.RuntimeCommands + + switch responseMessage := backendStartResponse.(type) { + case error: + log.Warnf("Failed to get response for backend: %s", responseMessage.Error()) + + err = backend.Stop() + + if err != nil { + log.Warnf("Failed to stop backend: %s", err.Error()) + } + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get status response from backend", + }) + + return + case *commonbackend.BackendStatusResponse: + if !responseMessage.IsRunning { err = backend.Stop() if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) + log.Warnf("Failed to start backend: %s", err.Error()) } - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get status response from backend", + var errorMessage string + + if responseMessage.Message == "" { + errorMessage = "Unkown error while trying to start the backend" + } else { + errorMessage = fmt.Sprintf("Failed to start backend: %s", responseMessage.Message) + } + + c.JSON(http.StatusBadRequest, gin.H{ + "error": errorMessage, }) return } + default: + log.Warnf("Got illegal response type for backend: %T", responseMessage) + } - switch responseMessage := backendStartResponse.(type) { - case *commonbackend.BackendStatusResponse: - if !responseMessage.IsRunning { - err = backend.Stop() + backendruntime.RunningBackends[backendInDatabase.ID] = backend - if err != nil { - log.Warnf("Failed to start backend: %s", err.Error()) - } - - var errorMessage string - - if responseMessage.Message == "" { - errorMessage = "Unkown error while trying to start the backend" - } else { - errorMessage = fmt.Sprintf("Failed to start backend: %s", responseMessage.Message) - } - - c.JSON(http.StatusBadRequest, gin.H{ - "error": errorMessage, - }) - - return - } - default: - log.Warnf("Got illegal response type for backend: %T", responseMessage) - } - - backendruntime.RunningBackends[backendInDatabase.ID] = backend - - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) + c.JSON(http.StatusOK, gin.H{ + "success": true, }) } diff --git a/backend/api/controllers/v1/backends/lookup.go b/backend/api/controllers/v1/backends/lookup.go index 6cbb386..fda1a8e 100644 --- a/backend/api/controllers/v1/backends/lookup.go +++ b/backend/api/controllers/v1/backends/lookup.go @@ -6,12 +6,13 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type BackendLookupRequest struct { @@ -26,9 +27,9 @@ type SanitizedBackend struct { Name string `json:"name"` BackendID uint `json:"id"` OwnerID uint `json:"ownerID"` - Description *string `json:"description,omitempty"` + Description *string `json:"description"` Backend string `json:"backend"` - BackendParameters *string `json:"connectionDetails,omitempty"` + BackendParameters *string `json:"connectionDetails"` Logs []string `json:"logs"` } @@ -37,80 +38,95 @@ type LookupResponse struct { Data []*SanitizedBackend `json:"data"` } -func SetupLookupBackend(state *state.State) { - state.Engine.POST("/api/v1/backends/lookup", func(c *gin.Context) { - var req BackendLookupRequest +func LookupBackend(c *gin.Context) { + var req BackendLookupRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := state.JWT.GetUserFromJWT(req.Token) + user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "backends.visible") { + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", }) return } + } - backends := []db.Backend{} - queryString := []string{} - queryParameters := []interface{}{} + if !permissions.UserHasPermission(user, "backends.visible") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) - if req.BackendID != nil { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, req.BackendID) - } + return + } - if req.Name != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } + backends := []dbcore.Backend{} + queryString := []string{} + queryParameters := []interface{}{} - if req.Description != nil { - queryString = append(queryString, "description = ?") - queryParameters = append(queryParameters, req.Description) - } + if req.BackendID != nil { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, req.BackendID) + } - if req.Backend != nil { - queryString = append(queryString, "is_bot = ?") - queryParameters = append(queryParameters, req.Backend) - } + if req.Name != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } - if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil { - log.Warnf("Failed to get backends: %s", err.Error()) + if req.Description != nil { + queryString = append(queryString, "description = ?") + queryParameters = append(queryParameters, req.Description) + } + + if req.Backend != nil { + queryString = append(queryString, "is_bot = ?") + queryParameters = append(queryParameters, req.Backend) + } + + if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil { + log.Warnf("Failed to get backends: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get backends", + }) + + return + } + + sanitizedBackends := make([]*SanitizedBackend, len(backends)) + hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis") + + for backendIndex, backend := range backends { + foundBackend, ok := backendruntime.RunningBackends[backend.ID] + + if !ok { + log.Warnf("Failed to get backend #%d controller", backend.ID) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get backends", @@ -119,46 +135,29 @@ func SetupLookupBackend(state *state.State) { return } - sanitizedBackends := make([]*SanitizedBackend, len(backends)) - hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis") - - for backendIndex, backend := range backends { - foundBackend, ok := backendruntime.RunningBackends[backend.ID] - - if !ok { - log.Warnf("Failed to get backend #%d controller", backend.ID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get backends", - }) - - return - } - - sanitizedBackends[backendIndex] = &SanitizedBackend{ - BackendID: backend.ID, - OwnerID: backend.UserID, - Name: backend.Name, - Description: backend.Description, - Backend: backend.Backend, - Logs: foundBackend.Logs, - } - - if backend.UserID == user.ID || hasSecretVisibility { - backendParametersBytes, err := base64.StdEncoding.DecodeString(backend.BackendParameters) - - if err != nil { - log.Warnf("Failed to decode base64 backend parameters: %s", err.Error()) - } - - backendParameters := string(backendParametersBytes) - sanitizedBackends[backendIndex].BackendParameters = &backendParameters - } + sanitizedBackends[backendIndex] = &SanitizedBackend{ + BackendID: backend.ID, + OwnerID: backend.UserID, + Name: backend.Name, + Description: backend.Description, + Backend: backend.Backend, + Logs: foundBackend.Logs, } - c.JSON(http.StatusOK, &LookupResponse{ - Success: true, - Data: sanitizedBackends, - }) + if backend.UserID == user.ID || hasSecretVisibility { + backendParametersBytes, err := base64.StdEncoding.DecodeString(backend.BackendParameters) + + if err != nil { + log.Warnf("Failed to decode base64 backend parameters: %s", err.Error()) + } + + backendParameters := string(backendParametersBytes) + sanitizedBackends[backendIndex].BackendParameters = &backendParameters + } + } + + c.JSON(http.StatusOK, &LookupResponse{ + Success: true, + Data: sanitizedBackends, }) } diff --git a/backend/api/controllers/v1/backends/remove.go b/backend/api/controllers/v1/backends/remove.go index 338ccbd..e1ea599 100644 --- a/backend/api/controllers/v1/backends/remove.go +++ b/backend/api/controllers/v1/backends/remove.go @@ -4,12 +4,13 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type BackendRemovalRequest struct { @@ -17,108 +18,106 @@ type BackendRemovalRequest struct { BackendID uint `json:"id" validate:"required"` } -func SetupRemoveBackend(state *state.State) { - state.Engine.POST("/api/v1/backends/remove", func(c *gin.Context) { - var req BackendRemovalRequest +func RemoveBackend(c *gin.Context) { + var req BackendRemovalRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", }) return } + } - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if !permissions.UserHasPermission(user, "backends.remove") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) - return - } + return + } - user, err := state.JWT.GetUserFromJWT(req.Token) + var backend *dbcore.Backend + backendRequest := dbcore.DB.Where("id = ?", req.BackendID).Find(&backend) + + if backendRequest.Error != nil { + log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if backend exists", + }) + + return + } + + backendExists := backendRequest.RowsAffected > 0 + + if !backendExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Backend doesn't exist", + }) + + return + } + + if err := dbcore.DB.Delete(backend).Error; err != nil { + log.Warnf("failed to delete backend: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete backend", + }) + + return + } + + backendInstance, ok := backendruntime.RunningBackends[req.BackendID] + + if ok { + err = backendInstance.Stop() if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "backends.remove") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - var backend *db.Backend - backendRequest := state.DB.DB.Where("id = ?", req.BackendID).Find(&backend) - - if backendRequest.Error != nil { - log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) + log.Warnf("Failed to stop backend: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if backend exists", + "error": "Backend deleted, but failed to stop", }) - return - } - - backendExists := backendRequest.RowsAffected > 0 - - if !backendExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Backend doesn't exist", - }) - - return - } - - if err := state.DB.DB.Delete(backend).Error; err != nil { - log.Warnf("failed to delete backend: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to delete backend", - }) - - return - } - - backendInstance, ok := backendruntime.RunningBackends[req.BackendID] - - if ok { - err = backendInstance.Stop() - - if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Backend deleted, but failed to stop", - }) - - delete(backendruntime.RunningBackends, req.BackendID) - return - } - delete(backendruntime.RunningBackends, req.BackendID) + return } - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) + delete(backendruntime.RunningBackends, req.BackendID) + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, }) } diff --git a/backend/api/controllers/v1/proxies/connections.go b/backend/api/controllers/v1/proxies/connections.go index 7ea42fb..68e3639 100644 --- a/backend/api/controllers/v1/proxies/connections.go +++ b/backend/api/controllers/v1/proxies/connections.go @@ -4,13 +4,14 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type ConnectionsRequest struct { @@ -36,130 +37,127 @@ type ConnectionsResponse struct { Data []*SanitizedConnection `json:"data"` } -func SetupGetConnections(state *state.State) { - state.Engine.POST("/api/v1/forward/connections", func(c *gin.Context) { - var req ConnectionsRequest +func GetConnections(c *gin.Context) { + var req ConnectionsRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.visibleConn") { + user, err := jwtcore.GetUserFromJWT(req.Token) + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", + "error": err.Error(), }) return - } - - var proxy db.Proxy - proxyRequest := state.DB.DB.Where("id = ?", req.Id).First(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error()) + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find forward entry", + "error": "Failed to parse token", }) return } + } - proxyExists := proxyRequest.RowsAffected > 0 + if !permissions.UserHasPermission(user, "routes.visibleConn") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) - if !proxyExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "No forward entry found", - }) + return + } - return - } + var proxy dbcore.Proxy + proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy) - backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID] + if proxyRequest.Error != nil { + log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error()) - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find forward entry", + }) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) + return + } - return - } + proxyExists := proxyRequest.RowsAffected > 0 - backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{}) + if !proxyExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "No forward entry found", + }) - if err != nil { - log.Warnf("Failed to get response for backend: %s", err.Error()) + return + } - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get status response from backend", - }) + backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID] - return - } + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyConnectionsResponse: - sanitizedConnections := []*SanitizedConnection{} + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) - for _, connection := range responseMessage.Connections { - if connection.SourceIP == proxy.SourceIP && connection.SourcePort == proxy.SourcePort && proxy.DestinationPort == proxy.DestinationPort { - sanitizedConnections = append(sanitizedConnections, &SanitizedConnection{ - ClientIP: connection.ClientIP, - Port: connection.ClientPort, + return + } - ConnectionDetails: &ConnectionDetailsForConnection{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - }, - }) - } + backendRuntime.RuntimeCommands <- &commonbackend.ProxyConnectionsRequest{ + Type: "proxyConnectionsRequest", + } + + backendResponse := <-backendRuntime.RuntimeCommands + + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend: %s", responseMessage.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get status response from backend", + }) + case *commonbackend.ProxyConnectionsResponse: + sanitizedConnections := []*SanitizedConnection{} + + for _, connection := range responseMessage.Connections { + if connection.SourceIP == proxy.SourceIP && connection.SourcePort == proxy.SourcePort && proxy.DestinationPort == proxy.DestinationPort { + sanitizedConnections = append(sanitizedConnections, &SanitizedConnection{ + ClientIP: connection.ClientIP, + Port: connection.ClientPort, + + ConnectionDetails: &ConnectionDetailsForConnection{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + }, + }) } - - c.JSON(http.StatusOK, &ConnectionsResponse{ - Success: true, - Data: sanitizedConnections, - }) - default: - log.Warnf("Got illegal response type for backend: %T", responseMessage) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got illegal response type", - }) } - }) + + c.JSON(http.StatusOK, &ConnectionsResponse{ + Success: true, + Data: sanitizedConnections, + }) + default: + log.Warnf("Got illegal response type for backend: %T", responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got illegal response type", + }) + } } diff --git a/backend/api/controllers/v1/proxies/create.go b/backend/api/controllers/v1/proxies/create.go index d790c49..40cea76 100644 --- a/backend/api/controllers/v1/proxies/create.go +++ b/backend/api/controllers/v1/proxies/create.go @@ -4,13 +4,14 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type ProxyCreationRequest struct { @@ -25,153 +26,149 @@ type ProxyCreationRequest struct { AutoStart *bool `json:"autoStart"` } -func SetupCreateProxy(state *state.State) { - state.Engine.POST("/api/v1/forward/create", func(c *gin.Context) { - var req ProxyCreationRequest +func CreateProxy(c *gin.Context) { + var req ProxyCreationRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.add") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - if req.Protocol != "tcp" && req.Protocol != "udp" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Protocol must be either 'tcp' or 'udp'", - }) - - return - } - - var backend db.Backend - backendRequest := state.DB.DB.Where("id = ?", req.ProviderID).First(&backend) - - if backendRequest.Error != nil { - log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if backend exists", - }) - } - - backendExists := backendRequest.RowsAffected > 0 - - if !backendExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Could not find backend", - }) - } - - autoStart := false - - if req.AutoStart != nil { - autoStart = *req.AutoStart - } - - proxy := &db.Proxy{ - UserID: user.ID, - BackendID: req.ProviderID, - Name: req.Name, - Description: req.Description, - Protocol: req.Protocol, - SourceIP: req.SourceIP, - SourcePort: req.SourcePort, - DestinationPort: req.DestinationPort, - AutoStart: autoStart, - } - - if result := state.DB.DB.Create(proxy); result.Error != nil { - log.Warnf("failed to create proxy: %s", result.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add forward rule to database", - }) - - return - } - - if autoStart { - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "id": proxy.ID, - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, - }) - - if err != nil { - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to get response from backend", - }) - - return - } - - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyStatusResponse: - if !responseMessage.IsActive { - log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID) - } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) - } - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "id": proxy.ID, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "routes.add") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + + if req.Protocol != "tcp" && req.Protocol != "udp" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Protocol must be either 'tcp' or 'udp'", + }) + + return + } + + var backend dbcore.Backend + backendRequest := dbcore.DB.Where("id = ?", req.ProviderID).First(&backend) + + if backendRequest.Error != nil { + log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if backend exists", + }) + } + + backendExists := backendRequest.RowsAffected > 0 + + if !backendExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Could not find backend", + }) + } + + autoStart := false + + if req.AutoStart != nil { + autoStart = *req.AutoStart + } + + proxy := &dbcore.Proxy{ + UserID: user.ID, + BackendID: req.ProviderID, + Name: req.Name, + Description: req.Description, + Protocol: req.Protocol, + SourceIP: req.SourceIP, + SourcePort: req.SourcePort, + DestinationPort: req.DestinationPort, + AutoStart: autoStart, + } + + if result := dbcore.DB.Create(proxy); result.Error != nil { + log.Warnf("failed to create proxy: %s", result.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add forward rule to database", + }) + } + + if autoStart { + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "id": proxy.ID, + }) + + return + } + + backend.RuntimeCommands <- &commonbackend.AddProxy{ + Type: "addProxy", + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, + } + + backendResponse := <-backend.RuntimeCommands + + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to get response from backend", + }) + + return + case *commonbackend.ProxyStatusResponse: + if !responseMessage.IsActive { + log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID) + } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + } + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "id": proxy.ID, }) } diff --git a/backend/api/controllers/v1/proxies/lookup.go b/backend/api/controllers/v1/proxies/lookup.go index bf2c3ea..c290e27 100644 --- a/backend/api/controllers/v1/proxies/lookup.go +++ b/backend/api/controllers/v1/proxies/lookup.go @@ -5,11 +5,12 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type ProxyLookupRequest struct { @@ -28,7 +29,7 @@ type ProxyLookupRequest struct { type SanitizedProxy struct { Id uint `json:"id"` Name string `json:"name"` - Description *string `json:"description,omitempty"` + Description *string `json:"description"` Protcol string `json:"protocol"` SourceIP string `json:"sourceIP"` SourcePort uint16 `json:"sourcePort"` @@ -42,143 +43,139 @@ type ProxyLookupResponse struct { Data []*SanitizedProxy `json:"data"` } -func SetupLookupProxy(state *state.State) { - state.Engine.POST("/api/v1/forward/lookup", func(c *gin.Context) { - var req ProxyLookupRequest +func LookupProxy(c *gin.Context) { + var req ProxyLookupRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := state.JWT.GetUserFromJWT(req.Token) + user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.visible") { + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", + "error": err.Error(), }) return - } - - if req.Protocol != nil { - if *req.Protocol != "tcp" && *req.Protocol != "udp" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Protocol specified in body must either be 'tcp' or 'udp'", - }) - - return - } - } - - proxies := []db.Proxy{} - - queryString := []string{} - queryParameters := []interface{}{} - - if req.Id != nil { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, req.Id) - } - - if req.Name != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } - - if req.Description != nil { - queryString = append(queryString, "description = ?") - queryParameters = append(queryParameters, req.Description) - } - - if req.SourceIP != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } - - if req.SourcePort != nil { - queryString = append(queryString, "source_port = ?") - queryParameters = append(queryParameters, req.SourcePort) - } - - if req.DestinationPort != nil { - queryString = append(queryString, "destination_port = ?") - queryParameters = append(queryParameters, req.DestinationPort) - } - - if req.ProviderID != nil { - queryString = append(queryString, "backend_id = ?") - queryParameters = append(queryParameters, req.ProviderID) - } - - if req.AutoStart != nil { - queryString = append(queryString, "auto_start = ?") - queryParameters = append(queryParameters, req.AutoStart) - } - - if req.Protocol != nil { - queryString = append(queryString, "protocol = ?") - queryParameters = append(queryParameters, req.Protocol) - } - - if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil { - log.Warnf("failed to get proxies: %s", err.Error()) + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get proxies", + "error": "Failed to parse token", }) return } + } - sanitizedProxies := make([]*SanitizedProxy, len(proxies)) - - for proxyIndex, proxy := range proxies { - sanitizedProxies[proxyIndex] = &SanitizedProxy{ - Id: proxy.ID, - Name: proxy.Name, - Description: proxy.Description, - Protcol: proxy.Protocol, - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestinationPort: proxy.DestinationPort, - ProviderID: proxy.BackendID, - AutoStart: proxy.AutoStart, - } - } - - c.JSON(http.StatusOK, &ProxyLookupResponse{ - Success: true, - Data: sanitizedProxies, + if !permissions.UserHasPermission(user, "routes.visible") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", }) + + return + } + + if req.Protocol != nil { + if *req.Protocol != "tcp" && *req.Protocol != "udp" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Protocol specified in body must either be 'tcp' or 'udp'", + }) + } + } + + proxies := []dbcore.Proxy{} + + queryString := []string{} + queryParameters := []interface{}{} + + if req.Id != nil { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, req.Id) + } + + if req.Name != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } + + if req.Description != nil { + queryString = append(queryString, "description = ?") + queryParameters = append(queryParameters, req.Description) + } + + if req.SourceIP != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } + + if req.SourcePort != nil { + queryString = append(queryString, "source_port = ?") + queryParameters = append(queryParameters, req.SourcePort) + } + + if req.DestinationPort != nil { + queryString = append(queryString, "destination_port = ?") + queryParameters = append(queryParameters, req.DestinationPort) + } + + if req.ProviderID != nil { + queryString = append(queryString, "backend_id = ?") + queryParameters = append(queryParameters, req.ProviderID) + } + + if req.AutoStart != nil { + queryString = append(queryString, "auto_start = ?") + queryParameters = append(queryParameters, req.AutoStart) + } + + if req.Protocol != nil { + queryString = append(queryString, "protocol = ?") + queryParameters = append(queryParameters, req.Protocol) + } + + if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil { + log.Warnf("failed to get proxies: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get proxies", + }) + + return + } + + sanitizedProxies := make([]*SanitizedProxy, len(proxies)) + + for proxyIndex, proxy := range proxies { + sanitizedProxies[proxyIndex] = &SanitizedProxy{ + Id: proxy.ID, + Name: proxy.Name, + Description: proxy.Description, + Protcol: proxy.Protocol, + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestinationPort: proxy.DestinationPort, + ProviderID: proxy.BackendID, + AutoStart: proxy.AutoStart, + } + } + + c.JSON(http.StatusOK, &ProxyLookupResponse{ + Success: true, + Data: sanitizedProxies, }) } diff --git a/backend/api/controllers/v1/proxies/remove.go b/backend/api/controllers/v1/proxies/remove.go index 304c5c7..c1b68f3 100644 --- a/backend/api/controllers/v1/proxies/remove.go +++ b/backend/api/controllers/v1/proxies/remove.go @@ -4,13 +4,14 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type ProxyRemovalRequest struct { @@ -18,133 +19,135 @@ type ProxyRemovalRequest struct { ID uint `validate:"required" json:"id"` } -func SetupRemoveProxy(state *state.State) { - state.Engine.POST("/api/v1/forward/remove", func(c *gin.Context) { - var req ProxyRemovalRequest +func RemoveProxy(c *gin.Context) { + var req ProxyRemovalRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.remove") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - var proxy *db.Proxy - proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if forward rule exists", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Forward rule doesn't exist", - }) - - return - } - - if err := state.DB.DB.Delete(proxy).Error; err != nil { - log.Warnf("failed to delete proxy: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to delete forward rule", - }) - - return - } - - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) - if err != nil { - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get response from backend. Proxy was still successfully deleted", + "error": "Failed to parse token", }) return } + } - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyStatusResponse: - if responseMessage.IsActive { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to stop proxy. Proxy was still successfully deleted", - }) - } else { - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) - } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + if !permissions.UserHasPermission(user, "routes.remove") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + return + } + + var proxy *dbcore.Proxy + proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy) + + if proxyRequest.Error != nil { + log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if forward rule exists", + }) + + return + } + + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Forward rule doesn't exist", + }) + + return + } + + if err := dbcore.DB.Delete(proxy).Error; err != nil { + log.Warnf("failed to delete proxy: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete forward rule", + }) + + return + } + + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backend.RuntimeCommands <- &commonbackend.RemoveProxy{ + Type: "removeProxy", + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, + } + + backendResponse := <-backend.RuntimeCommands + + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get response from backend. Proxy was still successfully deleted", + }) + + return + case *commonbackend.ProxyStatusResponse: + if responseMessage.IsActive { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got invalid response from backend. Proxy was still successfully deleted", + "error": "Failed to stop proxy. Proxy was still successfully deleted", }) + + return } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got invalid response from backend. Proxy was still successfully deleted", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, }) } diff --git a/backend/api/controllers/v1/proxies/start.go b/backend/api/controllers/v1/proxies/start.go index 1680ddf..ff3e65c 100644 --- a/backend/api/controllers/v1/proxies/start.go +++ b/backend/api/controllers/v1/proxies/start.go @@ -4,13 +4,14 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type ProxyStartRequest struct { @@ -18,119 +19,127 @@ type ProxyStartRequest struct { ID uint `validate:"required" json:"id"` } -func SetupStartProxy(state *state.State) { - state.Engine.POST("/api/v1/forward/start", func(c *gin.Context) { - var req ProxyStartRequest +func StartProxy(c *gin.Context) { + var req ProxyStartRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.start") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - var proxy *db.Proxy - proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if forward rule exists", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Forward rule doesn't exist", - }) - - return - } - - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to get response from backend", + "error": "Failed to parse token", }) - case *commonbackend.ProxyStatusResponse: - if !responseMessage.IsActive { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to start proxy", - }) - } else { - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) - } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got invalid response from backend. Proxy was likely still successfully started", - }) + return } + } + + if !permissions.UserHasPermission(user, "routes.start") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + + var proxy *dbcore.Proxy + proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy) + + if proxyRequest.Error != nil { + log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if forward rule exists", + }) + + return + } + + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Forward rule doesn't exist", + }) + + return + } + + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backend.RuntimeCommands <- &commonbackend.AddProxy{ + Type: "addProxy", + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, + } + + backendResponse := <-backend.RuntimeCommands + + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to get response from backend", + }) + + return + case *commonbackend.ProxyStatusResponse: + if !responseMessage.IsActive { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to start proxy", + }) + + return + } + + break + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got invalid response from backend. Proxy was still successfully deleted", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, }) } diff --git a/backend/api/controllers/v1/proxies/stop.go b/backend/api/controllers/v1/proxies/stop.go index 27d63ce..c3562a5 100644 --- a/backend/api/controllers/v1/proxies/stop.go +++ b/backend/api/controllers/v1/proxies/stop.go @@ -4,13 +4,14 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type ProxyStopRequest struct { @@ -18,119 +19,125 @@ type ProxyStopRequest struct { ID uint `validate:"required" json:"id"` } -func SetupStopProxy(state *state.State) { - state.Engine.POST("/api/v1/forward/stop", func(c *gin.Context) { - var req ProxyStartRequest +func StopProxy(c *gin.Context) { + var req ProxyStopRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.stop") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - var proxy *db.Proxy - proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if forward rule exists", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Forward rule doesn't exist", - }) - - return - } - - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to get response from backend", + "error": "Failed to parse token", }) - case *commonbackend.ProxyStatusResponse: - if responseMessage.IsActive { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to stop proxy", - }) - } else { - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) - } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got invalid response from backend. Proxy was likely still successfully stopped", - }) + return } + } + + if !permissions.UserHasPermission(user, "routes.stop") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + + var proxy *dbcore.Proxy + proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy) + + if proxyRequest.Error != nil { + log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if forward rule exists", + }) + + return + } + + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Forward rule doesn't exist", + }) + + return + } + + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backend.RuntimeCommands <- &commonbackend.RemoveProxy{ + Type: "removeProxy", + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, + } + + backendResponse := <-backend.RuntimeCommands + + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to get response from backend", + }) + + return + case *commonbackend.ProxyStatusResponse: + if responseMessage.IsActive { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to stop proxy", + }) + + return + } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got invalid response from backend. Proxy was still successfully deleted", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, }) } diff --git a/backend/api/controllers/v1/users/create.go b/backend/api/controllers/v1/users/create.go index f39aa95..92cb543 100644 --- a/backend/api/controllers/v1/users/create.go +++ b/backend/api/controllers/v1/users/create.go @@ -7,9 +7,11 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/backend/api/db" - permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" + "github.com/go-playground/validator/v10" + + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + permissionHelper "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" @@ -20,141 +22,142 @@ type UserCreationRequest struct { Email string `validate:"required"` Password string `validate:"required"` Username string `validate:"required"` - IsBot bool + + // TODO: implement support + ExistingUserToken string `json:"token"` + IsBot bool } -func SetupCreateUser(state *state.State) { - state.Engine.POST("/api/v1/users/create", func(c *gin.Context) { - if !signupEnabled && !unsafeSignup { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Signing up is not enabled at this time.", - }) - - return - } - - var req UserCreationRequest - - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - var user *db.User - userRequest := state.DB.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user) - - if userRequest.Error != nil { - log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if user exists", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if userExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "User already exists", - }) - - return - } - - passwordHashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - - if err != nil { - log.Warnf("Failed to generate password for client upon signup: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate password hash", - }) - - return - } - - permissions := []db.Permission{} - - for _, permission := range permissionHelper.DefaultPermissionNodes { - permissionEnabledState := false - - if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" { - permissionEnabledState = true - } - - permissions = append(permissions, db.Permission{ - PermissionNode: permission, - HasPermission: permissionEnabledState, - }) - } - - tokenRandomData := make([]byte, 80) - - if _, err := rand.Read(tokenRandomData); err != nil { - log.Warnf("Failed to read random data to use as token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - user = &db.User{ - Email: req.Email, - Username: req.Username, - Name: req.Name, - IsBot: &req.IsBot, - Password: base64.StdEncoding.EncodeToString(passwordHashed), - Permissions: permissions, - Tokens: []db.Token{ - { - Token: base64.StdEncoding.EncodeToString(tokenRandomData), - DisableExpiry: forceNoExpiryTokens, - CreationIPAddr: c.ClientIP(), - }, - }, - } - - if result := state.DB.DB.Create(&user); result.Error != nil { - log.Warnf("Failed to create user: %s", result.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add user into database", - }) - - return - } - - jwt, err := state.JWT.Generate(user.ID) - - if err != nil { - log.Warnf("Failed to generate JWT: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "token": jwt, - "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), +func CreateUser(c *gin.Context) { + if !signupEnabled && !unsafeSignup { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Signing up is not enabled at this time.", }) + + return + } + + var req UserCreationRequest + + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + var user *dbcore.User + userRequest := dbcore.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User already exists", + }) + + return + } + + passwordHashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + + if err != nil { + log.Warnf("Failed to generate password for client upon signup: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate password hash", + }) + + return + } + + permissions := []dbcore.Permission{} + + for _, permission := range permissionHelper.DefaultPermissionNodes { + permissionEnabledState := false + + if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" { + permissionEnabledState = true + } + + permissions = append(permissions, dbcore.Permission{ + PermissionNode: permission, + HasPermission: permissionEnabledState, + }) + } + + tokenRandomData := make([]byte, 80) + + if _, err := rand.Read(tokenRandomData); err != nil { + log.Warnf("Failed to read random data to use as token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + user = &dbcore.User{ + Email: req.Email, + Username: req.Username, + Name: req.Name, + IsBot: &req.IsBot, + Password: base64.StdEncoding.EncodeToString(passwordHashed), + Permissions: permissions, + Tokens: []dbcore.Token{ + { + Token: base64.StdEncoding.EncodeToString(tokenRandomData), + DisableExpiry: forceNoExpiryTokens, + CreationIPAddr: c.ClientIP(), + }, + }, + } + + if result := dbcore.DB.Create(&user); result.Error != nil { + log.Warnf("Failed to create user: %s", result.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add user into database", + }) + + return + } + + jwt, err := jwtcore.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, + "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), }) } diff --git a/backend/api/controllers/v1/users/login.go b/backend/api/controllers/v1/users/login.go index ea4f2f3..1db7a81 100644 --- a/backend/api/controllers/v1/users/login.go +++ b/backend/api/controllers/v1/users/login.go @@ -6,10 +6,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" "golang.org/x/crypto/bcrypt" ) @@ -20,139 +21,137 @@ type UserLoginRequest struct { Password string `validate:"required"` } -func SetupLoginUser(state *state.State) { - state.Engine.POST("/api/v1/users/login", func(c *gin.Context) { - var req UserLoginRequest +func LoginUser(c *gin.Context) { + var req UserLoginRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - if req.Email == nil && req.Username == nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Missing both email and username in body", - }) - - return - } - - userFindRequestArguments := make([]interface{}, 1) - userFindRequest := "" - - if req.Email != nil { - userFindRequestArguments[0] = &req.Email - userFindRequest += "email = ?" - } - - if req.Username != nil { - userFindRequestArguments[0] = &req.Username - userFindRequest += "username = ?" - } - - var user *db.User - userRequest := state.DB.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user) - - if userRequest.Error != nil { - log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if user exists", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "User not found", - }) - - return - } - - decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password))) - _, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password)) - - if err != nil { - log.Warnf("failed to decode password in database: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse database result for password", - }) - - return - } - - err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password)) - - if err != nil { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Invalid password", - }) - - return - } - - tokenRandomData := make([]byte, 80) - - if _, err := rand.Read(tokenRandomData); err != nil { - log.Warnf("Failed to read random data to use as token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - token := &db.Token{ - UserID: user.ID, - - Token: base64.StdEncoding.EncodeToString(tokenRandomData), - DisableExpiry: forceNoExpiryTokens, - CreationIPAddr: c.ClientIP(), - } - - if result := state.DB.DB.Create(&token); result.Error != nil { - log.Warnf("Failed to create user: %s", result.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add refresh token into database", - }) - - return - } - - jwt, err := state.JWT.Generate(user.ID) - - if err != nil { - log.Warnf("Failed to generate JWT: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "token": jwt, - "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + if req.Email == nil && req.Username == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing both email and username in body", + }) + + return + } + + userFindRequestArguments := make([]interface{}, 1) + userFindRequest := "" + + if req.Email != nil { + userFindRequestArguments[0] = &req.Email + userFindRequest += "email = ?" + } + + if req.Username != nil { + userFindRequestArguments[0] = &req.Username + userFindRequest += "username = ?" + } + + var user *dbcore.User + userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User not found", + }) + + return + } + + decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password))) + _, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password)) + + if err != nil { + log.Warnf("failed to decode password in database: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse database result for password", + }) + + return + } + + err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password)) + + if err != nil { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Invalid password", + }) + + return + } + + tokenRandomData := make([]byte, 80) + + if _, err := rand.Read(tokenRandomData); err != nil { + log.Warnf("Failed to read random data to use as token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + token := &dbcore.Token{ + UserID: user.ID, + + Token: base64.StdEncoding.EncodeToString(tokenRandomData), + DisableExpiry: forceNoExpiryTokens, + CreationIPAddr: c.ClientIP(), + } + + if result := dbcore.DB.Create(&token); result.Error != nil { + log.Warnf("Failed to create user: %s", result.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add refresh token into database", + }) + + return + } + + jwt, err := jwtcore.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, + "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), }) } diff --git a/backend/api/controllers/v1/users/lookup.go b/backend/api/controllers/v1/users/lookup.go index f5c14fc..2432470 100644 --- a/backend/api/controllers/v1/users/lookup.go +++ b/backend/api/controllers/v1/users/lookup.go @@ -5,11 +5,12 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type UserLookupRequest struct { @@ -34,104 +35,102 @@ type LookupResponse struct { Data []*SanitizedUsers `json:"data"` } -func SetupLookupUser(state *state.State) { - state.Engine.POST("/api/v1/users/lookup", func(c *gin.Context) { - var req UserLookupRequest +func LookupUser(c *gin.Context) { + var req UserLookupRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), }) return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - users := []db.User{} - queryString := []string{} - queryParameters := []interface{}{} - - if !permissions.UserHasPermission(user, "users.lookup") { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, user.ID) - } else if permissions.UserHasPermission(user, "users.lookup") && req.UID != nil { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, req.UID) - } - - if req.Name != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } - - if req.Email != nil { - queryString = append(queryString, "email = ?") - queryParameters = append(queryParameters, req.Email) - } - - if req.IsBot != nil { - queryString = append(queryString, "is_bot = ?") - queryParameters = append(queryParameters, req.IsBot) - } - - if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil { - log.Warnf("Failed to get users: %s", err.Error()) + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get users", + "error": "Failed to parse token", }) return } + } - sanitizedUsers := make([]*SanitizedUsers, len(users)) + users := []dbcore.User{} + queryString := []string{} + queryParameters := []interface{}{} - for userIndex, user := range users { - isBot := false + if !permissions.UserHasPermission(user, "users.lookup") { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, user.ID) + } else if permissions.UserHasPermission(user, "users.lookup") && req.UID != nil { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, req.UID) + } - if user.IsBot != nil { - isBot = *user.IsBot - } + if req.Name != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } - sanitizedUsers[userIndex] = &SanitizedUsers{ - UID: user.ID, - Name: user.Name, - Email: user.Email, - Username: user.Username, - IsBot: isBot, - } + if req.Email != nil { + queryString = append(queryString, "email = ?") + queryParameters = append(queryParameters, req.Email) + } + + if req.IsBot != nil { + queryString = append(queryString, "is_bot = ?") + queryParameters = append(queryParameters, req.IsBot) + } + + if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil { + log.Warnf("Failed to get users: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get users", + }) + + return + } + + sanitizedUsers := make([]*SanitizedUsers, len(users)) + + for userIndex, user := range users { + isBot := false + + if user.IsBot != nil { + isBot = *user.IsBot } - c.JSON(http.StatusOK, &LookupResponse{ - Success: true, - Data: sanitizedUsers, - }) + sanitizedUsers[userIndex] = &SanitizedUsers{ + UID: user.ID, + Name: user.Name, + Email: user.Email, + Username: user.Username, + IsBot: isBot, + } + } + + c.JSON(http.StatusOK, &LookupResponse{ + Success: true, + Data: sanitizedUsers, }) } diff --git a/backend/api/controllers/v1/users/refresh.go b/backend/api/controllers/v1/users/refresh.go index ce9fbaf..1bb9c1f 100644 --- a/backend/api/controllers/v1/users/refresh.go +++ b/backend/api/controllers/v1/users/refresh.go @@ -5,114 +5,113 @@ import ( "net/http" "time" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type UserRefreshRequest struct { Token string `validate:"required"` } -func SetupRefreshUserToken(state *state.State) { - state.Engine.POST("/api/v1/users/refresh", func(c *gin.Context) { - var req UserRefreshRequest +func RefreshUserToken(c *gin.Context) { + var req UserRefreshRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - var tokenInDatabase *db.Token - tokenRequest := state.DB.DB.Where("token = ?", req.Token).Find(&tokenInDatabase) - - if tokenRequest.Error != nil { - log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if token exists", - }) - - return - } - - tokenExists := tokenRequest.RowsAffected > 0 - - if !tokenExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Token not found", - }) - - return - } - - // First, we check to make sure that the key expiry is disabled before checking if the key is expired. - // Then, we check if the IP addresses differ, or if it has been 7 days since the token has been created. - if !tokenInDatabase.DisableExpiry && (c.ClientIP() != tokenInDatabase.CreationIPAddr || time.Now().Before(tokenInDatabase.CreatedAt.Add((24*7)*time.Hour))) { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Token has expired", - }) - - tx := state.DB.DB.Delete(tokenInDatabase) - - if tx.Error != nil { - log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error()) - } - - return - } - - // Get the user to check if the user exists before doing anything - var user *db.User - userRequest := state.DB.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user) - - if tokenRequest.Error != nil { - log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find user", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "User not found", - }) - - return - } - - jwt, err := state.JWT.Generate(user.ID) - - if err != nil { - log.Warnf("Failed to generate JWT: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "token": jwt, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + var tokenInDatabase *dbcore.Token + tokenRequest := dbcore.DB.Where("token = ?", req.Token).Find(&tokenInDatabase) + + if tokenRequest.Error != nil { + log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if token exists", + }) + + return + } + + tokenExists := tokenRequest.RowsAffected > 0 + + if !tokenExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Token not found", + }) + + return + } + + // First, we check to make sure that the key expiry is disabled before checking if the key is expired. + // Then, we check if the IP addresses differ, or if it has been 7 days since the token has been created. + if !tokenInDatabase.DisableExpiry && (c.ClientIP() != tokenInDatabase.CreationIPAddr || time.Now().Before(tokenInDatabase.CreatedAt.Add((24*7)*time.Hour))) { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Token has expired", + }) + + tx := dbcore.DB.Delete(tokenInDatabase) + + if tx.Error != nil { + log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error()) + } + + return + } + + // Get the user to check if the user exists before doing anything + var user *dbcore.User + userRequest := dbcore.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user) + + if tokenRequest.Error != nil { + log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find user", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "User not found", + }) + + return + } + + jwt, err := jwtcore.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, }) } diff --git a/backend/api/controllers/v1/users/remove.go b/backend/api/controllers/v1/users/remove.go index 59e460c..7edcf81 100644 --- a/backend/api/controllers/v1/users/remove.go +++ b/backend/api/controllers/v1/users/remove.go @@ -4,11 +4,12 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/permissions" - "git.terah.dev/imterah/hermes/backend/api/state" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" ) type UserRemovalRequest struct { @@ -16,91 +17,89 @@ type UserRemovalRequest struct { UID *uint `json:"uid"` } -func SetupRemoveUser(state *state.State) { - state.Engine.POST("/api/v1/users/remove", func(c *gin.Context) { - var req UserRemovalRequest +func RemoveUser(c *gin.Context) { + var req UserRemovalRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := state.Validator.Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := state.JWT.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - uid := user.ID - - if req.UID != nil { - uid = *req.UID - - if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - } - - // Make sure the user exists first if we have a custom UserID - - if uid != user.ID { - var customUser *db.User - userRequest := state.DB.DB.Where("id = ?", uid).Find(customUser) - - if userRequest.Error != nil { - log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if user exists", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "User doesn't exist", - }) - - return - } - } - - state.DB.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user) - - c.JSON(http.StatusOK, gin.H{ - "success": true, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := jwtcore.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + uid := user.ID + + if req.UID != nil { + uid = *req.UID + + if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + } + + // Make sure the user exists first if we have a custom UserID + + if uid != user.ID { + var customUser *dbcore.User + userRequest := dbcore.DB.Where("id = ?", uid).Find(customUser) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User doesn't exist", + }) + + return + } + } + + dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user) + + c.JSON(http.StatusOK, gin.H{ + "success": true, }) } diff --git a/backend/api/db/db.go b/backend/api/db/db.go deleted file mode 100644 index 295bff8..0000000 --- a/backend/api/db/db.go +++ /dev/null @@ -1,77 +0,0 @@ -package db - -import ( - "fmt" - "os" - - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -type DB struct { - DB *gorm.DB -} - -func New(backend, params string) (*DB, error) { - var err error - - dialector, err := initDialector(backend, params) - - if err != nil { - return nil, fmt.Errorf("failed to initialize physical database: %s", err) - } - - database, err := gorm.Open(dialector) - - if err != nil { - return nil, fmt.Errorf("failed to open database: %s", err) - } - - return &DB{DB: database}, nil -} - -func (db *DB) DoMigrations() error { - if err := db.DB.AutoMigrate(&Proxy{}); err != nil { - return err - } - - if err := db.DB.AutoMigrate(&Backend{}); err != nil { - return err - } - - if err := db.DB.AutoMigrate(&Permission{}); err != nil { - return err - } - - if err := db.DB.AutoMigrate(&Token{}); err != nil { - return err - } - - if err := db.DB.AutoMigrate(&User{}); err != nil { - return err - } - - return nil -} - -func initDialector(backend, params string) (gorm.Dialector, error) { - switch backend { - case "sqlite": - if params == "" { - return nil, fmt.Errorf("sqlite database file not specified") - } - - return sqlite.Open(params), nil - case "postgresql": - if params == "" { - return nil, fmt.Errorf("postgres DSN not specified") - } - - return postgres.Open(params), nil - case "": - return nil, fmt.Errorf("no database backend specified in environment variables") - default: - return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(backend)) - } -} diff --git a/backend/api/db/models.go b/backend/api/db/models.go deleted file mode 100644 index 290cd6e..0000000 --- a/backend/api/db/models.go +++ /dev/null @@ -1,66 +0,0 @@ -package db - -import ( - "gorm.io/gorm" -) - -type Backend struct { - gorm.Model - - UserID uint - - Name string - Description *string - Backend string - BackendParameters string - - Proxies []Proxy -} - -type Proxy struct { - gorm.Model - - BackendID uint - UserID uint - - Name string - Description *string - Protocol string - SourceIP string - SourcePort uint16 - DestinationPort uint16 - AutoStart bool -} - -type Permission struct { - gorm.Model - - PermissionNode string - HasPermission bool - UserID uint -} - -type Token struct { - gorm.Model - - UserID uint - - Token string - DisableExpiry bool - CreationIPAddr string -} - -type User struct { - gorm.Model - - Email string `gorm:"unique"` - Username string `gorm:"unique"` - Name string - Password string - IsBot *bool - - Permissions []Permission - OwnedProxies []Proxy - OwnedBackends []Backend - Tokens []Token -} diff --git a/backend/api/dbcore/db.go b/backend/api/dbcore/db.go new file mode 100644 index 0000000..b5e0676 --- /dev/null +++ b/backend/api/dbcore/db.go @@ -0,0 +1,142 @@ +package dbcore + +import ( + "fmt" + "os" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type Backend struct { + gorm.Model + + UserID uint + + Name string + Description *string + Backend string + BackendParameters string + + Proxies []Proxy +} + +type Proxy struct { + gorm.Model + + BackendID uint + UserID uint + + Name string + Description *string + Protocol string + SourceIP string + SourcePort uint16 + DestinationPort uint16 + AutoStart bool +} + +type Permission struct { + gorm.Model + + PermissionNode string + HasPermission bool + UserID uint +} + +type Token struct { + gorm.Model + + UserID uint + + Token string + DisableExpiry bool + CreationIPAddr string +} + +type User struct { + gorm.Model + + Email string `gorm:"unique"` + Username string `gorm:"unique"` + Name string + Password string + IsBot *bool + + Permissions []Permission + OwnedProxies []Proxy + OwnedBackends []Backend + Tokens []Token +} + +var DB *gorm.DB + +func InitializeDatabaseDialector() (gorm.Dialector, error) { + databaseBackend := os.Getenv("HERMES_DATABASE_BACKEND") + + switch databaseBackend { + case "sqlite": + filePath := os.Getenv("HERMES_SQLITE_FILEPATH") + + if filePath == "" { + return nil, fmt.Errorf("sqlite database file not specified (missing HERMES_SQLITE_FILEPATH)") + } + + return sqlite.Open(filePath), nil + case "postgresql": + postgresDSN := os.Getenv("HERMES_POSTGRES_DSN") + + if postgresDSN == "" { + return nil, fmt.Errorf("postgres DSN not specified (missing HERMES_POSTGRES_DSN)") + } + + return postgres.Open(postgresDSN), nil + case "": + return nil, fmt.Errorf("no database backend specified in environment variables (missing HERMES_DATABASE_BACKEND)") + default: + return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(databaseBackend)) + } +} + +func InitializeDatabase(config *gorm.Config) error { + var err error + + dialector, err := InitializeDatabaseDialector() + + if err != nil { + return fmt.Errorf("failed to initialize physical database: %s", err) + } + + DB, err = gorm.Open(dialector, config) + + if err != nil { + return fmt.Errorf("failed to open database: %s", err) + } + + return nil +} + +func DoDatabaseMigrations(db *gorm.DB) error { + if err := db.AutoMigrate(&Proxy{}); err != nil { + return err + } + + if err := db.AutoMigrate(&Backend{}); err != nil { + return err + } + + if err := db.AutoMigrate(&Permission{}); err != nil { + return err + } + + if err := db.AutoMigrate(&Token{}); err != nil { + return err + } + + if err := db.AutoMigrate(&User{}); err != nil { + return err + } + + return nil +} diff --git a/backend/api/jwt/jwt.go b/backend/api/jwt/jwt.go deleted file mode 100644 index 40e011b..0000000 --- a/backend/api/jwt/jwt.go +++ /dev/null @@ -1,107 +0,0 @@ -package jwt - -import ( - "errors" - "fmt" - "strconv" - "time" - - "git.terah.dev/imterah/hermes/backend/api/db" - "github.com/golang-jwt/jwt/v5" -) - -var ( - DevelopmentModeTimings = time.Duration(60*24) * time.Minute - NormalModeTimings = time.Duration(3) * time.Minute -) - -type JWTCore struct { - Key []byte - Database *db.DB - TimeMultiplier time.Duration -} - -func New(key []byte, database *db.DB, timeMultiplier time.Duration) *JWTCore { - jwtCore := &JWTCore{ - Key: key, - Database: database, - TimeMultiplier: timeMultiplier, - } - - return jwtCore -} - -func (jwtCore *JWTCore) Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) { - return jwt.Parse(tokenString, jwtCore.jwtKeyCallback, options...) -} - -func (jwtCore *JWTCore) GetUserFromJWT(token string) (*db.User, error) { - if jwtCore.Database == nil { - return nil, fmt.Errorf("database is not initialized") - } - - parsedJWT, err := jwtCore.Parse(token) - - if err != nil { - if errors.Is(err, jwt.ErrTokenExpired) { - return nil, fmt.Errorf("token is expired") - } else { - return nil, err - } - } - - audience, err := parsedJWT.Claims.GetAudience() - - if err != nil { - return nil, err - } - - if len(audience) < 1 { - return nil, fmt.Errorf("audience is too small") - } - - uid, err := strconv.Atoi(audience[0]) - - if err != nil { - return nil, err - } - - user := &db.User{} - userRequest := jwtCore.Database.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user) - - if userRequest.Error != nil { - return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error()) - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - return user, fmt.Errorf("user does not exist") - } - - return user, nil -} - -func (jwtCore *JWTCore) Generate(uid uint) (string, error) { - currentJWTTime := jwt.NewNumericDate(time.Now()) - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtCore.TimeMultiplier)), - IssuedAt: currentJWTTime, - NotBefore: currentJWTTime, - // Convert the user ID to a string, and then set it as the audience parameters only value (there's only 1 user per key) - Audience: []string{strconv.Itoa(int(uid))}, - }) - - signedToken, err := token.SignedString(jwtCore.Key) - - if err != nil { - return "", err - } - - return signedToken, nil -} - -func (jwtCore *JWTCore) jwtKeyCallback(*jwt.Token) (any, error) { - return jwtCore.Key, nil -} diff --git a/backend/api/jwtcore/jwt.go b/backend/api/jwtcore/jwt.go new file mode 100644 index 0000000..44c0f05 --- /dev/null +++ b/backend/api/jwtcore/jwt.go @@ -0,0 +1,117 @@ +package jwtcore + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + "strconv" + "time" + + "git.terah.dev/imterah/hermes/api/dbcore" + "github.com/golang-jwt/jwt/v5" +) + +var ( + JWTKey []byte + developmentMode bool +) + +func SetupJWT() error { + var err error + jwtDataString := os.Getenv("HERMES_JWT_SECRET") + + if jwtDataString == "" { + return fmt.Errorf("JWT secret isn't set (missing HERMES_JWT_SECRET)") + } + + if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" { + JWTKey, err = base64.StdEncoding.DecodeString(jwtDataString) + + if err != nil { + return fmt.Errorf("failed to decode base64 JWT: %s", err.Error()) + } + } else { + JWTKey = []byte(jwtDataString) + } + + if os.Getenv("HERMES_DEVELOPMENT_MODE") != "" { + developmentMode = true + } + + return nil +} + +func Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) { + return jwt.Parse(tokenString, JWTKeyCallback, options...) +} + +func GetUserFromJWT(token string) (*dbcore.User, error) { + parsedJWT, err := Parse(token) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, fmt.Errorf("token is expired") + } else { + return nil, err + } + } + + audience, err := parsedJWT.Claims.GetAudience() + + if err != nil { + return nil, err + } + + if len(audience) < 1 { + return nil, fmt.Errorf("audience is too small") + } + + uid, err := strconv.Atoi(audience[0]) + + if err != nil { + return nil, err + } + + user := &dbcore.User{} + userRequest := dbcore.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user) + + if userRequest.Error != nil { + return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error()) + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + return user, fmt.Errorf("user does not exist") + } + + return user, nil +} + +func Generate(uid uint) (string, error) { + timeMultiplier := 3 + + if developmentMode { + timeMultiplier = 60 * 24 + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeMultiplier) * time.Minute)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Audience: []string{strconv.Itoa(int(uid))}, + }) + + signedToken, err := token.SignedString(JWTKey) + + if err != nil { + return "", err + } + + return signedToken, nil +} + +func JWTKeyCallback(*jwt.Token) (interface{}, error) { + return JWTKey, nil +} diff --git a/backend/api/main.go b/backend/api/main.go index 1438af8..54e83d5 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -4,24 +4,22 @@ import ( "encoding/base64" "encoding/json" "fmt" - "net" "os" "path" "path/filepath" "strings" - "time" - "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends" - "git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies" - "git.terah.dev/imterah/hermes/backend/api/controllers/v1/users" - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/jwt" - "git.terah.dev/imterah/hermes/backend/api/state" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/api/backendruntime" + "git.terah.dev/imterah/hermes/api/controllers/v1/backends" + "git.terah.dev/imterah/hermes/api/controllers/v1/proxies" + "git.terah.dev/imterah/hermes/api/controllers/v1/users" + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/urfave/cli/v2" + "gorm.io/gorm" ) func apiEntrypoint(cCtx *cli.Context) error { @@ -35,26 +33,7 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Info("Hermes is initializing...") log.Debug("Initializing database and opening it...") - databaseBackendName := os.Getenv("HERMES_DATABASE_BACKEND") - var databaseBackendParams string - - if databaseBackendName == "sqlite" { - databaseBackendParams = os.Getenv("HERMES_SQLITE_FILEPATH") - - if databaseBackendParams == "" { - log.Fatal("HERMES_SQLITE_FILEPATH is not set") - } - } else if databaseBackendName == "postgresql" { - databaseBackendParams = os.Getenv("HERMES_POSTGRES_DSN") - - if databaseBackendParams == "" { - log.Fatal("HERMES_POSTGRES_DSN is not set") - } - } else { - log.Fatalf("Unsupported database backend: %s", databaseBackendName) - } - - dbInstance, err := db.New(databaseBackendName, databaseBackendParams) + err := dbcore.InitializeDatabase(&gorm.Config{}) if err != nil { log.Fatalf("Failed to initialize database: %s", err) @@ -62,38 +41,16 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Debug("Running database migrations...") - if err := dbInstance.DoMigrations(); err != nil { + if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil { return fmt.Errorf("Failed to run database migrations: %s", err) } log.Debug("Initializing the JWT subsystem...") - jwtDataString := os.Getenv("HERMES_JWT_SECRET") - var jwtKey []byte - var jwtValidityTimeDuration time.Duration - - if jwtDataString == "" { - log.Fatalf("HERMES_JWT_SECRET is not set") + if err := jwtcore.SetupJWT(); err != nil { + return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error()) } - if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" { - jwtKey, err = base64.StdEncoding.DecodeString(jwtDataString) - - if err != nil { - log.Fatalf("Failed to decode base64 JWT: %s", err.Error()) - } - } else { - jwtKey = []byte(jwtDataString) - } - - if developmentMode { - jwtValidityTimeDuration = jwt.DevelopmentModeTimings - } else { - jwtValidityTimeDuration = jwt.NormalModeTimings - } - - jwtInstance := jwt.New(jwtKey, dbInstance, jwtValidityTimeDuration) - log.Debug("Initializing the backend subsystem...") backendMetadataPath := cCtx.String("backends-path") @@ -118,9 +75,9 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Debug("Enumerating backends...") - backendList := []db.Backend{} + backendList := []dbcore.Backend{} - if err := dbInstance.DB.Find(&backendList).Error; err != nil { + if err := dbcore.DB.Find(&backendList).Error; err != nil { return fmt.Errorf("Failed to enumerate backends: %s", err.Error()) } @@ -141,94 +98,6 @@ func apiEntrypoint(cCtx *cli.Context) error { } backendInstance := backendruntime.NewBackend(backendRuntimeFilePath) - - backendInstance.OnCrashCallback = func(conn net.Conn) { - backendParameters, err := base64.StdEncoding.DecodeString(backend.BackendParameters) - - if err != nil { - log.Errorf("Failed to decode backend parameters for backend #%d: %s", backend.ID, err.Error()) - return - } - - marshalledStartCommand, err := commonbackend.Marshal(&commonbackend.Start{ - Arguments: backendParameters, - }) - - if err != nil { - log.Errorf("Failed to marshal start command for backend #%d: %s", backend.ID, err.Error()) - return - } - - if _, err := conn.Write(marshalledStartCommand); err != nil { - log.Errorf("Failed to send start command for backend #%d: %s", backend.ID, err.Error()) - return - } - - backendResponse, err := commonbackend.Unmarshal(conn) - - if err != nil { - log.Errorf("Failed to get start command response for backend #%d: %s", backend.ID, err.Error()) - return - } - - switch responseMessage := backendResponse.(type) { - case *commonbackend.BackendStatusResponse: - if !responseMessage.IsRunning { - log.Errorf("Failed to start backend #%d: %s", backend.ID, responseMessage.Message) - return - } - - log.Infof("Backend #%d has been reinitialized successfully", backend.ID) - } - - log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID) - - autoStartProxies := []db.Proxy{} - - if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { - log.Errorf("Failed to query proxies to autostart: %s", err.Error()) - return - } - - for _, proxy := range autoStartProxies { - log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) - - marhalledCommand, err := commonbackend.Marshal(&commonbackend.AddProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, - }) - - if err != nil { - log.Errorf("Failed to marshal proxy adding request for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) - continue - } - - if _, err := conn.Write(marhalledCommand); err != nil { - log.Errorf("Failed to send proxy adding request for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) - continue - } - - backendResponse, err := commonbackend.Unmarshal(conn) - - if err != nil { - log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) - continue - } - - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyStatusResponse: - if !responseMessage.IsActive { - log.Warnf("Failed to start proxy for backend #%d and route #%d", proxy.BackendID, proxy.ID) - } - default: - log.Errorf("Got illegal response type for backend #%d and proxy #%d: %T", proxy.BackendID, proxy.ID, responseMessage) - continue - } - } - } - err = backendInstance.Start() if err != nil { @@ -243,12 +112,16 @@ func apiEntrypoint(cCtx *cli.Context) error { continue } - backendStartResponse, err := backendInstance.ProcessCommand(&commonbackend.Start{ + backendInstance.RuntimeCommands <- &commonbackend.Start{ + Type: "start", Arguments: backendParameters, - }) + } - if err != nil { - log.Warnf("Failed to get response for backend #%d: %s", backend.ID, err.Error()) + backendStartResponse := <-backendInstance.RuntimeCommands + + switch responseMessage := backendStartResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", backend.ID, responseMessage.Error()) err = backendInstance.Stop() @@ -257,9 +130,6 @@ func apiEntrypoint(cCtx *cli.Context) error { } continue - } - - switch responseMessage := backendStartResponse.(type) { case *commonbackend.BackendStatusResponse: if !responseMessage.IsRunning { err = backendInstance.Stop() @@ -285,9 +155,9 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Infof("Successfully initialized backend #%d", backend.ID) - autoStartProxies := []db.Proxy{} + autoStartProxies := []dbcore.Proxy{} - if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { + if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { log.Errorf("Failed to query proxies to autostart: %s", err.Error()) continue } @@ -295,19 +165,20 @@ func apiEntrypoint(cCtx *cli.Context) error { for _, proxy := range autoStartProxies { log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) - backendResponse, err := backendInstance.ProcessCommand(&commonbackend.AddProxy{ + backendInstance.RuntimeCommands <- &commonbackend.AddProxy{ + Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, Protocol: proxy.Protocol, - }) - - if err != nil { - log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) - continue } + backendResponse := <-backendInstance.RuntimeCommands + switch responseMessage := backendResponse.(type) { + case error: + log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, responseMessage.Error()) + continue case *commonbackend.ProxyStatusResponse: if !responseMessage.IsActive { log.Warnf("Failed to start proxy for backend #%d and route #%d", proxy.BackendID, proxy.ID) @@ -351,25 +222,23 @@ func apiEntrypoint(cCtx *cli.Context) error { engine.SetTrustedProxies(nil) } - state := state.New(dbInstance, jwtInstance, engine) - // Initialize routes - users.SetupCreateUser(state) - users.SetupLoginUser(state) - users.SetupRefreshUserToken(state) - users.SetupRemoveUser(state) - users.SetupLookupUser(state) + engine.POST("/api/v1/users/create", users.CreateUser) + engine.POST("/api/v1/users/login", users.LoginUser) + engine.POST("/api/v1/users/refresh", users.RefreshUserToken) + engine.POST("/api/v1/users/remove", users.RemoveUser) + engine.POST("/api/v1/users/lookup", users.LookupUser) - backends.SetupCreateBackend(state) - backends.SetupRemoveBackend(state) - backends.SetupLookupBackend(state) + engine.POST("/api/v1/backends/create", backends.CreateBackend) + engine.POST("/api/v1/backends/remove", backends.RemoveBackend) + engine.POST("/api/v1/backends/lookup", backends.LookupBackend) - proxies.SetupCreateProxy(state) - proxies.SetupRemoveProxy(state) - proxies.SetupLookupProxy(state) - proxies.SetupStartProxy(state) - proxies.SetupStopProxy(state) - proxies.SetupGetConnections(state) + engine.POST("/api/v1/forward/create", proxies.CreateProxy) + engine.POST("/api/v1/forward/lookup", proxies.LookupProxy) + engine.POST("/api/v1/forward/remove", proxies.RemoveProxy) + engine.POST("/api/v1/forward/start", proxies.StartProxy) + engine.POST("/api/v1/forward/stop", proxies.StopProxy) + engine.POST("/api/v1/forward/connections", proxies.GetConnections) log.Infof("Listening on '%s'", listeningAddress) err = engine.Run(listeningAddress) @@ -406,6 +275,22 @@ func main() { app := &cli.App{ Name: "hermes", Usage: "port forwarding across boundaries", + Commands: []*cli.Command{ + { + Name: "import", + Usage: "imports from legacy NextNet/Hermes source", + Aliases: []string{"i"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "backup-path", + Aliases: []string{"bp"}, + Usage: "path to the backup file", + Required: true, + }, + }, + Action: backupRestoreEntrypoint, + }, + }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "backends-path", diff --git a/backend/api/permissions/permission_nodes.go b/backend/api/permissions/permission_nodes.go index f13e80a..812fafa 100644 --- a/backend/api/permissions/permission_nodes.go +++ b/backend/api/permissions/permission_nodes.go @@ -1,6 +1,6 @@ package permissions -import "git.terah.dev/imterah/hermes/backend/api/db" +import "git.terah.dev/imterah/hermes/api/dbcore" var DefaultPermissionNodes []string = []string{ "routes.add", @@ -27,7 +27,7 @@ var DefaultPermissionNodes []string = []string{ "users.edit", } -func UserHasPermission(user *db.User, node string) bool { +func UserHasPermission(user *dbcore.User, node string) bool { for _, permission := range user.Permissions { if permission.PermissionNode == node && permission.HasPermission { return true diff --git a/backend/api/state/state.go b/backend/api/state/state.go deleted file mode 100644 index 754ff56..0000000 --- a/backend/api/state/state.go +++ /dev/null @@ -1,24 +0,0 @@ -package state - -import ( - "git.terah.dev/imterah/hermes/backend/api/db" - "git.terah.dev/imterah/hermes/backend/api/jwt" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" -) - -type State struct { - DB *db.DB - JWT *jwt.JWTCore - Engine *gin.Engine - Validator *validator.Validate -} - -func New(db *db.DB, jwt *jwt.JWTCore, engine *gin.Engine) *State { - return &State{ - DB: db, - JWT: jwt, - Engine: engine, - Validator: validator.New(), - } -} diff --git a/backend/backends.dev.json b/backend/backends.dev.json index f314c69..9ff52ca 100644 --- a/backend/backends.dev.json +++ b/backend/backends.dev.json @@ -3,10 +3,6 @@ "name": "ssh", "path": "./sshbackend/sshbackend" }, - { - "name": "sshapp", - "path": "./sshappbackend/local-code/sshappbackend" - }, { "name": "dummy", "path": "./dummybackend/dummybackend" diff --git a/backend/backends.prod.json b/backend/backends.prod.json index 0ccfedc..9a9a09e 100644 --- a/backend/backends.prod.json +++ b/backend/backends.prod.json @@ -2,9 +2,5 @@ { "name": "ssh", "path": "./sshbackend" - }, - { - "name": "sshapp", - "path": "./sshappbackend" } ] diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index afa3147..2631209 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -1,10 +1,11 @@ package backendutil import ( + "fmt" "net" "os" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" ) @@ -17,14 +18,9 @@ type BackendApplicationHelper struct { func (helper *BackendApplicationHelper) Start() error { log.Debug("BackendApplicationHelper is starting") - err := ConfigureProfiling() - - if err != nil { - return err - } - log.Debug("Currently waiting for Unix socket connection...") + var err error helper.socket, err = net.Dial("unix", helper.SocketPath) if err != nil { @@ -34,15 +30,21 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") for { - commandRaw, err := commonbackend.Unmarshal(helper.socket) + commandType, commandRaw, err := commonbackend.Unmarshal(helper.socket) if err != nil { return err } - switch command := commandRaw.(type) { - case *commonbackend.Start: - ok, err := helper.Backend.StartBackend(command.Arguments) + switch commandType { + case "start": + command, ok := commandRaw.(*commonbackend.Start) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StartBackend(command.Arguments) var ( message string @@ -57,12 +59,13 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ + Type: "backendStatusResponse", IsRunning: ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response) + responseMarshalled, err := commonbackend.Marshal(response.Type, response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -70,37 +73,14 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case *commonbackend.BackendStatusRequest: - ok, err := helper.Backend.GetBackendStatus() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.Stop: - ok, err := helper.Backend.StopBackend() + case "stop": + _, ok := commandRaw.(*commonbackend.Stop) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StopBackend() var ( message string @@ -115,12 +95,13 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ + Type: "backendStatusResponse", IsRunning: !ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response) + responseMarshalled, err := commonbackend.Marshal(response.Type, response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -128,19 +109,26 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case *commonbackend.AddProxy: - ok, err := helper.Backend.StartProxy(command) + case "addProxy": + command, ok := commandRaw.(*commonbackend.AddProxy) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StartProxy(command) var hasAnyFailed bool - if err != nil { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) - hasAnyFailed = true - } else if !ok { + if !ok { log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) hasAnyFailed = true + } else if err != nil { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) + hasAnyFailed = true } response := &commonbackend.ProxyStatusResponse{ + Type: "proxyStatusResponse", SourceIP: command.SourceIP, SourcePort: command.SourcePort, DestPort: command.DestPort, @@ -148,7 +136,7 @@ func (helper *BackendApplicationHelper) Start() error { IsActive: !hasAnyFailed, } - responseMarshalled, err := commonbackend.Marshal(response) + responseMarshalled, err := commonbackend.Marshal(response.Type, response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -156,19 +144,26 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case *commonbackend.RemoveProxy: - ok, err := helper.Backend.StopProxy(command) + case "removeProxy": + command, ok := commandRaw.(*commonbackend.RemoveProxy) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StopProxy(command) var hasAnyFailed bool - if err != nil { - log.Warnf("failed to remove proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) - hasAnyFailed = true - } else if !ok { + if !ok { log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) hasAnyFailed = true + } else if err != nil { + log.Warnf("failed to remove proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) + hasAnyFailed = true } response := &commonbackend.ProxyStatusResponse{ + Type: "proxyStatusResponse", SourceIP: command.SourceIP, SourcePort: command.SourcePort, DestPort: command.DestPort, @@ -176,7 +171,7 @@ func (helper *BackendApplicationHelper) Start() error { IsActive: hasAnyFailed, } - responseMarshalled, err := commonbackend.Marshal(response) + responseMarshalled, err := commonbackend.Marshal(response.Type, response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -184,14 +179,21 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case *commonbackend.ProxyConnectionsRequest: + case "proxyConnectionsRequest": + _, ok := commandRaw.(*commonbackend.ProxyConnectionsRequest) + + if !ok { + return fmt.Errorf("failed to typecast") + } + connections := helper.Backend.GetAllClientConnections() serverParams := &commonbackend.ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", Connections: connections, } - byteData, err := commonbackend.Marshal(serverParams) + byteData, err := commonbackend.Marshal(serverParams.Type, serverParams) if err != nil { return err @@ -200,11 +202,18 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - case *commonbackend.CheckClientParameters: + case "checkClientParameters": + command, ok := commandRaw.(*commonbackend.CheckClientParameters) + + if !ok { + return fmt.Errorf("failed to typecast") + } + resp := helper.Backend.CheckParametersForConnections(command) + resp.Type = "checkParametersResponse" resp.InResponseTo = "checkClientParameters" - byteData, err := commonbackend.Marshal(resp) + byteData, err := commonbackend.Marshal(resp.Type, resp) if err != nil { return err @@ -213,11 +222,18 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - case *commonbackend.CheckServerParameters: + case "checkServerParameters": + command, ok := commandRaw.(*commonbackend.CheckServerParameters) + + if !ok { + return fmt.Errorf("failed to typecast") + } + resp := helper.Backend.CheckParametersForBackend(command.Arguments) + resp.Type = "checkParametersResponse" resp.InResponseTo = "checkServerParameters" - byteData, err := commonbackend.Marshal(resp) + byteData, err := commonbackend.Marshal(resp.Type, resp) if err != nil { return err @@ -226,8 +242,6 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - default: - log.Warnf("Unsupported command recieved: %T", command) } } } diff --git a/backend/backendutil/profiling_disabled.go b/backend/backendutil/profiling_disabled.go deleted file mode 100644 index 8538407..0000000 --- a/backend/backendutil/profiling_disabled.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !debug - -package backendutil - -var endProfileFunc func() - -func ConfigureProfiling() error { - return nil -} diff --git a/backend/backendutil/profiling_enabled.go b/backend/backendutil/profiling_enabled.go deleted file mode 100644 index 6fcb189..0000000 --- a/backend/backendutil/profiling_enabled.go +++ /dev/null @@ -1,91 +0,0 @@ -//go:build debug - -package backendutil - -import ( - "errors" - "fmt" - "os" - "os/signal" - "runtime/pprof" - "syscall" - "time" - - "github.com/charmbracelet/log" - "golang.org/x/exp/rand" -) - -func ConfigureProfiling() error { - profilingMode, err := os.ReadFile("/tmp/hermes.backendlauncher.profilebackends") - - if err != nil && errors.Is(err, os.ErrNotExist) { - return nil - } - - switch string(profilingMode) { - case "cpu": - log.Debug("Starting CPU profiling as a background task") - go doCPUProfiling() - case "mem": - log.Debug("Starting memory profiling as a background task") - go doMemoryProfiling() - default: - log.Warnf("Unknown profiling mode: %s", string(profilingMode)) - return nil - } - - return nil -} - -func doCPUProfiling() { - // (imterah) WTF? why isn't this being seeded on its own? according to Go docs, this should be seeded automatically... - rand.Seed(uint64(time.Now().UnixNano())) - - profileFileName := fmt.Sprintf("/tmp/hermes.backendlauncher.cpu.prof.%d", rand.Int()) - profileFile, err := os.Create(profileFileName) - - if err != nil { - log.Fatalf("Failed to create CPU profiling file: %s", err.Error()) - } - - log.Debugf("Writing CPU usage profile to '%s'. Will capture when Ctrl+C/SIGTERM is recieved.", profileFileName) - pprof.StartCPUProfile(profileFile) - - exitNotification := make(chan os.Signal, 1) - signal.Notify(exitNotification, os.Interrupt, syscall.SIGTERM) - <-exitNotification - - log.Debug("Recieved SIGTERM. Cleaning up and exiting...") - - pprof.StopCPUProfile() - profileFile.Close() - - log.Debug("Exiting...") - os.Exit(0) -} - -func doMemoryProfiling() { - // (imterah) WTF? why isn't this being seeded on its own? according to Go docs, this should be seeded automatically... - rand.Seed(uint64(time.Now().UnixNano())) - - profileFileName := fmt.Sprintf("/tmp/hermes.backendlauncher.mem.prof.%d", rand.Int()) - profileFile, err := os.Create(profileFileName) - - if err != nil { - log.Fatalf("Failed to create memory profiling file: %s", err.Error()) - } - - log.Debugf("Writing memory profile to '%s'. Will capture when Ctrl+C/SIGTERM is recieved.", profileFileName) - - exitNotification := make(chan os.Signal, 1) - signal.Notify(exitNotification, os.Interrupt, syscall.SIGTERM) - <-exitNotification - - log.Debug("Recieved SIGTERM. Cleaning up and exiting...") - - pprof.WriteHeapProfile(profileFile) - profileFile.Close() - - log.Debug("Exiting...") - os.Exit(0) -} diff --git a/backend/backendutil/structure.go b/backend/backendutil/structure.go index 0eb7116..5bdcfa4 100644 --- a/backend/backendutil/structure.go +++ b/backend/backendutil/structure.go @@ -1,11 +1,10 @@ package backendutil -import "git.terah.dev/imterah/hermes/backend/commonbackend" +import "git.terah.dev/imterah/hermes/commonbackend" type BackendInterface interface { StartBackend(arguments []byte) (bool, error) StopBackend() (bool, error) - GetBackendStatus() (bool, error) StartProxy(command *commonbackend.AddProxy) (bool, error) StopProxy(command *commonbackend.RemoveProxy) (bool, error) GetAllClientConnections() []*commonbackend.ProxyClientConnection diff --git a/backend/build.sh b/backend/build.sh index cee4440..8753daf 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -1,43 +1,20 @@ #!/usr/bin/env bash -pushd sshbackend > /dev/null -echo "building sshbackend" -go build -ldflags="-s -w" -trimpath . -popd > /dev/null +pushd sshbackend +CGO_ENABLED=0 GOOS=linux go build . +strip sshbackend +popd -pushd dummybackend > /dev/null -echo "building dummybackend" -go build -ldflags="-s -w" -trimpath . -popd > /dev/null +pushd dummybackend +CGO_ENABLED=0 GOOS=linux go build . +strip dummybackend +popd -pushd externalbackendlauncher > /dev/null -echo "building externalbackendlauncher" -go build -ldflags="-s -w" -trimpath . -popd > /dev/null +pushd externalbackendlauncher +go build . +strip externalbackendlauncher +popd -if [ ! -d "sshappbackend/local-code/remote-bin" ]; then - mkdir "sshappbackend/local-code/remote-bin" -fi - -pushd sshappbackend/remote-code > /dev/null -echo "building sshappbackend/remote-code" -# Disable dynamic linking by disabling CGo. -# We need to make the remote code as generic as possible, so we do this -echo " - building for arm64" -CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm64 . -echo " - building for arm" -CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm . -echo " - building for amd64" -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-amd64 . -echo " - building for i386" -CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-386 . -popd > /dev/null - -pushd sshappbackend/local-code > /dev/null -echo "building sshappbackend/local-code" -go build -ldflags="-s -w" -trimpath -o sshappbackend . -popd > /dev/null - -pushd api > /dev/null -echo "building api" -go build -ldflags="-s -w" -trimpath . -popd > /dev/null +pushd api +CGO_ENABLED=0 GOOS=linux go build . +strip api +popd diff --git a/backend/commonbackend/constants.go b/backend/commonbackend/constants.go index 6d5362b..cdb68f2 100644 --- a/backend/commonbackend/constants.go +++ b/backend/commonbackend/constants.go @@ -1,13 +1,16 @@ package commonbackend type Start struct { + Type string // Will be 'start' always Arguments []byte } type Stop struct { + Type string // Will be 'stop' always } type AddProxy struct { + Type string // Will be 'addProxy' always SourceIP string SourcePort uint16 DestPort uint16 @@ -15,6 +18,7 @@ type AddProxy struct { } type RemoveProxy struct { + Type string // Will be 'removeProxy' always SourceIP string SourcePort uint16 DestPort uint16 @@ -22,6 +26,7 @@ type RemoveProxy struct { } type ProxyStatusRequest struct { + Type string // Will be 'proxyStatusRequest' always SourceIP string SourcePort uint16 DestPort uint16 @@ -29,6 +34,7 @@ type ProxyStatusRequest struct { } type ProxyStatusResponse struct { + Type string // Will be 'proxyStatusResponse' always SourceIP string SourcePort uint16 DestPort uint16 @@ -44,22 +50,27 @@ type ProxyInstance struct { } type ProxyInstanceResponse struct { + Type string // Will be 'proxyConnectionResponse' always Proxies []*ProxyInstance // List of connections } type ProxyInstanceRequest struct { + Type string // Will be 'proxyConnectionRequest' always } type BackendStatusResponse struct { + Type string // Will be 'backendStatusResponse' always IsRunning bool // True if running, false if not running StatusCode int // Either the 'Success' or 'Failure' constant Message string // String message from the client (ex. failed to dial TCP) } type BackendStatusRequest struct { + Type string // Will be 'backendStatusRequest' always } type ProxyConnectionsRequest struct { + Type string // Will be 'proxyConnectionsRequest' always } // Client's connection to a specific proxy @@ -72,10 +83,12 @@ type ProxyClientConnection struct { } type ProxyConnectionsResponse struct { + Type string // Will be 'proxyConnectionsResponse' always Connections []*ProxyClientConnection // List of connections } type CheckClientParameters struct { + Type string // Will be 'checkClientParameters' always SourceIP string SourcePort uint16 DestPort uint16 @@ -83,11 +96,13 @@ type CheckClientParameters struct { } type CheckServerParameters struct { + Type string // Will be 'checkServerParameters' always Arguments []byte } // Sent as a response to either CheckClientParameters or CheckBackendParameters type CheckParametersResponse struct { + Type string // Will be 'checkParametersResponse' always InResponseTo string // Will be either 'checkClientParameters' or 'checkServerParameters' IsValid bool // If true, valid, and if false, invalid Message string // String message from the client (ex. failed to unmarshal JSON: x is not defined) diff --git a/backend/commonbackend/marshal.go b/backend/commonbackend/marshal.go index 7203ee3..31895de 100644 --- a/backend/commonbackend/marshal.go +++ b/backend/commonbackend/marshal.go @@ -84,19 +84,37 @@ func marshalIndividualProxyStruct(conn *ProxyInstance) ([]byte, error) { return proxyBlock, nil } -func Marshal(command interface{}) ([]byte, error) { - switch command := command.(type) { - case *Start: - startCommandBytes := make([]byte, 1+2+len(command.Arguments)) +func Marshal(commandType string, command interface{}) ([]byte, error) { + switch commandType { + case "start": + startCommand, ok := command.(*Start) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + startCommandBytes := make([]byte, 1+2+len(startCommand.Arguments)) startCommandBytes[0] = StartID - binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(command.Arguments))) - copy(startCommandBytes[3:], command.Arguments) + binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(startCommand.Arguments))) + copy(startCommandBytes[3:], startCommand.Arguments) return startCommandBytes, nil - case *Stop: + case "stop": + _, ok := command.(*Stop) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + return []byte{StopID}, nil - case *AddProxy: - sourceIP := net.ParseIP(command.SourceIP) + case "addProxy": + addConnectionCommand, ok := command.(*AddProxy) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(addConnectionCommand.SourceIP) var ipVer uint8 var ipBytes []byte @@ -116,14 +134,14 @@ func Marshal(command interface{}) ([]byte, error) { copy(addConnectionBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) - binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) + binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], addConnectionCommand.SourcePort) + binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], addConnectionCommand.DestPort) var protocol uint8 - if command.Protocol == "tcp" { + if addConnectionCommand.Protocol == "tcp" { protocol = TCP - } else if command.Protocol == "udp" { + } else if addConnectionCommand.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") @@ -132,8 +150,14 @@ func Marshal(command interface{}) ([]byte, error) { addConnectionBytes[6+len(ipBytes)] = protocol return addConnectionBytes, nil - case *RemoveProxy: - sourceIP := net.ParseIP(command.SourceIP) + case "removeProxy": + removeConnectionCommand, ok := command.(*RemoveProxy) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(removeConnectionCommand.SourceIP) var ipVer uint8 var ipBytes []byte @@ -151,14 +175,14 @@ func Marshal(command interface{}) ([]byte, error) { removeConnectionBytes[0] = RemoveProxyID removeConnectionBytes[1] = ipVer copy(removeConnectionBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) - binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) + binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], removeConnectionCommand.SourcePort) + binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], removeConnectionCommand.DestPort) var protocol uint8 - if command.Protocol == "tcp" { + if removeConnectionCommand.Protocol == "tcp" { protocol = TCP - } else if command.Protocol == "udp" { + } else if removeConnectionCommand.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") @@ -167,11 +191,17 @@ func Marshal(command interface{}) ([]byte, error) { removeConnectionBytes[6+len(ipBytes)] = protocol return removeConnectionBytes, nil - case *ProxyConnectionsResponse: - connectionsArray := make([][]byte, len(command.Connections)) + case "proxyConnectionsResponse": + allConnectionsCommand, ok := command.(*ProxyConnectionsResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + connectionsArray := make([][]byte, len(allConnectionsCommand.Connections)) totalSize := 0 - for connIndex, conn := range command.Connections { + for connIndex, conn := range allConnectionsCommand.Connections { connectionsArray[connIndex] = marshalIndividualConnectionStruct(conn) totalSize += len(connectionsArray[connIndex]) + 1 } @@ -193,8 +223,14 @@ func Marshal(command interface{}) ([]byte, error) { connectionCommandArray[totalSize] = '\n' return connectionCommandArray, nil - case *CheckClientParameters: - sourceIP := net.ParseIP(command.SourceIP) + case "checkClientParameters": + checkClientCommand, ok := command.(*CheckClientParameters) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(checkClientCommand.SourceIP) var ipVer uint8 var ipBytes []byte @@ -212,14 +248,14 @@ func Marshal(command interface{}) ([]byte, error) { checkClientBytes[0] = CheckClientParametersID checkClientBytes[1] = ipVer copy(checkClientBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) - binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) + binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], checkClientCommand.SourcePort) + binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], checkClientCommand.DestPort) var protocol uint8 - if command.Protocol == "tcp" { + if checkClientCommand.Protocol == "tcp" { protocol = TCP - } else if command.Protocol == "udp" { + } else if checkClientCommand.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") @@ -228,19 +264,31 @@ func Marshal(command interface{}) ([]byte, error) { checkClientBytes[6+len(ipBytes)] = protocol return checkClientBytes, nil - case *CheckServerParameters: - serverCommandBytes := make([]byte, 1+2+len(command.Arguments)) + case "checkServerParameters": + checkServerCommand, ok := command.(*CheckServerParameters) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + serverCommandBytes := make([]byte, 1+2+len(checkServerCommand.Arguments)) serverCommandBytes[0] = CheckServerParametersID - binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(command.Arguments))) - copy(serverCommandBytes[3:], command.Arguments) + binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(checkServerCommand.Arguments))) + copy(serverCommandBytes[3:], checkServerCommand.Arguments) return serverCommandBytes, nil - case *CheckParametersResponse: + case "checkParametersResponse": + checkParametersCommand, ok := command.(*CheckParametersResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + var checkMethod uint8 - if command.InResponseTo == "checkClientParameters" { + if checkParametersCommand.InResponseTo == "checkClientParameters" { checkMethod = CheckClientParametersID - } else if command.InResponseTo == "checkServerParameters" { + } else if checkParametersCommand.InResponseTo == "checkServerParameters" { checkMethod = CheckServerParametersID } else { return nil, fmt.Errorf("invalid mode recieved (must be either checkClientParameters or checkServerParameters)") @@ -248,50 +296,68 @@ func Marshal(command interface{}) ([]byte, error) { var isValid uint8 - if command.IsValid { + if checkParametersCommand.IsValid { isValid = 1 } - checkResponseBytes := make([]byte, 3+2+len(command.Message)) + checkResponseBytes := make([]byte, 3+2+len(checkParametersCommand.Message)) checkResponseBytes[0] = CheckParametersResponseID checkResponseBytes[1] = checkMethod checkResponseBytes[2] = isValid - binary.BigEndian.PutUint16(checkResponseBytes[3:5], uint16(len(command.Message))) + binary.BigEndian.PutUint16(checkResponseBytes[3:5], uint16(len(checkParametersCommand.Message))) - if len(command.Message) != 0 { - copy(checkResponseBytes[5:], []byte(command.Message)) + if len(checkParametersCommand.Message) != 0 { + copy(checkResponseBytes[5:], []byte(checkParametersCommand.Message)) } return checkResponseBytes, nil - case *BackendStatusResponse: + case "backendStatusResponse": + backendStatusResponse, ok := command.(*BackendStatusResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + var isRunning uint8 - if command.IsRunning { + if backendStatusResponse.IsRunning { isRunning = 1 } else { isRunning = 0 } - statusResponseBytes := make([]byte, 3+2+len(command.Message)) + statusResponseBytes := make([]byte, 3+2+len(backendStatusResponse.Message)) statusResponseBytes[0] = BackendStatusResponseID statusResponseBytes[1] = isRunning - statusResponseBytes[2] = byte(command.StatusCode) + statusResponseBytes[2] = byte(backendStatusResponse.StatusCode) - binary.BigEndian.PutUint16(statusResponseBytes[3:5], uint16(len(command.Message))) + binary.BigEndian.PutUint16(statusResponseBytes[3:5], uint16(len(backendStatusResponse.Message))) - if len(command.Message) != 0 { - copy(statusResponseBytes[5:], []byte(command.Message)) + if len(backendStatusResponse.Message) != 0 { + copy(statusResponseBytes[5:], []byte(backendStatusResponse.Message)) } return statusResponseBytes, nil - case *BackendStatusRequest: - statusRequestBytes := make([]byte, 1) + case "backendStatusRequest": + _, ok := command.(*BackendStatusRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + statusRequestBytes := make([]byte, 2) statusRequestBytes[0] = BackendStatusRequestID return statusRequestBytes, nil - case *ProxyStatusRequest: - sourceIP := net.ParseIP(command.SourceIP) + case "proxyStatusRequest": + proxyStatusRequest, ok := command.(*ProxyStatusRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(proxyStatusRequest.SourceIP) var ipVer uint8 var ipBytes []byte @@ -304,31 +370,37 @@ func Marshal(command interface{}) ([]byte, error) { ipVer = IPv4 } - commandBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) + proxyStatusRequestBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) - commandBytes[0] = ProxyStatusRequestID - commandBytes[1] = ipVer + proxyStatusRequestBytes[0] = ProxyStatusRequestID + proxyStatusRequestBytes[1] = ipVer - copy(commandBytes[2:2+len(ipBytes)], ipBytes) + copy(proxyStatusRequestBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(commandBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) - binary.BigEndian.PutUint16(commandBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) + binary.BigEndian.PutUint16(proxyStatusRequestBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusRequest.SourcePort) + binary.BigEndian.PutUint16(proxyStatusRequestBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusRequest.DestPort) var protocol uint8 - if command.Protocol == "tcp" { + if proxyStatusRequest.Protocol == "tcp" { protocol = TCP - } else if command.Protocol == "udp" { + } else if proxyStatusRequest.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") } - commandBytes[6+len(ipBytes)] = protocol + proxyStatusRequestBytes[6+len(ipBytes)] = protocol - return commandBytes, nil - case *ProxyStatusResponse: - sourceIP := net.ParseIP(command.SourceIP) + return proxyStatusRequestBytes, nil + case "proxyStatusResponse": + proxyStatusResponse, ok := command.(*ProxyStatusResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(proxyStatusResponse.SourceIP) var ipVer uint8 var ipBytes []byte @@ -341,44 +413,50 @@ func Marshal(command interface{}) ([]byte, error) { ipVer = IPv4 } - commandBytes := make([]byte, 1+1+len(ipBytes)+2+2+1+1) + proxyStatusResponseBytes := make([]byte, 1+1+len(ipBytes)+2+2+1+1) - commandBytes[0] = ProxyStatusResponseID - commandBytes[1] = ipVer + proxyStatusResponseBytes[0] = ProxyStatusResponseID + proxyStatusResponseBytes[1] = ipVer - copy(commandBytes[2:2+len(ipBytes)], ipBytes) + copy(proxyStatusResponseBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(commandBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) - binary.BigEndian.PutUint16(commandBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) + binary.BigEndian.PutUint16(proxyStatusResponseBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusResponse.SourcePort) + binary.BigEndian.PutUint16(proxyStatusResponseBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusResponse.DestPort) var protocol uint8 - if command.Protocol == "tcp" { + if proxyStatusResponse.Protocol == "tcp" { protocol = TCP - } else if command.Protocol == "udp" { + } else if proxyStatusResponse.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") } - commandBytes[6+len(ipBytes)] = protocol + proxyStatusResponseBytes[6+len(ipBytes)] = protocol var isActive uint8 - if command.IsActive { + if proxyStatusResponse.IsActive { isActive = 1 } else { isActive = 0 } - commandBytes[7+len(ipBytes)] = isActive + proxyStatusResponseBytes[7+len(ipBytes)] = isActive - return commandBytes, nil - case *ProxyInstanceResponse: - proxyArray := make([][]byte, len(command.Proxies)) + return proxyStatusResponseBytes, nil + case "proxyInstanceResponse": + proxyConectionResponse, ok := command.(*ProxyInstanceResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + proxyArray := make([][]byte, len(proxyConectionResponse.Proxies)) totalSize := 0 - for proxyIndex, proxy := range command.Proxies { + for proxyIndex, proxy := range proxyConectionResponse.Proxies { var err error proxyArray[proxyIndex], err = marshalIndividualProxyStruct(proxy) @@ -407,11 +485,23 @@ func Marshal(command interface{}) ([]byte, error) { connectionCommandArray[totalSize] = '\n' return connectionCommandArray, nil - case *ProxyInstanceRequest: + case "proxyInstanceRequest": + _, ok := command.(*ProxyInstanceRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + return []byte{ProxyInstanceRequestID}, nil - case *ProxyConnectionsRequest: + case "proxyConnectionsRequest": + _, ok := command.(*ProxyConnectionsRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + return []byte{ProxyConnectionsRequestID}, nil } - return nil, fmt.Errorf("couldn't match command type") + return nil, fmt.Errorf("couldn't match command name") } diff --git a/backend/commonbackend/marshalling_test.go b/backend/commonbackend/marshalling_test.go index c2b6375..1c93f94 100644 --- a/backend/commonbackend/marshalling_test.go +++ b/backend/commonbackend/marshalling_test.go @@ -9,12 +9,13 @@ import ( var logLevel = os.Getenv("HERMES_LOG_LEVEL") -func TestStart(t *testing.T) { +func TestStartCommandMarshalSupport(t *testing.T) { commandInput := &Start{ + Type: "start", Arguments: []byte("Hello from automated testing"), } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -25,27 +26,39 @@ func TestStart(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*Start) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) } } -func TestStop(t *testing.T) { - commandInput := &Stop{} +func TestStopCommandMarshalSupport(t *testing.T) { + commandInput := &Stop{ + Type: "stop", + } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -56,28 +69,39 @@ func TestStop(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - _, ok := commandUnmarshalledRaw.(*Stop) + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*Stop) if !ok { t.Fatal("failed typecast") } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } } -func TestAddConnection(t *testing.T) { +func TestAddConnectionCommandMarshalSupport(t *testing.T) { commandInput := &AddProxy{ + Type: "addProxy", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -88,18 +112,28 @@ func TestAddConnection(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*AddProxy) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -121,15 +155,16 @@ func TestAddConnection(t *testing.T) { } } -func TestRemoveConnection(t *testing.T) { +func TestRemoveConnectionCommandMarshalSupport(t *testing.T) { commandInput := &RemoveProxy{ + Type: "removeProxy", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -140,18 +175,28 @@ func TestRemoveConnection(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -173,8 +218,9 @@ func TestRemoveConnection(t *testing.T) { } } -func TestGetAllConnections(t *testing.T) { +func TestGetAllConnectionsCommandMarshalSupport(t *testing.T) { commandInput := &ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", Connections: []*ProxyClientConnection{ { SourceIP: "127.0.0.1", @@ -200,7 +246,7 @@ func TestGetAllConnections(t *testing.T) { }, } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -211,18 +257,28 @@ func TestGetAllConnections(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + for commandIndex, originalConnection := range commandInput.Connections { remoteConnection := commandUnmarshalled.Connections[commandIndex] @@ -253,15 +309,16 @@ func TestGetAllConnections(t *testing.T) { } } -func TestCheckClientParameters(t *testing.T) { +func TestCheckClientParametersMarshalSupport(t *testing.T) { commandInput := &CheckClientParameters{ + Type: "checkClientParameters", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -272,18 +329,28 @@ func TestCheckClientParameters(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckClientParameters) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -305,12 +372,13 @@ func TestCheckClientParameters(t *testing.T) { } } -func TestCheckServerParameters(t *testing.T) { +func TestCheckServerParametersMarshalSupport(t *testing.T) { commandInput := &CheckServerParameters{ + Type: "checkServerParameters", Arguments: []byte("Hello from automated testing"), } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -321,31 +389,42 @@ func TestCheckServerParameters(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckServerParameters) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) } } -func TestCheckParametersResponse(t *testing.T) { +func TestCheckParametersResponseMarshalSupport(t *testing.T) { commandInput := &CheckParametersResponse{ + Type: "checkParametersResponse", InResponseTo: "checkClientParameters", IsValid: true, Message: "Hello from automated testing", } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -356,18 +435,28 @@ func TestCheckParametersResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckParametersResponse) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.InResponseTo != commandUnmarshalled.InResponseTo { t.Fail() log.Printf("InResponseTo's are not equal (orig: %s, unmsh: %s)", commandInput.InResponseTo, commandUnmarshalled.InResponseTo) @@ -384,9 +473,12 @@ func TestCheckParametersResponse(t *testing.T) { } } -func TestBackendStatusRequest(t *testing.T) { - commandInput := &BackendStatusRequest{} - commandMarshalled, err := Marshal(commandInput) +func TestBackendStatusRequestMarshalSupport(t *testing.T) { + commandInput := &BackendStatusRequest{ + Type: "backendStatusRequest", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -397,27 +489,38 @@ func TestBackendStatusRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - _, ok := commandUnmarshalledRaw.(*BackendStatusRequest) + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusRequest) if !ok { t.Fatal("failed typecast") } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } } -func TestBackendStatusResponse(t *testing.T) { +func TestBackendStatusResponseMarshalSupport(t *testing.T) { commandInput := &BackendStatusResponse{ + Type: "backendStatusResponse", IsRunning: true, StatusCode: StatusFailure, Message: "Hello from automated testing", } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -428,18 +531,28 @@ func TestBackendStatusResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusResponse) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.IsRunning != commandUnmarshalled.IsRunning { t.Fail() log.Printf("IsRunning's are not equal (orig: %t, unmsh: %t)", commandInput.IsRunning, commandUnmarshalled.IsRunning) @@ -456,15 +569,16 @@ func TestBackendStatusResponse(t *testing.T) { } } -func TestProxyStatusRequest(t *testing.T) { +func TestProxyStatusRequestMarshalSupport(t *testing.T) { commandInput := &ProxyStatusRequest{ + Type: "proxyStatusRequest", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -475,18 +589,28 @@ func TestProxyStatusRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -508,8 +632,9 @@ func TestProxyStatusRequest(t *testing.T) { } } -func TestProxyStatusResponse(t *testing.T) { +func TestProxyStatusResponseMarshalSupport(t *testing.T) { commandInput := &ProxyStatusResponse{ + Type: "proxyStatusResponse", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, @@ -517,7 +642,7 @@ func TestProxyStatusResponse(t *testing.T) { IsActive: true, } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -528,18 +653,28 @@ func TestProxyStatusResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -566,10 +701,12 @@ func TestProxyStatusResponse(t *testing.T) { } } -func TestProxyConnectionRequest(t *testing.T) { - commandInput := &ProxyInstanceRequest{} +func TestProxyConnectionRequestMarshalSupport(t *testing.T) { + commandInput := &ProxyInstanceRequest{ + Type: "proxyInstanceRequest", + } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -580,21 +717,32 @@ func TestProxyConnectionRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - _, ok := commandUnmarshalledRaw.(*ProxyInstanceRequest) + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceRequest) if !ok { t.Fatal("failed typecast") } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } } -func TestProxyConnectionResponse(t *testing.T) { +func TestProxyConnectionResponseMarshalSupport(t *testing.T) { commandInput := &ProxyInstanceResponse{ + Type: "proxyInstanceResponse", Proxies: []*ProxyInstance{ { SourceIP: "192.168.0.168", @@ -617,7 +765,7 @@ func TestProxyConnectionResponse(t *testing.T) { }, } - commandMarshalled, err := Marshal(commandInput) + commandMarshalled, err := Marshal(commandInput.Type, commandInput) if err != nil { t.Fatal(err.Error()) @@ -628,18 +776,28 @@ func TestProxyConnectionResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) if !ok { t.Fatal("failed typecast") } + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + for proxyIndex, originalProxy := range commandInput.Proxies { remoteProxy := commandUnmarshalled.Proxies[proxyIndex] diff --git a/backend/commonbackend/unmarshal.go b/backend/commonbackend/unmarshal.go index 6bb5af4..b8500dd 100644 --- a/backend/commonbackend/unmarshal.go +++ b/backend/commonbackend/unmarshal.go @@ -142,11 +142,11 @@ func unmarshalIndividualProxyStruct(conn io.Reader) (*ProxyInstance, error) { }, nil } -func Unmarshal(conn io.Reader) (interface{}, error) { +func Unmarshal(conn io.Reader) (string, interface{}, error) { commandType := make([]byte, 1) if _, err := conn.Read(commandType); err != nil { - return nil, fmt.Errorf("couldn't read command") + return "", nil, fmt.Errorf("couldn't read command") } switch commandType[0] { @@ -154,25 +154,28 @@ func Unmarshal(conn io.Reader) (interface{}, error) { argumentsLength := make([]byte, 2) if _, err := conn.Read(argumentsLength); err != nil { - return nil, fmt.Errorf("couldn't read argument length") + return "", nil, fmt.Errorf("couldn't read argument length") } arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) if _, err := conn.Read(arguments); err != nil { - return nil, fmt.Errorf("couldn't read arguments") + return "", nil, fmt.Errorf("couldn't read arguments") } - return &Start{ + return "start", &Start{ + Type: "start", Arguments: arguments, }, nil case StopID: - return &Stop{}, nil + return "stop", &Stop{ + Type: "stop", + }, nil case AddProxyID: ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return nil, fmt.Errorf("couldn't read ip version") + return "", nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -182,44 +185,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return nil, fmt.Errorf("invalid IP version recieved") + return "", nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return nil, fmt.Errorf("couldn't read source IP") + return "", nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") + return "", nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return nil, fmt.Errorf("couldn't read destination port") + return "", nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return nil, fmt.Errorf("couldn't read protocol") + return "", nil, fmt.Errorf("couldn't read protocol") } var protocol string if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[0] == UDP { + } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return nil, fmt.Errorf("invalid protocol") + return "", nil, fmt.Errorf("invalid protocol") } - return &AddProxy{ + return "addProxy", &AddProxy{ + Type: "addProxy", SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -229,7 +233,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return nil, fmt.Errorf("couldn't read ip version") + return "", nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -239,44 +243,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return nil, fmt.Errorf("invalid IP version recieved") + return "", nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return nil, fmt.Errorf("couldn't read source IP") + return "", nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") + return "", nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return nil, fmt.Errorf("couldn't read destination port") + return "", nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return nil, fmt.Errorf("couldn't read protocol") + return "", nil, fmt.Errorf("couldn't read protocol") } var protocol string if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[0] == UDP { + } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return nil, fmt.Errorf("invalid protocol") + return "", nil, fmt.Errorf("invalid protocol") } - return &RemoveProxy{ + return "removeProxy", &RemoveProxy{ + Type: "removeProxy", SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -296,13 +301,13 @@ func Unmarshal(conn io.Reader) (interface{}, error) { break } - return nil, err + return "", nil, err } connections = append(connections, connection) if _, err := conn.Read(delimiter); err != nil { - return nil, fmt.Errorf("couldn't read delimiter") + return "", nil, fmt.Errorf("couldn't read delimiter") } if delimiter[0] == '\r' { @@ -316,14 +321,15 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } } - return &ProxyConnectionsResponse{ + return "proxyConnectionsResponse", &ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", Connections: connections, }, errorReturn case CheckClientParametersID: ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return nil, fmt.Errorf("couldn't read ip version") + return "", nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -333,44 +339,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return nil, fmt.Errorf("invalid IP version recieved") + return "", nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return nil, fmt.Errorf("couldn't read source IP") + return "", nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") + return "", nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return nil, fmt.Errorf("couldn't read destination port") + return "", nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return nil, fmt.Errorf("couldn't read protocol") + return "", nil, fmt.Errorf("couldn't read protocol") } var protocol string if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[0] == UDP { + } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return nil, fmt.Errorf("invalid protocol") + return "", nil, fmt.Errorf("invalid protocol") } - return &CheckClientParameters{ + return "checkClientParameters", &CheckClientParameters{ + Type: "checkClientParameters", SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -380,23 +387,24 @@ func Unmarshal(conn io.Reader) (interface{}, error) { argumentsLength := make([]byte, 2) if _, err := conn.Read(argumentsLength); err != nil { - return nil, fmt.Errorf("couldn't read argument length") + return "", nil, fmt.Errorf("couldn't read argument length") } arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) if _, err := conn.Read(arguments); err != nil { - return nil, fmt.Errorf("couldn't read arguments") + return "", nil, fmt.Errorf("couldn't read arguments") } - return &CheckServerParameters{ + return "checkServerParameters", &CheckServerParameters{ + Type: "checkServerParameters", Arguments: arguments, }, nil case CheckParametersResponseID: checkMethodByte := make([]byte, 1) if _, err := conn.Read(checkMethodByte); err != nil { - return nil, fmt.Errorf("couldn't read check method byte") + return "", nil, fmt.Errorf("couldn't read check method byte") } var checkMethod string @@ -406,19 +414,19 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } else if checkMethodByte[0] == CheckServerParametersID { checkMethod = "checkServerParameters" } else { - return nil, fmt.Errorf("invalid check method recieved") + return "", nil, fmt.Errorf("invalid check method recieved") } isValid := make([]byte, 1) if _, err := conn.Read(isValid); err != nil { - return nil, fmt.Errorf("couldn't read isValid byte") + return "", nil, fmt.Errorf("couldn't read isValid byte") } messageLengthBytes := make([]byte, 2) if _, err := conn.Read(messageLengthBytes); err != nil { - return nil, fmt.Errorf("couldn't read message length") + return "", nil, fmt.Errorf("couldn't read message length") } messageLength := binary.BigEndian.Uint16(messageLengthBytes) @@ -428,13 +436,14 @@ func Unmarshal(conn io.Reader) (interface{}, error) { messageBytes := make([]byte, messageLength) if _, err := conn.Read(messageBytes); err != nil { - return nil, fmt.Errorf("couldn't read message") + return "", nil, fmt.Errorf("couldn't read message") } message = string(messageBytes) } - return &CheckParametersResponse{ + return "checkParametersResponse", &CheckParametersResponse{ + Type: "checkParametersResponse", InResponseTo: checkMethod, IsValid: isValid[0] == 1, Message: message, @@ -443,19 +452,19 @@ func Unmarshal(conn io.Reader) (interface{}, error) { isRunning := make([]byte, 1) if _, err := conn.Read(isRunning); err != nil { - return nil, fmt.Errorf("couldn't read isRunning field") + return "", nil, fmt.Errorf("couldn't read isRunning field") } statusCode := make([]byte, 1) if _, err := conn.Read(statusCode); err != nil { - return nil, fmt.Errorf("couldn't read status code field") + return "", nil, fmt.Errorf("couldn't read status code field") } messageLengthBytes := make([]byte, 2) if _, err := conn.Read(messageLengthBytes); err != nil { - return nil, fmt.Errorf("couldn't read message length") + return "", nil, fmt.Errorf("couldn't read message length") } messageLength := binary.BigEndian.Uint16(messageLengthBytes) @@ -465,24 +474,27 @@ func Unmarshal(conn io.Reader) (interface{}, error) { messageBytes := make([]byte, messageLength) if _, err := conn.Read(messageBytes); err != nil { - return nil, fmt.Errorf("couldn't read message") + return "", nil, fmt.Errorf("couldn't read message") } message = string(messageBytes) } - return &BackendStatusResponse{ + return "backendStatusResponse", &BackendStatusResponse{ + Type: "backendStatusResponse", IsRunning: isRunning[0] == 1, StatusCode: int(statusCode[0]), Message: message, }, nil case BackendStatusRequestID: - return &BackendStatusRequest{}, nil + return "backendStatusRequest", &BackendStatusRequest{ + Type: "backendStatusRequest", + }, nil case ProxyStatusRequestID: ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return nil, fmt.Errorf("couldn't read ip version") + return "", nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -492,44 +504,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return nil, fmt.Errorf("invalid IP version recieved") + return "", nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return nil, fmt.Errorf("couldn't read source IP") + return "", nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") + return "", nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return nil, fmt.Errorf("couldn't read destination port") + return "", nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return nil, fmt.Errorf("couldn't read protocol") + return "", nil, fmt.Errorf("couldn't read protocol") } var protocol string if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[0] == UDP { + } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return nil, fmt.Errorf("invalid protocol") + return "", nil, fmt.Errorf("invalid protocol") } - return &ProxyStatusRequest{ + return "proxyStatusRequest", &ProxyStatusRequest{ + Type: "proxyStatusRequest", SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -539,7 +552,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return nil, fmt.Errorf("couldn't read ip version") + return "", nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -549,50 +562,51 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return nil, fmt.Errorf("invalid IP version recieved") + return "", nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return nil, fmt.Errorf("couldn't read source IP") + return "", nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") + return "", nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return nil, fmt.Errorf("couldn't read destination port") + return "", nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return nil, fmt.Errorf("couldn't read protocol") + return "", nil, fmt.Errorf("couldn't read protocol") } var protocol string if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[0] == UDP { + } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return nil, fmt.Errorf("invalid protocol") + return "", nil, fmt.Errorf("invalid protocol") } isActive := make([]byte, 1) if _, err := conn.Read(isActive); err != nil { - return nil, fmt.Errorf("couldn't read isActive field") + return "", nil, fmt.Errorf("couldn't read isActive field") } - return &ProxyStatusResponse{ + return "proxyStatusResponse", &ProxyStatusResponse{ + Type: "proxyStatusResponse", SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -600,7 +614,9 @@ func Unmarshal(conn io.Reader) (interface{}, error) { IsActive: isActive[0] == 1, }, nil case ProxyInstanceRequestID: - return &ProxyInstanceRequest{}, nil + return "proxyInstanceRequest", &ProxyInstanceRequest{ + Type: "proxyInstanceRequest", + }, nil case ProxyInstanceResponseID: proxies := []*ProxyInstance{} delimiter := make([]byte, 1) @@ -615,13 +631,13 @@ func Unmarshal(conn io.Reader) (interface{}, error) { break } - return nil, err + return "", nil, err } proxies = append(proxies, proxy) if _, err := conn.Read(delimiter); err != nil { - return nil, fmt.Errorf("couldn't read delimiter") + return "", nil, fmt.Errorf("couldn't read delimiter") } if delimiter[0] == '\r' { @@ -635,12 +651,15 @@ func Unmarshal(conn io.Reader) (interface{}, error) { } } - return &ProxyInstanceResponse{ + return "proxyInstanceResponse", &ProxyInstanceResponse{ + Type: "proxyInstanceResponse", Proxies: proxies, }, errorReturn case ProxyConnectionsRequestID: - return &ProxyConnectionsRequest{}, nil + return "proxyConnectionsRequest", &ProxyConnectionsRequest{ + Type: "proxyConnectionsRequest", + }, nil } - return nil, fmt.Errorf("couldn't match command ID") + return "", nil, fmt.Errorf("couldn't match command ID") } diff --git a/backend/dummybackend/main.go b/backend/dummybackend/main.go index f28615c..893944d 100644 --- a/backend/dummybackend/main.go +++ b/backend/dummybackend/main.go @@ -3,8 +3,8 @@ package main import ( "os" - "git.terah.dev/imterah/hermes/backend/backendutil" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backendutil" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" ) @@ -19,10 +19,6 @@ func (backend *DummyBackend) StopBackend() (bool, error) { return true, nil } -func (backend *DummyBackend) GetBackendStatus() (bool, error) { - return true, nil -} - func (backend *DummyBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { return true, nil } diff --git a/backend/externalbackendlauncher/main.go b/backend/externalbackendlauncher/main.go index c196866..aa5e8ad 100644 --- a/backend/externalbackendlauncher/main.go +++ b/backend/externalbackendlauncher/main.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "git.terah.dev/imterah/hermes/backend/backendlauncher" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backendlauncher" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/urfave/cli/v2" ) @@ -21,8 +21,11 @@ type ProxyInstance struct { Protocol string `json:"protocol"` } -type WriteLogger struct{} +type WriteLogger struct { + UseError bool +} +// TODO: deprecate UseError switching func (writer WriteLogger) Write(p []byte) (n int, err error) { logSplit := strings.Split(string(p), "\n") @@ -31,7 +34,11 @@ func (writer WriteLogger) Write(p []byte) (n int, err error) { continue } - log.Infof("application: %s", line) + if writer.UseError { + log.Errorf("application: %s", line) + } else { + log.Infof("application: %s", line) + } } return len(p), err @@ -112,10 +119,11 @@ func entrypoint(cCtx *cli.Context) error { defer sock.Close() startCommand := &commonbackend.Start{ + Type: "start", Arguments: backendParameters, } - startMarshalledCommand, err := commonbackend.Marshal(startCommand) + startMarshalledCommand, err := commonbackend.Marshal("start", startCommand) if err != nil { log.Errorf("failed to generate start command: %s", err.Error()) @@ -127,13 +135,18 @@ func entrypoint(cCtx *cli.Context) error { continue } - commandRaw, err := commonbackend.Unmarshal(sock) + commandType, commandRaw, err := commonbackend.Unmarshal(sock) if err != nil { log.Errorf("failed to read from/unmarshal from socket: %s", err.Error()) continue } + if commandType != "backendStatusResponse" { + log.Errorf("recieved commandType '%s', expecting 'backendStatusResponse'", commandType) + continue + } + command, ok := commandRaw.(*commonbackend.BackendStatusResponse) if !ok { @@ -162,13 +175,14 @@ func entrypoint(cCtx *cli.Context) error { log.Infof("initializing proxy %s:%d -> remote:%d", proxy.SourceIP, proxy.SourcePort, proxy.DestPort) proxyAddCommand := &commonbackend.AddProxy{ + Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestPort, Protocol: proxy.Protocol, } - marshalledProxyCommand, err := commonbackend.Marshal(proxyAddCommand) + marshalledProxyCommand, err := commonbackend.Marshal("addProxy", proxyAddCommand) if err != nil { log.Errorf("failed to generate start command: %s", err.Error()) @@ -182,7 +196,7 @@ func entrypoint(cCtx *cli.Context) error { continue } - commandRaw, err := commonbackend.Unmarshal(sock) + commandType, commandRaw, err := commonbackend.Unmarshal(sock) if err != nil { log.Errorf("failed to read from/unmarshal from socket: %s", err.Error()) @@ -190,6 +204,12 @@ func entrypoint(cCtx *cli.Context) error { continue } + if commandType != "proxyStatusResponse" { + log.Errorf("recieved commandType '%s', expecting 'proxyStatusResponse'", commandType) + hasAnyFailed = true + continue + } + command, ok := commandRaw.(*commonbackend.ProxyStatusResponse) if !ok { @@ -222,8 +242,13 @@ func entrypoint(cCtx *cli.Context) error { log.Debug("entering execution loop (in main goroutine)...") - stdout := WriteLogger{} - stderr := WriteLogger{} + stdout := WriteLogger{ + UseError: false, + } + + stderr := WriteLogger{ + UseError: true, + } for { log.Info("starting process...") diff --git a/go.mod b/backend/go.mod similarity index 77% rename from go.mod rename to backend/go.mod index b390542..360d142 100644 --- a/go.mod +++ b/backend/go.mod @@ -2,36 +2,24 @@ module git.terah.dev/imterah/hermes go 1.23.3 -require ( - github.com/charmbracelet/log v0.4.0 - github.com/gin-gonic/gin v1.10.0 - github.com/go-playground/validator/v10 v10.23.0 - github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/pkg/sftp v1.13.7 - github.com/urfave/cli/v2 v2.27.5 - golang.org/x/crypto v0.31.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/term v0.28.0 - gopkg.in/yaml.v3 v3.0.1 - gorm.io/driver/postgres v1.5.11 - gorm.io/driver/sqlite v1.5.7 - gorm.io/gorm v1.25.12 -) - require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bytedance/sonic v1.12.6 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/charmbracelet/log v0.4.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/goccy/go-json v0.10.4 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -40,8 +28,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/kr/fs v0.1.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -53,15 +39,21 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/urfave/cli/v2 v2.27.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.11 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect ) diff --git a/go.sum b/backend/go.sum similarity index 67% rename from go.sum rename to backend/go.sum index dd30942..8b2b29b 100644 --- a/go.sum +++ b/backend/go.sum @@ -15,9 +15,7 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/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/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= @@ -27,8 +25,6 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -39,8 +35,6 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -60,16 +54,12 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -88,16 +78,11 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= -github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= -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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -108,7 +93,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -118,66 +102,30 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -188,3 +136,4 @@ gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDa gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/sshappbackend/datacommands/constants.go b/backend/sshappbackend/datacommands/constants.go deleted file mode 100644 index 6385e98..0000000 --- a/backend/sshappbackend/datacommands/constants.go +++ /dev/null @@ -1,90 +0,0 @@ -package datacommands - -// DO NOT USE -type ProxyStatusRequest struct { - ProxyID uint16 -} - -type ProxyStatusResponse struct { - ProxyID uint16 - IsActive bool -} - -type RemoveProxy struct { - ProxyID uint16 -} - -type ProxyInstanceResponse struct { - Proxies []uint16 -} - -type ProxyConnectionsRequest struct { - ProxyID uint16 -} - -type ProxyConnectionsResponse struct { - Connections []uint16 -} - -type TCPConnectionOpened struct { - ProxyID uint16 - ConnectionID uint16 -} - -type TCPConnectionClosed struct { - ProxyID uint16 - ConnectionID uint16 -} - -type TCPProxyData struct { - ProxyID uint16 - ConnectionID uint16 - DataLength uint16 -} - -type UDPProxyData struct { - ProxyID uint16 - ClientIP string - ClientPort uint16 - DataLength uint16 -} - -type ProxyInformationRequest struct { - ProxyID uint16 -} - -type ProxyInformationResponse struct { - Exists bool - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' -} - -type ProxyConnectionInformationRequest struct { - ProxyID uint16 - ConnectionID uint16 -} - -type ProxyConnectionInformationResponse struct { - Exists bool - ClientIP string - ClientPort uint16 -} - -const ( - ProxyStatusRequestID = iota + 100 - ProxyStatusResponseID - RemoveProxyID - ProxyInstanceResponseID - ProxyConnectionsRequestID - ProxyConnectionsResponseID - TCPConnectionOpenedID - TCPConnectionClosedID - TCPProxyDataID - UDPProxyDataID - ProxyInformationRequestID - ProxyInformationResponseID - ProxyConnectionInformationRequestID - ProxyConnectionInformationResponseID -) diff --git a/backend/sshappbackend/datacommands/marshal.go b/backend/sshappbackend/datacommands/marshal.go deleted file mode 100644 index a2c13bf..0000000 --- a/backend/sshappbackend/datacommands/marshal.go +++ /dev/null @@ -1,323 +0,0 @@ -package datacommands - -import ( - "encoding/binary" - "fmt" - "net" -) - -// Example size and protocol constants — adjust as needed. -const ( - IPv4Size = 4 - IPv6Size = 16 - - TCP = 1 - UDP = 2 -) - -// Marshal takes a command (pointer to one of our structs) and converts it to a byte slice. -func Marshal(command interface{}) ([]byte, error) { - switch cmd := command.(type) { - // ProxyStatusRequest: 1 byte for the command ID + 2 bytes for the ProxyID. - case *ProxyStatusRequest: - buf := make([]byte, 1+2) - - buf[0] = ProxyStatusRequestID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - - return buf, nil - - // ProxyStatusResponse: 1 byte for the command ID, 2 bytes for ProxyID, and 1 byte for IsActive. - case *ProxyStatusResponse: - buf := make([]byte, 1+2+1) - - buf[0] = ProxyStatusResponseID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - - if cmd.IsActive { - buf[3] = 1 - } else { - buf[3] = 0 - } - - return buf, nil - - // RemoveProxy: 1 byte for the command ID + 2 bytes for the ProxyID. - case *RemoveProxy: - buf := make([]byte, 1+2) - - buf[0] = RemoveProxyID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - - return buf, nil - - // ProxyConnectionsRequest: 1 byte for the command ID + 2 bytes for the ProxyID. - case *ProxyConnectionsRequest: - buf := make([]byte, 1+2) - - buf[0] = ProxyConnectionsRequestID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - - return buf, nil - - // ProxyConnectionsResponse: 1 byte for the command ID + 2 bytes length of the Connections + 2 bytes for each - // number in the Connection array. - case *ProxyConnectionsResponse: - buf := make([]byte, 1+((len(cmd.Connections)+1)*2)) - - buf[0] = ProxyConnectionsResponseID - binary.BigEndian.PutUint16(buf[1:], uint16(len(cmd.Connections))) - - for connectionIndex, connection := range cmd.Connections { - binary.BigEndian.PutUint16(buf[3+(connectionIndex*2):], connection) - } - - return buf, nil - - // ProxyConnectionsResponse: 1 byte for the command ID + 2 bytes length of the Proxies + 2 bytes for each - // number in the Proxies array. - case *ProxyInstanceResponse: - buf := make([]byte, 1+((len(cmd.Proxies)+1)*2)) - - buf[0] = ProxyInstanceResponseID - binary.BigEndian.PutUint16(buf[1:], uint16(len(cmd.Proxies))) - - for connectionIndex, connection := range cmd.Proxies { - binary.BigEndian.PutUint16(buf[3+(connectionIndex*2):], connection) - } - - return buf, nil - - // TCPConnectionOpened: 1 byte for the command ID + 2 bytes ProxyID + 2 bytes ConnectionID. - case *TCPConnectionOpened: - buf := make([]byte, 1+2+2) - - buf[0] = TCPConnectionOpenedID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) - - return buf, nil - - // TCPConnectionClosed: 1 byte for the command ID + 2 bytes ProxyID + 2 bytes ConnectionID. - case *TCPConnectionClosed: - buf := make([]byte, 1+2+2) - - buf[0] = TCPConnectionClosedID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) - - return buf, nil - - // TCPProxyData: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + 2 bytes DataLength. - case *TCPProxyData: - buf := make([]byte, 1+2+2+2) - - buf[0] = TCPProxyDataID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) - binary.BigEndian.PutUint16(buf[5:], cmd.DataLength) - - return buf, nil - - // UDPProxyData: - // Format: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + - // 1 byte IP version + IP bytes + 2 bytes ClientPort + 2 bytes DataLength. - case *UDPProxyData: - ip := net.ParseIP(cmd.ClientIP) - if ip == nil { - return nil, fmt.Errorf("invalid client IP: %v", cmd.ClientIP) - } - - var ipVer uint8 - var ipBytes []byte - - if ip4 := ip.To4(); ip4 != nil { - ipBytes = ip4 - ipVer = 4 - } else if ip16 := ip.To16(); ip16 != nil { - ipBytes = ip16 - ipVer = 6 - } else { - return nil, fmt.Errorf("unable to detect IP version for: %v", cmd.ClientIP) - } - - totalSize := 1 + // id - 2 + // ProxyID - 1 + // IP version - len(ipBytes) + // client IP bytes - 2 + // ClientPort - 2 // DataLength - - buf := make([]byte, totalSize) - offset := 0 - buf[offset] = UDPProxyDataID - offset++ - - binary.BigEndian.PutUint16(buf[offset:], cmd.ProxyID) - offset += 2 - - buf[offset] = ipVer - offset++ - - copy(buf[offset:], ipBytes) - offset += len(ipBytes) - - binary.BigEndian.PutUint16(buf[offset:], cmd.ClientPort) - offset += 2 - - binary.BigEndian.PutUint16(buf[offset:], cmd.DataLength) - - return buf, nil - - // ProxyInformationRequest: 1 byte ID + 2 bytes ProxyID. - case *ProxyInformationRequest: - buf := make([]byte, 1+2) - buf[0] = ProxyInformationRequestID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - return buf, nil - - // ProxyInformationResponse: - // Format: 1 byte ID + 1 byte Exists + (if exists:) - // 1 byte IP version + IP bytes + 2 bytes SourcePort + 2 bytes DestPort + 1 byte Protocol. - // (For simplicity, this marshaller always writes the IP and port info even if !Exists.) - case *ProxyInformationResponse: - if !cmd.Exists { - buf := make([]byte, 1+1) - buf[0] = ProxyInformationResponseID - buf[1] = 0 /* false */ - - return buf, nil - } - - ip := net.ParseIP(cmd.SourceIP) - - if ip == nil { - return nil, fmt.Errorf("invalid source IP: %v", cmd.SourceIP) - } - - var ipVer uint8 - var ipBytes []byte - - if ip4 := ip.To4(); ip4 != nil { - ipBytes = ip4 - ipVer = 4 - } else if ip16 := ip.To16(); ip16 != nil { - ipBytes = ip16 - ipVer = 6 - } else { - return nil, fmt.Errorf("unable to detect IP version for: %v", cmd.SourceIP) - } - - totalSize := 1 + // id - 1 + // Exists flag - 1 + // IP version - len(ipBytes) + - 2 + // SourcePort - 2 + // DestPort - 1 // Protocol - - buf := make([]byte, totalSize) - - offset := 0 - buf[offset] = ProxyInformationResponseID - offset++ - - // We already handle this above - buf[offset] = 1 /* true */ - offset++ - - buf[offset] = ipVer - offset++ - - copy(buf[offset:], ipBytes) - offset += len(ipBytes) - - binary.BigEndian.PutUint16(buf[offset:], cmd.SourcePort) - offset += 2 - - binary.BigEndian.PutUint16(buf[offset:], cmd.DestPort) - offset += 2 - - // Encode protocol as 1 byte. - switch cmd.Protocol { - case "tcp": - buf[offset] = TCP - case "udp": - buf[offset] = UDP - default: - return nil, fmt.Errorf("invalid protocol: %v", cmd.Protocol) - } - - // offset++ (not needed since we are at the end) - return buf, nil - - // ProxyConnectionInformationRequest: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. - case *ProxyConnectionInformationRequest: - buf := make([]byte, 1+2+2) - - buf[0] = ProxyConnectionInformationRequestID - binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) - binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) - - return buf, nil - - // ProxyConnectionInformationResponse: - // Format: 1 byte ID + 1 byte Exists + (if exists:) - // 1 byte IP version + IP bytes + 2 bytes ClientPort. - // This marshaller only writes the rest of the data if Exists. - case *ProxyConnectionInformationResponse: - if !cmd.Exists { - buf := make([]byte, 1+1) - buf[0] = ProxyConnectionInformationResponseID - buf[1] = 0 /* false */ - - return buf, nil - } - - ip := net.ParseIP(cmd.ClientIP) - - if ip == nil { - return nil, fmt.Errorf("invalid client IP: %v", cmd.ClientIP) - } - - var ipVer uint8 - var ipBytes []byte - if ip4 := ip.To4(); ip4 != nil { - ipBytes = ip4 - ipVer = 4 - } else if ip16 := ip.To16(); ip16 != nil { - ipBytes = ip16 - ipVer = 6 - } else { - return nil, fmt.Errorf("unable to detect IP version for: %v", cmd.ClientIP) - } - - totalSize := 1 + // id - 1 + // Exists flag - 1 + // IP version - len(ipBytes) + - 2 // ClientPort - - buf := make([]byte, totalSize) - offset := 0 - buf[offset] = ProxyConnectionInformationResponseID - offset++ - - // We already handle this above - buf[offset] = 1 /* true */ - offset++ - - buf[offset] = ipVer - offset++ - - copy(buf[offset:], ipBytes) - offset += len(ipBytes) - - binary.BigEndian.PutUint16(buf[offset:], cmd.ClientPort) - - return buf, nil - - default: - return nil, fmt.Errorf("unsupported command type") - } -} diff --git a/backend/sshappbackend/datacommands/marshalling_test.go b/backend/sshappbackend/datacommands/marshalling_test.go deleted file mode 100644 index 5b2e5ab..0000000 --- a/backend/sshappbackend/datacommands/marshalling_test.go +++ /dev/null @@ -1,652 +0,0 @@ -package datacommands - -import ( - "bytes" - "log" - "os" - "testing" -) - -var logLevel = os.Getenv("HERMES_LOG_LEVEL") - -func TestProxyStatusRequest(t *testing.T) { - commandInput := &ProxyStatusRequest{ - ProxyID: 19132, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } -} - -func TestProxyStatusResponse(t *testing.T) { - commandInput := &ProxyStatusResponse{ - ProxyID: 19132, - IsActive: true, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } - - if commandInput.IsActive != commandUnmarshalled.IsActive { - t.Fail() - log.Printf("IsActive's are not equal (orig: '%t', unmsh: '%t')", commandInput.IsActive, commandUnmarshalled.IsActive) - } -} - -func TestRemoveProxy(t *testing.T) { - commandInput := &RemoveProxy{ - ProxyID: 19132, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } -} - -func TestProxyConnectionsRequest(t *testing.T) { - commandInput := &ProxyConnectionsRequest{ - ProxyID: 19132, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } -} - -func TestProxyConnectionsResponse(t *testing.T) { - commandInput := &ProxyConnectionsResponse{ - Connections: []uint16{12831, 9455, 64219, 12, 32}, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) - - if !ok { - t.Fatal("failed typecast") - } - - for connectionIndex, originalConnection := range commandInput.Connections { - remoteConnection := commandUnmarshalled.Connections[connectionIndex] - - if originalConnection != remoteConnection { - t.Fail() - log.Printf("(in #%d) SourceIP's are not equal (orig: %d, unmsh: %d)", connectionIndex, originalConnection, connectionIndex) - } - } -} - -func TestProxyInstanceResponse(t *testing.T) { - commandInput := &ProxyInstanceResponse{ - Proxies: []uint16{12831, 9455, 64219, 12, 32}, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) - - if !ok { - t.Fatal("failed typecast") - } - - for proxyIndex, originalProxy := range commandInput.Proxies { - remoteProxy := commandUnmarshalled.Proxies[proxyIndex] - - if originalProxy != remoteProxy { - t.Fail() - log.Printf("(in #%d) Proxy IDs are not equal (orig: %d, unmsh: %d)", proxyIndex, originalProxy, remoteProxy) - } - } -} - -func TestTCPConnectionOpened(t *testing.T) { - commandInput := &TCPConnectionOpened{ - ProxyID: 19132, - ConnectionID: 25565, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPConnectionOpened) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } - - if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { - t.Fail() - log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) - } -} - -func TestTCPConnectionClosed(t *testing.T) { - commandInput := &TCPConnectionClosed{ - ProxyID: 19132, - ConnectionID: 25565, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPConnectionClosed) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } - - if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { - t.Fail() - log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) - } -} - -func TestTCPProxyData(t *testing.T) { - commandInput := &TCPProxyData{ - ProxyID: 19132, - ConnectionID: 25565, - DataLength: 1234, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPProxyData) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } - - if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { - t.Fail() - log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) - } - - if commandInput.DataLength != commandUnmarshalled.DataLength { - t.Fail() - log.Printf("DataLength's are not equal (orig: '%d', unmsh: '%d')", commandInput.DataLength, commandUnmarshalled.DataLength) - } -} - -func TestUDPProxyData(t *testing.T) { - commandInput := &UDPProxyData{ - ProxyID: 19132, - ClientIP: "68.51.23.54", - ClientPort: 28173, - DataLength: 1234, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*UDPProxyData) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } - - if commandInput.ClientIP != commandUnmarshalled.ClientIP { - t.Fail() - log.Printf("ClientIP's are not equal (orig: '%s', unmsh: '%s')", commandInput.ClientIP, commandUnmarshalled.ClientIP) - } - - if commandInput.ClientPort != commandUnmarshalled.ClientPort { - t.Fail() - log.Printf("ClientPort's are not equal (orig: '%d', unmsh: '%d')", commandInput.ClientPort, commandUnmarshalled.ClientPort) - } - - if commandInput.DataLength != commandUnmarshalled.DataLength { - t.Fail() - log.Printf("DataLength's are not equal (orig: '%d', unmsh: '%d')", commandInput.DataLength, commandUnmarshalled.DataLength) - } -} - -func TestProxyInformationRequest(t *testing.T) { - commandInput := &ProxyInformationRequest{ - ProxyID: 19132, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } -} - -func TestProxyInformationResponseExists(t *testing.T) { - commandInput := &ProxyInformationResponse{ - Exists: true, - SourceIP: "192.168.0.139", - SourcePort: 19132, - DestPort: 19132, - Protocol: "tcp", - } - - commandMarshalled, err := Marshal(commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Exists != commandUnmarshalled.Exists { - t.Fail() - log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) - } - - if commandInput.SourceIP != commandUnmarshalled.SourceIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) - } - - if commandInput.SourcePort != commandUnmarshalled.SourcePort { - t.Fail() - log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) - } - - if commandInput.DestPort != commandUnmarshalled.DestPort { - t.Fail() - log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) - } - - if commandInput.Protocol != commandUnmarshalled.Protocol { - t.Fail() - log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) - } -} - -func TestProxyInformationResponseNoExist(t *testing.T) { - commandInput := &ProxyInformationResponse{ - Exists: false, - } - - commandMarshalled, err := Marshal(commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Exists != commandUnmarshalled.Exists { - t.Fail() - log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) - } -} - -func TestProxyConnectionInformationRequest(t *testing.T) { - commandInput := &ProxyConnectionInformationRequest{ - ProxyID: 19132, - ConnectionID: 25565, - } - - commandMarshalled, err := Marshal(commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.ProxyID != commandUnmarshalled.ProxyID { - t.Fail() - log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) - } - - if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { - t.Fail() - log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) - } -} - -func TestProxyConnectionInformationResponseExists(t *testing.T) { - commandInput := &ProxyConnectionInformationResponse{ - Exists: true, - ClientIP: "192.168.0.139", - ClientPort: 19132, - } - - commandMarshalled, err := Marshal(commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Exists != commandUnmarshalled.Exists { - t.Fail() - log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) - } - - if commandInput.ClientIP != commandUnmarshalled.ClientIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.ClientIP, commandUnmarshalled.ClientIP) - } - - if commandInput.ClientPort != commandUnmarshalled.ClientPort { - t.Fail() - log.Printf("ClientPort's are not equal (orig: %d, unmsh: %d)", commandInput.ClientPort, commandUnmarshalled.ClientPort) - } -} - -func TestProxyConnectionInformationResponseNoExists(t *testing.T) { - commandInput := &ProxyConnectionInformationResponse{ - Exists: false, - } - - commandMarshalled, err := Marshal(commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Exists != commandUnmarshalled.Exists { - t.Fail() - log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) - } -} diff --git a/backend/sshappbackend/datacommands/unmarshal.go b/backend/sshappbackend/datacommands/unmarshal.go deleted file mode 100644 index d9d0523..0000000 --- a/backend/sshappbackend/datacommands/unmarshal.go +++ /dev/null @@ -1,422 +0,0 @@ -package datacommands - -import ( - "encoding/binary" - "fmt" - "io" - "net" -) - -// Unmarshal reads from the provided connection and returns -// the message type (as a string), the unmarshalled struct, or an error. -func Unmarshal(conn io.Reader) (interface{}, error) { - // Every command starts with a 1-byte command ID. - header := make([]byte, 1) - - if _, err := io.ReadFull(conn, header); err != nil { - return nil, fmt.Errorf("couldn't read command ID: %w", err) - } - - cmdID := header[0] - switch cmdID { - // ProxyStatusRequest: 1 byte ID + 2 bytes ProxyID. - case ProxyStatusRequestID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyStatusRequest ProxyID: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf) - - return &ProxyStatusRequest{ - ProxyID: proxyID, - }, nil - - // ProxyStatusResponse: 1 byte ID + 2 bytes ProxyID + 1 byte IsActive. - case ProxyStatusResponseID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyStatusResponse ProxyID: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf) - boolBuf := make([]byte, 1) - - if _, err := io.ReadFull(conn, boolBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyStatusResponse IsActive: %w", err) - } - - isActive := boolBuf[0] != 0 - - return &ProxyStatusResponse{ - ProxyID: proxyID, - IsActive: isActive, - }, nil - - // RemoveProxy: 1 byte ID + 2 bytes ProxyID. - case RemoveProxyID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read RemoveProxy ProxyID: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf) - - return &RemoveProxy{ - ProxyID: proxyID, - }, nil - - // ProxyConnectionsRequest: 1 byte ID + 2 bytes ProxyID. - case ProxyConnectionsRequestID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionsRequest ProxyID: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf) - - return &ProxyConnectionsRequest{ - ProxyID: proxyID, - }, nil - - // ProxyConnectionsResponse: 1 byte ID + 2 bytes Connections length + 2 bytes for each Connection in Connections. - case ProxyConnectionsResponseID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) - } - - length := binary.BigEndian.Uint16(buf) - connections := make([]uint16, length) - - var failedDuringReading error - - for connectionIndex := range connections { - if _, err := io.ReadFull(conn, buf); err != nil { - failedDuringReading = fmt.Errorf("couldn't read ProxyConnectionsResponse with position of %d: %w", connectionIndex, err) - break - } - - connections[connectionIndex] = binary.BigEndian.Uint16(buf) - } - - return &ProxyConnectionsResponse{ - Connections: connections, - }, failedDuringReading - - // ProxyInstanceResponse: 1 byte ID + 2 bytes Proxies length + 2 bytes for each Proxy in Proxies. - case ProxyInstanceResponseID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) - } - - length := binary.BigEndian.Uint16(buf) - proxies := make([]uint16, length) - - var failedDuringReading error - - for connectionIndex := range proxies { - if _, err := io.ReadFull(conn, buf); err != nil { - failedDuringReading = fmt.Errorf("couldn't read ProxyConnectionsResponse with position of %d: %w", connectionIndex, err) - break - } - - proxies[connectionIndex] = binary.BigEndian.Uint16(buf) - } - - return &ProxyInstanceResponse{ - Proxies: proxies, - }, failedDuringReading - - // TCPConnectionOpened: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. - case TCPConnectionOpenedID: - buf := make([]byte, 2+2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read TCPConnectionOpened fields: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf[0:2]) - connectionID := binary.BigEndian.Uint16(buf[2:4]) - - return &TCPConnectionOpened{ - ProxyID: proxyID, - ConnectionID: connectionID, - }, nil - - // TCPConnectionClosed: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. - case TCPConnectionClosedID: - buf := make([]byte, 2+2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read TCPConnectionClosed fields: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf[0:2]) - connectionID := binary.BigEndian.Uint16(buf[2:4]) - - return &TCPConnectionClosed{ - ProxyID: proxyID, - ConnectionID: connectionID, - }, nil - - // TCPProxyData: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + 2 bytes DataLength. - case TCPProxyDataID: - buf := make([]byte, 2+2+2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read TCPProxyData fields: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf[0:2]) - connectionID := binary.BigEndian.Uint16(buf[2:4]) - dataLength := binary.BigEndian.Uint16(buf[4:6]) - - return &TCPProxyData{ - ProxyID: proxyID, - ConnectionID: connectionID, - DataLength: dataLength, - }, nil - - // UDPProxyData: - // Format: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + - // 1 byte IP version + IP bytes + 2 bytes ClientPort + 2 bytes DataLength. - case UDPProxyDataID: - // Read 2 bytes ProxyID + 2 bytes ConnectionID. - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read UDPProxyData ProxyID/ConnectionID: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf) - - // Read IP version. - ipVerBuf := make([]byte, 1) - - if _, err := io.ReadFull(conn, ipVerBuf); err != nil { - return nil, fmt.Errorf("couldn't read UDPProxyData IP version: %w", err) - } - - var ipSize int - - if ipVerBuf[0] == 4 { - ipSize = IPv4Size - } else if ipVerBuf[0] == 6 { - ipSize = IPv6Size - } else { - return nil, fmt.Errorf("invalid IP version received: %v", ipVerBuf[0]) - } - - // Read the IP bytes. - ipBytes := make([]byte, ipSize) - if _, err := io.ReadFull(conn, ipBytes); err != nil { - return nil, fmt.Errorf("couldn't read UDPProxyData IP bytes: %w", err) - } - clientIP := net.IP(ipBytes).String() - - // Read ClientPort. - portBuf := make([]byte, 2) - - if _, err := io.ReadFull(conn, portBuf); err != nil { - return nil, fmt.Errorf("couldn't read UDPProxyData ClientPort: %w", err) - } - - clientPort := binary.BigEndian.Uint16(portBuf) - - // Read DataLength. - dataLengthBuf := make([]byte, 2) - - if _, err := io.ReadFull(conn, dataLengthBuf); err != nil { - return nil, fmt.Errorf("couldn't read UDPProxyData DataLength: %w", err) - } - - dataLength := binary.BigEndian.Uint16(dataLengthBuf) - - return &UDPProxyData{ - ProxyID: proxyID, - ClientIP: clientIP, - ClientPort: clientPort, - DataLength: dataLength, - }, nil - - // ProxyInformationRequest: 1 byte ID + 2 bytes ProxyID. - case ProxyInformationRequestID: - buf := make([]byte, 2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyInformationRequest ProxyID: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf) - - return &ProxyInformationRequest{ - ProxyID: proxyID, - }, nil - - // ProxyInformationResponse: - // Format: 1 byte ID + 1 byte Exists + - // 1 byte IP version + IP bytes + 2 bytes SourcePort + 2 bytes DestPort + 1 byte Protocol. - case ProxyInformationResponseID: - // Read Exists flag. - boolBuf := make([]byte, 1) - - if _, err := io.ReadFull(conn, boolBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyInformationResponse Exists flag: %w", err) - } - - exists := boolBuf[0] != 0 - - if !exists { - return &ProxyInformationResponse{ - Exists: exists, - }, nil - } - - // Read IP version. - ipVerBuf := make([]byte, 1) - - if _, err := io.ReadFull(conn, ipVerBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyInformationResponse IP version: %w", err) - } - - var ipSize int - - if ipVerBuf[0] == 4 { - ipSize = IPv4Size - } else if ipVerBuf[0] == 6 { - ipSize = IPv6Size - } else { - return nil, fmt.Errorf("invalid IP version in ProxyInformationResponse: %v", ipVerBuf[0]) - } - - // Read the source IP bytes. - ipBytes := make([]byte, ipSize) - - if _, err := io.ReadFull(conn, ipBytes); err != nil { - return nil, fmt.Errorf("couldn't read ProxyInformationResponse IP bytes: %w", err) - } - - sourceIP := net.IP(ipBytes).String() - - // Read SourcePort and DestPort. - portsBuf := make([]byte, 2+2) - - if _, err := io.ReadFull(conn, portsBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyInformationResponse ports: %w", err) - } - - sourcePort := binary.BigEndian.Uint16(portsBuf[0:2]) - destPort := binary.BigEndian.Uint16(portsBuf[2:4]) - - // Read protocol. - protoBuf := make([]byte, 1) - - if _, err := io.ReadFull(conn, protoBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyInformationResponse protocol: %w", err) - } - - var protocol string - - if protoBuf[0] == TCP { - protocol = "tcp" - } else if protoBuf[0] == UDP { - protocol = "udp" - } else { - return nil, fmt.Errorf("invalid protocol value in ProxyInformationResponse: %d", protoBuf[0]) - } - - return &ProxyInformationResponse{ - Exists: exists, - SourceIP: sourceIP, - SourcePort: sourcePort, - DestPort: destPort, - Protocol: protocol, - }, nil - - // ProxyConnectionInformationRequest: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. - case ProxyConnectionInformationRequestID: - buf := make([]byte, 2+2) - - if _, err := io.ReadFull(conn, buf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionInformationRequest fields: %w", err) - } - - proxyID := binary.BigEndian.Uint16(buf[0:2]) - connectionID := binary.BigEndian.Uint16(buf[2:4]) - - return &ProxyConnectionInformationRequest{ - ProxyID: proxyID, - ConnectionID: connectionID, - }, nil - - // ProxyConnectionInformationResponse: - // Format: 1 byte ID + 1 byte Exists + 1 byte IP version + IP bytes + 2 bytes ClientPort. - case ProxyConnectionInformationResponseID: - // Read Exists flag. - boolBuf := make([]byte, 1) - if _, err := io.ReadFull(conn, boolBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse Exists flag: %w", err) - } - - exists := boolBuf[0] != 0 - - if !exists { - return &ProxyConnectionInformationResponse{ - Exists: exists, - }, nil - } - - // Read IP version. - ipVerBuf := make([]byte, 1) - - if _, err := io.ReadFull(conn, ipVerBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP version: %w", err) - } - - if ipVerBuf[0] != 4 && ipVerBuf[0] != 6 { - return nil, fmt.Errorf("invalid IP version in ProxyConnectionInformationResponse: %v", ipVerBuf[0]) - } - - var ipSize int - - if ipVerBuf[0] == 4 { - ipSize = IPv4Size - } else { - ipSize = IPv6Size - } - - // Read IP bytes. - ipBytes := make([]byte, ipSize) - - if _, err := io.ReadFull(conn, ipBytes); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP bytes: %w", err) - } - - clientIP := net.IP(ipBytes).String() - - // Read ClientPort. - portBuf := make([]byte, 2) - - if _, err := io.ReadFull(conn, portBuf); err != nil { - return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse ClientPort: %w", err) - } - - clientPort := binary.BigEndian.Uint16(portBuf) - - return &ProxyConnectionInformationResponse{ - Exists: exists, - ClientIP: clientIP, - ClientPort: clientPort, - }, nil - default: - return nil, fmt.Errorf("unknown command id: %v", cmdID) - } -} diff --git a/backend/sshappbackend/gaslighter/gaslighter.go b/backend/sshappbackend/gaslighter/gaslighter.go deleted file mode 100644 index ecccec7..0000000 --- a/backend/sshappbackend/gaslighter/gaslighter.go +++ /dev/null @@ -1,30 +0,0 @@ -package gaslighter - -import "io" - -type Gaslighter struct { - Byte byte - HasGaslit bool - ProxiedReader io.Reader -} - -func (gaslighter *Gaslighter) Read(p []byte) (n int, err error) { - if gaslighter.HasGaslit { - return gaslighter.ProxiedReader.Read(p) - } - - if len(p) == 0 { - return 0, nil - } - - p[0] = gaslighter.Byte - gaslighter.HasGaslit = true - - if len(p) > 1 { - n, err := gaslighter.ProxiedReader.Read(p[1:]) - - return n + 1, err - } else { - return 1, nil - } -} diff --git a/backend/sshappbackend/local-code/fs.go b/backend/sshappbackend/local-code/fs.go deleted file mode 100644 index 7fe43e4..0000000 --- a/backend/sshappbackend/local-code/fs.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import ( - "embed" -) - -//go:embed remote-bin -var binFiles embed.FS diff --git a/backend/sshappbackend/local-code/logger.go b/backend/sshappbackend/local-code/logger.go deleted file mode 100644 index d8ed3f9..0000000 --- a/backend/sshappbackend/local-code/logger.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "strings" - - "github.com/charmbracelet/log" -) - -type WriteLogger struct{} - -func (writer WriteLogger) Write(p []byte) (n int, err error) { - logSplit := strings.Split(string(p), "\n") - - for _, line := range logSplit { - if line == "" { - continue - } - - log.Infof("Process: %s", line) - } - - return len(p), err -} diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go deleted file mode 100644 index 34a85d0..0000000 --- a/backend/sshappbackend/local-code/main.go +++ /dev/null @@ -1,868 +0,0 @@ -package main - -import ( - "bytes" - "crypto/md5" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "math/rand/v2" - "net" - "os" - "strings" - "sync" - "time" - - "git.terah.dev/imterah/hermes/backend/backendutil" - "git.terah.dev/imterah/hermes/backend/commonbackend" - "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" - "git.terah.dev/imterah/hermes/backend/sshappbackend/gaslighter" - "git.terah.dev/imterah/hermes/backend/sshappbackend/local-code/porttranslation" - "github.com/charmbracelet/log" - "github.com/go-playground/validator/v10" - "github.com/pkg/sftp" - "golang.org/x/crypto/ssh" -) - -var validatorInstance *validator.Validate - -type TCPProxy struct { - proxyInformation *commonbackend.AddProxy - connections map[uint16]net.Conn -} - -type UDPProxy struct { - proxyInformation *commonbackend.AddProxy - portTranslation *porttranslation.PortTranslation -} - -type SSHAppBackendData struct { - IP string `json:"ip" validate:"required"` - Port uint16 `json:"port" validate:"required"` - Username string `json:"username" validate:"required"` - PrivateKey string `json:"privateKey" validate:"required"` - ListenOnIPs []string `json:"listenOnIPs"` -} - -type SSHAppBackend struct { - config *SSHAppBackendData - conn *ssh.Client - listener net.Listener - currentSock net.Conn - - tcpProxies map[uint16]*TCPProxy - udpProxies map[uint16]*UDPProxy - - // globalNonCriticalMessageLock: Locks all messages that don't need low-latency transmissions & high - // speed behind a lock. This ensures safety when it comes to handling messages correctly. - globalNonCriticalMessageLock sync.Mutex - // globalNonCriticalMessageChan: Channel for handling messages that need a reply / aren't critical. - globalNonCriticalMessageChan chan interface{} -} - -func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { - log.Info("SSHAppBackend is initializing...") - - if validatorInstance == nil { - validatorInstance = validator.New() - } - - backend.globalNonCriticalMessageChan = make(chan interface{}) - backend.tcpProxies = map[uint16]*TCPProxy{} - backend.udpProxies = map[uint16]*UDPProxy{} - - var backendData SSHAppBackendData - - if err := json.Unmarshal(configBytes, &backendData); err != nil { - return false, err - } - - if err := validatorInstance.Struct(&backendData); err != nil { - return false, err - } - - backend.config = &backendData - - if len(backend.config.ListenOnIPs) == 0 { - backend.config.ListenOnIPs = []string{"0.0.0.0"} - } - - signer, err := ssh.ParsePrivateKey([]byte(backendData.PrivateKey)) - - if err != nil { - log.Warnf("Failed to initialize: %s", err.Error()) - return false, err - } - - auth := ssh.PublicKeys(signer) - - config := &ssh.ClientConfig{ - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - User: backendData.Username, - Auth: []ssh.AuthMethod{ - auth, - }, - } - - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config) - - if err != nil { - log.Warnf("Failed to initialize: %s", err.Error()) - return false, err - } - - backend.conn = conn - - log.Debug("SSHAppBackend has connected successfully.") - log.Debug("Getting CPU architecture...") - - session, err := backend.conn.NewSession() - - if err != nil { - log.Warnf("Failed to create session: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - var stdoutBuf bytes.Buffer - session.Stdout = &stdoutBuf - - err = session.Run("uname -m") - - if err != nil { - log.Warnf("Failed to run uname command: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - cpuArchBytes := make([]byte, stdoutBuf.Len()) - stdoutBuf.Read(cpuArchBytes) - - cpuArch := string(cpuArchBytes) - cpuArch = cpuArch[:len(cpuArch)-1] - - var backendBinary string - - // Ordered in (subjective) popularity - if cpuArch == "x86_64" { - backendBinary = "remote-bin/rt-amd64" - } else if cpuArch == "aarch64" { - backendBinary = "remote-bin/rt-arm64" - } else if cpuArch == "arm" { - backendBinary = "remote-bin/rt-arm" - } else if len(cpuArch) == 4 && string(cpuArch[0]) == "i" && strings.HasSuffix(cpuArch, "86") { - backendBinary = "remote-bin/rt-386" - } else { - log.Warn("Failed to determine executable to use: CPU architecture not compiled/supported currently") - conn.Close() - backend.conn = nil - return false, fmt.Errorf("CPU architecture not compiled/supported currently") - } - - log.Debug("Checking if we need to copy the application...") - - var binary []byte - needsToCopyBinary := true - - session, err = backend.conn.NewSession() - - if err != nil { - log.Warnf("Failed to create session: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - session.Stdout = &stdoutBuf - - err = session.Start("[ -f /tmp/sshappbackend.runtime ] && md5sum /tmp/sshappbackend.runtime | cut -d \" \" -f 1") - - if err != nil { - log.Warnf("Failed to calculate hash of possibly existing backend: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - fileExists := stdoutBuf.Len() != 0 - - if fileExists { - remoteMD5HashStringBuf := make([]byte, stdoutBuf.Len()) - stdoutBuf.Read(remoteMD5HashStringBuf) - - remoteMD5HashString := string(remoteMD5HashStringBuf) - remoteMD5HashString = remoteMD5HashString[:len(remoteMD5HashString)-1] - - remoteMD5Hash, err := hex.DecodeString(remoteMD5HashString) - - if err != nil { - log.Warnf("Failed to decode hex: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - binary, err = binFiles.ReadFile(backendBinary) - - if err != nil { - log.Warnf("Failed to read file in the embedded FS: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, fmt.Errorf("(embedded FS): %s", err.Error()) - } - - localMD5Hash := md5.Sum(binary) - - log.Infof("remote: %s, local: %s", remoteMD5HashString, hex.EncodeToString(localMD5Hash[:])) - - if bytes.Compare(localMD5Hash[:], remoteMD5Hash) == 0 { - needsToCopyBinary = false - } - } - - if needsToCopyBinary { - log.Debug("Copying binary...") - - sftpInstance, err := sftp.NewClient(conn) - - if err != nil { - log.Warnf("Failed to initialize SFTP: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - defer sftpInstance.Close() - - if len(binary) == 0 { - binary, err = binFiles.ReadFile(backendBinary) - - if err != nil { - log.Warnf("Failed to read file in the embedded FS: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, fmt.Errorf("(embedded FS): %s", err.Error()) - } - } - - var file *sftp.File - - if fileExists { - file, err = sftpInstance.OpenFile("/tmp/sshappbackend.runtime", os.O_WRONLY) - } else { - file, err = sftpInstance.Create("/tmp/sshappbackend.runtime") - } - - if err != nil { - log.Warnf("Failed to create (or open) file: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - _, err = file.Write(binary) - - if err != nil { - log.Warnf("Failed to write file: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - err = file.Chmod(0755) - - if err != nil { - log.Warnf("Failed to change permissions on file: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - log.Debug("Done copying file.") - sftpInstance.Close() - } else { - log.Debug("Skipping copying as there's a copy on disk already.") - } - - log.Debug("Initializing Unix socket...") - - socketPath := fmt.Sprintf("/tmp/sock-%d.sock", rand.Uint()) - listener, err := conn.ListenUnix(socketPath) - - if err != nil { - log.Warnf("Failed to listen on socket: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - log.Debug("Starting process...") - - session, err = backend.conn.NewSession() - - if err != nil { - log.Warnf("Failed to create session: %s", err.Error()) - conn.Close() - backend.conn = nil - return false, err - } - - backend.listener = listener - - session.Stdout = WriteLogger{} - session.Stderr = WriteLogger{} - - go func() { - for { - err := session.Run(fmt.Sprintf("HERMES_LOG_LEVEL=\"%s\" HERMES_API_SOCK=\"%s\" /tmp/sshappbackend.runtime", os.Getenv("HERMES_LOG_LEVEL"), socketPath)) - - if err != nil && !errors.Is(err, &ssh.ExitError{}) && !errors.Is(err, &ssh.ExitMissingError{}) { - log.Errorf("Critically failed during execution of remote code: %s", err.Error()) - return - } else { - log.Warn("Remote code failed for an unknown reason. Restarting...") - } - } - }() - - go backend.sockServerHandler() - - log.Debug("Started process. Waiting for Unix socket connection...") - - for backend.currentSock == nil { - time.Sleep(10 * time.Millisecond) - } - - log.Debug("Detected connection. Sending initialization command...") - - proxyStatusRaw, err := backend.SendNonCriticalMessage(&commonbackend.Start{ - Arguments: []byte{}, - }) - - if err != nil { - return false, err - } - - proxyStatus, ok := proxyStatusRaw.(*commonbackend.BackendStatusResponse) - - if !ok { - return false, fmt.Errorf("recieved invalid response type: %T", proxyStatusRaw) - } - - if proxyStatus.StatusCode == commonbackend.StatusFailure { - if proxyStatus.Message == "" { - return false, fmt.Errorf("failed to initialize backend in remote code") - } else { - return false, fmt.Errorf("failed to initialize backend in remote code: %s", proxyStatus.Message) - } - } - - log.Info("SSHAppBackend has initialized successfully.") - - return true, nil -} - -func (backend *SSHAppBackend) StopBackend() (bool, error) { - err := backend.conn.Close() - - if err != nil { - return false, err - } - - return true, nil -} - -func (backend *SSHAppBackend) GetBackendStatus() (bool, error) { - return backend.conn != nil, nil -} - -func (backend *SSHAppBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { - proxyStatusRaw, err := backend.SendNonCriticalMessage(command) - - if err != nil { - return false, err - } - - proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse) - - if !ok { - return false, fmt.Errorf("recieved invalid response type: %T", proxyStatusRaw) - } - - if !proxyStatus.IsActive { - return false, fmt.Errorf("failed to initialize proxy in remote code") - } - - if command.Protocol == "tcp" { - backend.tcpProxies[proxyStatus.ProxyID] = &TCPProxy{ - proxyInformation: command, - } - - backend.tcpProxies[proxyStatus.ProxyID].connections = map[uint16]net.Conn{} - } else if command.Protocol == "udp" { - backend.udpProxies[proxyStatus.ProxyID] = &UDPProxy{ - proxyInformation: command, - portTranslation: &porttranslation.PortTranslation{}, - } - - backend.udpProxies[proxyStatus.ProxyID].portTranslation.UDPAddr = &net.UDPAddr{ - IP: net.ParseIP(command.SourceIP), - Port: int(command.SourcePort), - } - - udpMessageCommand := &datacommands.UDPProxyData{} - udpMessageCommand.ProxyID = proxyStatus.ProxyID - - backend.udpProxies[proxyStatus.ProxyID].portTranslation.WriteFrom = func(ip string, port uint16, data []byte) { - udpMessageCommand.ClientIP = ip - udpMessageCommand.ClientPort = port - udpMessageCommand.DataLength = uint16(len(data)) - - marshalledCommand, err := datacommands.Marshal(udpMessageCommand) - - if err != nil { - log.Warnf("Failed to marshal UDP message header") - return - } - - if _, err := backend.currentSock.Write(marshalledCommand); err != nil { - log.Warnf("Failed to write UDP message header") - return - } - - if _, err := backend.currentSock.Write(data); err != nil { - log.Warnf("Failed to write UDP message") - return - } - } - - go func() { - for { - time.Sleep(3 * time.Minute) - - // Checks if the proxy still exists before continuing - _, ok := backend.udpProxies[proxyStatus.ProxyID] - - if !ok { - return - } - - // Then attempt to run cleanup tasks - log.Debug("Running UDP proxy cleanup tasks (invoking CleanupPorts() on portTranslation)") - backend.udpProxies[proxyStatus.ProxyID].portTranslation.CleanupPorts() - } - }() - } - - return true, nil -} - -func (backend *SSHAppBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, error) { - if command.Protocol == "tcp" { - for proxyIndex, proxy := range backend.tcpProxies { - if proxy.proxyInformation.DestPort != command.DestPort { - continue - } - - onDisconnect := &datacommands.TCPConnectionClosed{ - ProxyID: proxyIndex, - } - - for connectionIndex, connection := range proxy.connections { - connection.Close() - delete(proxy.connections, connectionIndex) - - onDisconnect.ConnectionID = connectionIndex - disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect) - - if err != nil { - log.Errorf("failed to marshal disconnection message: %s", err.Error()) - } - - backend.currentSock.Write(disconnectionCommandMarshalled) - } - - proxyStatusRaw, err := backend.SendNonCriticalMessage(&datacommands.RemoveProxy{ - ProxyID: proxyIndex, - }) - - if err != nil { - return false, err - } - - proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse) - - if !ok { - log.Warn("Failed to stop proxy: typecast failed") - return true, fmt.Errorf("failed to stop proxy: typecast failed") - } - - if proxyStatus.IsActive { - log.Warn("Failed to stop proxy: still running") - return true, fmt.Errorf("failed to stop proxy: still running") - } - } - } else if command.Protocol == "udp" { - for proxyIndex, proxy := range backend.udpProxies { - if proxy.proxyInformation.DestPort != command.DestPort { - continue - } - - proxyStatusRaw, err := backend.SendNonCriticalMessage(&datacommands.RemoveProxy{ - ProxyID: proxyIndex, - }) - - if err != nil { - return false, err - } - - proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse) - - if !ok { - log.Warn("Failed to stop proxy: typecast failed") - return true, fmt.Errorf("failed to stop proxy: typecast failed") - } - - if proxyStatus.IsActive { - log.Warn("Failed to stop proxy: still running") - return true, fmt.Errorf("failed to stop proxy: still running") - } - - proxy.portTranslation.StopAllPorts() - delete(backend.udpProxies, proxyIndex) - } - } - - return false, fmt.Errorf("could not find the proxy") -} - -func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection { - connections := []*commonbackend.ProxyClientConnection{} - informationRequest := &datacommands.ProxyConnectionInformationRequest{} - - for proxyID, tcpProxy := range backend.tcpProxies { - informationRequest.ProxyID = proxyID - - for connectionID := range tcpProxy.connections { - informationRequest.ConnectionID = connectionID - - proxyStatusRaw, err := backend.SendNonCriticalMessage(informationRequest) - - if err != nil { - log.Warnf("Failed to get connection information for Proxy ID: %d, Connection ID: %d: %s", proxyID, connectionID, err.Error()) - return connections - } - - connectionStatus, ok := proxyStatusRaw.(*datacommands.ProxyConnectionInformationResponse) - - if !ok { - log.Warn("Failed to get connection response: typecast failed") - return connections - } - - if !connectionStatus.Exists { - log.Warnf("Connection with proxy ID: %d, Connection ID: %d is reported to not exist!", proxyID, connectionID) - tcpProxy.connections[connectionID].Close() - } - - connections = append(connections, &commonbackend.ProxyClientConnection{ - SourceIP: tcpProxy.proxyInformation.SourceIP, - SourcePort: tcpProxy.proxyInformation.SourcePort, - DestPort: tcpProxy.proxyInformation.DestPort, - ClientIP: connectionStatus.ClientIP, - ClientPort: connectionStatus.ClientPort, - }) - } - } - - return connections -} - -// We don't have any parameter limitations, so we should be good. -func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { - return &commonbackend.CheckParametersResponse{ - IsValid: true, - } -} - -func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { - var backendData SSHAppBackendData - - if validatorInstance == nil { - validatorInstance = validator.New() - } - - if err := json.Unmarshal(arguments, &backendData); err != nil { - return &commonbackend.CheckParametersResponse{ - IsValid: false, - Message: fmt.Sprintf("could not read json: %s", err.Error()), - } - } - - if err := validatorInstance.Struct(&backendData); err != nil { - return &commonbackend.CheckParametersResponse{ - IsValid: false, - Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()), - } - } - - return &commonbackend.CheckParametersResponse{ - IsValid: true, - } -} - -func (backend *SSHAppBackend) OnTCPConnectionOpened(proxyID, connectionID uint16) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", backend.tcpProxies[proxyID].proxyInformation.SourceIP, backend.tcpProxies[proxyID].proxyInformation.SourcePort)) - - if err != nil { - log.Warnf("failed to dial sock: %s", err.Error()) - } - - go func() { - dataBuf := make([]byte, 65535) - - tcpData := &datacommands.TCPProxyData{ - ProxyID: proxyID, - ConnectionID: connectionID, - } - - for { - len, err := conn.Read(dataBuf) - - if err != nil { - if errors.Is(err, net.ErrClosed) { - return - } else if err.Error() != "EOF" { - log.Warnf("failed to read from sock: %s", err.Error()) - } - - conn.Close() - break - } - - tcpData.DataLength = uint16(len) - marshalledMessageCommand, err := datacommands.Marshal(tcpData) - - if err != nil { - log.Warnf("failed to marshal message data: %s", err.Error()) - - conn.Close() - break - } - - if _, err := backend.currentSock.Write(marshalledMessageCommand); err != nil { - log.Warnf("failed to send marshalled message data: %s", err.Error()) - - conn.Close() - break - } - - if _, err := backend.currentSock.Write(dataBuf[:len]); err != nil { - log.Warnf("failed to send raw message data: %s", err.Error()) - - conn.Close() - break - } - } - - onDisconnect := &datacommands.TCPConnectionClosed{ - ProxyID: proxyID, - ConnectionID: connectionID, - } - - disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect) - - if err != nil { - log.Errorf("failed to marshal disconnection message: %s", err.Error()) - } - - backend.currentSock.Write(disconnectionCommandMarshalled) - }() - - backend.tcpProxies[proxyID].connections[connectionID] = conn -} - -func (backend *SSHAppBackend) OnTCPConnectionClosed(proxyID, connectionID uint16) { - proxy, ok := backend.tcpProxies[proxyID] - - if !ok { - log.Warn("Could not find TCP proxy") - } - - connection, ok := proxy.connections[connectionID] - - if !ok { - log.Warn("Could not find connection in TCP proxy") - } - - connection.Close() - delete(proxy.connections, connectionID) -} - -func (backend *SSHAppBackend) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) { - proxy, ok := backend.tcpProxies[message.ProxyID] - - if !ok { - log.Warn("Could not find TCP proxy") - } - - connection, ok := proxy.connections[message.ConnectionID] - - if !ok { - log.Warn("Could not find connection in TCP proxy") - } - - connection.Write(data) -} - -func (backend *SSHAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) { - proxy, ok := backend.udpProxies[message.ProxyID] - - if !ok { - log.Warn("Could not find UDP proxy") - } - - if _, err := proxy.portTranslation.WriteTo(message.ClientIP, message.ClientPort, data); err != nil { - log.Warnf("Failed to write to UDP: %s", err.Error()) - } -} - -func (backend *SSHAppBackend) SendNonCriticalMessage(iface interface{}) (interface{}, error) { - if backend.currentSock == nil { - return nil, fmt.Errorf("socket connection not initialized yet") - } - - bytes, err := datacommands.Marshal(iface) - - if err != nil && err.Error() == "unsupported command type" { - bytes, err = commonbackend.Marshal(iface) - - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - - backend.globalNonCriticalMessageLock.Lock() - - if _, err := backend.currentSock.Write(bytes); err != nil { - backend.globalNonCriticalMessageLock.Unlock() - return nil, fmt.Errorf("failed to write message: %s", err.Error()) - } - - reply, ok := <-backend.globalNonCriticalMessageChan - - if !ok { - backend.globalNonCriticalMessageLock.Unlock() - return nil, fmt.Errorf("failed to get reply back: chan not OK") - } - - backend.globalNonCriticalMessageLock.Unlock() - return reply, nil -} - -func (backend *SSHAppBackend) sockServerHandler() { - for { - conn, err := backend.listener.Accept() - - if err != nil { - log.Warnf("Failed to accept remote connection: %s", err.Error()) - } - - log.Debug("Successfully connected.") - - backend.currentSock = conn - - commandID := make([]byte, 1) - - gaslighter := &gaslighter.Gaslighter{} - gaslighter.ProxiedReader = conn - - dataBuffer := make([]byte, 65535) - - var commandRaw interface{} - - for { - if _, err := conn.Read(commandID); err != nil { - log.Warnf("Failed to read command ID: %s", err.Error()) - return - } - - gaslighter.Byte = commandID[0] - gaslighter.HasGaslit = false - - if gaslighter.Byte > 100 { - commandRaw, err = datacommands.Unmarshal(gaslighter) - } else { - commandRaw, err = commonbackend.Unmarshal(gaslighter) - } - - if err != nil { - log.Warnf("Failed to parse command: %s", err.Error()) - } - - switch command := commandRaw.(type) { - case *datacommands.TCPConnectionOpened: - backend.OnTCPConnectionOpened(command.ProxyID, command.ConnectionID) - case *datacommands.TCPConnectionClosed: - backend.OnTCPConnectionClosed(command.ProxyID, command.ConnectionID) - case *datacommands.TCPProxyData: - if _, err := io.ReadFull(conn, dataBuffer[:command.DataLength]); err != nil { - log.Warnf("Failed to read entire data buffer: %s", err.Error()) - break - } - - backend.HandleTCPMessage(command, dataBuffer[:command.DataLength]) - case *datacommands.UDPProxyData: - if _, err := io.ReadFull(conn, dataBuffer[:command.DataLength]); err != nil { - log.Warnf("Failed to read entire data buffer: %s", err.Error()) - break - } - - backend.HandleUDPMessage(command, dataBuffer[:command.DataLength]) - default: - select { - case backend.globalNonCriticalMessageChan <- command: - default: - } - } - } - } -} - -func main() { - logLevel := os.Getenv("HERMES_LOG_LEVEL") - - if logLevel != "" { - switch logLevel { - case "debug": - log.SetLevel(log.DebugLevel) - - case "info": - log.SetLevel(log.InfoLevel) - - case "warn": - log.SetLevel(log.WarnLevel) - - case "error": - log.SetLevel(log.ErrorLevel) - - case "fatal": - log.SetLevel(log.FatalLevel) - } - } - - backend := &SSHAppBackend{} - - application := backendutil.NewHelper(backend) - err := application.Start() - - if err != nil { - log.Fatalf("failed execution in application: %s", err.Error()) - } -} diff --git a/backend/sshappbackend/local-code/porttranslation/translation.go b/backend/sshappbackend/local-code/porttranslation/translation.go deleted file mode 100644 index b8c0454..0000000 --- a/backend/sshappbackend/local-code/porttranslation/translation.go +++ /dev/null @@ -1,112 +0,0 @@ -package porttranslation - -import ( - "fmt" - "net" - "sync" - "time" -) - -type connectionData struct { - udpConn *net.UDPConn - buf []byte - hasBeenAliveFor time.Time -} - -type PortTranslation struct { - UDPAddr *net.UDPAddr - WriteFrom func(ip string, port uint16, data []byte) - - newConnectionLock sync.Mutex - connections map[string]map[uint16]*connectionData -} - -func (translation *PortTranslation) CleanupPorts() { - if translation.connections == nil { - translation.connections = map[string]map[uint16]*connectionData{} - return - } - - for connectionIPIndex, connectionPorts := range translation.connections { - anyAreAlive := false - - for connectionPortIndex, connectionData := range connectionPorts { - if time.Now().Before(connectionData.hasBeenAliveFor.Add(3 * time.Minute)) { - anyAreAlive = true - continue - } - - connectionData.udpConn.Close() - delete(connectionPorts, connectionPortIndex) - } - - if !anyAreAlive { - delete(translation.connections, connectionIPIndex) - } - } -} - -func (translation *PortTranslation) StopAllPorts() { - if translation.connections == nil { - return - } - - for connectionIPIndex, connectionPorts := range translation.connections { - for connectionPortIndex, connectionData := range connectionPorts { - connectionData.udpConn.Close() - delete(connectionPorts, connectionPortIndex) - } - - delete(translation.connections, connectionIPIndex) - } - - translation.connections = nil -} - -func (translation *PortTranslation) WriteTo(ip string, port uint16, data []byte) (int, error) { - if translation.connections == nil { - translation.connections = map[string]map[uint16]*connectionData{} - } - - connectionPortData, ok := translation.connections[ip] - - if !ok { - translation.connections[ip] = map[uint16]*connectionData{} - connectionPortData = translation.connections[ip] - } - - connectionStruct, ok := connectionPortData[port] - - if !ok { - connectionPortData[port] = &connectionData{} - connectionStruct = connectionPortData[port] - - udpConn, err := net.DialUDP("udp", nil, translation.UDPAddr) - - if err != nil { - return 0, fmt.Errorf("failed to initialize UDP socket: %s", err.Error()) - } - - connectionStruct.udpConn = udpConn - connectionStruct.buf = make([]byte, 65535) - - go func() { - for { - n, err := udpConn.Read(connectionStruct.buf) - - if err != nil { - udpConn.Close() - delete(connectionPortData, port) - - return - } - - connectionStruct.hasBeenAliveFor = time.Now() - translation.WriteFrom(ip, port, connectionStruct.buf[:n]) - } - }() - } - - connectionStruct.hasBeenAliveFor = time.Now() - return connectionStruct.udpConn.Write(data) -} diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go deleted file mode 100644 index 2747f28..0000000 --- a/backend/sshappbackend/remote-code/backendutil_custom/application.go +++ /dev/null @@ -1,306 +0,0 @@ -package backendutil_custom - -import ( - "io" - "net" - "os" - - "git.terah.dev/imterah/hermes/backend/backendutil" - "git.terah.dev/imterah/hermes/backend/commonbackend" - "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" - "git.terah.dev/imterah/hermes/backend/sshappbackend/gaslighter" - "github.com/charmbracelet/log" -) - -type BackendApplicationHelper struct { - Backend BackendInterface - SocketPath string - - socket net.Conn -} - -func (helper *BackendApplicationHelper) Start() error { - log.Debug("BackendApplicationHelper is starting") - err := backendutil.ConfigureProfiling() - - if err != nil { - return err - } - - log.Debug("Currently waiting for Unix socket connection...") - - helper.socket, err = net.Dial("unix", helper.SocketPath) - - if err != nil { - return err - } - - helper.Backend.OnSocketConnection(helper.socket) - - log.Debug("Sucessfully connected") - - gaslighter := &gaslighter.Gaslighter{} - gaslighter.ProxiedReader = helper.socket - - commandID := make([]byte, 1) - - for { - if _, err := helper.socket.Read(commandID); err != nil { - return err - } - - gaslighter.Byte = commandID[0] - gaslighter.HasGaslit = false - - var commandRaw interface{} - - if gaslighter.Byte > 100 { - commandRaw, err = datacommands.Unmarshal(gaslighter) - } else { - commandRaw, err = commonbackend.Unmarshal(gaslighter) - } - - if err != nil { - return err - } - - switch command := commandRaw.(type) { - case *datacommands.ProxyConnectionsRequest: - connections := helper.Backend.GetAllClientConnections(command.ProxyID) - - serverParams := &datacommands.ProxyConnectionsResponse{ - Connections: connections, - } - - byteData, err := datacommands.Marshal(serverParams) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - case *datacommands.RemoveProxy: - ok, err := helper.Backend.StopProxy(command) - var hasAnyFailed bool - - if !ok { - log.Warnf("failed to remove proxy (ID %d): RemoveProxy returned into failure state", command.ProxyID) - hasAnyFailed = true - } else if err != nil { - log.Warnf("failed to remove proxy (ID %d): %s", command.ProxyID, err.Error()) - hasAnyFailed = true - } - - response := &datacommands.ProxyStatusResponse{ - ProxyID: command.ProxyID, - IsActive: hasAnyFailed, - } - - responseMarshalled, err := datacommands.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *datacommands.ProxyInformationRequest: - response := helper.Backend.ResolveProxy(command.ProxyID) - responseMarshalled, err := datacommands.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *datacommands.ProxyConnectionInformationRequest: - response := helper.Backend.ResolveConnection(command.ProxyID, command.ConnectionID) - responseMarshalled, err := datacommands.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *datacommands.TCPConnectionClosed: - helper.Backend.OnTCPConnectionClosed(command.ProxyID, command.ConnectionID) - case *datacommands.TCPProxyData: - bytes := make([]byte, command.DataLength) - _, err := io.ReadFull(helper.socket, bytes) - - if err != nil { - log.Warn("failed to read TCP data") - } - - helper.Backend.HandleTCPMessage(command, bytes) - case *datacommands.UDPProxyData: - bytes := make([]byte, command.DataLength) - _, err := io.ReadFull(helper.socket, bytes) - - if err != nil { - log.Warn("failed to read TCP data") - } - - helper.Backend.HandleUDPMessage(command, bytes) - case *commonbackend.Start: - ok, err := helper.Backend.StartBackend(command.Arguments) - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.Stop: - ok, err := helper.Backend.StopBackend() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: !ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.BackendStatusRequest: - ok, err := helper.Backend.GetBackendStatus() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.AddProxy: - id, ok, err := helper.Backend.StartProxy(command) - var hasAnyFailed bool - - if !ok { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) - hasAnyFailed = true - } - - response := &datacommands.ProxyStatusResponse{ - ProxyID: id, - IsActive: !hasAnyFailed, - } - - responseMarshalled, err := datacommands.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.CheckClientParameters: - resp := helper.Backend.CheckParametersForConnections(command) - resp.InResponseTo = "checkClientParameters" - - byteData, err := commonbackend.Marshal(resp) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - case *commonbackend.CheckServerParameters: - resp := helper.Backend.CheckParametersForBackend(command.Arguments) - resp.InResponseTo = "checkServerParameters" - - byteData, err := commonbackend.Marshal(resp) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - default: - log.Warnf("Unsupported command recieved: %T", command) - } - } -} - -func NewHelper(backend BackendInterface) *BackendApplicationHelper { - socketPath, ok := os.LookupEnv("HERMES_API_SOCK") - - if !ok { - log.Warn("HERMES_API_SOCK is not defined! This will cause an issue unless the backend manually overwrites it") - } - - helper := &BackendApplicationHelper{ - Backend: backend, - SocketPath: socketPath, - } - - return helper -} diff --git a/backend/sshappbackend/remote-code/backendutil_custom/structure.go b/backend/sshappbackend/remote-code/backendutil_custom/structure.go deleted file mode 100644 index 65c5a23..0000000 --- a/backend/sshappbackend/remote-code/backendutil_custom/structure.go +++ /dev/null @@ -1,26 +0,0 @@ -package backendutil_custom - -import ( - "net" - - "git.terah.dev/imterah/hermes/backend/commonbackend" - "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" -) - -type BackendInterface interface { - StartBackend(arguments []byte) (bool, error) - StopBackend() (bool, error) - GetBackendStatus() (bool, error) - StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) - StopProxy(command *datacommands.RemoveProxy) (bool, error) - GetAllProxies() []uint16 - ResolveProxy(proxyID uint16) *datacommands.ProxyInformationResponse - GetAllClientConnections(proxyID uint16) []uint16 - ResolveConnection(proxyID, connectionID uint16) *datacommands.ProxyConnectionInformationResponse - CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse - CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse - OnTCPConnectionClosed(proxyID, connectionID uint16) - HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) - HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) - OnSocketConnection(sock net.Conn) -} diff --git a/backend/sshappbackend/remote-code/main.go b/backend/sshappbackend/remote-code/main.go deleted file mode 100644 index d56a7a3..0000000 --- a/backend/sshappbackend/remote-code/main.go +++ /dev/null @@ -1,460 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net" - "os" - "strconv" - "strings" - "sync" - - "git.terah.dev/imterah/hermes/backend/commonbackend" - "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" - "git.terah.dev/imterah/hermes/backend/sshappbackend/remote-code/backendutil_custom" - "github.com/charmbracelet/log" -) - -type TCPProxy struct { - connectionIDIndex uint16 - connectionIDLock sync.Mutex - - proxyInformation *commonbackend.AddProxy - connections map[uint16]net.Conn - server net.Listener -} - -type UDPProxy struct { - server *net.UDPConn - proxyInformation *commonbackend.AddProxy -} - -type SSHRemoteAppBackend struct { - proxyIDIndex uint16 - proxyIDLock sync.Mutex - - tcpProxies map[uint16]*TCPProxy - udpProxies map[uint16]*UDPProxy - - isRunning bool - - sock net.Conn -} - -func (backend *SSHRemoteAppBackend) StartBackend(byte []byte) (bool, error) { - backend.tcpProxies = map[uint16]*TCPProxy{} - backend.udpProxies = map[uint16]*UDPProxy{} - - backend.isRunning = true - - return true, nil -} - -func (backend *SSHRemoteAppBackend) StopBackend() (bool, error) { - for tcpProxyIndex, tcpProxy := range backend.tcpProxies { - for _, tcpConnection := range tcpProxy.connections { - tcpConnection.Close() - } - - tcpProxy.server.Close() - delete(backend.tcpProxies, tcpProxyIndex) - } - - for udpProxyIndex, udpProxy := range backend.udpProxies { - udpProxy.server.Close() - delete(backend.udpProxies, udpProxyIndex) - } - - backend.isRunning = false - return true, nil -} - -func (backend *SSHRemoteAppBackend) GetBackendStatus() (bool, error) { - return backend.isRunning, nil -} - -func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) { - // Allocate a new proxy ID - backend.proxyIDLock.Lock() - proxyID := backend.proxyIDIndex - backend.proxyIDIndex++ - backend.proxyIDLock.Unlock() - - if command.Protocol == "tcp" { - backend.tcpProxies[proxyID] = &TCPProxy{ - connections: map[uint16]net.Conn{}, - proxyInformation: command, - } - - server, err := net.Listen("tcp", fmt.Sprintf(":%d", command.DestPort)) - - if err != nil { - return 0, false, fmt.Errorf("failed to open server: %s", err.Error()) - } - - backend.tcpProxies[proxyID].server = server - - go func() { - for { - conn, err := server.Accept() - - if err != nil { - log.Warnf("failed to accept connection: %s", err.Error()) - return - } - - go func() { - backend.tcpProxies[proxyID].connectionIDLock.Lock() - connectionID := backend.tcpProxies[proxyID].connectionIDIndex - backend.tcpProxies[proxyID].connectionIDIndex++ - backend.tcpProxies[proxyID].connectionIDLock.Unlock() - - backend.tcpProxies[proxyID].connections[connectionID] = conn - - dataBuf := make([]byte, 65535) - - onConnection := &datacommands.TCPConnectionOpened{ - ProxyID: proxyID, - ConnectionID: connectionID, - } - - connectionCommandMarshalled, err := datacommands.Marshal(onConnection) - - if err != nil { - log.Errorf("failed to marshal connection message: %s", err.Error()) - } - - backend.sock.Write(connectionCommandMarshalled) - - tcpData := &datacommands.TCPProxyData{ - ProxyID: proxyID, - ConnectionID: connectionID, - } - - for { - len, err := conn.Read(dataBuf) - - if err != nil { - if errors.Is(err, net.ErrClosed) { - return - } else if err.Error() != "EOF" { - log.Warnf("failed to read from sock: %s", err.Error()) - } - - conn.Close() - break - } - - tcpData.DataLength = uint16(len) - marshalledMessageCommand, err := datacommands.Marshal(tcpData) - - if err != nil { - log.Warnf("failed to marshal message data: %s", err.Error()) - - conn.Close() - break - } - - if _, err := backend.sock.Write(marshalledMessageCommand); err != nil { - log.Warnf("failed to send marshalled message data: %s", err.Error()) - - conn.Close() - break - } - - if _, err := backend.sock.Write(dataBuf[:len]); err != nil { - log.Warnf("failed to send raw message data: %s", err.Error()) - - conn.Close() - break - } - } - - onDisconnect := &datacommands.TCPConnectionClosed{ - ProxyID: proxyID, - ConnectionID: connectionID, - } - - disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect) - - if err != nil { - log.Errorf("failed to marshal disconnection message: %s", err.Error()) - } - - backend.sock.Write(disconnectionCommandMarshalled) - }() - } - }() - } else if command.Protocol == "udp" { - backend.udpProxies[proxyID] = &UDPProxy{ - proxyInformation: command, - } - - server, err := net.ListenUDP("udp", &net.UDPAddr{ - IP: net.IPv4(0, 0, 0, 0), - Port: int(command.DestPort), - }) - - if err != nil { - return 0, false, fmt.Errorf("failed to open server: %s", err.Error()) - } - - backend.udpProxies[proxyID].server = server - dataBuf := make([]byte, 65535) - - udpProxyData := &datacommands.UDPProxyData{ - ProxyID: proxyID, - } - - go func() { - for { - len, addr, err := server.ReadFromUDP(dataBuf) - - if err != nil { - log.Warnf("failed to read from UDP socket: %s", err.Error()) - continue - } - - udpProxyData.ClientIP = addr.IP.String() - udpProxyData.ClientPort = uint16(addr.Port) - udpProxyData.DataLength = uint16(len) - - marshalledMessageCommand, err := datacommands.Marshal(udpProxyData) - - if err != nil { - log.Warnf("failed to marshal message data: %s", err.Error()) - continue - } - - if _, err := backend.sock.Write(marshalledMessageCommand); err != nil { - log.Warnf("failed to send marshalled message data: %s", err.Error()) - continue - } - - if _, err := backend.sock.Write(dataBuf[:len]); err != nil { - log.Warnf("failed to send raw message data: %s", err.Error()) - continue - } - } - }() - } - - return proxyID, true, nil -} - -func (backend *SSHRemoteAppBackend) StopProxy(command *datacommands.RemoveProxy) (bool, error) { - tcpProxy, ok := backend.tcpProxies[command.ProxyID] - - if !ok { - udpProxy, ok := backend.udpProxies[command.ProxyID] - - if !ok { - return ok, fmt.Errorf("could not find proxy") - } - - udpProxy.server.Close() - delete(backend.udpProxies, command.ProxyID) - } else { - for _, tcpConnection := range tcpProxy.connections { - tcpConnection.Close() - } - - tcpProxy.server.Close() - delete(backend.tcpProxies, command.ProxyID) - } - - return true, nil -} - -func (backend *SSHRemoteAppBackend) GetAllProxies() []uint16 { - proxyList := make([]uint16, len(backend.tcpProxies)+len(backend.udpProxies)) - - currentPos := 0 - - for tcpProxy := range backend.tcpProxies { - proxyList[currentPos] = tcpProxy - currentPos += 1 - } - - for udpProxy := range backend.udpProxies { - proxyList[currentPos] = udpProxy - currentPos += 1 - } - - return proxyList -} - -func (backend *SSHRemoteAppBackend) ResolveProxy(proxyID uint16) *datacommands.ProxyInformationResponse { - var proxyInformation *commonbackend.AddProxy - response := &datacommands.ProxyInformationResponse{} - - tcpProxy, ok := backend.tcpProxies[proxyID] - - if !ok { - udpProxy, ok := backend.udpProxies[proxyID] - - if !ok { - response.Exists = false - return response - } - - proxyInformation = udpProxy.proxyInformation - } else { - proxyInformation = tcpProxy.proxyInformation - } - - response.Exists = true - response.SourceIP = proxyInformation.SourceIP - response.SourcePort = proxyInformation.SourcePort - response.DestPort = proxyInformation.DestPort - response.Protocol = proxyInformation.Protocol - - return response -} - -func (backend *SSHRemoteAppBackend) GetAllClientConnections(proxyID uint16) []uint16 { - tcpProxy, ok := backend.tcpProxies[proxyID] - - if !ok { - return []uint16{} - } - - connectionsArray := make([]uint16, len(tcpProxy.connections)) - currentPos := 0 - - for connectionIndex := range tcpProxy.connections { - connectionsArray[currentPos] = connectionIndex - currentPos++ - } - - return connectionsArray -} - -func (backend *SSHRemoteAppBackend) ResolveConnection(proxyID, connectionID uint16) *datacommands.ProxyConnectionInformationResponse { - response := &datacommands.ProxyConnectionInformationResponse{} - tcpProxy, ok := backend.tcpProxies[proxyID] - - if !ok { - response.Exists = false - return response - } - - connection, ok := tcpProxy.connections[connectionID] - - if !ok { - response.Exists = false - return response - } - - addr := connection.RemoteAddr().String() - ip := addr[:strings.LastIndex(addr, ":")] - port, err := strconv.Atoi(addr[strings.LastIndex(addr, ":")+1:]) - - if err != nil { - log.Warnf("failed to parse client port: %s", err.Error()) - response.Exists = false - - return response - } - - response.ClientIP = ip - response.ClientPort = uint16(port) - - return response -} - -func (backend *SSHRemoteAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { - return &commonbackend.CheckParametersResponse{ - IsValid: true, - } -} - -func (backend *SSHRemoteAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { - return &commonbackend.CheckParametersResponse{ - IsValid: true, - } -} - -func (backend *SSHRemoteAppBackend) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) { - tcpProxy, ok := backend.tcpProxies[message.ProxyID] - - if !ok { - log.Warnf("could not find tcp proxy (ID %d)", message.ProxyID) - return - } - - connection, ok := tcpProxy.connections[message.ConnectionID] - - if !ok { - log.Warnf("could not find tcp proxy (ID %d) with connection ID (%d)", message.ProxyID, message.ConnectionID) - return - } - - connection.Write(data) -} - -func (backend *SSHRemoteAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) { - udpProxy, ok := backend.udpProxies[message.ProxyID] - - if !ok { - return - } - - udpProxy.server.WriteToUDP(data, &net.UDPAddr{ - IP: net.ParseIP(message.ClientIP), - Port: int(message.ClientPort), - }) -} - -func (backend *SSHRemoteAppBackend) OnTCPConnectionClosed(proxyID, connectionID uint16) { - tcpProxy, ok := backend.tcpProxies[proxyID] - - if !ok { - return - } - - connection, ok := tcpProxy.connections[connectionID] - - if !ok { - return - } - - connection.Close() - delete(tcpProxy.connections, connectionID) -} - -func (backend *SSHRemoteAppBackend) OnSocketConnection(sock net.Conn) { - backend.sock = sock -} - -func main() { - logLevel := os.Getenv("HERMES_LOG_LEVEL") - - if logLevel != "" { - switch logLevel { - case "debug": - log.SetLevel(log.DebugLevel) - - case "info": - log.SetLevel(log.InfoLevel) - - case "warn": - log.SetLevel(log.WarnLevel) - - case "error": - log.SetLevel(log.ErrorLevel) - - case "fatal": - log.SetLevel(log.FatalLevel) - } - } - - backend := &SSHRemoteAppBackend{} - - application := backendutil_custom.NewHelper(backend) - err := application.Start() - - if err != nil { - log.Fatalf("failed execution in application: %s", err.Error()) - } -} diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index d46b330..963c1bd 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -2,51 +2,20 @@ package main import ( "encoding/json" - "errors" "fmt" "net" "os" - "slices" "strconv" "strings" "sync" - "time" - "git.terah.dev/imterah/hermes/backend/backendutil" - "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backendutil" + "git.terah.dev/imterah/hermes/commonbackend" "github.com/charmbracelet/log" "github.com/go-playground/validator/v10" "golang.org/x/crypto/ssh" ) -var validatorInstance *validator.Validate - -type ConnWithTimeout struct { - net.Conn - ReadTimeout time.Duration - WriteTimeout time.Duration -} - -func (c *ConnWithTimeout) Read(b []byte) (int, error) { - err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)) - - if err != nil { - return 0, err - } - - return c.Conn.Read(b) -} - -func (c *ConnWithTimeout) Write(b []byte) (int, error) { - err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) - - if err != nil { - return 0, err - } - - return c.Conn.Write(b) -} - type SSHListener struct { SourceIP string SourcePort uint16 @@ -55,46 +24,31 @@ type SSHListener struct { Listeners []net.Listener } -type SSHBackendData struct { - IP string `json:"ip" validate:"required"` - Port uint16 `json:"port" validate:"required"` - Username string `json:"username" validate:"required"` - PrivateKey string `json:"privateKey" validate:"required"` - DisablePIDCheck bool `json:"disablePIDCheck"` - ListenOnIPs []string `json:"listenOnIPs"` -} - type SSHBackend struct { config *SSHBackendData conn *ssh.Client clients []*commonbackend.ProxyClientConnection proxies []*SSHListener arrayPropMutex sync.Mutex - pid int - isReady bool - inReinitLoop bool +} + +type SSHBackendData struct { + IP string `json:"ip" validate:"required"` + Port uint16 `json:"port" validate:"required"` + Username string `json:"username" validate:"required"` + PrivateKey string `json:"privateKey" validate:"required"` + ListenOnIPs []string `json:"listenOnIPs"` } func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { log.Info("SSHBackend is initializing...") - - if validatorInstance == nil { - validatorInstance = validator.New() - } - - if backend.inReinitLoop { - for !backend.isReady { - time.Sleep(100 * time.Millisecond) - } - } - var backendData SSHBackendData if err := json.Unmarshal(bytes, &backendData); err != nil { return false, err } - if err := validatorInstance.Struct(&backendData); err != nil { + if err := validator.New().Struct(&backendData); err != nil { return false, err } @@ -120,71 +74,15 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { }, } - addr := fmt.Sprintf("%s:%d", backendData.IP, backendData.Port) - timeout := time.Duration(10 * time.Second) - - rawTCPConn, err := net.DialTimeout("tcp", addr, timeout) + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config) if err != nil { return false, err } - connWithTimeout := &ConnWithTimeout{ - Conn: rawTCPConn, - ReadTimeout: timeout, - WriteTimeout: timeout, - } - - c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config) - - if err != nil { - return false, err - } - - client := ssh.NewClient(c, chans, reqs) - backend.conn = client - - if !backendData.DisablePIDCheck { - if backend.pid != 0 { - session, err := client.NewSession() - - if err != nil { - return false, err - } - - err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid)) - - if err != nil { - log.Warnf("Failed to kill process: %s", err.Error()) - } - } - - session, err := client.NewSession() - - if err != nil { - return false, err - } - - // Get the parent PID of the shell so we can kill it if we disconnect - output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'") - - if err != nil { - return false, err - } - - // Strip the new line and convert to int - backend.pid, err = strconv.Atoi(string(output)[:len(output)-1]) - - if err != nil { - return false, err - } - } - - go backend.backendDisconnectHandler() - go backend.backendKeepaliveHandler() + backend.conn = conn log.Info("SSHBackend has initialized successfully.") - return true, nil } @@ -198,10 +96,6 @@ func (backend *SSHBackend) StopBackend() (bool, error) { return true, nil } -func (backend *SSHBackend) GetBackendStatus() (bool, error) { - return backend.conn != nil, nil -} - func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { listenerObject := &SSHListener{ SourceIP: command.SourceIP, @@ -240,11 +134,6 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er if err != nil { log.Warnf("failed to accept listener connection: %s", err.Error()) - - if err.Error() == "EOF" { - return - } - continue } @@ -301,7 +190,8 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er // Splice out the clientInstance by clientIndex // TODO: change approach. It works but it's a bit wonky imho - backend.clients = slices.Delete(backend.clients, clientIndex, clientIndex+1) + // I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively + backend.clients = append(backend.clients[:clientIndex], backend.clients[clientIndex+1:]...) return } } @@ -317,20 +207,14 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er for { len, err := forwardedConn.Read(forwardedBuffer) - if err != nil { - if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to read from forwarded connection: %s", err.Error()) - } - + log.Errorf("failed to read from forwarded connection: %s", err.Error()) return } - if _, err = sourceConn.Write(forwardedBuffer[:len]); err != nil { - if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to write to source connection: %s", err.Error()) - } - + _, err = sourceConn.Write(forwardedBuffer[:len]) + if err != nil { + log.Errorf("failed to write to source connection: %s", err.Error()) return } } @@ -341,20 +225,14 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er for { len, err := sourceConn.Read(sourceBuffer) - - if err != nil { - if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to read from source connection: %s", err.Error()) - } - + if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + log.Errorf("failed to read from source connection: %s", err.Error()) return } - if _, err = forwardedConn.Write(sourceBuffer[:len]); err != nil { - if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to write to forwarded connection: %s", err.Error()) - } - + _, err = forwardedConn.Write(sourceBuffer[:len]) + if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + log.Errorf("failed to write to forwarded connection: %s", err.Error()) return } } @@ -375,7 +253,10 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, backend.arrayPropMutex.Lock() for proxyIndex, proxy := range backend.proxies { + // Check if memory addresses are equal for the pointer if command.SourceIP == proxy.SourceIP && command.SourcePort == proxy.SourcePort && command.DestPort == proxy.DestPort && command.Protocol == proxy.Protocol { + log.Debug("found proxy in StopProxy. shutting down listeners") + for _, listener := range proxy.Listeners { err := listener.Close() @@ -385,8 +266,10 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, } // Splice out the proxy instance by proxyIndex + // TODO: change approach. It works but it's a bit wonky imho - backend.proxies = slices.Delete(backend.proxies, proxyIndex, proxyIndex+1) + // I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively + backend.proxies = append(backend.proxies[:proxyIndex], backend.proxies[proxyIndex+1:]...) return true, nil } } @@ -417,10 +300,6 @@ func (backend *SSHBackend) CheckParametersForConnections(clientParameters *commo func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { var backendData SSHBackendData - if validatorInstance == nil { - validatorInstance = validator.New() - } - if err := json.Unmarshal(arguments, &backendData); err != nil { return &commonbackend.CheckParametersResponse{ IsValid: false, @@ -428,7 +307,7 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba } } - if err := validatorInstance.Struct(&backendData); err != nil { + if err := validator.New().Struct(&backendData); err != nil { return &commonbackend.CheckParametersResponse{ IsValid: false, Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()), @@ -440,152 +319,6 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba } } -func (backend *SSHBackend) backendKeepaliveHandler() { - for { - if backend.conn != nil { - _, _, err := backend.conn.SendRequest("keepalive@openssh.com", true, nil) - - if err != nil { - log.Warn("Keepalive message failed!") - return - } - } - - time.Sleep(5 * time.Second) - } -} - -func (backend *SSHBackend) backendDisconnectHandler() { - for { - if backend.conn != nil { - backend.conn.Wait() - backend.conn.Close() - - backend.isReady = false - backend.inReinitLoop = true - - log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...") - } else { - log.Info("Retrying reconnection in 5 seconds...") - } - - time.Sleep(5 * time.Second) - - // Make the connection nil to accurately report our status incase GetBackendStatus is called - backend.conn = nil - - // Use the last half of the code from the main initialization - signer, err := ssh.ParsePrivateKey([]byte(backend.config.PrivateKey)) - - if err != nil { - log.Errorf("Failed to parse private key: %s", err.Error()) - return - } - - auth := ssh.PublicKeys(signer) - - config := &ssh.ClientConfig{ - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - User: backend.config.Username, - Auth: []ssh.AuthMethod{ - auth, - }, - } - - addr := fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port) - timeout := time.Duration(10 * time.Second) - - rawTCPConn, err := net.DialTimeout("tcp", addr, timeout) - - if err != nil { - log.Errorf("Failed to establish connection to the server: %s", err.Error()) - continue - } - - connWithTimeout := &ConnWithTimeout{ - Conn: rawTCPConn, - ReadTimeout: timeout, - WriteTimeout: timeout, - } - - c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config) - - if err != nil { - log.Errorf("Failed to create SSH client connection: %s", err.Error()) - rawTCPConn.Close() - continue - } - - client := ssh.NewClient(c, chans, reqs) - backend.conn = client - - if !backend.config.DisablePIDCheck { - if backend.pid != 0 { - session, err := client.NewSession() - - if err != nil { - log.Warnf("Failed to create SSH command session: %s", err.Error()) - return - } - - err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid)) - - if err != nil { - log.Warnf("Failed to kill process: %s", err.Error()) - } - } - - session, err := client.NewSession() - - if err != nil { - log.Warnf("Failed to create SSH command session: %s", err.Error()) - return - } - - // Get the parent PID of the shell so we can kill it if we disconnect - output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'") - - if err != nil { - log.Warnf("Failed to execute command to fetch PID: %s", err.Error()) - return - } - - // Strip the new line and convert to int - backend.pid, err = strconv.Atoi(string(output)[:len(output)-1]) - - if err != nil { - log.Warnf("Failed to parse PID: %s", err.Error()) - return - } - } - - go backend.backendKeepaliveHandler() - - log.Info("SSHBackend has reconnected successfully. Attempting to set up proxies again...") - - for _, proxy := range backend.proxies { - ok, err := backend.StartProxy(&commonbackend.AddProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestPort, - Protocol: proxy.Protocol, - }) - - if err != nil { - log.Errorf("Failed to set up proxy: %s", err.Error()) - continue - } - - if !ok { - log.Errorf("Failed to set up proxy: OK status is false") - continue - } - } - - log.Info("SSHBackend has reinitialized and restored state successfully.") - } -} - func main() { logLevel := os.Getenv("HERMES_LOG_LEVEL") diff --git a/docker-compose.yml b/docker-compose.yml index a549035..7ac3334 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,12 +6,27 @@ services: environment: DATABASE_URL: postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB}?schema=nextnet HERMES_POSTGRES_DSN: postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB} - HERMES_JWT_SECRET: ${JWT_SECRET} HERMES_DATABASE_BACKEND: postgresql depends_on: - db ports: - 3000:3000 + + # WARN: The LOM is deprecated and likely broken currently. + # + # NOTE: For this to work correctly, the nextnet-api must be version > 0.1.1 + # or have a version with backported username support, incl. logins + lom: + image: ghcr.io/imterah/hermes-lom:latest + container_name: hermes-lom + restart: always + ports: + - 2222:2222 + depends_on: + - api + volumes: + - ssh_key_data:/app/keys + db: image: postgres:17.2 container_name: nextnet-postgres @@ -22,6 +37,7 @@ services: POSTGRES_USER: ${POSTGRES_USERNAME} volumes: - postgres_data:/var/lib/postgresql/data + volumes: postgres_data: ssh_key_data: diff --git a/docs/profiling.md b/docs/profiling.md deleted file mode 100644 index 06ceb7d..0000000 --- a/docs/profiling.md +++ /dev/null @@ -1,6 +0,0 @@ -# Profiling -To profile any backend code based on `backendutil`, follow these steps: -1. Rebuild the backend with the `debug` flag: `cd $BACKEND_HERE; GOOS=linux go build -tags debug .; cd ..` -2. Copy the binary to the target machine (if applicable), and stop the API server. -3. If you want to profile the CPU utilization, write `cpu` to the file `/tmp/hermes.backendlauncher.profilebackends`: `echo -n "cpu" > /tmp/hermes.backendlauncher.profilebackends`. Else, replace `cpu` with `mem`. -4. Start the API server, with development mode and debug logging enabled. diff --git a/frontend/commands/users/create.go b/frontend/commands/users/create.go deleted file mode 100644 index 3d6c94b..0000000 --- a/frontend/commands/users/create.go +++ /dev/null @@ -1,115 +0,0 @@ -package users - -import ( - "errors" - "fmt" - "os" - "syscall" - - "git.terah.dev/imterah/hermes/apiclient" - "git.terah.dev/imterah/hermes/frontend/config" - "github.com/charmbracelet/log" - "github.com/urfave/cli/v2" - "golang.org/x/term" - "gopkg.in/yaml.v3" -) - -func CreateUserCommand(cCtx *cli.Context) error { - configPath := cCtx.String("config-path") - - var configContents *config.Config - - _, err := os.Stat(configPath) - - if err != nil { - if errors.Is(err, os.ErrNotExist) { - configContents = &config.Config{} - } else { - return fmt.Errorf("failed to get configuration file information: %s", err.Error()) - } - } else { - configContents, err = config.ReadAndParseConfig(configPath) - - if err != nil { - return fmt.Errorf("failed to read and parse configuration file: %s", err.Error()) - } - } - - username := cCtx.String("username") - - if username == "" { - if configContents.Username == "" { - return fmt.Errorf("username not specified and username is not in the configuration file") - } - - username = configContents.Username - } - - var password string - - if cCtx.Bool("ask-password") { - fmt.Print("Password: ") - passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Print("\n") - - if err != nil { - return fmt.Errorf("failed to read password from console: %s", err.Error()) - } - - password = string(passwordBytes) - } else { - password = cCtx.String("password") - - if password == "" { - return fmt.Errorf("password is not specified and password asking is not enabled") - } - } - - var serverURL string - - if cCtx.String("server-url") == "" { - if configContents.APIPath == "" { - return fmt.Errorf("server URL not specified and server URL is not in the configuration file") - } - - serverURL = configContents.APIPath - } else { - serverURL = cCtx.String("server-url") - } - - fullName := cCtx.String("full-name") - email := cCtx.String("email") - isBot := cCtx.Bool("user-is-bot") - - log.Info("Creating user...") - - api := &apiclient.HermesAPIClient{ - URL: serverURL, - } - - refreshToken, err := api.UserCreate(fullName, username, email, password, isBot) - - if err != nil { - return fmt.Errorf("failed to create user: %s", err.Error()) - } - - log.Info("Successfully created user.") - - if cCtx.Bool("do-not-save-configuration") { - return nil - } - - configContents.Username = username - configContents.RefreshToken = refreshToken - configContents.APIPath = serverURL - - data, err := yaml.Marshal(configContents) - - if err != nil { - return fmt.Errorf("failed to marshal configuration data: %s", err.Error()) - } - - os.WriteFile(configPath, data, 0644) - - return nil -} diff --git a/frontend/commands/users/login.go b/frontend/commands/users/login.go deleted file mode 100644 index 8248b1f..0000000 --- a/frontend/commands/users/login.go +++ /dev/null @@ -1,98 +0,0 @@ -package users - -import ( - "errors" - "fmt" - "os" - "syscall" - - "git.terah.dev/imterah/hermes/apiclient" - "git.terah.dev/imterah/hermes/frontend/config" - "github.com/charmbracelet/log" - "github.com/urfave/cli/v2" - "golang.org/x/term" - "gopkg.in/yaml.v3" -) - -func GetRefreshTokenCommand(cCtx *cli.Context) error { - configPath := cCtx.String("config-path") - - var configContents *config.Config - - _, err := os.Stat(configPath) - - if err != nil { - if errors.Is(err, os.ErrNotExist) { - configContents = &config.Config{} - } else { - return fmt.Errorf("failed to get configuration file information: %s", err.Error()) - } - } else { - configContents, err = config.ReadAndParseConfig(configPath) - - if err != nil { - return fmt.Errorf("failed to read and parse configuration file: %s", err.Error()) - } - } - - var username string - var password string - - if cCtx.String("username") == "" { - if configContents.Username == "" { - return fmt.Errorf("username not specified and username is not in the configuration file") - } - - username = configContents.Username - } else { - username = cCtx.String("username") - } - - if cCtx.Bool("ask-password") { - fmt.Print("Password: ") - passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Print("\n") - - if err != nil { - return fmt.Errorf("failed to read password from console: %s", err.Error()) - } - - password = string(passwordBytes) - } else { - password = cCtx.String("password") - - if password == "" { - return fmt.Errorf("password is not specified and password asking is not enabled") - } - } - - serverURL := cCtx.String("server-url") - log.Info("Authenticating with API...") - - api := &apiclient.HermesAPIClient{ - URL: serverURL, - } - - refreshToken, err := api.UserGetRefreshToken(&username, nil, password) - - if err != nil { - return fmt.Errorf("failed to authenticate with the API: %s", err.Error()) - } - - configContents.Username = username - configContents.RefreshToken = refreshToken - configContents.APIPath = serverURL - - log.Info("Writing configuration file...") - data, err := yaml.Marshal(configContents) - - if err != nil { - return fmt.Errorf("failed to marshal configuration data: %s", err.Error()) - } - - log.Infof("config path: %s", configPath) - - os.WriteFile(configPath, data, 0644) - - return nil -} diff --git a/frontend/config/config.go b/frontend/config/config.go deleted file mode 100644 index ff9cb69..0000000 --- a/frontend/config/config.go +++ /dev/null @@ -1,30 +0,0 @@ -package config - -import ( - "os" - - "gopkg.in/yaml.v3" -) - -type Config struct { - Username string `json:"username"` - RefreshToken string `json:"token"` - APIPath string `json:"api_path"` -} - -func ReadAndParseConfig(configFile string) (*Config, error) { - configFileContents, err := os.ReadFile(configFile) - - if err != nil { - return nil, err - } - - config := &Config{} - err = yaml.Unmarshal(configFileContents, config) - - if err != nil { - return nil, err - } - - return config, nil -} diff --git a/frontend/dev.env b/frontend/dev.env deleted file mode 100644 index ccd7c30..0000000 --- a/frontend/dev.env +++ /dev/null @@ -1 +0,0 @@ -HERMES_LOG_LEVEL=debug diff --git a/frontend/main.go b/frontend/main.go deleted file mode 100644 index 95b366f..0000000 --- a/frontend/main.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "os" - "path" - - "git.terah.dev/imterah/hermes/frontend/commands/users" - "github.com/charmbracelet/log" - "github.com/urfave/cli/v2" -) - -func main() { - logLevel := os.Getenv("HERMES_LOG_LEVEL") - - if logLevel != "" { - switch logLevel { - case "debug": - log.SetLevel(log.DebugLevel) - - case "info": - log.SetLevel(log.InfoLevel) - - case "warn": - log.SetLevel(log.WarnLevel) - - case "error": - log.SetLevel(log.ErrorLevel) - - case "fatal": - log.SetLevel(log.FatalLevel) - } - } - - configDir, err := os.UserConfigDir() - - if err != nil { - log.Fatalf("Failed to get configuration directory: %s", err.Error()) - } - - app := &cli.App{ - Name: "hermcli", - Usage: "client for Hermes -- port forwarding across boundaries", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "config-path", - Aliases: []string{"config", "cp", "c"}, - Value: path.Join(configDir, "hermcli.yml"), - }, - }, - Commands: []*cli.Command{ - { - Name: "login", - Usage: "log in to the API", - Action: users.GetRefreshTokenCommand, - Aliases: []string{"l"}, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"user", "u"}, - Usage: "username to authenticate as", - }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"pass", "p"}, - Usage: "password to authenticate with", - }, - &cli.StringFlag{ - Name: "server-url", - Aliases: []string{"server", "s"}, - Usage: "URL of the server to authenticate with", - }, - &cli.BoolFlag{ - Name: "ask-password", - Aliases: []string{"ask-pass", "ap"}, - Usage: "asks you the password to authenticate with", - }, - }, - }, - { - Name: "users", - Usage: "user management commands", - Aliases: []string{"u"}, - Subcommands: []*cli.Command{ - { - Name: "create", - Aliases: []string{"c"}, - Usage: "create a user", - Action: users.CreateUserCommand, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "full-name", - Aliases: []string{"name", "n"}, - Usage: "full name for the user", - Required: true, - }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"user", "us"}, - Usage: "username to give the user", - Required: true, - }, - &cli.StringFlag{ - Name: "email", - Aliases: []string{"e"}, - Usage: "email to give the user", - Required: true, - }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"pass", "p"}, - Usage: "password to give the user", - }, - &cli.StringFlag{ - Name: "server-url", - Aliases: []string{"server", "s"}, - Usage: "URL of the server to connect with", - }, - &cli.BoolFlag{ - Name: "ask-password", - Aliases: []string{"ask-pass", "ap"}, - Usage: "asks you the password to give the user", - }, - &cli.BoolFlag{ - Name: "user-is-bot", - Aliases: []string{"user-bot", "ub", "u"}, - Usage: "if set, makes the user flagged as a bot", - }, - &cli.BoolFlag{ - Name: "do-not-save-configuration", - Aliases: []string{"no-save", "ns"}, - Usage: "doesn't save the authenticated user credentials", - }, - }, - }, - }, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} diff --git a/init.sh b/init.sh index 65aaee0..1fe457d 100644 --- a/init.sh +++ b/init.sh @@ -7,11 +7,11 @@ if [ ! -d "backend/.tmp" ]; then mkdir backend/.tmp fi -if [ ! -f "frontend/.env" ]; then - cp frontend/dev.env frontend/.env +if [ ! -f "backend-legacy/.env" ]; then + cp backend-legacy/dev.env backend-legacy/.env fi set -a +source backend-legacy/.env source backend/.env -source frontend/.env set +a diff --git a/migration-entrypoint.sh b/migration-entrypoint.sh new file mode 100755 index 0000000..c1eb766 --- /dev/null +++ b/migration-entrypoint.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +echo "Welcome to the Hermes migration assistant." + +if [ ! -f "/tmp/db.json.gz" ]; then + echo "Exporting database contents..." + cd /app/legacy + node out/tools/exportDBContents.js /tmp/db.json.gz + BACKUP_EXIT_CODE=$? + + if [ $BACKUP_EXIT_CODE -ne 0 ]; then + echo "Failed to export database contents!" + exit 1 + fi + + echo "!! IMPORTANT !!" + echo "Database backup contents below:" + echo "==== BEGIN BACKUP ====" + cat /tmp/db.json.gz | base64 + echo "==== END BACKUP ====" + echo "When copying, do NOT copy the BEGIN and END sections." +fi + +echo "Wiping old database..." +cat >> /tmp/wipe.sql << EOF +CREATE DATABASE temp; +\c temp +DROP DATABASE $HERMES_MIGRATE_POSTGRES_DATABASE; +CREATE DATABASE $HERMES_MIGRATE_POSTGRES_DATABASE; +\c nextnet +DROP DATABASE temp; +EOF +psql "$HERMES_POSTGRES_DSN" < /tmp/wipe.sql +rm -rf /tmp/wipe.sql +echo "Restoring backup..." + +cd /app/modern +./hermes -b ./backends.json import --bp /tmp/db.json.gz + +echo "Restored backup. If this restore fails after the database has wiped, get a shell into the container," +echo "copy the backup contents into the container (base64 decoded) at '/tmp/db.json.gz'," +echo "and rerun /app/entrypoint.sh." +echo "" +echo "If further issues continue, open an issue at 'https://git.terah.dev/imterah/hermes'." +echo "If the migration succeeded, congratulations!" + +sleep 10000 diff --git a/prod-docker.env b/prod-docker.env index 954c20f..fe5da79 100644 --- a/prod-docker.env +++ b/prod-docker.env @@ -1,4 +1,5 @@ +# These are default values, please change these! + POSTGRES_USERNAME=hermes POSTGRES_PASSWORD=hermes POSTGRES_DB=hermes -JWT_SECRET=hermes diff --git a/routes/Hermes API/Backend/Lookup.bru b/routes/Hermes API/Backend/Lookup.bru index 8c50b52..0da7fa7 100644 --- a/routes/Hermes API/Backend/Lookup.bru +++ b/routes/Hermes API/Backend/Lookup.bru @@ -12,8 +12,8 @@ post { body:json { { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzYyMzI2NjEsIm5iZiI6MTczNjE0NjI2MSwiaWF0IjoxNzM2MTQ2MjYxfQ.juoZ74xs-FBnbbT9Zlei1LmcNx7kTEfzymHlVbeMmtQ", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzUwNzY0MTEsIm5iZiI6MTczNDk5MDAxMSwiaWF0IjoxNzM0OTkwMDExfQ.N9TLraX4peHt7FKv8tPcHuEzL0K7T2IBEw3piQS_4OY", "name": "SSH", - "id": 1 + "id": 2 } } diff --git a/shell.nix b/shell.nix index ea1aa8e..7078f6e 100644 --- a/shell.nix +++ b/shell.nix @@ -3,13 +3,18 @@ }: pkgs.mkShell { buildInputs = with pkgs; [ # api/ + nodejs + openssl + lsof go gopls ]; shellHook = '' - if [ -f init.sh ]; then - source init.sh - fi + export PRISMA_QUERY_ENGINE_BINARY=${pkgs.prisma-engines}/bin/query-engine + export PRISMA_QUERY_ENGINE_LIBRARY=${pkgs.prisma-engines}/lib/libquery_engine.node + export PRISMA_SCHEMA_ENGINE_BINARY=${pkgs.prisma-engines}/bin/schema-engine + + source init.sh ''; } diff --git a/sshfrontend/.gitignore b/sshfrontend/.gitignore new file mode 100644 index 0000000..37df925 --- /dev/null +++ b/sshfrontend/.gitignore @@ -0,0 +1,133 @@ +# Output +out + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/sshfrontend/Dockerfile b/sshfrontend/Dockerfile new file mode 100644 index 0000000..df64739 --- /dev/null +++ b/sshfrontend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:22.11.0-bookworm +LABEL org.opencontainers.image.source="https://github.com/greysoh/nextnet" +WORKDIR /app/ +COPY src /app/src +COPY tsconfig.json /app/ +COPY package.json /app/ +COPY package-lock.json /app/ +COPY docker-entrypoint.sh /app/ +RUN npm install --save-dev +RUN npm run build +RUN rm out/**/*.ts out/**/*.map +RUN rm -rf src +RUN npm prune --production +ENTRYPOINT sh docker-entrypoint.sh diff --git a/sshfrontend/LICENSE b/sshfrontend/LICENSE new file mode 100644 index 0000000..8914588 --- /dev/null +++ b/sshfrontend/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Greyson + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/sshfrontend/README.md b/sshfrontend/README.md new file mode 100644 index 0000000..16cb312 --- /dev/null +++ b/sshfrontend/README.md @@ -0,0 +1,2 @@ +# NextNet LOM +Lights Out Management, NextNet style \ No newline at end of file diff --git a/sshfrontend/docker-entrypoint.sh b/sshfrontend/docker-entrypoint.sh new file mode 100755 index 0000000..36942f1 --- /dev/null +++ b/sshfrontend/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +export NODE_ENV="production" + +if [[ "$SERVER_BASE_URL" == "" ]]; then + export SERVER_BASE_URL="http://nextnet-api:3000/" +fi + +npm start diff --git a/sshfrontend/eslint.config.js b/sshfrontend/eslint.config.js new file mode 100644 index 0000000..0afc6f7 --- /dev/null +++ b/sshfrontend/eslint.config.js @@ -0,0 +1,19 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + + { + languageOptions: { + globals: globals.node, + }, + + rules: { + "@typescript-eslint/no-explicit-any": "off", + "no-constant-condition": "warn", + }, + }, +]; diff --git a/sshfrontend/package-lock.json b/sshfrontend/package-lock.json new file mode 100644 index 0000000..ec26875 --- /dev/null +++ b/sshfrontend/package-lock.json @@ -0,0 +1,2564 @@ +{ + "name": "nextnet-lom", + "version": "1.1.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextnet-lom", + "version": "1.1.2", + "license": "BSD-3-Clause", + "dependencies": { + "axios": "^1.7.8", + "commander": "^12.1.0", + "patch-package": "^8.0.0", + "ssh2": "^1.16.0", + "string-argv": "^0.3.2" + }, + "devDependencies": { + "@eslint/js": "^9.16.0", + "@types/node": "^22.10.1", + "@types/ssh2": "^1.15.1", + "@types/yargs": "^17.0.33", + "eslint": "^9.16.0", + "globals": "^15.12.0", + "nodemon": "^3.1.7", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", + "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "license": "BSD-2-Clause" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/axios": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.16.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", + "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", + "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", + "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.16.0", + "@typescript-eslint/parser": "8.16.0", + "@typescript-eslint/utils": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sshfrontend/package.json b/sshfrontend/package.json new file mode 100644 index 0000000..66a2a78 --- /dev/null +++ b/sshfrontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "nextnet-lom", + "version": "1.1.2", + "description": "Lights Out Management, NextNet style", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "start": "cd out && node --enable-source-maps index.js", + "dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\"" + }, + "keywords": [], + "author": "greysoh", + "license": "BSD-3-Clause", + "devDependencies": { + "@eslint/js": "^9.16.0", + "@types/node": "^22.10.1", + "@types/ssh2": "^1.15.1", + "@types/yargs": "^17.0.33", + "eslint": "^9.16.0", + "globals": "^15.12.0", + "nodemon": "^3.1.7", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0" + }, + "dependencies": { + "axios": "^1.7.8", + "commander": "^12.1.0", + "patch-package": "^8.0.0", + "ssh2": "^1.16.0", + "string-argv": "^0.3.2" + } +} diff --git a/sshfrontend/src/commands.ts b/sshfrontend/src/commands.ts new file mode 100644 index 0000000..bb06314 --- /dev/null +++ b/sshfrontend/src/commands.ts @@ -0,0 +1,65 @@ +import type { Axios } from "axios"; + +import { run as connection } from "./commands/connections.js"; +import { run as backends } from "./commands/backends.js"; +import { run as users } from "./commands/users.js"; + +export type PrintLine = (...str: unknown[]) => void; +export type KeyboardRead = (disableEcho?: boolean) => Promise; + +type Command = ( + args: string[], + println: PrintLine, + axios: Axios, + apiKey: string, + keyboardRead: KeyboardRead, +) => Promise; + +type Commands = { + name: string; + description: string; + run: Command; +}[]; + +export const commands: Commands = [ + { + name: "help", + description: "Prints help", + async run(_args: string[], printf: PrintLine) { + commands.forEach(command => { + printf(`${command.name}: ${command.description}\n`); + }); + + printf( + "\nRun a command of your choosing with --help to see more options.\n", + ); + }, + }, + { + name: "clear", + description: "Clears screen", + async run(_args: string[], printf: PrintLine) { + printf("\x1B[2J\x1B[3J\x1B[H"); + }, + }, + { + name: "conn", + description: "Manages connections for NextNet", + run: connection, + }, + { + name: "user", + description: "Manages users for NextNet", + run: users, + }, + { + name: "backend", + description: "Manages backends for NextNet", + run: backends, + }, + { + name: "back", + description: "(alias) Manages backends for NextNet", + run: backends, + }, +]; diff --git a/sshfrontend/src/commands/backends.ts b/sshfrontend/src/commands/backends.ts new file mode 100644 index 0000000..0930bbb --- /dev/null +++ b/sshfrontend/src/commands/backends.ts @@ -0,0 +1,519 @@ +import type { Axios } from "axios"; + +import { SSHCommand } from "../libs/patchCommander.js"; +import type { PrintLine, KeyboardRead } from "../commands.js"; + +type BackendLookupSuccess = { + success: boolean; + data: { + id: number; + + name: string; + description: string; + backend: string; + connectionDetails?: string; + logs: string[]; + }[]; +}; + +const addRequiredOptions = { + ssh: ["sshKey", "username", "host"], + + passyfire: ["host"], +}; + +export async function run( + argv: string[], + println: PrintLine, + axios: Axios, + token: string, + readKeyboard: KeyboardRead, +) { + const program = new SSHCommand(println); + program.description("Manages backends for NextNet"); + program.version("v1.0.0"); + + const addBackend = new SSHCommand(println, "add"); + + addBackend.description("Adds a backend"); + addBackend.argument("", "Name of the backend"); + + addBackend.argument( + "", + "Provider of the backend (ex. passyfire, ssh)", + ); + + addBackend.option( + "-d, --description ", + "Description for the backend", + ); + + addBackend.option( + "-f, --force-custom-parameters", + "If turned on, this forces you to use custom parameters", + ); + + addBackend.option( + "-c, --custom-parameters ", + "Custom parameters. Use this if the backend you're using isn't native to SSH yet, or if you manually turn on -f.", + ); + + // SSH provider + addBackend.option( + "-k, --ssh-key ", + "(SSH) SSH private key to use to authenticate with the server", + ); + + addBackend.option( + "-u, --username ", + "(SSH, PassyFire) Username to authenticate with. With PassyFire, it's the username you create", + ); + + addBackend.option( + "-h, --host ", + "(SSH, PassyFire) Host to connect to. With PassyFire, it's what you listen on", + ); + + // PassyFire provider + addBackend.option( + "-pe, --is-proxied", + "(PassyFire) Specify if you're behind a proxy or not so we can get the right IP", + ); + + addBackend.option( + "-pp, --proxied-port ", + "(PassyFire) If you're behind a proxy, and the port is different, specify the port to return", + ); + + addBackend.option("-g, --guest", "(PassyFire) Enable the guest user"); + + addBackend.option( + "-ua, --user-ask", + "(PassyFire) Ask what users you want to create", + ); + + addBackend.option( + "-p, --password ", + "(PassyFire) What password you want to use for the primary user", + ); + + addBackend.action( + async ( + name: string, + provider: string, + options: { + description?: string; + forceCustomParameters?: boolean; + customParameters?: string; + + // SSH (mostly) + sshKey?: string; + username?: string; + host?: string; + + // PassyFire (mostly) + isProxied?: boolean; + proxiedPort?: string; + guest?: boolean; + userAsk?: boolean; + password?: string; + }, + ) => { + // @ts-expect-error: Yes it can index for what we need it to do. + const isUnsupportedPlatform: boolean = !addRequiredOptions[provider]; + + if (isUnsupportedPlatform) { + println( + "WARNING: Platform is not natively supported by the LOM yet!\n", + ); + } + + let connectionDetails: string = ""; + + if (options.forceCustomParameters || isUnsupportedPlatform) { + if (typeof options.customParameters != "string") { + return println( + "ERROR: You are missing the custom parameters option!\n", + ); + } + + connectionDetails = options.customParameters; + } else if (provider == "ssh") { + for (const argument of addRequiredOptions["ssh"]) { + // @ts-expect-error: No. + const hasArgument = options[argument]; + + if (!hasArgument) { + return println("ERROR: Missing argument '%s'\n", argument); + } + } + + const unstringifiedArguments: { + ip?: string; + port?: number; + username?: string; + privateKey?: string; + } = {}; + + if (options.host) { + const sourceSplit: string[] = options.host.split(":"); + + const sourceIP: string = sourceSplit[0]; + const sourcePort: number = + sourceSplit.length >= 2 ? parseInt(sourceSplit[1]) : 22; + + unstringifiedArguments.ip = sourceIP; + unstringifiedArguments.port = sourcePort; + } + + unstringifiedArguments.username = options.username; + unstringifiedArguments.privateKey = options.sshKey?.replaceAll( + "\\n", + "\n", + ); + + connectionDetails = JSON.stringify(unstringifiedArguments); + } else if (provider == "passyfire") { + for (const argument of addRequiredOptions["passyfire"]) { + // @ts-expect-error: No. + const hasArgument = options[argument]; + + if (!hasArgument) { + return println("ERROR: Missing argument '%s'\n", argument); + } + } + + const unstringifiedArguments: { + ip?: string; + port?: number; + publicPort?: number; + isProxied?: boolean; + users: { + username: string; + password: string; + }[]; + } = { + users: [], + }; + + if (options.guest) { + unstringifiedArguments.users.push({ + username: "guest", + password: "guest", + }); + } + + if (options.username) { + if (!options.password) { + return println("Password must not be left blank\n"); + } + + unstringifiedArguments.users.push({ + username: options.username, + password: options.password, + }); + } + + if (options.userAsk) { + let shouldContinueAsking: boolean = true; + + while (shouldContinueAsking) { + println("Creating a user.\nUsername: "); + const username = await readKeyboard(); + + let passwordConfirmOne = "a"; + let passwordConfirmTwo = "b"; + + println("\n"); + + while (passwordConfirmOne != passwordConfirmTwo) { + println("Password: "); + passwordConfirmOne = await readKeyboard(true); + + println("\nConfirm password: "); + passwordConfirmTwo = await readKeyboard(true); + + println("\n"); + + if (passwordConfirmOne != passwordConfirmTwo) { + println("Passwords do not match! Try again.\n\n"); + } + } + + unstringifiedArguments.users.push({ + username, + password: passwordConfirmOne, + }); + + println("\nShould we continue creating users? (y/n) "); + shouldContinueAsking = (await readKeyboard()) + .toLowerCase() + .trim() + .startsWith("y"); + + println("\n\n"); + } + } + + if (unstringifiedArguments.users.length == 0) { + return println( + "No users will be created with your current arguments! You must have users set up.\n", + ); + } + + unstringifiedArguments.isProxied = Boolean(options.isProxied); + + if (options.proxiedPort) { + unstringifiedArguments.publicPort = parseInt( + options.proxiedPort ?? "", + ); + + if (Number.isNaN(unstringifiedArguments.publicPort)) { + println("UID (%s) is not a number.\n", options.proxiedPort); + return; + } + } + + if (options.host) { + const sourceSplit: string[] = options.host.split(":"); + + if (sourceSplit.length != 2) { + return println( + "Source could not be splitted down (are you missing the ':' in the source to specify port?)\n", + ); + } + + const sourceIP: string = sourceSplit[0]; + const sourcePort: number = parseInt(sourceSplit[1]); + + if (Number.isNaN(sourcePort)) { + println("UID (%s) is not a number.\n", sourcePort); + return; + } + + unstringifiedArguments.ip = sourceIP; + unstringifiedArguments.port = sourcePort; + } + + connectionDetails = JSON.stringify(unstringifiedArguments); + } + + const response = await axios.post("/api/v1/backends/create", { + token, + + name, + description: options.description, + backend: provider, + + connectionDetails, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error creating a backend!\n"); + } + + return; + } + + println("Successfully created the backend.\n"); + }, + ); + + const removeBackend = new SSHCommand(println, "rm"); + removeBackend.description("Removes a backend"); + removeBackend.argument("", "ID of the backend"); + + removeBackend.action(async (idStr: string) => { + const id: number = parseInt(idStr); + + if (Number.isNaN(id)) { + println("ID (%s) is not a number.\n", idStr); + return; + } + + const response = await axios.post("/api/v1/backends/remove", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error deleting backend!\n"); + } + + return; + } + + println("Backend has been successfully deleted.\n"); + }); + + const lookupBackend = new SSHCommand(println, "find"); + lookupBackend.description("Looks up a backend based on your arguments"); + + lookupBackend.option("-n, --name ", "Name of the backend"); + + lookupBackend.option( + "-p, --provider ", + "Provider of the backend (ex. passyfire, ssh)", + ); + + lookupBackend.option( + "-d, --description ", + "Description for the backend", + ); + + lookupBackend.option( + "-e, --parse-connection-details", + "If specified, we automatically parse the connection details to make them human readable, if standard JSON.", + ); + + lookupBackend.action( + async (options: { + name?: string; + provider?: string; + description?: string; + parseConnectionDetails?: boolean; + }) => { + const response = await axios.post("/api/v1/backends/lookup", { + token, + + name: options.name, + description: options.description, + + backend: options.provider, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error looking up backends!\n"); + } + + return; + } + + const { data }: BackendLookupSuccess = response.data; + + for (const backend of data) { + println("ID: %s:\n", backend.id); + println(" - Name: %s\n", backend.name); + println(" - Description: %s\n", backend.description); + println(" - Using Backend: %s\n", backend.backend); + + if (backend.connectionDetails) { + if (options.parseConnectionDetails) { + // We don't know what we're recieving. We just try to parse it (hence the any type) + // {} is more accurate but TS yells at us if we do that :( + + // eslint-disable-next-line + let parsedJSONData: any | undefined; + + try { + parsedJSONData = JSON.parse(backend.connectionDetails); + } catch (e) { + println(" - Connection Details: %s\n", backend.connectionDetails); + continue; + } + + if (!parsedJSONData) { + // Not really an assertion but I don't care right now + println( + "Assertion failed: parsedJSONData should not be undefined\n", + ); + continue; + } + + println(" - Connection details:\n"); + + for (const key of Object.keys(parsedJSONData)) { + let value: string | number = parsedJSONData[key]; + + if (typeof value == "string") { + value = value.replaceAll("\n", "\n" + " ".repeat(16)); + } + + if (typeof value == "object") { + // TODO: implement? + value = JSON.stringify(value); + } + + println(" - %s: %s\n", key, value); + } + } else { + println(" - Connection Details: %s\n", backend.connectionDetails); + } + } + + println("\n"); + } + + println("%s backends found.\n", data.length); + }, + ); + + const logsCommand = new SSHCommand(println, "logs"); + logsCommand.description("View logs for a backend"); + logsCommand.argument("", "ID of the backend"); + + logsCommand.action(async (idStr: string) => { + const id: number = parseInt(idStr); + + if (Number.isNaN(id)) { + println("ID (%s) is not a number.\n", idStr); + return; + } + + const response = await axios.post("/api/v1/backends/lookup", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error getting logs!\n"); + } + + return; + } + + const { data }: BackendLookupSuccess = response.data; + const ourBackend = data.find(i => i.id == id); + + if (!ourBackend) return println("Could not find the backend!\n"); + ourBackend.logs.forEach(log => println("%s\n", log)); + }); + + program.addCommand(addBackend); + program.addCommand(removeBackend); + program.addCommand(lookupBackend); + program.addCommand(logsCommand); + + program.parse(argv); + + // It would make sense to check this, then parse argv, however this causes issues with + // the application name not displaying correctly. + + if (argv.length == 1) { + println("No arguments specified!\n\n"); + program.help(); + return; + } + + await new Promise(resolve => program.onExit(resolve)); +} diff --git a/sshfrontend/src/commands/connections.ts b/sshfrontend/src/commands/connections.ts new file mode 100644 index 0000000..29f4eb6 --- /dev/null +++ b/sshfrontend/src/commands/connections.ts @@ -0,0 +1,504 @@ +import type { Axios } from "axios"; + +import { SSHCommand } from "../libs/patchCommander.js"; +import type { PrintLine } from "../commands.js"; + +// https://stackoverflow.com/questions/37938504/what-is-the-best-way-to-find-all-items-are-deleted-inserted-from-original-arra +function difference(a: any[], b: any[]) { + return a.filter(x => b.indexOf(x) < 0); +} + +type InboundConnectionSuccess = { + success: true; + data: { + ip: string; + port: number; + + connectionDetails: { + sourceIP: string; + sourcePort: number; + destPort: number; + enabled: boolean; + }; + }[]; +}; + +type LookupCommandSuccess = { + success: true; + data: { + id: number; + name: string; + description: string; + sourceIP: string; + sourcePort: number; + destPort: number; + providerID: number; + autoStart: boolean; + }[]; +}; + +export async function run( + argv: string[], + println: PrintLine, + axios: Axios, + token: string, +) { + if (argv.length == 1) + return println( + "error: no arguments specified! run %s --help to see commands.\n", + argv[0], + ); + + const program = new SSHCommand(println); + program.description("Manages connections for NextNet"); + program.version("v1.0.0"); + + const addCommand = new SSHCommand(println, "add"); + addCommand.description("Creates a new connection"); + + addCommand.argument( + "", + "The backend ID to use. Can be fetched by the command 'backend search'", + ); + + addCommand.argument("", "The name for the tunnel"); + addCommand.argument("", "The protocol to use. Either TCP or UDP"); + + addCommand.argument( + "", + "Source IP and port combo (ex. '192.168.0.63:25565'", + ); + + addCommand.argument("", "Destination port to use"); + addCommand.option("-d, --description", "Description for the tunnel"); + + addCommand.action( + async ( + providerIDStr: string, + name: string, + protocolRaw: string, + source: string, + destPortRaw: string, + options: { + description?: string; + }, + ) => { + const providerID = parseInt(providerIDStr); + + if (Number.isNaN(providerID)) { + println("ID (%s) is not a number\n", providerIDStr); + return; + } + + const protocol = protocolRaw.toLowerCase().trim(); + + if (protocol != "tcp" && protocol != "udp") { + return println("Protocol is not a valid option (not tcp or udp)\n"); + } + + const sourceSplit: string[] = source.split(":"); + + if (sourceSplit.length != 2) { + return println( + "Source could not be splitted down (are you missing the ':' in the source to specify port?)\n", + ); + } + + const sourceIP: string = sourceSplit[0]; + const sourcePort: number = parseInt(sourceSplit[1]); + + if (Number.isNaN(sourcePort)) { + return println("Port splitted is not a number\n"); + } + + const destinationPort: number = parseInt(destPortRaw); + + if (Number.isNaN(destinationPort)) { + return println("Destination port could not be parsed into a number\n"); + } + + const response = await axios.post("/api/v1/forward/create", { + token, + + name, + description: options.description, + + protocol, + + sourceIP, + sourcePort, + + destinationPort, + + providerID, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error creating a connection!\n"); + } + + return; + } + + println("Successfully created connection.\n"); + }, + ); + + const lookupCommand = new SSHCommand(println, "find"); + + lookupCommand.description( + "Looks up all connections based on the arguments you specify", + ); + + lookupCommand.option( + "-b, --backend-id ", + "The backend ID to use. Can be fetched by 'back find'", + ); + + lookupCommand.option("-n, --name ", "The name for the tunnel"); + + lookupCommand.option( + "-p, --protocol ", + "The protocol to use. Either TCP or UDP", + ); + + lookupCommand.option( + "-s , --source", + "Source IP and port combo (ex. '192.168.0.63:25565'", + ); + + lookupCommand.option("-d, --dest-port ", "Destination port to use"); + + lookupCommand.option( + "-o, --description ", + "Description for the tunnel", + ); + + lookupCommand.action( + async (options: { + backendId?: string; + destPort?: string; + name?: string; + protocol?: string; + source?: string; + description?: string; + }) => { + let numberBackendID: number | undefined; + + let sourceIP: string | undefined; + let sourcePort: number | undefined; + + let destPort: number | undefined; + + if (options.backendId) { + numberBackendID = parseInt(options.backendId); + + if (Number.isNaN(numberBackendID)) { + println("ID (%s) is not a number\n", options.backendId); + return; + } + } + + if (options.source) { + const sourceSplit: string[] = options.source.split(":"); + + if (sourceSplit.length != 2) { + return println( + "Source could not be splitted down (are you missing the ':' in the source to specify port?)\n", + ); + } + + sourceIP = sourceSplit[0]; + sourcePort = parseInt(sourceSplit[1]); + + if (Number.isNaN(sourcePort)) { + return println("Port splitted is not a number\n"); + } + } + + if (options.destPort) { + destPort = parseInt(options.destPort); + + if (Number.isNaN(destPort)) { + println("ID (%s) is not a number\n", options.destPort); + return; + } + } + + const response = await axios.post("/api/v1/forward/lookup", { + token, + + name: options.name, + description: options.description, + + protocol: options.protocol, + + sourceIP, + sourcePort, + + destinationPort: destPort, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error requesting connections!\n"); + } + + return; + } + + const { data }: LookupCommandSuccess = response.data; + + for (const connection of data) { + println( + "ID: %s%s:\n", + connection.id, + connection.autoStart ? " (automatically starts)" : "", + ); + println(" - Backend ID: %s\n", connection.providerID); + println(" - Name: %s\n", connection.name); + if (connection.description) + println(" - Description: %s\n", connection.description); + println( + " - Source: %s:%s\n", + connection.sourceIP, + connection.sourcePort, + ); + println(" - Destination port: %s\n", connection.destPort); + + println("\n"); + } + + println("%s connections found.\n", data.length); + }, + ); + + const startTunnel = new SSHCommand(println, "start"); + startTunnel.description("Starts a tunnel"); + startTunnel.argument("", "Tunnel ID to start"); + + startTunnel.action(async (idStr: string) => { + const id = parseInt(idStr); + + if (Number.isNaN(id)) { + println("ID (%s) is not a number\n", idStr); + return; + } + + const response = await axios.post("/api/v1/forward/start", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error starting the connection!\n"); + } + + return; + } + + println("Successfully started tunnel.\n"); + return; + }); + + const stopTunnel = new SSHCommand(println, "stop"); + stopTunnel.description("Stops a tunnel"); + stopTunnel.argument("", "Tunnel ID to stop"); + + stopTunnel.action(async (idStr: string) => { + const id = parseInt(idStr); + + if (Number.isNaN(id)) { + println("ID (%s) is not a number\n", idStr); + return; + } + + const response = await axios.post("/api/v1/forward/stop", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error stopping a connection!\n"); + } + + return; + } + + println("Successfully stopped tunnel.\n"); + }); + + const getInbound = new SSHCommand(println, "get-inbound"); + getInbound.description("Shows all current connections"); + getInbound.argument("", "Tunnel ID to view inbound connections of"); + getInbound.option("-t, --tail", "Live-view of connection list"); + getInbound.option( + "-s, --tail-pull-rate ", + "Controls the speed to pull at (in ms)", + ); + + getInbound.action( + async ( + idStr: string, + options: { + tail?: boolean; + tailPullRate?: string; + }, + ): Promise => { + const pullRate: number = options.tailPullRate + ? parseInt(options.tailPullRate) + : 2000; + const id = parseInt(idStr); + + if (Number.isNaN(id)) { + println("ID (%s) is not a number\n", idStr); + return; + } + + if (Number.isNaN(pullRate)) { + println("Pull rate is not a number\n"); + return; + } + + if (options.tail) { + let previousEntries: string[] = []; + + // FIXME? + // eslint-disable-next-line no-constant-condition + while (true) { + const response = await axios.post("/api/v1/forward/connections", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error requesting inbound connections!\n"); + } + + return; + } + + const { data }: InboundConnectionSuccess = response.data; + const simplifiedArray: string[] = data.map(i => `${i.ip}:${i.port}`); + + const insertedItems: string[] = difference( + simplifiedArray, + previousEntries, + ); + + const removedItems: string[] = difference( + previousEntries, + simplifiedArray, + ); + + insertedItems.forEach(i => println("CONNECTED: %s\n", i)); + removedItems.forEach(i => println("DISCONNECTED: %s\n", i)); + + previousEntries = simplifiedArray; + + await new Promise(i => setTimeout(i, pullRate)); + } + } else { + const response = await axios.post("/api/v1/forward/connections", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error requesting connections!\n"); + } + + return; + } + + const { data }: InboundConnectionSuccess = response.data; + + if (data.length == 0) { + println("There are currently no connected clients.\n"); + return; + } + + println( + "Connected clients (for source: %s:%s):\n", + data[0].connectionDetails.sourceIP, + data[0].connectionDetails.sourcePort, + ); + + for (const entry of data) { + println(" - %s:%s\n", entry.ip, entry.port); + } + } + }, + ); + + const removeTunnel = new SSHCommand(println, "rm"); + removeTunnel.description("Removes a tunnel"); + removeTunnel.argument("", "Tunnel ID to remove"); + + removeTunnel.action(async (idStr: string) => { + const id = parseInt(idStr); + + if (Number.isNaN(id)) { + println("ID (%s) is not a number\n", idStr); + return; + } + + const response = await axios.post("/api/v1/forward/remove", { + token, + id, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error deleting connection!\n"); + } + + return; + } + + println("Successfully deleted connection.\n"); + }); + + program.addCommand(addCommand); + program.addCommand(lookupCommand); + program.addCommand(startTunnel); + program.addCommand(stopTunnel); + program.addCommand(getInbound); + program.addCommand(removeTunnel); + + program.parse(argv); + await new Promise(resolve => program.onExit(resolve)); +} diff --git a/sshfrontend/src/commands/users.ts b/sshfrontend/src/commands/users.ts new file mode 100644 index 0000000..2c9452b --- /dev/null +++ b/sshfrontend/src/commands/users.ts @@ -0,0 +1,215 @@ +import type { Axios } from "axios"; + +import { SSHCommand } from "../libs/patchCommander.js"; +import type { PrintLine, KeyboardRead } from "../commands.js"; + +type UserLookupSuccess = { + success: true; + data: { + id: number; + isServiceAccount: boolean; + username: string; + name: string; + email: string; + }[]; +}; + +export async function run( + argv: string[], + println: PrintLine, + axios: Axios, + apiKey: string, + readKeyboard: KeyboardRead, +) { + if (argv.length == 1) + return println( + "error: no arguments specified! run %s --help to see commands.\n", + argv[0], + ); + + const program = new SSHCommand(println); + program.description("Manages users for NextNet"); + program.version("v1.0.0"); + + const addCommand = new SSHCommand(println, "add"); + addCommand.description("Create a new user"); + addCommand.argument("", "Username of new user"); + addCommand.argument("", "Email of new user"); + addCommand.argument("[name]", "Name of new user (defaults to username)"); + + addCommand.option("-p, --password", "Password of User"); + addCommand.option( + "-a, --ask-password, --ask-pass, --askpass", + "Asks for a password. Hides output", + ); + + addCommand.option( + "-s, --service-account, --service", + "Turns the user into a service account", + ); + + addCommand.action( + async ( + username: string, + email: string, + name: string, + options: { + password?: string; + askPassword?: boolean; + isServiceAccount?: boolean; + }, + ) => { + if (!options.password && !options.askPassword) { + println("No password supplied, and askpass has not been supplied.\n"); + return; + } + + let password: string = ""; + + if (options.askPassword) { + let passwordConfirmOne = "a"; + let passwordConfirmTwo = "b"; + + while (passwordConfirmOne != passwordConfirmTwo) { + println("Password: "); + passwordConfirmOne = await readKeyboard(true); + + println("\nConfirm password: "); + passwordConfirmTwo = await readKeyboard(true); + + println("\n"); + + if (passwordConfirmOne != passwordConfirmTwo) { + println("Passwords do not match! Try again.\n\n"); + } + } + + password = passwordConfirmOne; + } else { + // @ts-expect-error: From the first check we do, we know this is safe (you MUST specify a password) + password = options.password; + } + + const response = await axios.post("/api/v1/users/create", { + name, + username, + email, + password, + + allowUnsafeGlobalTokens: options.isServiceAccount, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error creating users!\n"); + } + + return; + } + + println("User created successfully.\nToken: %s\n", response.data.token); + }, + ); + + const removeCommand = new SSHCommand(println, "rm"); + removeCommand.description("Remove a user"); + removeCommand.argument("", "ID of user to remove"); + + removeCommand.action(async (uidStr: string) => { + const uid = parseInt(uidStr); + + if (Number.isNaN(uid)) { + println("UID (%s) is not a number.\n", uid); + return; + } + + const response = await axios.post("/api/v1/users/remove", { + token: apiKey, + uid, + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error deleting user!\n"); + } + + return; + } + + println("User has been successfully deleted.\n"); + }); + + const lookupCommand = new SSHCommand(println, "find"); + lookupCommand.description("Find a user"); + lookupCommand.option("-i, --id ", "UID of User"); + lookupCommand.option("-n, --name ", "Name of User"); + lookupCommand.option("-u, --username ", "Username of User"); + lookupCommand.option("-e, --email ", "Email of User"); + lookupCommand.option("-s, --service", "The user is a service account"); + + lookupCommand.action(async options => { + // FIXME: redundant parseInt calls + + if (options.id) { + const uid = parseInt(options.id); + + if (Number.isNaN(uid)) { + println("UID (%s) is not a number.\n", uid); + return; + } + } + + const response = await axios.post("/api/v1/users/lookup", { + token: apiKey, + id: options.id ? parseInt(options.id) : undefined, + name: options.name, + username: options.username, + email: options.email, + service: Boolean(options.service), + }); + + if (response.status != 200) { + if (process.env.NODE_ENV != "production") console.log(response); + + if (response.data.error) { + println(`Error: ${response.data.error}\n`); + } else { + println("Error finding users!\n"); + } + + return; + } + + const { data }: UserLookupSuccess = response.data; + + for (const user of data) { + println( + "UID: %s%s:\n", + user.id, + user.isServiceAccount ? " (service)" : "", + ); + println("- Username: %s\n", user.username); + println("- Name: %s\n", user.name); + println("- Email: %s\n", user.email); + + println("\n"); + } + + println("%s users found.\n", data.length); + }); + + program.addCommand(addCommand); + program.addCommand(removeCommand); + program.addCommand(lookupCommand); + + program.parse(argv); + await new Promise(resolve => program.onExit(resolve)); +} diff --git a/sshfrontend/src/copyID.ts b/sshfrontend/src/copyID.ts new file mode 100644 index 0000000..c306db4 --- /dev/null +++ b/sshfrontend/src/copyID.ts @@ -0,0 +1,48 @@ +import { writeFile } from "node:fs/promises"; +import ssh2 from "ssh2"; + +import { readFromKeyboard } from "./libs/readFromKeyboard.js"; +import type { ClientKeys } from "./index.js"; + +export async function runCopyID( + username: string, + password: string, + keys: ClientKeys, + stream: ssh2.ServerChannel, +) { + stream.write( + "Hey there! I think you're using ssh-copy-id. If this is an error, you may close this terminal.\n", + ); + + stream.write("Please wait...\n"); + + const keyData = await readFromKeyboard(stream, true); + stream.write("Parsing key...\n"); + + const parsedKey = ssh2.utils.parseKey(keyData); + + if (parsedKey instanceof Error) { + stream.write(parsedKey.message + "\n"); + return stream.close(); + } + + stream.write("Passed checks. Writing changes...\n"); + + keys.push({ + username, + password, + publicKey: keyData, + }); + + try { + await writeFile("../keys/clients.json", JSON.stringify(keys, null, 2)); + } catch (e) { + console.log(e); + return stream.write( + "ERROR: Failed to save changes! If you're the administrator, view the console for details.\n", + ); + } + + stream.write("Success!\n"); + return stream.close(); +} diff --git a/sshfrontend/src/index.ts b/sshfrontend/src/index.ts new file mode 100644 index 0000000..c2d93ef --- /dev/null +++ b/sshfrontend/src/index.ts @@ -0,0 +1,242 @@ +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { timingSafeEqual } from "node:crypto"; +import { format } from "node:util"; + +import parseArgsStringToArgv from "string-argv"; +import baseAxios from "axios"; +import ssh2 from "ssh2"; + +import { readFromKeyboard } from "./libs/readFromKeyboard.js"; +import { commands } from "./commands.js"; +import { runCopyID } from "./copyID.js"; + +export type ClientKeys = { + publicKey: string; + username: string; + password: string; +}[]; + +function checkValue(input: Buffer, allowed: Buffer): boolean { + const autoReject = input.length !== allowed.length; + if (autoReject) allowed = input; + + const isMatch = timingSafeEqual(input, allowed); + return !autoReject && isMatch; +} + +let serverKeyFile: Buffer | string | undefined; +let clientKeys: ClientKeys = []; + +const serverBaseURL: string = + process.env.SERVER_BASE_URL ?? "http://127.0.0.1:3000/"; + +const axios = baseAxios.create({ + baseURL: serverBaseURL, + validateStatus: () => true, +}); + +try { + clientKeys = JSON.parse(await readFile("../keys/clients.json", "utf8")); +} catch (e) { + console.log("INFO: We don't have the client key file."); +} + +try { + serverKeyFile = await readFile("../keys/host.key"); +} catch (e) { + console.log( + "ERROR: Failed to read the host key file! Creating new keypair...", + ); + await mkdir("../keys").catch(() => null); + + const keyPair: { private: string; public: string } = await new Promise( + resolve => + ssh2.utils.generateKeyPair("ed25519", (err, keyPair) => resolve(keyPair)), + ); + + await writeFile("../keys/host.key", keyPair.private); + await writeFile("../keys/host.pub", keyPair.public); + + serverKeyFile = keyPair.private; +} + +if (!serverKeyFile) throw new Error("Somehow failed to fetch the key file!"); + +const server: ssh2.Server = new ssh2.Server({ + hostKeys: [serverKeyFile], + banner: "NextNet-LOM (c) NextNet project et al.", +}); + +server.on("connection", client => { + let token: string = ""; + + let username: string = ""; + let password: string = ""; + + client.on("authentication", async auth => { + if (auth.method == "password") { + const response = await axios.post("/api/v1/users/login", { + username: auth.username, + password: auth.password, + }); + + if (response.status == 403) { + return auth.reject(["password", "publickey"]); + } + + token = response.data.token; + + username = auth.username; + password = auth.password; + + auth.accept(); + } else if (auth.method == "publickey") { + const userData = { + username: "", + password: "", + }; + + for (const rawKey of clientKeys) { + const key = ssh2.utils.parseKey(rawKey.publicKey); + + if (key instanceof Error) { + console.log(key); + continue; + } + + if ( + (rawKey.username == auth.username && + auth.key.algo == key.type && + checkValue(auth.key.data, key.getPublicSSH())) || + (auth.signature && + key.verify(auth.blob as Buffer, auth.signature, auth.key.algo)) + ) { + userData.username = rawKey.username; + userData.password = rawKey.password; + } + } + + if (!userData.username || !userData.password) + return auth.reject(["password", "publickey"]); + + const response = await axios.post("/api/v1/users/login", userData); + + if (response.status == 403) { + return auth.reject(["password", "publickey"]); + } + + token = response.data.token; + + username = userData.username; + password = userData.password; + + auth.accept(); + } else { + return auth.reject(["password", "publickey"]); + } + }); + + client.on("ready", () => { + client.on("session", accept => { + const conn = accept(); + + conn.on("exec", async (accept, reject, info) => { + const stream = accept(); + + if ( + info.command.includes(".ssh/authorized_keys") && + info.command.startsWith("exec sh -c") + ) { + return await runCopyID(username, password, clientKeys, stream); + } + + // Matches on ; and && + const commandsRecv = info.command.split(/;|&&/).map(i => i.trim()); + + function println(...data: unknown[]) { + stream.write(format(...data).replaceAll("\n", "\r\n")); + } + + for (const command of commandsRecv) { + const argv = parseArgsStringToArgv(command); + + if (argv[0] == "exit") { + stream.close(); + } else { + const command = commands.find(i => i.name == argv[0]); + + if (!command) { + stream.write(`Unknown command ${argv[0]}.\r\n`); + + continue; + } + + await command.run(argv, println, axios, token, disableEcho => + readFromKeyboard(stream, disableEcho), + ); + } + } + + return stream.close(); + }); + + // We're dumb. We don't really care. + conn.on("pty", accept => accept()); + conn.on("window-change", accept => { + if (typeof accept != "function") return; + accept(); + }); + + conn.on("shell", async accept => { + const stream = accept(); + stream.write( + "Welcome to NextNet LOM. Run 'help' to see commands.\r\n\r\n~$ ", + ); + + function println(...data: unknown[]) { + stream.write(format(...data).replaceAll("\n", "\r\n")); + } + + // FIXME (greysoh): wtf? this isn't setting correctly. + // @eslint-disable-next-line + while (true) { + const line = await readFromKeyboard(stream); + stream.write("\r\n"); + + if (line == "") { + stream.write(`~$ `); + continue; + } + + const argv = parseArgsStringToArgv(line); + + if (argv[0] == "exit") { + stream.close(); + } else { + const command = commands.find(i => i.name == argv[0]); + + if (!command) { + stream.write( + `Unknown command ${argv[0]}. Run 'help' to see commands.\r\n~$ `, + ); + + continue; + } + + await command.run(argv, println, axios, token, disableEcho => + readFromKeyboard(stream, disableEcho), + ); + stream.write("~$ "); + } + } + }); + }); + }); +}); + +server.listen( + 2222, + process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1", +); + +console.log("Started server at ::2222"); diff --git a/sshfrontend/src/libs/patchCommander.ts b/sshfrontend/src/libs/patchCommander.ts new file mode 100644 index 0000000..f30e492 --- /dev/null +++ b/sshfrontend/src/libs/patchCommander.ts @@ -0,0 +1,108 @@ +import { Command, type ParseOptions } from "commander"; +import { PrintLine } from "../commands"; + +export class SSHCommand extends Command { + hasRecievedExitSignal: boolean; + println: PrintLine; + + exitEventHandlers: ((...any: unknown[]) => void)[]; + parent: SSHCommand | null; + + /** + * Modified version of the Commander command with slight automated patches, to work with our SSH environment. + * @param println PrintLine function to use + * @param name Optional field for the name of the command + */ + constructor( + println: PrintLine, + name?: string, + disableSSHHelpPatching: boolean = false, + ) { + super(name); + + this.exitEventHandlers = []; + + this.configureOutput({ + writeOut: str => println(str), + writeErr: str => { + if (this.hasRecievedExitSignal) return; + println(str); + }, + }); + + if (!disableSSHHelpPatching) { + const sshCommand = new SSHCommand(println, "help", true); + + sshCommand.description("display help for command"); + sshCommand.argument("[command]", "command to show help for"); + sshCommand.action(() => { + this.hasRecievedExitSignal = true; + + if (process.env.NODE_ENV != "production") { + println( + "Caught irrecoverable crash (command help call) in patchCommander\n", + ); + } else { + println("Aborted\n"); + } + }); + + this.addCommand(sshCommand); + } + } + + recvExitDispatch() { + this.hasRecievedExitSignal = true; + this.exitEventHandlers.forEach(eventHandler => eventHandler()); + + let parentElement = this.parent; + + while (parentElement instanceof SSHCommand) { + parentElement.hasRecievedExitSignal = true; + parentElement.exitEventHandlers.forEach(eventHandler => eventHandler()); + + parentElement = parentElement.parent; + } + } + + onExit(callback: (...any: any[]) => void) { + this.exitEventHandlers.push(callback); + if (this.hasRecievedExitSignal) callback(); + } + + _exit() { + this.recvExitDispatch(); + } + + _exitCallback() { + this.recvExitDispatch(); + } + + action(fn: (...args: any[]) => void | Promise): this { + super.action(fn); + + // @ts-expect-error: This parameter is private, but we need control over it. + // prettier-ignore + const oldActionHandler: (...args: any[]) => void | Promise = this._actionHandler; + + // @ts-expect-error: Overriding private parameters (but this works) + this._actionHandler = async (...args: any[]): Promise => { + if (this.hasRecievedExitSignal) return; + await oldActionHandler(...args); + + this.recvExitDispatch(); + }; + + return this; + } + + parse(argv?: readonly string[], options?: ParseOptions): this { + super.parse(["nextruntime", ...(argv ?? [])], options); + return this; + } + + createCommand(name: string) { + const command = new SSHCommand(this.println, name); + return command; + } +} diff --git a/sshfrontend/src/libs/readFromKeyboard.ts b/sshfrontend/src/libs/readFromKeyboard.ts new file mode 100644 index 0000000..9ac19ee --- /dev/null +++ b/sshfrontend/src/libs/readFromKeyboard.ts @@ -0,0 +1,109 @@ +import type { ServerChannel } from "ssh2"; + +const pullRate = process.env.KEYBOARD_PULLING_RATE + ? parseInt(process.env.KEYBOARD_PULLING_RATE) + : 5; + +const leftEscape = "\x1B[D"; +const rightEscape = "\x1B[C"; + +const ourBackspace = "\u0008"; +const clientBackspace = "\x7F"; + +export async function readFromKeyboard( + stream: ServerChannel, + disableEcho: boolean = false, +): Promise { + let promise: (value: string | PromiseLike) => void; + + let line = ""; + let lineIndex = 0; + + async function eventLoop(): Promise { + const readStreamDataBuf = stream.read(); + if (readStreamDataBuf == null) return setTimeout(eventLoop, pullRate); + + const readStreamData = readStreamDataBuf.toString(); + + // Fixes several bugs (incl. potential social eng. exploits, ssh-copy-id being broken, etc) + for (const character of readStreamData.split("")) { + if (character == "\x03") { + stream.write("^C"); + return promise(""); + } else if (character == "\r" || character == "\n") { + return promise(line.replace("\r", "")); + } else if (character == clientBackspace) { + if (line.length == 0) return setTimeout(eventLoop, pullRate); // Here because if we do it in the parent if statement, shit breaks + line = line.substring(0, lineIndex - 1) + line.substring(lineIndex); + + if (!disableEcho) { + const deltaCursor = line.length - lineIndex; + + if (deltaCursor == line.length) + return setTimeout(eventLoop, pullRate); + + if (deltaCursor < 0) { + // Use old technique if the delta is < 0, as the new one is tailored to the start + 1 to end - 1 + stream.write(ourBackspace + " " + ourBackspace); + } else { + // Jump forward to the front, and remove the last character + stream.write(rightEscape.repeat(deltaCursor) + " " + ourBackspace); + + // Go backwards & rerender text & go backwards again (wtf?) + stream.write( + leftEscape.repeat(deltaCursor + 1) + + line.substring(lineIndex - 1) + + leftEscape.repeat(deltaCursor + 1), + ); + } + + lineIndex -= 1; + } + } else if (character == "\x1B") { + if (character == rightEscape) { + if (lineIndex + 1 > line.length) + return setTimeout(eventLoop, pullRate); + lineIndex += 1; + } else if (character == leftEscape) { + if (lineIndex - 1 < 0) return setTimeout(eventLoop, pullRate); + lineIndex -= 1; + } else { + return setTimeout(eventLoop, pullRate); + } + + if (!disableEcho) stream.write(character); + } else { + lineIndex += 1; + + // There isn't a splice method for String prototypes. So, ugh: + line = + line.substring(0, lineIndex - 1) + + character + + line.substring(lineIndex - 1); + + if (!disableEcho) { + let deltaCursor = line.length - lineIndex; + + // wtf? + if (deltaCursor < 0) { + console.log( + "FIXME: somehow, our deltaCursor value is negative! please investigate me", + ); + deltaCursor = 0; + } + + stream.write( + line.substring(lineIndex - 1) + leftEscape.repeat(deltaCursor), + ); + } + } + } + + setTimeout(eventLoop, pullRate); + } + + return new Promise(resolve => { + setTimeout(eventLoop, pullRate); + promise = resolve; + }); +} diff --git a/sshfrontend/tsconfig.json b/sshfrontend/tsconfig.json new file mode 100644 index 0000000..99ab26e --- /dev/null +++ b/sshfrontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "es2022", + "moduleResolution": "node", + + "lib": [ + "es2021" + ], + + "outDir": "./out", + "rootDir": "./src", + + "strict": true, + "esModuleInterop": true, + "sourceMap": true, + + "declaration": true, + "declarationMap": true, + + "strictPropertyInitialization": false, + }, + + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file