Compare commits

..

No commits in common. "dev" and "v2.0.1" have entirely different histories.
dev ... v2.0.1

114 changed files with 11505 additions and 7209 deletions

View file

@ -13,7 +13,9 @@
}, },
// run arguments passed to docker // run arguments passed to docker
"runArgs": ["--security-opt", "label=disable"], "runArgs": [
"--security-opt", "label=disable"
],
"containerEnv": { "containerEnv": {
// extensions to preload before other extensions // extensions to preload before other extensions
@ -29,11 +31,15 @@
"onCreateCommand": "nix-shell --command 'echo done building nix dev environment'", "onCreateCommand": "nix-shell --command 'echo done building nix dev environment'",
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [8000], "forwardPorts": [
3000
],
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": ["arrterian.nix-env-selector"] "extensions": [
"arrterian.nix-env-selector"
]
} }
} }

View file

@ -32,12 +32,23 @@ jobs:
username: imterah username: imterah
password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}} password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}}
- name: Build Docker image - name: Build Docker images
run: | 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: | run: |
docker tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME ghcr.io/imterah/hermes:latest 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:$GITHUB_REF_NAME
docker push ghcr.io/imterah/hermes:latest 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

2
.gitconfig Normal file
View file

@ -0,0 +1,2 @@
[core]
hooksPath = .githooks/

18
.githooks/pre-commit Executable file
View file

@ -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

8
.gitignore vendored
View file

@ -1,15 +1,15 @@
# Go artifacts # Go artifacts
backend/api/api
backend/sshbackend/sshbackend backend/sshbackend/sshbackend
backend/dummybackend/dummybackend backend/dummybackend/dummybackend
backend/sshappbackend/local-code/remote-bin
backend/sshappbackend/local-code/sshappbackend
backend/externalbackendlauncher/externalbackendlauncher backend/externalbackendlauncher/externalbackendlauncher
frontend/frontend backend/api/api
# Backup artifacts # Backup artifacts
*.json.gz *.json.gz
# LOM
sshfrontend/keys
# Output # Output
out out

16
.prettierrc Normal file
View file

@ -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
}

49
CHANGELOG.md Normal file
View file

@ -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)*

View file

@ -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"]

View file

@ -1,6 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2024, Tera Copyright (c) 2024, Greyson
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

25
MigrationDockerfile Normal file
View file

@ -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

View file

@ -32,7 +32,7 @@
1. Copy and change the default password (or username & db name too) from the template file `prod-docker.env`: 1. Copy and change the default password (or username & db name too) from the template file `prod-docker.env`:
```bash ```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` 2. Build the docker stack: `docker compose --env-file .env up -d`

View file

@ -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)
}

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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
}

15
backend-legacy/Dockerfile Normal file
View file

@ -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

7
backend-legacy/dev.env Normal file
View file

@ -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"

View file

@ -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

View file

@ -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",
},
},
];

3302
backend-legacy/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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;

View file

@ -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";

View file

@ -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;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "username" TEXT;

View file

@ -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"

View file

@ -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[]
}

View file

@ -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.");
}
});

View file

@ -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"]
}

10
backend/Dockerfile Normal file
View file

@ -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"]

View file

@ -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"
}

View file

@ -6,47 +6,50 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"strings"
"sync"
"time" "time"
"git.terah.dev/imterah/hermes/backend/backendlauncher" "git.terah.dev/imterah/hermes/backendlauncher"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
) )
// TODO TODO TODO(imterah): var (
// 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. AvailableBackends []*Backend
RunningBackends map[uint]*Runtime
TempDir string
)
func handleCommand(command interface{}, sock net.Conn, rtcChan chan interface{}) error { func init() {
bytes, err := commonbackend.Marshal(command) 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 { if err != nil {
log.Warnf("Failed to marshal message: %s", err.Error()) log.Warnf("Failed to marshal message: %s", err.Error())
rtcChan <- fmt.Errorf("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 { if _, err := sock.Write(bytes); err != nil {
log.Warnf("Failed to write message: %s", err.Error()) log.Warnf("Failed to write message: %s", err.Error())
rtcChan <- fmt.Errorf("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 { if err != nil {
log.Warnf("Failed to unmarshal message: %s", err.Error()) log.Warnf("Failed to unmarshal message: %s", err.Error())
rtcChan <- fmt.Errorf("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 rtcChan <- data
return nil
} }
func (runtime *Runtime) goRoutineHandler() error { func (runtime *Runtime) goRoutineHandler() error {
@ -66,7 +69,7 @@ func (runtime *Runtime) goRoutineHandler() error {
log.Debugf("Acquired unix socket at: %s", sockPath) log.Debugf("Acquired unix socket at: %s", sockPath)
go func() { go func() {
log.Debug("Created new Goroutine for socket connection handling") log.Debug("Creating new goroutine for socket connection handling")
for { for {
log.Debug("Waiting for Unix socket connections...") log.Debug("Waiting for Unix socket connections...")
@ -77,122 +80,56 @@ func (runtime *Runtime) goRoutineHandler() error {
return 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 { for {
if !runtime.isRuntimeRunning { commandRaw := <-runtime.RuntimeCommands
return
}
// Asking for the backend status seems to be a "good-enough" keepalive system. Plus, it provides useful telemetry. log.Debug("Got message from server")
// 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 { switch command := commandRaw.(type) {
log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", err.Error()) case *commonbackend.AddProxy:
log.Debugf("Attempting to close socket...") handleCommand("addProxy", command, sock, runtime.RuntimeCommands)
err := sock.Close() case *commonbackend.BackendStatusRequest:
handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands)
if err != nil {
log.Debugf("Failed to close socket: %s", err.Error())
}
continue
}
switch responseMessage := statusResponse.(type) {
case *commonbackend.BackendStatusResponse: case *commonbackend.BackendStatusResponse:
if !responseMessage.IsRunning { handleCommand("backendStatusResponse", command, sock, runtime.RuntimeCommands)
if hasFailedBackendRunningCheckAlready { case *commonbackend.CheckClientParameters:
if responseMessage.Message != "" { handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands)
log.Warnf("Backend (in backend keepalive) is up but not active: %s", responseMessage.Message) case *commonbackend.CheckParametersResponse:
} else { handleCommand("checkParametersResponse", command, sock, runtime.RuntimeCommands)
log.Warnf("Backend (in backend keepalive) is up but not active") case *commonbackend.CheckServerParameters:
} handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands)
} case *commonbackend.ProxyClientConnection:
handleCommand("proxyClientConnection", command, sock, runtime.RuntimeCommands)
hasFailedBackendRunningCheckAlready = true 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: default:
log.Errorf("Got illegal response type for backend (in backend keepalive): %T", responseMessage) log.Warnf("Recieved unknown command type from channel: %q", command)
log.Debugf("Attempting to close socket...") runtime.RuntimeCommands <- fmt.Errorf("unknown command recieved")
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
for chanIndex, messageData := range runtime.messageBuffer {
if messageData == nil {
continue
}
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
}
runtime.isRuntimeCurrentlyProcessing = false
}
sock.Close()
}
}()
runtime.processRestartNotification <- false
for { for {
log.Debug("Starting process...") log.Debug("Starting process...")
@ -224,14 +161,6 @@ func (runtime *Runtime) goRoutineHandler() error {
log.Debug("Sleeping 5 seconds, and then restarting process") log.Debug("Sleeping 5 seconds, and then restarting process")
time.Sleep(5 * time.Second) 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") return fmt.Errorf("runtime already running")
} }
runtime.messageBuffer = make([]*messageForBuf, 10) runtime.RuntimeCommands = make(chan interface{})
runtime.messageBufferLock = sync.Mutex{}
runtime.startProcessingNotification = make(chan bool)
runtime.processRestartNotification = make(chan bool, 1)
runtime.logger = &writeLogger{ runtime.logger = &writeLogger{
Runtime: runtime, Runtime: runtime,
@ -292,90 +217,6 @@ func (runtime *Runtime) Stop() error {
return nil 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 { func NewBackend(path string) *Runtime {
return &Runtime{ return &Runtime{
ProcessPath: path, ProcessPath: path,

View file

@ -4,9 +4,6 @@ import (
"net" "net"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"github.com/charmbracelet/log"
) )
type Backend struct { type Backend struct {
@ -14,28 +11,15 @@ type Backend struct {
Path string `validate:"required"` 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 { type Runtime struct {
isRuntimeRunning bool isRuntimeRunning bool
isRuntimeCurrentlyProcessing bool
startProcessingNotification chan bool
logger *writeLogger logger *writeLogger
currentProcess *exec.Cmd currentProcess *exec.Cmd
currentListener net.Listener currentListener net.Listener
processRestartNotification chan bool
messageBufferLock sync.Mutex
messageBuffer []*messageForBuf
ProcessPath string ProcessPath string
Logs []string Logs []string
RuntimeCommands chan interface{}
OnCrashCallback func(sock net.Conn)
} }
type writeLogger struct { type writeLogger struct {
@ -44,17 +28,6 @@ type writeLogger struct {
func (writer writeLogger) Write(p []byte) (n int, err error) { func (writer writeLogger) Write(p []byte) (n int, err error) {
logSplit := strings.Split(string(p), "\n") 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...) writer.Runtime.Logs = append(writer.Runtime.Logs, logSplit...)
return len(p), err return len(p), err

301
backend/api/backup.go Normal file
View file

@ -0,0 +1,301 @@
package main
import (
"compress/gzip"
"encoding/base64"
"encoding/hex"
"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
}
bcryptPassword, err := hex.DecodeString(user.Password)
if err != nil {
log.Errorf("Failed to decode hex encoded password: %s", err.Error())
continue
}
userDatabase := &dbcore.User{
Email: user.Email,
Username: username,
Name: user.Name,
Password: base64.StdEncoding.EncodeToString(bcryptPassword),
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
}

View file

@ -6,13 +6,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type BackendCreationRequest struct { type BackendCreationRequest struct {
@ -23,8 +24,7 @@ type BackendCreationRequest struct {
BackendParameters interface{} `json:"connectionDetails" validate:"required"` BackendParameters interface{} `json:"connectionDetails" validate:"required"`
} }
func SetupCreateBackend(state *state.State) { func CreateBackend(c *gin.Context) {
state.Engine.POST("/api/v1/backends/create", func(c *gin.Context) {
var req BackendCreationRequest var req BackendCreationRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -35,7 +35,7 @@ func SetupCreateBackend(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -43,7 +43,7 @@ func SetupCreateBackend(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
@ -125,12 +125,16 @@ func SetupCreateBackend(state *state.State) {
return return
} }
backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{ backend.RuntimeCommands <- &commonbackend.CheckServerParameters{
Type: "checkServerParameters",
Arguments: backendParameters, Arguments: backendParameters,
}) }
if err != nil { backendParamCheckResponse := <-backend.RuntimeCommands
log.Warnf("Failed to get response for backend: %s", err.Error())
switch responseMessage := backendParamCheckResponse.(type) {
case error:
log.Warnf("Failed to get response for backend: %s", responseMessage.Error())
err = backend.Stop() err = backend.Stop()
@ -143,9 +147,6 @@ func SetupCreateBackend(state *state.State) {
}) })
return return
}
switch responseMessage := backendParamCheckResponse.(type) {
case *commonbackend.CheckParametersResponse: case *commonbackend.CheckParametersResponse:
if responseMessage.InResponseTo != "checkServerParameters" { if responseMessage.InResponseTo != "checkServerParameters" {
log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo) log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo)
@ -190,7 +191,7 @@ func SetupCreateBackend(state *state.State) {
log.Info("Passed backend checks successfully") log.Info("Passed backend checks successfully")
backendInDatabase := &db.Backend{ backendInDatabase := &dbcore.Backend{
UserID: user.ID, UserID: user.ID,
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
@ -198,7 +199,7 @@ func SetupCreateBackend(state *state.State) {
BackendParameters: base64.StdEncoding.EncodeToString(backendParameters), BackendParameters: base64.StdEncoding.EncodeToString(backendParameters),
} }
if result := state.DB.DB.Create(&backendInDatabase); result.Error != nil { if result := dbcore.DB.Create(&backendInDatabase); result.Error != nil {
log.Warnf("Failed to create backend: %s", result.Error.Error()) log.Warnf("Failed to create backend: %s", result.Error.Error())
err = backend.Stop() err = backend.Stop()
@ -214,12 +215,16 @@ func SetupCreateBackend(state *state.State) {
return return
} }
backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{ backend.RuntimeCommands <- &commonbackend.Start{
Type: "start",
Arguments: backendParameters, Arguments: backendParameters,
}) }
if err != nil { backendStartResponse := <-backend.RuntimeCommands
log.Warnf("Failed to get response for backend: %s", err.Error())
switch responseMessage := backendStartResponse.(type) {
case error:
log.Warnf("Failed to get response for backend: %s", responseMessage.Error())
err = backend.Stop() err = backend.Stop()
@ -232,9 +237,6 @@ func SetupCreateBackend(state *state.State) {
}) })
return return
}
switch responseMessage := backendStartResponse.(type) {
case *commonbackend.BackendStatusResponse: case *commonbackend.BackendStatusResponse:
if !responseMessage.IsRunning { if !responseMessage.IsRunning {
err = backend.Stop() err = backend.Stop()
@ -266,5 +268,4 @@ func SetupCreateBackend(state *state.State) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
}) })
})
} }

View file

@ -6,12 +6,13 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type BackendLookupRequest struct { type BackendLookupRequest struct {
@ -26,9 +27,9 @@ type SanitizedBackend struct {
Name string `json:"name"` Name string `json:"name"`
BackendID uint `json:"id"` BackendID uint `json:"id"`
OwnerID uint `json:"ownerID"` OwnerID uint `json:"ownerID"`
Description *string `json:"description,omitempty"` Description *string `json:"description"`
Backend string `json:"backend"` Backend string `json:"backend"`
BackendParameters *string `json:"connectionDetails,omitempty"` BackendParameters *string `json:"connectionDetails"`
Logs []string `json:"logs"` Logs []string `json:"logs"`
} }
@ -37,8 +38,7 @@ type LookupResponse struct {
Data []*SanitizedBackend `json:"data"` Data []*SanitizedBackend `json:"data"`
} }
func SetupLookupBackend(state *state.State) { func LookupBackend(c *gin.Context) {
state.Engine.POST("/api/v1/backends/lookup", func(c *gin.Context) {
var req BackendLookupRequest var req BackendLookupRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -49,7 +49,7 @@ func SetupLookupBackend(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -57,7 +57,7 @@ func SetupLookupBackend(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
@ -85,7 +85,7 @@ func SetupLookupBackend(state *state.State) {
return return
} }
backends := []db.Backend{} backends := []dbcore.Backend{}
queryString := []string{} queryString := []string{}
queryParameters := []interface{}{} queryParameters := []interface{}{}
@ -109,7 +109,7 @@ func SetupLookupBackend(state *state.State) {
queryParameters = append(queryParameters, req.Backend) queryParameters = append(queryParameters, req.Backend)
} }
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil { if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil {
log.Warnf("Failed to get backends: %s", err.Error()) log.Warnf("Failed to get backends: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -160,5 +160,4 @@ func SetupLookupBackend(state *state.State) {
Success: true, Success: true,
Data: sanitizedBackends, Data: sanitizedBackends,
}) })
})
} }

View file

@ -4,12 +4,13 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type BackendRemovalRequest struct { type BackendRemovalRequest struct {
@ -17,8 +18,7 @@ type BackendRemovalRequest struct {
BackendID uint `json:"id" validate:"required"` BackendID uint `json:"id" validate:"required"`
} }
func SetupRemoveBackend(state *state.State) { func RemoveBackend(c *gin.Context) {
state.Engine.POST("/api/v1/backends/remove", func(c *gin.Context) {
var req BackendRemovalRequest var req BackendRemovalRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -29,7 +29,7 @@ func SetupRemoveBackend(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -37,7 +37,7 @@ func SetupRemoveBackend(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
@ -65,8 +65,8 @@ func SetupRemoveBackend(state *state.State) {
return return
} }
var backend *db.Backend var backend *dbcore.Backend
backendRequest := state.DB.DB.Where("id = ?", req.BackendID).Find(&backend) backendRequest := dbcore.DB.Where("id = ?", req.BackendID).Find(&backend)
if backendRequest.Error != nil { if backendRequest.Error != nil {
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
@ -88,7 +88,7 @@ func SetupRemoveBackend(state *state.State) {
return return
} }
if err := state.DB.DB.Delete(backend).Error; err != nil { if err := dbcore.DB.Delete(backend).Error; err != nil {
log.Warnf("failed to delete backend: %s", err.Error()) log.Warnf("failed to delete backend: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -120,5 +120,4 @@ func SetupRemoveBackend(state *state.State) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
}) })
})
} }

View file

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type ConnectionsRequest struct { type ConnectionsRequest struct {
@ -36,8 +37,7 @@ type ConnectionsResponse struct {
Data []*SanitizedConnection `json:"data"` Data []*SanitizedConnection `json:"data"`
} }
func SetupGetConnections(state *state.State) { func GetConnections(c *gin.Context) {
state.Engine.POST("/api/v1/forward/connections", func(c *gin.Context) {
var req ConnectionsRequest var req ConnectionsRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -48,7 +48,7 @@ func SetupGetConnections(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -56,8 +56,7 @@ func SetupGetConnections(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
c.JSON(http.StatusForbidden, gin.H{ c.JSON(http.StatusForbidden, gin.H{
@ -84,8 +83,8 @@ func SetupGetConnections(state *state.State) {
return return
} }
var proxy db.Proxy var proxy dbcore.Proxy
proxyRequest := state.DB.DB.Where("id = ?", req.Id).First(&proxy) proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy)
if proxyRequest.Error != nil { if proxyRequest.Error != nil {
log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error()) log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error())
@ -119,19 +118,19 @@ func SetupGetConnections(state *state.State) {
return return
} }
backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{}) backendRuntime.RuntimeCommands <- &commonbackend.ProxyConnectionsRequest{
Type: "proxyConnectionsRequest",
}
if err != nil { backendResponse := <-backendRuntime.RuntimeCommands
log.Warnf("Failed to get response for backend: %s", err.Error())
switch responseMessage := backendResponse.(type) {
case error:
log.Warnf("Failed to get response for backend: %s", responseMessage.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get status response from backend", "error": "Failed to get status response from backend",
}) })
return
}
switch responseMessage := backendResponse.(type) {
case *commonbackend.ProxyConnectionsResponse: case *commonbackend.ProxyConnectionsResponse:
sanitizedConnections := []*SanitizedConnection{} sanitizedConnections := []*SanitizedConnection{}
@ -161,5 +160,4 @@ func SetupGetConnections(state *state.State) {
"error": "Got illegal response type", "error": "Got illegal response type",
}) })
} }
})
} }

View file

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type ProxyCreationRequest struct { type ProxyCreationRequest struct {
@ -25,8 +26,7 @@ type ProxyCreationRequest struct {
AutoStart *bool `json:"autoStart"` AutoStart *bool `json:"autoStart"`
} }
func SetupCreateProxy(state *state.State) { func CreateProxy(c *gin.Context) {
state.Engine.POST("/api/v1/forward/create", func(c *gin.Context) {
var req ProxyCreationRequest var req ProxyCreationRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -37,7 +37,7 @@ func SetupCreateProxy(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -45,8 +45,7 @@ func SetupCreateProxy(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
c.JSON(http.StatusForbidden, gin.H{ c.JSON(http.StatusForbidden, gin.H{
@ -81,8 +80,8 @@ func SetupCreateProxy(state *state.State) {
return return
} }
var backend db.Backend var backend dbcore.Backend
backendRequest := state.DB.DB.Where("id = ?", req.ProviderID).First(&backend) backendRequest := dbcore.DB.Where("id = ?", req.ProviderID).First(&backend)
if backendRequest.Error != nil { if backendRequest.Error != nil {
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
@ -106,7 +105,7 @@ func SetupCreateProxy(state *state.State) {
autoStart = *req.AutoStart autoStart = *req.AutoStart
} }
proxy := &db.Proxy{ proxy := &dbcore.Proxy{
UserID: user.ID, UserID: user.ID,
BackendID: req.ProviderID, BackendID: req.ProviderID,
Name: req.Name, Name: req.Name,
@ -118,14 +117,12 @@ func SetupCreateProxy(state *state.State) {
AutoStart: autoStart, AutoStart: autoStart,
} }
if result := state.DB.DB.Create(proxy); result.Error != nil { if result := dbcore.DB.Create(proxy); result.Error != nil {
log.Warnf("failed to create proxy: %s", result.Error.Error()) log.Warnf("failed to create proxy: %s", result.Error.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to add forward rule to database", "error": "Failed to add forward rule to database",
}) })
return
} }
if autoStart { if autoStart {
@ -142,24 +139,25 @@ func SetupCreateProxy(state *state.State) {
return return
} }
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ backend.RuntimeCommands <- &commonbackend.AddProxy{
Type: "addProxy",
SourceIP: proxy.SourceIP, SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort, SourcePort: proxy.SourcePort,
DestPort: proxy.DestinationPort, DestPort: proxy.DestinationPort,
Protocol: proxy.Protocol, Protocol: proxy.Protocol,
}) }
if err != nil { backendResponse := <-backend.RuntimeCommands
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
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{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to get response from backend", "error": "failed to get response from backend",
}) })
return return
}
switch responseMessage := backendResponse.(type) {
case *commonbackend.ProxyStatusResponse: case *commonbackend.ProxyStatusResponse:
if !responseMessage.IsActive { if !responseMessage.IsActive {
log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID) log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID)
@ -173,5 +171,4 @@ func SetupCreateProxy(state *state.State) {
"success": true, "success": true,
"id": proxy.ID, "id": proxy.ID,
}) })
})
} }

View file

@ -5,11 +5,12 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type ProxyLookupRequest struct { type ProxyLookupRequest struct {
@ -28,7 +29,7 @@ type ProxyLookupRequest struct {
type SanitizedProxy struct { type SanitizedProxy struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description"`
Protcol string `json:"protocol"` Protcol string `json:"protocol"`
SourceIP string `json:"sourceIP"` SourceIP string `json:"sourceIP"`
SourcePort uint16 `json:"sourcePort"` SourcePort uint16 `json:"sourcePort"`
@ -42,8 +43,7 @@ type ProxyLookupResponse struct {
Data []*SanitizedProxy `json:"data"` Data []*SanitizedProxy `json:"data"`
} }
func SetupLookupProxy(state *state.State) { func LookupProxy(c *gin.Context) {
state.Engine.POST("/api/v1/forward/lookup", func(c *gin.Context) {
var req ProxyLookupRequest var req ProxyLookupRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -54,7 +54,7 @@ func SetupLookupProxy(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -62,7 +62,7 @@ func SetupLookupProxy(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
@ -95,12 +95,10 @@ func SetupLookupProxy(state *state.State) {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": "Protocol specified in body must either be 'tcp' or 'udp'", "error": "Protocol specified in body must either be 'tcp' or 'udp'",
}) })
return
} }
} }
proxies := []db.Proxy{} proxies := []dbcore.Proxy{}
queryString := []string{} queryString := []string{}
queryParameters := []interface{}{} queryParameters := []interface{}{}
@ -150,7 +148,7 @@ func SetupLookupProxy(state *state.State) {
queryParameters = append(queryParameters, req.Protocol) queryParameters = append(queryParameters, req.Protocol)
} }
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil { if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil {
log.Warnf("failed to get proxies: %s", err.Error()) log.Warnf("failed to get proxies: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -180,5 +178,4 @@ func SetupLookupProxy(state *state.State) {
Success: true, Success: true,
Data: sanitizedProxies, Data: sanitizedProxies,
}) })
})
} }

View file

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type ProxyRemovalRequest struct { type ProxyRemovalRequest struct {
@ -18,8 +19,7 @@ type ProxyRemovalRequest struct {
ID uint `validate:"required" json:"id"` ID uint `validate:"required" json:"id"`
} }
func SetupRemoveProxy(state *state.State) { func RemoveProxy(c *gin.Context) {
state.Engine.POST("/api/v1/forward/remove", func(c *gin.Context) {
var req ProxyRemovalRequest var req ProxyRemovalRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -30,7 +30,7 @@ func SetupRemoveProxy(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -38,8 +38,7 @@ func SetupRemoveProxy(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
c.JSON(http.StatusForbidden, gin.H{ c.JSON(http.StatusForbidden, gin.H{
@ -66,8 +65,8 @@ func SetupRemoveProxy(state *state.State) {
return return
} }
var proxy *db.Proxy var proxy *dbcore.Proxy
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
if proxyRequest.Error != nil { if proxyRequest.Error != nil {
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
@ -89,7 +88,7 @@ func SetupRemoveProxy(state *state.State) {
return return
} }
if err := state.DB.DB.Delete(proxy).Error; err != nil { if err := dbcore.DB.Delete(proxy).Error; err != nil {
log.Warnf("failed to delete proxy: %s", err.Error()) log.Warnf("failed to delete proxy: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -111,33 +110,32 @@ func SetupRemoveProxy(state *state.State) {
return return
} }
backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ backend.RuntimeCommands <- &commonbackend.RemoveProxy{
Type: "removeProxy",
SourceIP: proxy.SourceIP, SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort, SourcePort: proxy.SourcePort,
DestPort: proxy.DestinationPort, DestPort: proxy.DestinationPort,
Protocol: proxy.Protocol, Protocol: proxy.Protocol,
}) }
if err != nil { backendResponse := <-backend.RuntimeCommands
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
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{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get response from backend. Proxy was still successfully deleted", "error": "Failed to get response from backend. Proxy was still successfully deleted",
}) })
return return
}
switch responseMessage := backendResponse.(type) {
case *commonbackend.ProxyStatusResponse: case *commonbackend.ProxyStatusResponse:
if responseMessage.IsActive { if responseMessage.IsActive {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to stop proxy. Proxy was still successfully deleted", "error": "Failed to stop proxy. Proxy was still successfully deleted",
}) })
} else {
c.JSON(http.StatusOK, gin.H{ return
"success": true,
})
} }
default: default:
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
@ -145,6 +143,11 @@ func SetupRemoveProxy(state *state.State) {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Got invalid response from backend. Proxy was still successfully deleted", "error": "Got invalid response from backend. Proxy was still successfully deleted",
}) })
return
} }
c.JSON(http.StatusOK, gin.H{
"success": true,
}) })
} }

View file

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type ProxyStartRequest struct { type ProxyStartRequest struct {
@ -18,8 +19,7 @@ type ProxyStartRequest struct {
ID uint `validate:"required" json:"id"` ID uint `validate:"required" json:"id"`
} }
func SetupStartProxy(state *state.State) { func StartProxy(c *gin.Context) {
state.Engine.POST("/api/v1/forward/start", func(c *gin.Context) {
var req ProxyStartRequest var req ProxyStartRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -30,7 +30,7 @@ func SetupStartProxy(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -38,8 +38,7 @@ func SetupStartProxy(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
c.JSON(http.StatusForbidden, gin.H{ c.JSON(http.StatusForbidden, gin.H{
@ -66,8 +65,8 @@ func SetupStartProxy(state *state.State) {
return return
} }
var proxy *db.Proxy var proxy *dbcore.Proxy
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
if proxyRequest.Error != nil { if proxyRequest.Error != nil {
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
@ -101,12 +100,15 @@ func SetupStartProxy(state *state.State) {
return return
} }
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ backend.RuntimeCommands <- &commonbackend.AddProxy{
Type: "addProxy",
SourceIP: proxy.SourceIP, SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort, SourcePort: proxy.SourcePort,
DestPort: proxy.DestinationPort, DestPort: proxy.DestinationPort,
Protocol: proxy.Protocol, Protocol: proxy.Protocol,
}) }
backendResponse := <-backend.RuntimeCommands
switch responseMessage := backendResponse.(type) { switch responseMessage := backendResponse.(type) {
case error: case error:
@ -115,22 +117,29 @@ func SetupStartProxy(state *state.State) {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to get response from backend", "error": "failed to get response from backend",
}) })
return
case *commonbackend.ProxyStatusResponse: case *commonbackend.ProxyStatusResponse:
if !responseMessage.IsActive { if !responseMessage.IsActive {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to start proxy", "error": "failed to start proxy",
}) })
} else {
c.JSON(http.StatusOK, gin.H{ return
"success": true,
})
} }
break
default: default:
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Got invalid response from backend. Proxy was likely still successfully started", "error": "Got invalid response from backend. Proxy was still successfully deleted",
}) })
return
} }
c.JSON(http.StatusOK, gin.H{
"success": true,
}) })
} }

View file

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type ProxyStopRequest struct { type ProxyStopRequest struct {
@ -18,9 +19,8 @@ type ProxyStopRequest struct {
ID uint `validate:"required" json:"id"` ID uint `validate:"required" json:"id"`
} }
func SetupStopProxy(state *state.State) { func StopProxy(c *gin.Context) {
state.Engine.POST("/api/v1/forward/stop", func(c *gin.Context) { var req ProxyStopRequest
var req ProxyStartRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
@ -30,7 +30,7 @@ func SetupStopProxy(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -38,8 +38,7 @@ func SetupStopProxy(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
c.JSON(http.StatusForbidden, gin.H{ c.JSON(http.StatusForbidden, gin.H{
@ -66,8 +65,8 @@ func SetupStopProxy(state *state.State) {
return return
} }
var proxy *db.Proxy var proxy *dbcore.Proxy
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
if proxyRequest.Error != nil { if proxyRequest.Error != nil {
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
@ -101,12 +100,15 @@ func SetupStopProxy(state *state.State) {
return return
} }
backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ backend.RuntimeCommands <- &commonbackend.RemoveProxy{
Type: "removeProxy",
SourceIP: proxy.SourceIP, SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort, SourcePort: proxy.SourcePort,
DestPort: proxy.DestinationPort, DestPort: proxy.DestinationPort,
Protocol: proxy.Protocol, Protocol: proxy.Protocol,
}) }
backendResponse := <-backend.RuntimeCommands
switch responseMessage := backendResponse.(type) { switch responseMessage := backendResponse.(type) {
case error: case error:
@ -115,22 +117,27 @@ func SetupStopProxy(state *state.State) {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to get response from backend", "error": "failed to get response from backend",
}) })
return
case *commonbackend.ProxyStatusResponse: case *commonbackend.ProxyStatusResponse:
if responseMessage.IsActive { if responseMessage.IsActive {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to stop proxy", "error": "failed to stop proxy",
}) })
} else {
c.JSON(http.StatusOK, gin.H{ return
"success": true,
})
} }
default: default:
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Got invalid response from backend. Proxy was likely still successfully stopped", "error": "Got invalid response from backend. Proxy was still successfully deleted",
}) })
return
} }
c.JSON(http.StatusOK, gin.H{
"success": true,
}) })
} }

View file

@ -7,9 +7,11 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.terah.dev/imterah/hermes/backend/api/db" "github.com/go-playground/validator/v10"
permissionHelper "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"
permissionHelper "git.terah.dev/imterah/hermes/api/permissions"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -20,11 +22,13 @@ type UserCreationRequest struct {
Email string `validate:"required"` Email string `validate:"required"`
Password string `validate:"required"` Password string `validate:"required"`
Username string `validate:"required"` Username string `validate:"required"`
// TODO: implement support
ExistingUserToken string `json:"token"`
IsBot bool IsBot bool
} }
func SetupCreateUser(state *state.State) { func CreateUser(c *gin.Context) {
state.Engine.POST("/api/v1/users/create", func(c *gin.Context) {
if !signupEnabled && !unsafeSignup { if !signupEnabled && !unsafeSignup {
c.JSON(http.StatusForbidden, gin.H{ c.JSON(http.StatusForbidden, gin.H{
"error": "Signing up is not enabled at this time.", "error": "Signing up is not enabled at this time.",
@ -43,7 +47,7 @@ func SetupCreateUser(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -51,8 +55,8 @@ func SetupCreateUser(state *state.State) {
return return
} }
var user *db.User var user *dbcore.User
userRequest := state.DB.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user) userRequest := dbcore.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user)
if userRequest.Error != nil { if userRequest.Error != nil {
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
@ -86,7 +90,7 @@ func SetupCreateUser(state *state.State) {
return return
} }
permissions := []db.Permission{} permissions := []dbcore.Permission{}
for _, permission := range permissionHelper.DefaultPermissionNodes { for _, permission := range permissionHelper.DefaultPermissionNodes {
permissionEnabledState := false permissionEnabledState := false
@ -95,7 +99,7 @@ func SetupCreateUser(state *state.State) {
permissionEnabledState = true permissionEnabledState = true
} }
permissions = append(permissions, db.Permission{ permissions = append(permissions, dbcore.Permission{
PermissionNode: permission, PermissionNode: permission,
HasPermission: permissionEnabledState, HasPermission: permissionEnabledState,
}) })
@ -113,14 +117,14 @@ func SetupCreateUser(state *state.State) {
return return
} }
user = &db.User{ user = &dbcore.User{
Email: req.Email, Email: req.Email,
Username: req.Username, Username: req.Username,
Name: req.Name, Name: req.Name,
IsBot: &req.IsBot, IsBot: &req.IsBot,
Password: base64.StdEncoding.EncodeToString(passwordHashed), Password: base64.StdEncoding.EncodeToString(passwordHashed),
Permissions: permissions, Permissions: permissions,
Tokens: []db.Token{ Tokens: []dbcore.Token{
{ {
Token: base64.StdEncoding.EncodeToString(tokenRandomData), Token: base64.StdEncoding.EncodeToString(tokenRandomData),
DisableExpiry: forceNoExpiryTokens, DisableExpiry: forceNoExpiryTokens,
@ -129,7 +133,7 @@ func SetupCreateUser(state *state.State) {
}, },
} }
if result := state.DB.DB.Create(&user); result.Error != nil { if result := dbcore.DB.Create(&user); result.Error != nil {
log.Warnf("Failed to create user: %s", result.Error.Error()) log.Warnf("Failed to create user: %s", result.Error.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -139,7 +143,7 @@ func SetupCreateUser(state *state.State) {
return return
} }
jwt, err := state.JWT.Generate(user.ID) jwt, err := jwtcore.Generate(user.ID)
if err != nil { if err != nil {
log.Warnf("Failed to generate JWT: %s", err.Error()) log.Warnf("Failed to generate JWT: %s", err.Error())
@ -156,5 +160,4 @@ func SetupCreateUser(state *state.State) {
"token": jwt, "token": jwt,
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
}) })
})
} }

View file

@ -6,10 +6,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/jwtcore"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -20,8 +21,7 @@ type UserLoginRequest struct {
Password string `validate:"required"` Password string `validate:"required"`
} }
func SetupLoginUser(state *state.State) { func LoginUser(c *gin.Context) {
state.Engine.POST("/api/v1/users/login", func(c *gin.Context) {
var req UserLoginRequest var req UserLoginRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -32,7 +32,7 @@ func SetupLoginUser(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -61,8 +61,8 @@ func SetupLoginUser(state *state.State) {
userFindRequest += "username = ?" userFindRequest += "username = ?"
} }
var user *db.User var user *dbcore.User
userRequest := state.DB.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user) userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user)
if userRequest.Error != nil { if userRequest.Error != nil {
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
@ -119,7 +119,7 @@ func SetupLoginUser(state *state.State) {
return return
} }
token := &db.Token{ token := &dbcore.Token{
UserID: user.ID, UserID: user.ID,
Token: base64.StdEncoding.EncodeToString(tokenRandomData), Token: base64.StdEncoding.EncodeToString(tokenRandomData),
@ -127,7 +127,7 @@ func SetupLoginUser(state *state.State) {
CreationIPAddr: c.ClientIP(), CreationIPAddr: c.ClientIP(),
} }
if result := state.DB.DB.Create(&token); result.Error != nil { if result := dbcore.DB.Create(&token); result.Error != nil {
log.Warnf("Failed to create user: %s", result.Error.Error()) log.Warnf("Failed to create user: %s", result.Error.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -137,7 +137,7 @@ func SetupLoginUser(state *state.State) {
return return
} }
jwt, err := state.JWT.Generate(user.ID) jwt, err := jwtcore.Generate(user.ID)
if err != nil { if err != nil {
log.Warnf("Failed to generate JWT: %s", err.Error()) log.Warnf("Failed to generate JWT: %s", err.Error())
@ -154,5 +154,4 @@ func SetupLoginUser(state *state.State) {
"token": jwt, "token": jwt,
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
}) })
})
} }

View file

@ -5,11 +5,12 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type UserLookupRequest struct { type UserLookupRequest struct {
@ -34,8 +35,7 @@ type LookupResponse struct {
Data []*SanitizedUsers `json:"data"` Data []*SanitizedUsers `json:"data"`
} }
func SetupLookupUser(state *state.State) { func LookupUser(c *gin.Context) {
state.Engine.POST("/api/v1/users/lookup", func(c *gin.Context) {
var req UserLookupRequest var req UserLookupRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -46,7 +46,7 @@ func SetupLookupUser(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -54,7 +54,7 @@ func SetupLookupUser(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
@ -74,7 +74,7 @@ func SetupLookupUser(state *state.State) {
} }
} }
users := []db.User{} users := []dbcore.User{}
queryString := []string{} queryString := []string{}
queryParameters := []interface{}{} queryParameters := []interface{}{}
@ -101,7 +101,7 @@ func SetupLookupUser(state *state.State) {
queryParameters = append(queryParameters, req.IsBot) queryParameters = append(queryParameters, req.IsBot)
} }
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil { if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil {
log.Warnf("Failed to get users: %s", err.Error()) log.Warnf("Failed to get users: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@ -133,5 +133,4 @@ func SetupLookupUser(state *state.State) {
Success: true, Success: true,
Data: sanitizedUsers, Data: sanitizedUsers,
}) })
})
} }

View file

@ -5,18 +5,18 @@ import (
"net/http" "net/http"
"time" "time"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/jwtcore"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type UserRefreshRequest struct { type UserRefreshRequest struct {
Token string `validate:"required"` Token string `validate:"required"`
} }
func SetupRefreshUserToken(state *state.State) { func RefreshUserToken(c *gin.Context) {
state.Engine.POST("/api/v1/users/refresh", func(c *gin.Context) {
var req UserRefreshRequest var req UserRefreshRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -27,7 +27,7 @@ func SetupRefreshUserToken(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -35,8 +35,8 @@ func SetupRefreshUserToken(state *state.State) {
return return
} }
var tokenInDatabase *db.Token var tokenInDatabase *dbcore.Token
tokenRequest := state.DB.DB.Where("token = ?", req.Token).Find(&tokenInDatabase) tokenRequest := dbcore.DB.Where("token = ?", req.Token).Find(&tokenInDatabase)
if tokenRequest.Error != nil { if tokenRequest.Error != nil {
log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error()) log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error())
@ -65,7 +65,7 @@ func SetupRefreshUserToken(state *state.State) {
"error": "Token has expired", "error": "Token has expired",
}) })
tx := state.DB.DB.Delete(tokenInDatabase) tx := dbcore.DB.Delete(tokenInDatabase)
if tx.Error != nil { if tx.Error != nil {
log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error()) log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error())
@ -75,8 +75,8 @@ func SetupRefreshUserToken(state *state.State) {
} }
// Get the user to check if the user exists before doing anything // Get the user to check if the user exists before doing anything
var user *db.User var user *dbcore.User
userRequest := state.DB.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user) userRequest := dbcore.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user)
if tokenRequest.Error != nil { if tokenRequest.Error != nil {
log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error()) log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error())
@ -98,7 +98,7 @@ func SetupRefreshUserToken(state *state.State) {
return return
} }
jwt, err := state.JWT.Generate(user.ID) jwt, err := jwtcore.Generate(user.ID)
if err != nil { if err != nil {
log.Warnf("Failed to generate JWT: %s", err.Error()) log.Warnf("Failed to generate JWT: %s", err.Error())
@ -114,5 +114,4 @@ func SetupRefreshUserToken(state *state.State) {
"success": true, "success": true,
"token": jwt, "token": jwt,
}) })
})
} }

View file

@ -4,11 +4,12 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/permissions" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/api/permissions"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type UserRemovalRequest struct { type UserRemovalRequest struct {
@ -16,8 +17,7 @@ type UserRemovalRequest struct {
UID *uint `json:"uid"` UID *uint `json:"uid"`
} }
func SetupRemoveUser(state *state.State) { func RemoveUser(c *gin.Context) {
state.Engine.POST("/api/v1/users/remove", func(c *gin.Context) {
var req UserRemovalRequest var req UserRemovalRequest
if err := c.BindJSON(&req); err != nil { if err := c.BindJSON(&req); err != nil {
@ -28,7 +28,7 @@ func SetupRemoveUser(state *state.State) {
return return
} }
if err := state.Validator.Struct(&req); err != nil { if err := validator.New().Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()), "error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
}) })
@ -36,7 +36,7 @@ func SetupRemoveUser(state *state.State) {
return return
} }
user, err := state.JWT.GetUserFromJWT(req.Token) user, err := jwtcore.GetUserFromJWT(req.Token)
if err != nil { if err != nil {
if err.Error() == "token is expired" || err.Error() == "user does not exist" { if err.Error() == "token is expired" || err.Error() == "user does not exist" {
@ -73,8 +73,8 @@ func SetupRemoveUser(state *state.State) {
// Make sure the user exists first if we have a custom UserID // Make sure the user exists first if we have a custom UserID
if uid != user.ID { if uid != user.ID {
var customUser *db.User var customUser *dbcore.User
userRequest := state.DB.DB.Where("id = ?", uid).Find(customUser) userRequest := dbcore.DB.Where("id = ?", uid).Find(customUser)
if userRequest.Error != nil { if userRequest.Error != nil {
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
@ -97,10 +97,9 @@ func SetupRemoveUser(state *state.State) {
} }
} }
state.DB.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user) dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
}) })
})
} }

View file

@ -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))
}
}

View file

@ -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
}

142
backend/api/dbcore/db.go Normal file
View file

@ -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
}

View file

@ -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
}

117
backend/api/jwtcore/jwt.go Normal file
View file

@ -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
}

View file

@ -4,24 +4,22 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends" "git.terah.dev/imterah/hermes/api/controllers/v1/backends"
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies" "git.terah.dev/imterah/hermes/api/controllers/v1/proxies"
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/users" "git.terah.dev/imterah/hermes/api/controllers/v1/users"
"git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/jwt" "git.terah.dev/imterah/hermes/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/commonbackend"
"git.terah.dev/imterah/hermes/backend/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gorm.io/gorm"
) )
func apiEntrypoint(cCtx *cli.Context) error { func apiEntrypoint(cCtx *cli.Context) error {
@ -35,26 +33,7 @@ func apiEntrypoint(cCtx *cli.Context) error {
log.Info("Hermes is initializing...") log.Info("Hermes is initializing...")
log.Debug("Initializing database and opening it...") log.Debug("Initializing database and opening it...")
databaseBackendName := os.Getenv("HERMES_DATABASE_BACKEND") err := dbcore.InitializeDatabase(&gorm.Config{})
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)
if err != nil { if err != nil {
log.Fatalf("Failed to initialize database: %s", err) log.Fatalf("Failed to initialize database: %s", err)
@ -62,38 +41,16 @@ func apiEntrypoint(cCtx *cli.Context) error {
log.Debug("Running database migrations...") 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) return fmt.Errorf("Failed to run database migrations: %s", err)
} }
log.Debug("Initializing the JWT subsystem...") log.Debug("Initializing the JWT subsystem...")
jwtDataString := os.Getenv("HERMES_JWT_SECRET") if err := jwtcore.SetupJWT(); err != nil {
var jwtKey []byte return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error())
var jwtValidityTimeDuration time.Duration
if jwtDataString == "" {
log.Fatalf("HERMES_JWT_SECRET is not set")
} }
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...") log.Debug("Initializing the backend subsystem...")
backendMetadataPath := cCtx.String("backends-path") backendMetadataPath := cCtx.String("backends-path")
@ -118,9 +75,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
log.Debug("Enumerating backends...") 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()) 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 := 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() err = backendInstance.Start()
if err != nil { if err != nil {
@ -243,12 +112,16 @@ func apiEntrypoint(cCtx *cli.Context) error {
continue continue
} }
backendStartResponse, err := backendInstance.ProcessCommand(&commonbackend.Start{ backendInstance.RuntimeCommands <- &commonbackend.Start{
Type: "start",
Arguments: backendParameters, Arguments: backendParameters,
}) }
if err != nil { backendStartResponse := <-backendInstance.RuntimeCommands
log.Warnf("Failed to get response for backend #%d: %s", backend.ID, err.Error())
switch responseMessage := backendStartResponse.(type) {
case error:
log.Warnf("Failed to get response for backend #%d: %s", backend.ID, responseMessage.Error())
err = backendInstance.Stop() err = backendInstance.Stop()
@ -257,9 +130,6 @@ func apiEntrypoint(cCtx *cli.Context) error {
} }
continue continue
}
switch responseMessage := backendStartResponse.(type) {
case *commonbackend.BackendStatusResponse: case *commonbackend.BackendStatusResponse:
if !responseMessage.IsRunning { if !responseMessage.IsRunning {
err = backendInstance.Stop() err = backendInstance.Stop()
@ -285,9 +155,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
log.Infof("Successfully initialized backend #%d", backend.ID) 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()) log.Errorf("Failed to query proxies to autostart: %s", err.Error())
continue continue
} }
@ -295,19 +165,20 @@ func apiEntrypoint(cCtx *cli.Context) error {
for _, proxy := range autoStartProxies { for _, proxy := range autoStartProxies {
log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) 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, SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort, SourcePort: proxy.SourcePort,
DestPort: proxy.DestinationPort, DestPort: proxy.DestinationPort,
Protocol: proxy.Protocol, 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) { 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: case *commonbackend.ProxyStatusResponse:
if !responseMessage.IsActive { if !responseMessage.IsActive {
log.Warnf("Failed to start proxy for backend #%d and route #%d", proxy.BackendID, proxy.ID) 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) engine.SetTrustedProxies(nil)
} }
state := state.New(dbInstance, jwtInstance, engine)
// Initialize routes // Initialize routes
users.SetupCreateUser(state) engine.POST("/api/v1/users/create", users.CreateUser)
users.SetupLoginUser(state) engine.POST("/api/v1/users/login", users.LoginUser)
users.SetupRefreshUserToken(state) engine.POST("/api/v1/users/refresh", users.RefreshUserToken)
users.SetupRemoveUser(state) engine.POST("/api/v1/users/remove", users.RemoveUser)
users.SetupLookupUser(state) engine.POST("/api/v1/users/lookup", users.LookupUser)
backends.SetupCreateBackend(state) engine.POST("/api/v1/backends/create", backends.CreateBackend)
backends.SetupRemoveBackend(state) engine.POST("/api/v1/backends/remove", backends.RemoveBackend)
backends.SetupLookupBackend(state) engine.POST("/api/v1/backends/lookup", backends.LookupBackend)
proxies.SetupCreateProxy(state) engine.POST("/api/v1/forward/create", proxies.CreateProxy)
proxies.SetupRemoveProxy(state) engine.POST("/api/v1/forward/lookup", proxies.LookupProxy)
proxies.SetupLookupProxy(state) engine.POST("/api/v1/forward/remove", proxies.RemoveProxy)
proxies.SetupStartProxy(state) engine.POST("/api/v1/forward/start", proxies.StartProxy)
proxies.SetupStopProxy(state) engine.POST("/api/v1/forward/stop", proxies.StopProxy)
proxies.SetupGetConnections(state) engine.POST("/api/v1/forward/connections", proxies.GetConnections)
log.Infof("Listening on '%s'", listeningAddress) log.Infof("Listening on '%s'", listeningAddress)
err = engine.Run(listeningAddress) err = engine.Run(listeningAddress)
@ -406,6 +275,22 @@ func main() {
app := &cli.App{ app := &cli.App{
Name: "hermes", Name: "hermes",
Usage: "port forwarding across boundaries", 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{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "backends-path", Name: "backends-path",

View file

@ -1,6 +1,6 @@
package permissions package permissions
import "git.terah.dev/imterah/hermes/backend/api/db" import "git.terah.dev/imterah/hermes/api/dbcore"
var DefaultPermissionNodes []string = []string{ var DefaultPermissionNodes []string = []string{
"routes.add", "routes.add",
@ -27,7 +27,7 @@ var DefaultPermissionNodes []string = []string{
"users.edit", "users.edit",
} }
func UserHasPermission(user *db.User, node string) bool { func UserHasPermission(user *dbcore.User, node string) bool {
for _, permission := range user.Permissions { for _, permission := range user.Permissions {
if permission.PermissionNode == node && permission.HasPermission { if permission.PermissionNode == node && permission.HasPermission {
return true return true

View file

@ -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(),
}
}

View file

@ -3,10 +3,6 @@
"name": "ssh", "name": "ssh",
"path": "./sshbackend/sshbackend" "path": "./sshbackend/sshbackend"
}, },
{
"name": "sshapp",
"path": "./sshappbackend/local-code/sshappbackend"
},
{ {
"name": "dummy", "name": "dummy",
"path": "./dummybackend/dummybackend" "path": "./dummybackend/dummybackend"

View file

@ -2,9 +2,5 @@
{ {
"name": "ssh", "name": "ssh",
"path": "./sshbackend" "path": "./sshbackend"
},
{
"name": "sshapp",
"path": "./sshappbackend"
} }
] ]

View file

@ -1,10 +1,11 @@
package backendutil package backendutil
import ( import (
"fmt"
"net" "net"
"os" "os"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
) )
@ -17,14 +18,9 @@ type BackendApplicationHelper struct {
func (helper *BackendApplicationHelper) Start() error { func (helper *BackendApplicationHelper) Start() error {
log.Debug("BackendApplicationHelper is starting") log.Debug("BackendApplicationHelper is starting")
err := ConfigureProfiling()
if err != nil {
return err
}
log.Debug("Currently waiting for Unix socket connection...") log.Debug("Currently waiting for Unix socket connection...")
var err error
helper.socket, err = net.Dial("unix", helper.SocketPath) helper.socket, err = net.Dial("unix", helper.SocketPath)
if err != nil { if err != nil {
@ -34,15 +30,21 @@ func (helper *BackendApplicationHelper) Start() error {
log.Debug("Sucessfully connected") log.Debug("Sucessfully connected")
for { for {
commandRaw, err := commonbackend.Unmarshal(helper.socket) commandType, commandRaw, err := commonbackend.Unmarshal(helper.socket)
if err != nil { if err != nil {
return err return err
} }
switch command := commandRaw.(type) { switch commandType {
case *commonbackend.Start: case "start":
ok, err := helper.Backend.StartBackend(command.Arguments) command, ok := commandRaw.(*commonbackend.Start)
if !ok {
return fmt.Errorf("failed to typecast")
}
ok, err = helper.Backend.StartBackend(command.Arguments)
var ( var (
message string message string
@ -57,12 +59,13 @@ func (helper *BackendApplicationHelper) Start() error {
} }
response := &commonbackend.BackendStatusResponse{ response := &commonbackend.BackendStatusResponse{
Type: "backendStatusResponse",
IsRunning: ok, IsRunning: ok,
StatusCode: statusCode, StatusCode: statusCode,
Message: message, Message: message,
} }
responseMarshalled, err := commonbackend.Marshal(response) responseMarshalled, err := commonbackend.Marshal(response.Type, response)
if err != nil { if err != nil {
log.Error("failed to marshal response: %s", err.Error()) log.Error("failed to marshal response: %s", err.Error())
@ -70,37 +73,14 @@ func (helper *BackendApplicationHelper) Start() error {
} }
helper.socket.Write(responseMarshalled) helper.socket.Write(responseMarshalled)
case *commonbackend.BackendStatusRequest: case "stop":
ok, err := helper.Backend.GetBackendStatus() _, ok := commandRaw.(*commonbackend.Stop)
var ( if !ok {
message string return fmt.Errorf("failed to typecast")
statusCode int }
)
ok, err = helper.Backend.StopBackend()
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 ( var (
message string message string
@ -115,12 +95,13 @@ func (helper *BackendApplicationHelper) Start() error {
} }
response := &commonbackend.BackendStatusResponse{ response := &commonbackend.BackendStatusResponse{
Type: "backendStatusResponse",
IsRunning: !ok, IsRunning: !ok,
StatusCode: statusCode, StatusCode: statusCode,
Message: message, Message: message,
} }
responseMarshalled, err := commonbackend.Marshal(response) responseMarshalled, err := commonbackend.Marshal(response.Type, response)
if err != nil { if err != nil {
log.Error("failed to marshal response: %s", err.Error()) log.Error("failed to marshal response: %s", err.Error())
@ -128,19 +109,26 @@ func (helper *BackendApplicationHelper) Start() error {
} }
helper.socket.Write(responseMarshalled) helper.socket.Write(responseMarshalled)
case *commonbackend.AddProxy: case "addProxy":
ok, err := helper.Backend.StartProxy(command) command, ok := commandRaw.(*commonbackend.AddProxy)
if !ok {
return fmt.Errorf("failed to typecast")
}
ok, err = helper.Backend.StartProxy(command)
var hasAnyFailed bool var hasAnyFailed bool
if err != nil { if !ok {
log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error())
hasAnyFailed = true
} else if !ok {
log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort)
hasAnyFailed = true 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{ response := &commonbackend.ProxyStatusResponse{
Type: "proxyStatusResponse",
SourceIP: command.SourceIP, SourceIP: command.SourceIP,
SourcePort: command.SourcePort, SourcePort: command.SourcePort,
DestPort: command.DestPort, DestPort: command.DestPort,
@ -148,7 +136,7 @@ func (helper *BackendApplicationHelper) Start() error {
IsActive: !hasAnyFailed, IsActive: !hasAnyFailed,
} }
responseMarshalled, err := commonbackend.Marshal(response) responseMarshalled, err := commonbackend.Marshal(response.Type, response)
if err != nil { if err != nil {
log.Error("failed to marshal response: %s", err.Error()) log.Error("failed to marshal response: %s", err.Error())
@ -156,19 +144,26 @@ func (helper *BackendApplicationHelper) Start() error {
} }
helper.socket.Write(responseMarshalled) helper.socket.Write(responseMarshalled)
case *commonbackend.RemoveProxy: case "removeProxy":
ok, err := helper.Backend.StopProxy(command) command, ok := commandRaw.(*commonbackend.RemoveProxy)
if !ok {
return fmt.Errorf("failed to typecast")
}
ok, err = helper.Backend.StopProxy(command)
var hasAnyFailed bool var hasAnyFailed bool
if err != nil { if !ok {
log.Warnf("failed to remove proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error())
hasAnyFailed = true
} else if !ok {
log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort)
hasAnyFailed = true 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{ response := &commonbackend.ProxyStatusResponse{
Type: "proxyStatusResponse",
SourceIP: command.SourceIP, SourceIP: command.SourceIP,
SourcePort: command.SourcePort, SourcePort: command.SourcePort,
DestPort: command.DestPort, DestPort: command.DestPort,
@ -176,7 +171,7 @@ func (helper *BackendApplicationHelper) Start() error {
IsActive: hasAnyFailed, IsActive: hasAnyFailed,
} }
responseMarshalled, err := commonbackend.Marshal(response) responseMarshalled, err := commonbackend.Marshal(response.Type, response)
if err != nil { if err != nil {
log.Error("failed to marshal response: %s", err.Error()) log.Error("failed to marshal response: %s", err.Error())
@ -184,14 +179,21 @@ func (helper *BackendApplicationHelper) Start() error {
} }
helper.socket.Write(responseMarshalled) 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() connections := helper.Backend.GetAllClientConnections()
serverParams := &commonbackend.ProxyConnectionsResponse{ serverParams := &commonbackend.ProxyConnectionsResponse{
Type: "proxyConnectionsResponse",
Connections: connections, Connections: connections,
} }
byteData, err := commonbackend.Marshal(serverParams) byteData, err := commonbackend.Marshal(serverParams.Type, serverParams)
if err != nil { if err != nil {
return err return err
@ -200,11 +202,18 @@ func (helper *BackendApplicationHelper) Start() error {
if _, err = helper.socket.Write(byteData); err != nil { if _, err = helper.socket.Write(byteData); err != nil {
return err 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 := helper.Backend.CheckParametersForConnections(command)
resp.Type = "checkParametersResponse"
resp.InResponseTo = "checkClientParameters" resp.InResponseTo = "checkClientParameters"
byteData, err := commonbackend.Marshal(resp) byteData, err := commonbackend.Marshal(resp.Type, resp)
if err != nil { if err != nil {
return err return err
@ -213,11 +222,18 @@ func (helper *BackendApplicationHelper) Start() error {
if _, err = helper.socket.Write(byteData); err != nil { if _, err = helper.socket.Write(byteData); err != nil {
return err 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 := helper.Backend.CheckParametersForBackend(command.Arguments)
resp.Type = "checkParametersResponse"
resp.InResponseTo = "checkServerParameters" resp.InResponseTo = "checkServerParameters"
byteData, err := commonbackend.Marshal(resp) byteData, err := commonbackend.Marshal(resp.Type, resp)
if err != nil { if err != nil {
return err return err
@ -226,8 +242,6 @@ func (helper *BackendApplicationHelper) Start() error {
if _, err = helper.socket.Write(byteData); err != nil { if _, err = helper.socket.Write(byteData); err != nil {
return err return err
} }
default:
log.Warnf("Unsupported command recieved: %T", command)
} }
} }
} }

View file

@ -1,9 +0,0 @@
//go:build !debug
package backendutil
var endProfileFunc func()
func ConfigureProfiling() error {
return nil
}

View file

@ -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)
}

View file

@ -1,11 +1,10 @@
package backendutil package backendutil
import "git.terah.dev/imterah/hermes/backend/commonbackend" import "git.terah.dev/imterah/hermes/commonbackend"
type BackendInterface interface { type BackendInterface interface {
StartBackend(arguments []byte) (bool, error) StartBackend(arguments []byte) (bool, error)
StopBackend() (bool, error) StopBackend() (bool, error)
GetBackendStatus() (bool, error)
StartProxy(command *commonbackend.AddProxy) (bool, error) StartProxy(command *commonbackend.AddProxy) (bool, error)
StopProxy(command *commonbackend.RemoveProxy) (bool, error) StopProxy(command *commonbackend.RemoveProxy) (bool, error)
GetAllClientConnections() []*commonbackend.ProxyClientConnection GetAllClientConnections() []*commonbackend.ProxyClientConnection

View file

@ -1,43 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
pushd sshbackend > /dev/null pushd sshbackend
echo "building sshbackend" CGO_ENABLED=0 GOOS=linux go build .
go build -ldflags="-s -w" -trimpath . strip sshbackend
popd > /dev/null popd
pushd dummybackend > /dev/null pushd dummybackend
echo "building dummybackend" CGO_ENABLED=0 GOOS=linux go build .
go build -ldflags="-s -w" -trimpath . strip dummybackend
popd > /dev/null popd
pushd externalbackendlauncher > /dev/null pushd externalbackendlauncher
echo "building externalbackendlauncher" go build .
go build -ldflags="-s -w" -trimpath . strip externalbackendlauncher
popd > /dev/null popd
if [ ! -d "sshappbackend/local-code/remote-bin" ]; then pushd api
mkdir "sshappbackend/local-code/remote-bin" CGO_ENABLED=0 GOOS=linux go build .
fi strip api
popd
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

View file

@ -1,13 +1,16 @@
package commonbackend package commonbackend
type Start struct { type Start struct {
Type string // Will be 'start' always
Arguments []byte Arguments []byte
} }
type Stop struct { type Stop struct {
Type string // Will be 'stop' always
} }
type AddProxy struct { type AddProxy struct {
Type string // Will be 'addProxy' always
SourceIP string SourceIP string
SourcePort uint16 SourcePort uint16
DestPort uint16 DestPort uint16
@ -15,6 +18,7 @@ type AddProxy struct {
} }
type RemoveProxy struct { type RemoveProxy struct {
Type string // Will be 'removeProxy' always
SourceIP string SourceIP string
SourcePort uint16 SourcePort uint16
DestPort uint16 DestPort uint16
@ -22,6 +26,7 @@ type RemoveProxy struct {
} }
type ProxyStatusRequest struct { type ProxyStatusRequest struct {
Type string // Will be 'proxyStatusRequest' always
SourceIP string SourceIP string
SourcePort uint16 SourcePort uint16
DestPort uint16 DestPort uint16
@ -29,6 +34,7 @@ type ProxyStatusRequest struct {
} }
type ProxyStatusResponse struct { type ProxyStatusResponse struct {
Type string // Will be 'proxyStatusResponse' always
SourceIP string SourceIP string
SourcePort uint16 SourcePort uint16
DestPort uint16 DestPort uint16
@ -44,22 +50,27 @@ type ProxyInstance struct {
} }
type ProxyInstanceResponse struct { type ProxyInstanceResponse struct {
Type string // Will be 'proxyConnectionResponse' always
Proxies []*ProxyInstance // List of connections Proxies []*ProxyInstance // List of connections
} }
type ProxyInstanceRequest struct { type ProxyInstanceRequest struct {
Type string // Will be 'proxyConnectionRequest' always
} }
type BackendStatusResponse struct { type BackendStatusResponse struct {
Type string // Will be 'backendStatusResponse' always
IsRunning bool // True if running, false if not running IsRunning bool // True if running, false if not running
StatusCode int // Either the 'Success' or 'Failure' constant StatusCode int // Either the 'Success' or 'Failure' constant
Message string // String message from the client (ex. failed to dial TCP) Message string // String message from the client (ex. failed to dial TCP)
} }
type BackendStatusRequest struct { type BackendStatusRequest struct {
Type string // Will be 'backendStatusRequest' always
} }
type ProxyConnectionsRequest struct { type ProxyConnectionsRequest struct {
Type string // Will be 'proxyConnectionsRequest' always
} }
// Client's connection to a specific proxy // Client's connection to a specific proxy
@ -72,10 +83,12 @@ type ProxyClientConnection struct {
} }
type ProxyConnectionsResponse struct { type ProxyConnectionsResponse struct {
Type string // Will be 'proxyConnectionsResponse' always
Connections []*ProxyClientConnection // List of connections Connections []*ProxyClientConnection // List of connections
} }
type CheckClientParameters struct { type CheckClientParameters struct {
Type string // Will be 'checkClientParameters' always
SourceIP string SourceIP string
SourcePort uint16 SourcePort uint16
DestPort uint16 DestPort uint16
@ -83,11 +96,13 @@ type CheckClientParameters struct {
} }
type CheckServerParameters struct { type CheckServerParameters struct {
Type string // Will be 'checkServerParameters' always
Arguments []byte Arguments []byte
} }
// Sent as a response to either CheckClientParameters or CheckBackendParameters // Sent as a response to either CheckClientParameters or CheckBackendParameters
type CheckParametersResponse struct { type CheckParametersResponse struct {
Type string // Will be 'checkParametersResponse' always
InResponseTo string // Will be either 'checkClientParameters' or 'checkServerParameters' InResponseTo string // Will be either 'checkClientParameters' or 'checkServerParameters'
IsValid bool // If true, valid, and if false, invalid 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) Message string // String message from the client (ex. failed to unmarshal JSON: x is not defined)

View file

@ -84,19 +84,37 @@ func marshalIndividualProxyStruct(conn *ProxyInstance) ([]byte, error) {
return proxyBlock, nil return proxyBlock, nil
} }
func Marshal(command interface{}) ([]byte, error) { func Marshal(commandType string, command interface{}) ([]byte, error) {
switch command := command.(type) { switch commandType {
case *Start: case "start":
startCommandBytes := make([]byte, 1+2+len(command.Arguments)) startCommand, ok := command.(*Start)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
startCommandBytes := make([]byte, 1+2+len(startCommand.Arguments))
startCommandBytes[0] = StartID startCommandBytes[0] = StartID
binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(command.Arguments))) binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(startCommand.Arguments)))
copy(startCommandBytes[3:], command.Arguments) copy(startCommandBytes[3:], startCommand.Arguments)
return startCommandBytes, nil return startCommandBytes, nil
case *Stop: case "stop":
_, ok := command.(*Stop)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
return []byte{StopID}, nil return []byte{StopID}, nil
case *AddProxy: case "addProxy":
sourceIP := net.ParseIP(command.SourceIP) addConnectionCommand, ok := command.(*AddProxy)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
sourceIP := net.ParseIP(addConnectionCommand.SourceIP)
var ipVer uint8 var ipVer uint8
var ipBytes []byte var ipBytes []byte
@ -116,14 +134,14 @@ func Marshal(command interface{}) ([]byte, error) {
copy(addConnectionBytes[2:2+len(ipBytes)], ipBytes) copy(addConnectionBytes[2:2+len(ipBytes)], ipBytes)
binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], addConnectionCommand.SourcePort)
binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], addConnectionCommand.DestPort)
var protocol uint8 var protocol uint8
if command.Protocol == "tcp" { if addConnectionCommand.Protocol == "tcp" {
protocol = TCP protocol = TCP
} else if command.Protocol == "udp" { } else if addConnectionCommand.Protocol == "udp" {
protocol = UDP protocol = UDP
} else { } else {
return nil, fmt.Errorf("invalid protocol") return nil, fmt.Errorf("invalid protocol")
@ -132,8 +150,14 @@ func Marshal(command interface{}) ([]byte, error) {
addConnectionBytes[6+len(ipBytes)] = protocol addConnectionBytes[6+len(ipBytes)] = protocol
return addConnectionBytes, nil return addConnectionBytes, nil
case *RemoveProxy: case "removeProxy":
sourceIP := net.ParseIP(command.SourceIP) removeConnectionCommand, ok := command.(*RemoveProxy)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
sourceIP := net.ParseIP(removeConnectionCommand.SourceIP)
var ipVer uint8 var ipVer uint8
var ipBytes []byte var ipBytes []byte
@ -151,14 +175,14 @@ func Marshal(command interface{}) ([]byte, error) {
removeConnectionBytes[0] = RemoveProxyID removeConnectionBytes[0] = RemoveProxyID
removeConnectionBytes[1] = ipVer removeConnectionBytes[1] = ipVer
copy(removeConnectionBytes[2:2+len(ipBytes)], ipBytes) copy(removeConnectionBytes[2:2+len(ipBytes)], ipBytes)
binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], removeConnectionCommand.SourcePort)
binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], removeConnectionCommand.DestPort)
var protocol uint8 var protocol uint8
if command.Protocol == "tcp" { if removeConnectionCommand.Protocol == "tcp" {
protocol = TCP protocol = TCP
} else if command.Protocol == "udp" { } else if removeConnectionCommand.Protocol == "udp" {
protocol = UDP protocol = UDP
} else { } else {
return nil, fmt.Errorf("invalid protocol") return nil, fmt.Errorf("invalid protocol")
@ -167,11 +191,17 @@ func Marshal(command interface{}) ([]byte, error) {
removeConnectionBytes[6+len(ipBytes)] = protocol removeConnectionBytes[6+len(ipBytes)] = protocol
return removeConnectionBytes, nil return removeConnectionBytes, nil
case *ProxyConnectionsResponse: case "proxyConnectionsResponse":
connectionsArray := make([][]byte, len(command.Connections)) allConnectionsCommand, ok := command.(*ProxyConnectionsResponse)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
connectionsArray := make([][]byte, len(allConnectionsCommand.Connections))
totalSize := 0 totalSize := 0
for connIndex, conn := range command.Connections { for connIndex, conn := range allConnectionsCommand.Connections {
connectionsArray[connIndex] = marshalIndividualConnectionStruct(conn) connectionsArray[connIndex] = marshalIndividualConnectionStruct(conn)
totalSize += len(connectionsArray[connIndex]) + 1 totalSize += len(connectionsArray[connIndex]) + 1
} }
@ -193,8 +223,14 @@ func Marshal(command interface{}) ([]byte, error) {
connectionCommandArray[totalSize] = '\n' connectionCommandArray[totalSize] = '\n'
return connectionCommandArray, nil return connectionCommandArray, nil
case *CheckClientParameters: case "checkClientParameters":
sourceIP := net.ParseIP(command.SourceIP) checkClientCommand, ok := command.(*CheckClientParameters)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
sourceIP := net.ParseIP(checkClientCommand.SourceIP)
var ipVer uint8 var ipVer uint8
var ipBytes []byte var ipBytes []byte
@ -212,14 +248,14 @@ func Marshal(command interface{}) ([]byte, error) {
checkClientBytes[0] = CheckClientParametersID checkClientBytes[0] = CheckClientParametersID
checkClientBytes[1] = ipVer checkClientBytes[1] = ipVer
copy(checkClientBytes[2:2+len(ipBytes)], ipBytes) copy(checkClientBytes[2:2+len(ipBytes)], ipBytes)
binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], checkClientCommand.SourcePort)
binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], checkClientCommand.DestPort)
var protocol uint8 var protocol uint8
if command.Protocol == "tcp" { if checkClientCommand.Protocol == "tcp" {
protocol = TCP protocol = TCP
} else if command.Protocol == "udp" { } else if checkClientCommand.Protocol == "udp" {
protocol = UDP protocol = UDP
} else { } else {
return nil, fmt.Errorf("invalid protocol") return nil, fmt.Errorf("invalid protocol")
@ -228,19 +264,31 @@ func Marshal(command interface{}) ([]byte, error) {
checkClientBytes[6+len(ipBytes)] = protocol checkClientBytes[6+len(ipBytes)] = protocol
return checkClientBytes, nil return checkClientBytes, nil
case *CheckServerParameters: case "checkServerParameters":
serverCommandBytes := make([]byte, 1+2+len(command.Arguments)) checkServerCommand, ok := command.(*CheckServerParameters)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
serverCommandBytes := make([]byte, 1+2+len(checkServerCommand.Arguments))
serverCommandBytes[0] = CheckServerParametersID serverCommandBytes[0] = CheckServerParametersID
binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(command.Arguments))) binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(checkServerCommand.Arguments)))
copy(serverCommandBytes[3:], command.Arguments) copy(serverCommandBytes[3:], checkServerCommand.Arguments)
return serverCommandBytes, nil return serverCommandBytes, nil
case *CheckParametersResponse: case "checkParametersResponse":
checkParametersCommand, ok := command.(*CheckParametersResponse)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
var checkMethod uint8 var checkMethod uint8
if command.InResponseTo == "checkClientParameters" { if checkParametersCommand.InResponseTo == "checkClientParameters" {
checkMethod = CheckClientParametersID checkMethod = CheckClientParametersID
} else if command.InResponseTo == "checkServerParameters" { } else if checkParametersCommand.InResponseTo == "checkServerParameters" {
checkMethod = CheckServerParametersID checkMethod = CheckServerParametersID
} else { } else {
return nil, fmt.Errorf("invalid mode recieved (must be either checkClientParameters or checkServerParameters)") 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 var isValid uint8
if command.IsValid { if checkParametersCommand.IsValid {
isValid = 1 isValid = 1
} }
checkResponseBytes := make([]byte, 3+2+len(command.Message)) checkResponseBytes := make([]byte, 3+2+len(checkParametersCommand.Message))
checkResponseBytes[0] = CheckParametersResponseID checkResponseBytes[0] = CheckParametersResponseID
checkResponseBytes[1] = checkMethod checkResponseBytes[1] = checkMethod
checkResponseBytes[2] = isValid 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 { if len(checkParametersCommand.Message) != 0 {
copy(checkResponseBytes[5:], []byte(command.Message)) copy(checkResponseBytes[5:], []byte(checkParametersCommand.Message))
} }
return checkResponseBytes, nil return checkResponseBytes, nil
case *BackendStatusResponse: case "backendStatusResponse":
backendStatusResponse, ok := command.(*BackendStatusResponse)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
var isRunning uint8 var isRunning uint8
if command.IsRunning { if backendStatusResponse.IsRunning {
isRunning = 1 isRunning = 1
} else { } else {
isRunning = 0 isRunning = 0
} }
statusResponseBytes := make([]byte, 3+2+len(command.Message)) statusResponseBytes := make([]byte, 3+2+len(backendStatusResponse.Message))
statusResponseBytes[0] = BackendStatusResponseID statusResponseBytes[0] = BackendStatusResponseID
statusResponseBytes[1] = isRunning 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 { if len(backendStatusResponse.Message) != 0 {
copy(statusResponseBytes[5:], []byte(command.Message)) copy(statusResponseBytes[5:], []byte(backendStatusResponse.Message))
} }
return statusResponseBytes, nil return statusResponseBytes, nil
case *BackendStatusRequest: case "backendStatusRequest":
statusRequestBytes := make([]byte, 1) _, ok := command.(*BackendStatusRequest)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
statusRequestBytes := make([]byte, 2)
statusRequestBytes[0] = BackendStatusRequestID statusRequestBytes[0] = BackendStatusRequestID
return statusRequestBytes, nil return statusRequestBytes, nil
case *ProxyStatusRequest: case "proxyStatusRequest":
sourceIP := net.ParseIP(command.SourceIP) proxyStatusRequest, ok := command.(*ProxyStatusRequest)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
sourceIP := net.ParseIP(proxyStatusRequest.SourceIP)
var ipVer uint8 var ipVer uint8
var ipBytes []byte var ipBytes []byte
@ -304,31 +370,37 @@ func Marshal(command interface{}) ([]byte, error) {
ipVer = IPv4 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 proxyStatusRequestBytes[0] = ProxyStatusRequestID
commandBytes[1] = ipVer 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(proxyStatusRequestBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusRequest.SourcePort)
binary.BigEndian.PutUint16(commandBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) binary.BigEndian.PutUint16(proxyStatusRequestBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusRequest.DestPort)
var protocol uint8 var protocol uint8
if command.Protocol == "tcp" { if proxyStatusRequest.Protocol == "tcp" {
protocol = TCP protocol = TCP
} else if command.Protocol == "udp" { } else if proxyStatusRequest.Protocol == "udp" {
protocol = UDP protocol = UDP
} else { } else {
return nil, fmt.Errorf("invalid protocol") return nil, fmt.Errorf("invalid protocol")
} }
commandBytes[6+len(ipBytes)] = protocol proxyStatusRequestBytes[6+len(ipBytes)] = protocol
return commandBytes, nil return proxyStatusRequestBytes, nil
case *ProxyStatusResponse: case "proxyStatusResponse":
sourceIP := net.ParseIP(command.SourceIP) proxyStatusResponse, ok := command.(*ProxyStatusResponse)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
sourceIP := net.ParseIP(proxyStatusResponse.SourceIP)
var ipVer uint8 var ipVer uint8
var ipBytes []byte var ipBytes []byte
@ -341,44 +413,50 @@ func Marshal(command interface{}) ([]byte, error) {
ipVer = IPv4 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 proxyStatusResponseBytes[0] = ProxyStatusResponseID
commandBytes[1] = ipVer 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(proxyStatusResponseBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusResponse.SourcePort)
binary.BigEndian.PutUint16(commandBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) binary.BigEndian.PutUint16(proxyStatusResponseBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusResponse.DestPort)
var protocol uint8 var protocol uint8
if command.Protocol == "tcp" { if proxyStatusResponse.Protocol == "tcp" {
protocol = TCP protocol = TCP
} else if command.Protocol == "udp" { } else if proxyStatusResponse.Protocol == "udp" {
protocol = UDP protocol = UDP
} else { } else {
return nil, fmt.Errorf("invalid protocol") return nil, fmt.Errorf("invalid protocol")
} }
commandBytes[6+len(ipBytes)] = protocol proxyStatusResponseBytes[6+len(ipBytes)] = protocol
var isActive uint8 var isActive uint8
if command.IsActive { if proxyStatusResponse.IsActive {
isActive = 1 isActive = 1
} else { } else {
isActive = 0 isActive = 0
} }
commandBytes[7+len(ipBytes)] = isActive proxyStatusResponseBytes[7+len(ipBytes)] = isActive
return commandBytes, nil return proxyStatusResponseBytes, nil
case *ProxyInstanceResponse: case "proxyInstanceResponse":
proxyArray := make([][]byte, len(command.Proxies)) proxyConectionResponse, ok := command.(*ProxyInstanceResponse)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
proxyArray := make([][]byte, len(proxyConectionResponse.Proxies))
totalSize := 0 totalSize := 0
for proxyIndex, proxy := range command.Proxies { for proxyIndex, proxy := range proxyConectionResponse.Proxies {
var err error var err error
proxyArray[proxyIndex], err = marshalIndividualProxyStruct(proxy) proxyArray[proxyIndex], err = marshalIndividualProxyStruct(proxy)
@ -407,11 +485,23 @@ func Marshal(command interface{}) ([]byte, error) {
connectionCommandArray[totalSize] = '\n' connectionCommandArray[totalSize] = '\n'
return connectionCommandArray, nil return connectionCommandArray, nil
case *ProxyInstanceRequest: case "proxyInstanceRequest":
_, ok := command.(*ProxyInstanceRequest)
if !ok {
return nil, fmt.Errorf("failed to typecast")
}
return []byte{ProxyInstanceRequestID}, nil 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 []byte{ProxyConnectionsRequestID}, nil
} }
return nil, fmt.Errorf("couldn't match command type") return nil, fmt.Errorf("couldn't match command name")
} }

View file

@ -9,12 +9,13 @@ import (
var logLevel = os.Getenv("HERMES_LOG_LEVEL") var logLevel = os.Getenv("HERMES_LOG_LEVEL")
func TestStart(t *testing.T) { func TestStartCommandMarshalSupport(t *testing.T) {
commandInput := &Start{ commandInput := &Start{
Type: "start",
Arguments: []byte("Hello from automated testing"), Arguments: []byte("Hello from automated testing"),
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -25,27 +26,39 @@ func TestStart(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*Start) commandUnmarshalled, ok := commandUnmarshalledRaw.(*Start)
if !ok { if !ok {
t.Fatal("failed typecast") 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) { if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) {
log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments))
} }
} }
func TestStop(t *testing.T) { func TestStopCommandMarshalSupport(t *testing.T) {
commandInput := &Stop{} commandInput := &Stop{
Type: "stop",
}
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -56,28 +69,39 @@ func TestStop(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) 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 { if !ok {
t.Fatal("failed typecast") 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{ commandInput := &AddProxy{
Type: "addProxy",
SourceIP: "192.168.0.139", SourceIP: "192.168.0.139",
SourcePort: 19132, SourcePort: 19132,
DestPort: 19132, DestPort: 19132,
Protocol: "tcp", Protocol: "tcp",
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -88,18 +112,28 @@ func TestAddConnection(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*AddProxy) commandUnmarshalled, ok := commandUnmarshalledRaw.(*AddProxy)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.SourceIP != commandUnmarshalled.SourceIP {
t.Fail() t.Fail()
log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) 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{ commandInput := &RemoveProxy{
Type: "removeProxy",
SourceIP: "192.168.0.139", SourceIP: "192.168.0.139",
SourcePort: 19132, SourcePort: 19132,
DestPort: 19132, DestPort: 19132,
Protocol: "tcp", Protocol: "tcp",
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -140,18 +175,28 @@ func TestRemoveConnection(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.SourceIP != commandUnmarshalled.SourceIP {
t.Fail() t.Fail()
log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) 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{ commandInput := &ProxyConnectionsResponse{
Type: "proxyConnectionsResponse",
Connections: []*ProxyClientConnection{ Connections: []*ProxyClientConnection{
{ {
SourceIP: "127.0.0.1", 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 { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -211,18 +257,28 @@ func TestGetAllConnections(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { for commandIndex, originalConnection := range commandInput.Connections {
remoteConnection := commandUnmarshalled.Connections[commandIndex] 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{ commandInput := &CheckClientParameters{
Type: "checkClientParameters",
SourceIP: "192.168.0.139", SourceIP: "192.168.0.139",
SourcePort: 19132, SourcePort: 19132,
DestPort: 19132, DestPort: 19132,
Protocol: "tcp", Protocol: "tcp",
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -272,18 +329,28 @@ func TestCheckClientParameters(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) 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) commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckClientParameters)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.SourceIP != commandUnmarshalled.SourceIP {
t.Fail() t.Fail()
log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) 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{ commandInput := &CheckServerParameters{
Type: "checkServerParameters",
Arguments: []byte("Hello from automated testing"), Arguments: []byte("Hello from automated testing"),
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -321,31 +389,42 @@ func TestCheckServerParameters(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckServerParameters) commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckServerParameters)
if !ok { if !ok {
t.Fatal("failed typecast") 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) { if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) {
log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(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{ commandInput := &CheckParametersResponse{
Type: "checkParametersResponse",
InResponseTo: "checkClientParameters", InResponseTo: "checkClientParameters",
IsValid: true, IsValid: true,
Message: "Hello from automated testing", Message: "Hello from automated testing",
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -356,18 +435,28 @@ func TestCheckParametersResponse(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) 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) commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckParametersResponse)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.InResponseTo != commandUnmarshalled.InResponseTo {
t.Fail() t.Fail()
log.Printf("InResponseTo's are not equal (orig: %s, unmsh: %s)", commandInput.InResponseTo, commandUnmarshalled.InResponseTo) 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) { func TestBackendStatusRequestMarshalSupport(t *testing.T) {
commandInput := &BackendStatusRequest{} commandInput := &BackendStatusRequest{
commandMarshalled, err := Marshal(commandInput) Type: "backendStatusRequest",
}
commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -397,27 +489,38 @@ func TestBackendStatusRequest(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) 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 { if !ok {
t.Fatal("failed typecast") 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{ commandInput := &BackendStatusResponse{
Type: "backendStatusResponse",
IsRunning: true, IsRunning: true,
StatusCode: StatusFailure, StatusCode: StatusFailure,
Message: "Hello from automated testing", Message: "Hello from automated testing",
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -428,18 +531,28 @@ func TestBackendStatusResponse(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusResponse) commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusResponse)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.IsRunning != commandUnmarshalled.IsRunning {
t.Fail() t.Fail()
log.Printf("IsRunning's are not equal (orig: %t, unmsh: %t)", commandInput.IsRunning, commandUnmarshalled.IsRunning) 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{ commandInput := &ProxyStatusRequest{
Type: "proxyStatusRequest",
SourceIP: "192.168.0.139", SourceIP: "192.168.0.139",
SourcePort: 19132, SourcePort: 19132,
DestPort: 19132, DestPort: 19132,
Protocol: "tcp", Protocol: "tcp",
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -475,18 +589,28 @@ func TestProxyStatusRequest(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.SourceIP != commandUnmarshalled.SourceIP {
t.Fail() t.Fail()
log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) 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{ commandInput := &ProxyStatusResponse{
Type: "proxyStatusResponse",
SourceIP: "192.168.0.139", SourceIP: "192.168.0.139",
SourcePort: 19132, SourcePort: 19132,
DestPort: 19132, DestPort: 19132,
@ -517,7 +642,7 @@ func TestProxyStatusResponse(t *testing.T) {
IsActive: true, IsActive: true,
} }
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -528,18 +653,28 @@ func TestProxyStatusResponse(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { if commandInput.SourceIP != commandUnmarshalled.SourceIP {
t.Fail() t.Fail()
log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) 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) { func TestProxyConnectionRequestMarshalSupport(t *testing.T) {
commandInput := &ProxyInstanceRequest{} commandInput := &ProxyInstanceRequest{
Type: "proxyInstanceRequest",
}
commandMarshalled, err := Marshal(commandInput) commandMarshalled, err := Marshal(commandInput.Type, commandInput)
if logLevel == "debug" { if logLevel == "debug" {
log.Printf("Generated array contents: %v", commandMarshalled) log.Printf("Generated array contents: %v", commandMarshalled)
@ -580,21 +717,32 @@ func TestProxyConnectionRequest(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) 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 { if !ok {
t.Fatal("failed typecast") 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{ commandInput := &ProxyInstanceResponse{
Type: "proxyInstanceResponse",
Proxies: []*ProxyInstance{ Proxies: []*ProxyInstance{
{ {
SourceIP: "192.168.0.168", 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 { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
@ -628,18 +776,28 @@ func TestProxyConnectionResponse(t *testing.T) {
} }
buf := bytes.NewBuffer(commandMarshalled) buf := bytes.NewBuffer(commandMarshalled)
commandUnmarshalledRaw, err := Unmarshal(buf) commandType, commandUnmarshalledRaw, err := Unmarshal(buf)
if err != nil { if err != nil {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
if commandType != commandInput.Type {
t.Fail()
log.Print("command type does not match up!")
}
commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse)
if !ok { if !ok {
t.Fatal("failed typecast") 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 { for proxyIndex, originalProxy := range commandInput.Proxies {
remoteProxy := commandUnmarshalled.Proxies[proxyIndex] remoteProxy := commandUnmarshalled.Proxies[proxyIndex]

View file

@ -142,11 +142,11 @@ func unmarshalIndividualProxyStruct(conn io.Reader) (*ProxyInstance, error) {
}, nil }, nil
} }
func Unmarshal(conn io.Reader) (interface{}, error) { func Unmarshal(conn io.Reader) (string, interface{}, error) {
commandType := make([]byte, 1) commandType := make([]byte, 1)
if _, err := conn.Read(commandType); err != nil { 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] { switch commandType[0] {
@ -154,25 +154,28 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
argumentsLength := make([]byte, 2) argumentsLength := make([]byte, 2)
if _, err := conn.Read(argumentsLength); err != nil { 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)) arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength))
if _, err := conn.Read(arguments); err != nil { 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, Arguments: arguments,
}, nil }, nil
case StopID: case StopID:
return &Stop{}, nil return "stop", &Stop{
Type: "stop",
}, nil
case AddProxyID: case AddProxyID:
ipVersion := make([]byte, 1) ipVersion := make([]byte, 1)
if _, err := conn.Read(ipVersion); err != nil { 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 var ipSize uint8
@ -182,44 +185,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} else if ipVersion[0] == 6 { } else if ipVersion[0] == 6 {
ipSize = IPv6Size ipSize = IPv6Size
} else { } else {
return nil, fmt.Errorf("invalid IP version recieved") return "", nil, fmt.Errorf("invalid IP version recieved")
} }
ip := make(net.IP, ipSize) ip := make(net.IP, ipSize)
if _, err := conn.Read(ip); err != nil { 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) sourcePort := make([]byte, 2)
if _, err := conn.Read(sourcePort); err != nil { 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) destPort := make([]byte, 2)
if _, err := conn.Read(destPort); err != nil { 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) protocolBytes := make([]byte, 1)
if _, err := conn.Read(protocolBytes); err != nil { 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 var protocol string
if protocolBytes[0] == TCP { if protocolBytes[0] == TCP {
protocol = "tcp" protocol = "tcp"
} else if protocolBytes[0] == UDP { } else if protocolBytes[1] == UDP {
protocol = "udp" protocol = "udp"
} else { } else {
return nil, fmt.Errorf("invalid protocol") return "", nil, fmt.Errorf("invalid protocol")
} }
return &AddProxy{ return "addProxy", &AddProxy{
Type: "addProxy",
SourceIP: ip.String(), SourceIP: ip.String(),
SourcePort: binary.BigEndian.Uint16(sourcePort), SourcePort: binary.BigEndian.Uint16(sourcePort),
DestPort: binary.BigEndian.Uint16(destPort), DestPort: binary.BigEndian.Uint16(destPort),
@ -229,7 +233,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
ipVersion := make([]byte, 1) ipVersion := make([]byte, 1)
if _, err := conn.Read(ipVersion); err != nil { 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 var ipSize uint8
@ -239,44 +243,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} else if ipVersion[0] == 6 { } else if ipVersion[0] == 6 {
ipSize = IPv6Size ipSize = IPv6Size
} else { } else {
return nil, fmt.Errorf("invalid IP version recieved") return "", nil, fmt.Errorf("invalid IP version recieved")
} }
ip := make(net.IP, ipSize) ip := make(net.IP, ipSize)
if _, err := conn.Read(ip); err != nil { 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) sourcePort := make([]byte, 2)
if _, err := conn.Read(sourcePort); err != nil { 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) destPort := make([]byte, 2)
if _, err := conn.Read(destPort); err != nil { 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) protocolBytes := make([]byte, 1)
if _, err := conn.Read(protocolBytes); err != nil { 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 var protocol string
if protocolBytes[0] == TCP { if protocolBytes[0] == TCP {
protocol = "tcp" protocol = "tcp"
} else if protocolBytes[0] == UDP { } else if protocolBytes[1] == UDP {
protocol = "udp" protocol = "udp"
} else { } else {
return nil, fmt.Errorf("invalid protocol") return "", nil, fmt.Errorf("invalid protocol")
} }
return &RemoveProxy{ return "removeProxy", &RemoveProxy{
Type: "removeProxy",
SourceIP: ip.String(), SourceIP: ip.String(),
SourcePort: binary.BigEndian.Uint16(sourcePort), SourcePort: binary.BigEndian.Uint16(sourcePort),
DestPort: binary.BigEndian.Uint16(destPort), DestPort: binary.BigEndian.Uint16(destPort),
@ -296,13 +301,13 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
break break
} }
return nil, err return "", nil, err
} }
connections = append(connections, connection) connections = append(connections, connection)
if _, err := conn.Read(delimiter); err != nil { 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' { if delimiter[0] == '\r' {
@ -316,14 +321,15 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} }
} }
return &ProxyConnectionsResponse{ return "proxyConnectionsResponse", &ProxyConnectionsResponse{
Type: "proxyConnectionsResponse",
Connections: connections, Connections: connections,
}, errorReturn }, errorReturn
case CheckClientParametersID: case CheckClientParametersID:
ipVersion := make([]byte, 1) ipVersion := make([]byte, 1)
if _, err := conn.Read(ipVersion); err != nil { 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 var ipSize uint8
@ -333,44 +339,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} else if ipVersion[0] == 6 { } else if ipVersion[0] == 6 {
ipSize = IPv6Size ipSize = IPv6Size
} else { } else {
return nil, fmt.Errorf("invalid IP version recieved") return "", nil, fmt.Errorf("invalid IP version recieved")
} }
ip := make(net.IP, ipSize) ip := make(net.IP, ipSize)
if _, err := conn.Read(ip); err != nil { 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) sourcePort := make([]byte, 2)
if _, err := conn.Read(sourcePort); err != nil { 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) destPort := make([]byte, 2)
if _, err := conn.Read(destPort); err != nil { 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) protocolBytes := make([]byte, 1)
if _, err := conn.Read(protocolBytes); err != nil { 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 var protocol string
if protocolBytes[0] == TCP { if protocolBytes[0] == TCP {
protocol = "tcp" protocol = "tcp"
} else if protocolBytes[0] == UDP { } else if protocolBytes[1] == UDP {
protocol = "udp" protocol = "udp"
} else { } else {
return nil, fmt.Errorf("invalid protocol") return "", nil, fmt.Errorf("invalid protocol")
} }
return &CheckClientParameters{ return "checkClientParameters", &CheckClientParameters{
Type: "checkClientParameters",
SourceIP: ip.String(), SourceIP: ip.String(),
SourcePort: binary.BigEndian.Uint16(sourcePort), SourcePort: binary.BigEndian.Uint16(sourcePort),
DestPort: binary.BigEndian.Uint16(destPort), DestPort: binary.BigEndian.Uint16(destPort),
@ -380,23 +387,24 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
argumentsLength := make([]byte, 2) argumentsLength := make([]byte, 2)
if _, err := conn.Read(argumentsLength); err != nil { 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)) arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength))
if _, err := conn.Read(arguments); err != nil { 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, Arguments: arguments,
}, nil }, nil
case CheckParametersResponseID: case CheckParametersResponseID:
checkMethodByte := make([]byte, 1) checkMethodByte := make([]byte, 1)
if _, err := conn.Read(checkMethodByte); err != nil { 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 var checkMethod string
@ -406,19 +414,19 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} else if checkMethodByte[0] == CheckServerParametersID { } else if checkMethodByte[0] == CheckServerParametersID {
checkMethod = "checkServerParameters" checkMethod = "checkServerParameters"
} else { } else {
return nil, fmt.Errorf("invalid check method recieved") return "", nil, fmt.Errorf("invalid check method recieved")
} }
isValid := make([]byte, 1) isValid := make([]byte, 1)
if _, err := conn.Read(isValid); err != nil { 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) messageLengthBytes := make([]byte, 2)
if _, err := conn.Read(messageLengthBytes); err != nil { 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) messageLength := binary.BigEndian.Uint16(messageLengthBytes)
@ -428,13 +436,14 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
messageBytes := make([]byte, messageLength) messageBytes := make([]byte, messageLength)
if _, err := conn.Read(messageBytes); err != nil { 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) message = string(messageBytes)
} }
return &CheckParametersResponse{ return "checkParametersResponse", &CheckParametersResponse{
Type: "checkParametersResponse",
InResponseTo: checkMethod, InResponseTo: checkMethod,
IsValid: isValid[0] == 1, IsValid: isValid[0] == 1,
Message: message, Message: message,
@ -443,19 +452,19 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
isRunning := make([]byte, 1) isRunning := make([]byte, 1)
if _, err := conn.Read(isRunning); err != nil { 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) statusCode := make([]byte, 1)
if _, err := conn.Read(statusCode); err != nil { 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) messageLengthBytes := make([]byte, 2)
if _, err := conn.Read(messageLengthBytes); err != nil { 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) messageLength := binary.BigEndian.Uint16(messageLengthBytes)
@ -465,24 +474,27 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
messageBytes := make([]byte, messageLength) messageBytes := make([]byte, messageLength)
if _, err := conn.Read(messageBytes); err != nil { 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) message = string(messageBytes)
} }
return &BackendStatusResponse{ return "backendStatusResponse", &BackendStatusResponse{
Type: "backendStatusResponse",
IsRunning: isRunning[0] == 1, IsRunning: isRunning[0] == 1,
StatusCode: int(statusCode[0]), StatusCode: int(statusCode[0]),
Message: message, Message: message,
}, nil }, nil
case BackendStatusRequestID: case BackendStatusRequestID:
return &BackendStatusRequest{}, nil return "backendStatusRequest", &BackendStatusRequest{
Type: "backendStatusRequest",
}, nil
case ProxyStatusRequestID: case ProxyStatusRequestID:
ipVersion := make([]byte, 1) ipVersion := make([]byte, 1)
if _, err := conn.Read(ipVersion); err != nil { 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 var ipSize uint8
@ -492,44 +504,45 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} else if ipVersion[0] == 6 { } else if ipVersion[0] == 6 {
ipSize = IPv6Size ipSize = IPv6Size
} else { } else {
return nil, fmt.Errorf("invalid IP version recieved") return "", nil, fmt.Errorf("invalid IP version recieved")
} }
ip := make(net.IP, ipSize) ip := make(net.IP, ipSize)
if _, err := conn.Read(ip); err != nil { 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) sourcePort := make([]byte, 2)
if _, err := conn.Read(sourcePort); err != nil { 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) destPort := make([]byte, 2)
if _, err := conn.Read(destPort); err != nil { 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) protocolBytes := make([]byte, 1)
if _, err := conn.Read(protocolBytes); err != nil { 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 var protocol string
if protocolBytes[0] == TCP { if protocolBytes[0] == TCP {
protocol = "tcp" protocol = "tcp"
} else if protocolBytes[0] == UDP { } else if protocolBytes[1] == UDP {
protocol = "udp" protocol = "udp"
} else { } else {
return nil, fmt.Errorf("invalid protocol") return "", nil, fmt.Errorf("invalid protocol")
} }
return &ProxyStatusRequest{ return "proxyStatusRequest", &ProxyStatusRequest{
Type: "proxyStatusRequest",
SourceIP: ip.String(), SourceIP: ip.String(),
SourcePort: binary.BigEndian.Uint16(sourcePort), SourcePort: binary.BigEndian.Uint16(sourcePort),
DestPort: binary.BigEndian.Uint16(destPort), DestPort: binary.BigEndian.Uint16(destPort),
@ -539,7 +552,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
ipVersion := make([]byte, 1) ipVersion := make([]byte, 1)
if _, err := conn.Read(ipVersion); err != nil { 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 var ipSize uint8
@ -549,50 +562,51 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} else if ipVersion[0] == 6 { } else if ipVersion[0] == 6 {
ipSize = IPv6Size ipSize = IPv6Size
} else { } else {
return nil, fmt.Errorf("invalid IP version recieved") return "", nil, fmt.Errorf("invalid IP version recieved")
} }
ip := make(net.IP, ipSize) ip := make(net.IP, ipSize)
if _, err := conn.Read(ip); err != nil { 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) sourcePort := make([]byte, 2)
if _, err := conn.Read(sourcePort); err != nil { 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) destPort := make([]byte, 2)
if _, err := conn.Read(destPort); err != nil { 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) protocolBytes := make([]byte, 1)
if _, err := conn.Read(protocolBytes); err != nil { 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 var protocol string
if protocolBytes[0] == TCP { if protocolBytes[0] == TCP {
protocol = "tcp" protocol = "tcp"
} else if protocolBytes[0] == UDP { } else if protocolBytes[1] == UDP {
protocol = "udp" protocol = "udp"
} else { } else {
return nil, fmt.Errorf("invalid protocol") return "", nil, fmt.Errorf("invalid protocol")
} }
isActive := make([]byte, 1) isActive := make([]byte, 1)
if _, err := conn.Read(isActive); err != nil { 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(), SourceIP: ip.String(),
SourcePort: binary.BigEndian.Uint16(sourcePort), SourcePort: binary.BigEndian.Uint16(sourcePort),
DestPort: binary.BigEndian.Uint16(destPort), DestPort: binary.BigEndian.Uint16(destPort),
@ -600,7 +614,9 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
IsActive: isActive[0] == 1, IsActive: isActive[0] == 1,
}, nil }, nil
case ProxyInstanceRequestID: case ProxyInstanceRequestID:
return &ProxyInstanceRequest{}, nil return "proxyInstanceRequest", &ProxyInstanceRequest{
Type: "proxyInstanceRequest",
}, nil
case ProxyInstanceResponseID: case ProxyInstanceResponseID:
proxies := []*ProxyInstance{} proxies := []*ProxyInstance{}
delimiter := make([]byte, 1) delimiter := make([]byte, 1)
@ -615,13 +631,13 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
break break
} }
return nil, err return "", nil, err
} }
proxies = append(proxies, proxy) proxies = append(proxies, proxy)
if _, err := conn.Read(delimiter); err != nil { 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' { if delimiter[0] == '\r' {
@ -635,12 +651,15 @@ func Unmarshal(conn io.Reader) (interface{}, error) {
} }
} }
return &ProxyInstanceResponse{ return "proxyInstanceResponse", &ProxyInstanceResponse{
Type: "proxyInstanceResponse",
Proxies: proxies, Proxies: proxies,
}, errorReturn }, errorReturn
case ProxyConnectionsRequestID: 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")
} }

View file

@ -3,8 +3,8 @@ package main
import ( import (
"os" "os"
"git.terah.dev/imterah/hermes/backend/backendutil" "git.terah.dev/imterah/hermes/backendutil"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
) )
@ -19,10 +19,6 @@ func (backend *DummyBackend) StopBackend() (bool, error) {
return true, nil return true, nil
} }
func (backend *DummyBackend) GetBackendStatus() (bool, error) {
return true, nil
}
func (backend *DummyBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { func (backend *DummyBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) {
return true, nil return true, nil
} }

View file

@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"git.terah.dev/imterah/hermes/backend/backendlauncher" "git.terah.dev/imterah/hermes/backendlauncher"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -21,8 +21,11 @@ type ProxyInstance struct {
Protocol string `json:"protocol"` 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) { func (writer WriteLogger) Write(p []byte) (n int, err error) {
logSplit := strings.Split(string(p), "\n") logSplit := strings.Split(string(p), "\n")
@ -31,8 +34,12 @@ func (writer WriteLogger) Write(p []byte) (n int, err error) {
continue continue
} }
if writer.UseError {
log.Errorf("application: %s", line)
} else {
log.Infof("application: %s", line) log.Infof("application: %s", line)
} }
}
return len(p), err return len(p), err
} }
@ -112,10 +119,11 @@ func entrypoint(cCtx *cli.Context) error {
defer sock.Close() defer sock.Close()
startCommand := &commonbackend.Start{ startCommand := &commonbackend.Start{
Type: "start",
Arguments: backendParameters, Arguments: backendParameters,
} }
startMarshalledCommand, err := commonbackend.Marshal(startCommand) startMarshalledCommand, err := commonbackend.Marshal("start", startCommand)
if err != nil { if err != nil {
log.Errorf("failed to generate start command: %s", err.Error()) log.Errorf("failed to generate start command: %s", err.Error())
@ -127,13 +135,18 @@ func entrypoint(cCtx *cli.Context) error {
continue continue
} }
commandRaw, err := commonbackend.Unmarshal(sock) commandType, commandRaw, err := commonbackend.Unmarshal(sock)
if err != nil { if err != nil {
log.Errorf("failed to read from/unmarshal from socket: %s", err.Error()) log.Errorf("failed to read from/unmarshal from socket: %s", err.Error())
continue continue
} }
if commandType != "backendStatusResponse" {
log.Errorf("recieved commandType '%s', expecting 'backendStatusResponse'", commandType)
continue
}
command, ok := commandRaw.(*commonbackend.BackendStatusResponse) command, ok := commandRaw.(*commonbackend.BackendStatusResponse)
if !ok { 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) log.Infof("initializing proxy %s:%d -> remote:%d", proxy.SourceIP, proxy.SourcePort, proxy.DestPort)
proxyAddCommand := &commonbackend.AddProxy{ proxyAddCommand := &commonbackend.AddProxy{
Type: "addProxy",
SourceIP: proxy.SourceIP, SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort, SourcePort: proxy.SourcePort,
DestPort: proxy.DestPort, DestPort: proxy.DestPort,
Protocol: proxy.Protocol, Protocol: proxy.Protocol,
} }
marshalledProxyCommand, err := commonbackend.Marshal(proxyAddCommand) marshalledProxyCommand, err := commonbackend.Marshal("addProxy", proxyAddCommand)
if err != nil { if err != nil {
log.Errorf("failed to generate start command: %s", err.Error()) log.Errorf("failed to generate start command: %s", err.Error())
@ -182,7 +196,7 @@ func entrypoint(cCtx *cli.Context) error {
continue continue
} }
commandRaw, err := commonbackend.Unmarshal(sock) commandType, commandRaw, err := commonbackend.Unmarshal(sock)
if err != nil { if err != nil {
log.Errorf("failed to read from/unmarshal from socket: %s", err.Error()) log.Errorf("failed to read from/unmarshal from socket: %s", err.Error())
@ -190,6 +204,12 @@ func entrypoint(cCtx *cli.Context) error {
continue continue
} }
if commandType != "proxyStatusResponse" {
log.Errorf("recieved commandType '%s', expecting 'proxyStatusResponse'", commandType)
hasAnyFailed = true
continue
}
command, ok := commandRaw.(*commonbackend.ProxyStatusResponse) command, ok := commandRaw.(*commonbackend.ProxyStatusResponse)
if !ok { if !ok {
@ -222,8 +242,13 @@ func entrypoint(cCtx *cli.Context) error {
log.Debug("entering execution loop (in main goroutine)...") log.Debug("entering execution loop (in main goroutine)...")
stdout := WriteLogger{} stdout := WriteLogger{
stderr := WriteLogger{} UseError: false,
}
stderr := WriteLogger{
UseError: true,
}
for { for {
log.Info("starting process...") log.Info("starting process...")

View file

@ -2,36 +2,24 @@ module git.terah.dev/imterah/hermes
go 1.23.3 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 ( require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bytedance/sonic v1.12.6 // indirect github.com/bytedance/sonic v1.12.6 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/charmbracelet/lipgloss v0.10.0 // 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/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/gin-contrib/sse v0.1.0 // 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-logfmt/logfmt v0.6.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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/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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.2 // 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/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // 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/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/muesli/termenv v0.15.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/rivo/uniseg v0.4.7 // 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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // 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 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/arch v0.12.0 // 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/net v0.33.0 // indirect
golang.org/x/sync v0.10.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 golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.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
) )

View file

@ -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/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 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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.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/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 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= 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/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 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 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/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 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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/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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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= 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/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 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= 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.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.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.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/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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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/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 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 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 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 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/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 h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 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/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 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View file

@ -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
)

View file

@ -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")
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -1,8 +0,0 @@
package main
import (
"embed"
)
//go:embed remote-bin
var binFiles embed.FS

View file

@ -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
}

View file

@ -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())
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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())
}
}

View file

@ -2,51 +2,20 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net" "net"
"os" "os"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"git.terah.dev/imterah/hermes/backend/backendutil" "git.terah.dev/imterah/hermes/backendutil"
"git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/commonbackend"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"golang.org/x/crypto/ssh" "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 { type SSHListener struct {
SourceIP string SourceIP string
SourcePort uint16 SourcePort uint16
@ -55,46 +24,31 @@ type SSHListener struct {
Listeners []net.Listener 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 { type SSHBackend struct {
config *SSHBackendData config *SSHBackendData
conn *ssh.Client conn *ssh.Client
clients []*commonbackend.ProxyClientConnection clients []*commonbackend.ProxyClientConnection
proxies []*SSHListener proxies []*SSHListener
arrayPropMutex sync.Mutex 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) { func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) {
log.Info("SSHBackend is initializing...") 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 var backendData SSHBackendData
if err := json.Unmarshal(bytes, &backendData); err != nil { if err := json.Unmarshal(bytes, &backendData); err != nil {
return false, err return false, err
} }
if err := validatorInstance.Struct(&backendData); err != nil { if err := validator.New().Struct(&backendData); err != nil {
return false, err return false, err
} }
@ -120,71 +74,15 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) {
}, },
} }
addr := fmt.Sprintf("%s:%d", backendData.IP, backendData.Port) conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config)
timeout := time.Duration(10 * time.Second)
rawTCPConn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil { if err != nil {
return false, err return false, err
} }
connWithTimeout := &ConnWithTimeout{ backend.conn = conn
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()
log.Info("SSHBackend has initialized successfully.") log.Info("SSHBackend has initialized successfully.")
return true, nil return true, nil
} }
@ -198,10 +96,6 @@ func (backend *SSHBackend) StopBackend() (bool, error) {
return true, nil return true, nil
} }
func (backend *SSHBackend) GetBackendStatus() (bool, error) {
return backend.conn != nil, nil
}
func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) {
listenerObject := &SSHListener{ listenerObject := &SSHListener{
SourceIP: command.SourceIP, SourceIP: command.SourceIP,
@ -240,11 +134,6 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er
if err != nil { if err != nil {
log.Warnf("failed to accept listener connection: %s", err.Error()) log.Warnf("failed to accept listener connection: %s", err.Error())
if err.Error() == "EOF" {
return
}
continue continue
} }
@ -301,7 +190,8 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er
// Splice out the clientInstance by clientIndex // Splice out the clientInstance by clientIndex
// TODO: change approach. It works but it's a bit wonky imho // 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 return
} }
} }
@ -317,20 +207,14 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er
for { for {
len, err := forwardedConn.Read(forwardedBuffer) len, err := forwardedConn.Read(forwardedBuffer)
if err != nil { 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 return
} }
if _, err = sourceConn.Write(forwardedBuffer[:len]); err != nil { _, err = sourceConn.Write(forwardedBuffer[:len])
if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { if err != nil {
log.Errorf("failed to write to source connection: %s", err.Error()) log.Errorf("failed to write to source connection: %s", err.Error())
}
return return
} }
} }
@ -341,20 +225,14 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er
for { for {
len, err := sourceConn.Read(sourceBuffer) len, err := sourceConn.Read(sourceBuffer)
if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") {
if err != nil {
if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) {
log.Errorf("failed to read from source connection: %s", err.Error()) log.Errorf("failed to read from source connection: %s", err.Error())
}
return return
} }
if _, err = forwardedConn.Write(sourceBuffer[:len]); err != nil { _, err = forwardedConn.Write(sourceBuffer[:len])
if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { 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()) log.Errorf("failed to write to forwarded connection: %s", err.Error())
}
return return
} }
} }
@ -375,7 +253,10 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool,
backend.arrayPropMutex.Lock() backend.arrayPropMutex.Lock()
for proxyIndex, proxy := range backend.proxies { 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 { 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 { for _, listener := range proxy.Listeners {
err := listener.Close() err := listener.Close()
@ -385,8 +266,10 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool,
} }
// Splice out the proxy instance by proxyIndex // Splice out the proxy instance by proxyIndex
// TODO: change approach. It works but it's a bit wonky imho // 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 return true, nil
} }
} }
@ -417,10 +300,6 @@ func (backend *SSHBackend) CheckParametersForConnections(clientParameters *commo
func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse {
var backendData SSHBackendData var backendData SSHBackendData
if validatorInstance == nil {
validatorInstance = validator.New()
}
if err := json.Unmarshal(arguments, &backendData); err != nil { if err := json.Unmarshal(arguments, &backendData); err != nil {
return &commonbackend.CheckParametersResponse{ return &commonbackend.CheckParametersResponse{
IsValid: false, 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{ return &commonbackend.CheckParametersResponse{
IsValid: false, IsValid: false,
Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()), 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() { func main() {
logLevel := os.Getenv("HERMES_LOG_LEVEL") logLevel := os.Getenv("HERMES_LOG_LEVEL")

View file

@ -6,12 +6,27 @@ services:
environment: environment:
DATABASE_URL: postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB}?schema=nextnet 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_POSTGRES_DSN: postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB}
HERMES_JWT_SECRET: ${JWT_SECRET}
HERMES_DATABASE_BACKEND: postgresql HERMES_DATABASE_BACKEND: postgresql
depends_on: depends_on:
- db - db
ports: ports:
- 3000:3000 - 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: db:
image: postgres:17.2 image: postgres:17.2
container_name: nextnet-postgres container_name: nextnet-postgres
@ -22,6 +37,7 @@ services:
POSTGRES_USER: ${POSTGRES_USERNAME} POSTGRES_USER: ${POSTGRES_USERNAME}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
volumes: volumes:
postgres_data: postgres_data:
ssh_key_data: ssh_key_data:

View file

@ -28,7 +28,7 @@ Below are new environment variables that may need to be set up:
6. Go get the container logs, and make sure no errors get output to the console. 6. Go get the container logs, and make sure no errors get output to the console.
7. Copy the backup as instructed in the log file. 7. Copy the backup as instructed in the log file.
8. DO NOT RESTART THE CONTAINER IF SUCCESSFUL. YOU WILL LOSE ALL YOUR DATA. If the migration fails, follow the steps mentioned in the logs. You do not need to copy the DB backup if it failed to connect or read the database. 8. DO NOT RESTART THE CONTAINER IF SUCCESSFUL. YOU WILL LOSE ALL YOUR DATA. If the migration fails, follow the steps mentioned in the logs. You do not need to copy the DB backup if it failed to connect or read the database.
9. If successful, remove the environment variables `HERMES_MIGRATE_POSTGRES_DATABASE` and `DATABASE_URL`. 9. If successful, remove the environment variable `HERMES_MIGRATE_POSTGRES_DATABASE`.
10. Switch the API docker image from `ghcr.io/imterah/hermes-backend-migration:latest` to `ghcr.io/imterah/hermes:latest`. 10. Switch the API docker image from `ghcr.io/imterah/hermes-backend-migration:latest` to `ghcr.io/imterah/hermes:latest`.
11. Start the backend. 11. Start the backend.
## Failed Migration / Manual Restoration Steps ## Failed Migration / Manual Restoration Steps

View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -1 +0,0 @@
HERMES_LOG_LEVEL=debug

View file

@ -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)
}
}

35
init.sh
View file

@ -1,4 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
owned_docker=1
# Test if Postgres is up
lsof -i:5432 2> /dev/null > /dev/null
if [ $? -ne 0 ]; then
owned_docker=0
docker compose -f dev-docker-compose.yml up -d
fi
if [ ! -f "backend/.env" ]; then if [ ! -f "backend/.env" ]; then
cp backend/dev.env backend/.env cp backend/dev.env backend/.env
fi fi
@ -7,11 +17,30 @@ if [ ! -d "backend/.tmp" ]; then
mkdir backend/.tmp mkdir backend/.tmp
fi fi
if [ ! -f "frontend/.env" ]; then if [ ! -f "backend-legacy/.env" ]; then
cp frontend/dev.env frontend/.env cp backend-legacy/dev.env backend-legacy/.env
fi
if [ ! -d "backend-legacy/node_modules" ]; then
pushd backend-legacy > /dev/null
npm install --save-dev
npx prisma migrate dev
popd > /dev/null
fi fi
set -a set -a
source backend-legacy/.env
source backend/.env source backend/.env
source frontend/.env
set +a set +a
on_exit() {
cd $(git rev-parse --show-toplevel)
if [ $owned_docker -ne 0 ]; then
return
fi
docker compose -f dev-docker-compose.yml down
}
trap "on_exit" exit

46
migration-entrypoint.sh Executable file
View file

@ -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

View file

@ -1,4 +1,5 @@
# These are default values, please change these!
POSTGRES_USERNAME=hermes POSTGRES_USERNAME=hermes
POSTGRES_PASSWORD=hermes POSTGRES_PASSWORD=hermes
POSTGRES_DB=hermes POSTGRES_DB=hermes
JWT_SECRET=hermes

View file

@ -12,8 +12,8 @@ post {
body:json { body:json {
{ {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzYyMzI2NjEsIm5iZiI6MTczNjE0NjI2MSwiaWF0IjoxNzM2MTQ2MjYxfQ.juoZ74xs-FBnbbT9Zlei1LmcNx7kTEfzymHlVbeMmtQ", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzUwNzY0MTEsIm5iZiI6MTczNDk5MDAxMSwiaWF0IjoxNzM0OTkwMDExfQ.N9TLraX4peHt7FKv8tPcHuEzL0K7T2IBEw3piQS_4OY",
"name": "SSH", "name": "SSH",
"id": 1 "id": 2
} }
} }

View file

@ -3,13 +3,18 @@
}: pkgs.mkShell { }: pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
# api/ # api/
nodejs
openssl
lsof
go go
gopls gopls
]; ];
shellHook = '' shellHook = ''
if [ -f init.sh ]; then 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 source init.sh
fi
''; '';
} }

133
sshfrontend/.gitignore vendored Normal file
View file

@ -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.*

14
sshfrontend/Dockerfile Normal file
View file

@ -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

28
sshfrontend/LICENSE Normal file
View file

@ -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.

Some files were not shown because too many files have changed in this diff Show more