From 8be7f90c34f1d2a3163a78bf2dc23bb51d176772 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Freitas?= <1160907@isep.ipp.pt>
Date: Thu, 5 Dec 2024 15:45:55 +0000
Subject: [PATCH] refactor: bootstrap configuration values if .env file does
not exist (#2)
* refactor: bootstrap node configuration values if .env cannot be lookup
* refactor: bootstrap master configuration values if .env cannot be lookup
* test: create pub/pem key files before tests run
* fix: use Listen instead of Dial to connect sub server
* chore: add initialization scripts for windows environments
* refactor: remove init program
* fix: remove init args from homepage code snippets
* fix: dont return if .env fails before checking config dir
* chore: add debug print informing that .env file couldn't be lookup
* docs: reference bolt db env key as required
---
.env.tpl | 2 +-
.github/get-master | 18 --
.github/get-master.bat | 100 +++++++++
.github/get-node | 18 --
.github/get-node.bat | 110 +++++-----
.github/workflows/release.yml | 1 -
README.md | 4 +-
TODO.md | 5 +-
cmd/init/init.go | 230 --------------------
cmd/master/master.go | 51 +++--
cmd/node/node.go | 34 ++-
docs/env.md | 2 +-
internal/bootstrap/boostrap.go | 89 ++++++++
internal/bootstrap/helper.go | 57 +++++
internal/data/db/db-bolt/connection.go | 19 --
internal/data/db/db-bolt/connection_test.go | 14 --
internal/env/env.go | 122 ++++++++++-
internal/http/connection.go | 29 +--
internal/http/handler+dashboard.go | 5 +-
internal/http/handler+util.go | 6 +
internal/http/virtual-host.go | 11 +-
internal/mq/authentication.go | 23 --
internal/mq/connection.go | 35 +--
internal/mq/crypto.go | 68 ++----
internal/mq/crypto_int_test.go | 101 +++++++++
internal/mq/crypto_test.go | 28 ---
internal/mq/pub.go | 16 +-
public/index.gohtml | 9 +-
28 files changed, 645 insertions(+), 562 deletions(-)
create mode 100644 .github/get-master.bat
delete mode 100644 cmd/init/init.go
create mode 100644 internal/bootstrap/boostrap.go
create mode 100644 internal/bootstrap/helper.go
delete mode 100644 internal/data/db/db-bolt/connection.go
delete mode 100644 internal/data/db/db-bolt/connection_test.go
create mode 100644 internal/mq/crypto_int_test.go
diff --git a/.env.tpl b/.env.tpl
index be72d47..171d306 100644
--- a/.env.tpl
+++ b/.env.tpl
@@ -13,7 +13,7 @@ server_virtual_host=/
mq_sub_host=0.0.0.0
mq_sub_port=36113
-## If these variables aren't filled, pem/pub files are lookup on configuration directory
+## Supply public/private keys for encrypting message queue
mq_transport_pem_key=
mq_transport_pub_key=
diff --git a/.github/get-master b/.github/get-master
index e104267..5b23849 100755
--- a/.github/get-master
+++ b/.github/get-master
@@ -1,8 +1,5 @@
#!/usr/bin/env bash
-# Save program arguments to later pass on init binary.
-init_args="$@"
-
# Common urls.
new_issue_url="https://github.com/guackamolly/zero-monitor/issues/new"
latest_release_url="https://api.github.com/repos/guackamolly/zero-monitor/releases/latest"
@@ -11,7 +8,6 @@ jq_release_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1"
# Installation directory and program paths.
install_dir="$HOME/.config/zero-monitor"
bin_path="$install_dir/master"
-init_bin_path="$install_dir/init"
jq_bin_path="$install_dir/jq"
if [ ! -d "$install_dir" ]; then
@@ -28,10 +24,6 @@ jq() {
}
exec_bin() {
- if [ ! -z "$init_args" ]; then
- "$init_bin_path" $init_args
- fi
-
exec "$bin_path"
}
@@ -79,20 +71,10 @@ if [ -z "$latest_release_version" ]; then
fatal "Failed to extract release version, please raise an issue to alert maintainers about this bug.\n$new_issue_url"
fi
-# If local init binary does not exist and no arguments have been passed, then bootstrap master.
-if [[ ! -f "$init_bin_path" && -z "$init_args" ]]; then
- init_args="master"
-fi
-
# If local target binary version is different than the latest release version, download it again.
if ! [[ -f "$bin_path" && "$latest_release_version" != "$($bin_path -version)" ]]; then
download "$(echo $response | jq -r '.assets[] | select(.name == "master_'${os}'_'${arch}'") | .browser_download_url')"
fi
-# If local init binary version is different than the latest release version, download it again.
-if ! [[ -f "$init_bin_path " && "$latest_release_version" != "$($init_bin_path -version)" ]]; then
- download "$(echo $response | jq -r '.assets[] | select(.name == "init_'${os}'_'${arch}'") | .browser_download_url')"
-fi
-
# Run the binary.
exec_bin
diff --git a/.github/get-master.bat b/.github/get-master.bat
new file mode 100644
index 0000000..3941afa
--- /dev/null
+++ b/.github/get-master.bat
@@ -0,0 +1,100 @@
+@ECHO OFF & setlocal enabledelayedexpansion
+
+rem Common urls.
+set new_issue_url=https://github.com/guackamolly/zero-monitor/issues/new
+set latest_release_url=https://api.github.com/repos/guackamolly/zero-monitor/releases/latest
+set jq_release_url=https://github.com/jqlang/jq/releases/download/jq-1.7.1
+
+rem Installation directory and program paths.
+set install_dir=%APPDATA%\zero-monitor
+set temp_input_dir=%TEMP%\.zero-monitor
+set bin_path=%install_dir%\master.exe
+set jq_bin_path=%install_dir%\jq.exe
+
+
+if not exist "%install_dir%" (
+ mkdir "%install_dir%"
+)
+
+if not exist "%temp_input_dir%" (
+ mkdir "%temp_input_dir%"
+)
+
+
+rem Query host OS
+set os=windows
+
+rem Query CPU architecture
+set arch=%PROCESSOR_ARCHITECTURE%
+if "%arch%" EQU "AMD64" (
+ set arch=amd64
+) else if "%arch%" EQU "X86" (
+ set arch=386
+) else if "%arch%" EQU "ARM64" (
+ set arch=arm64
+) else (
+ echo "%arch%" is not supported right now, please raise an issue to get support on this architecture. ^%new_issue_url%
+ goto :fatal
+)
+
+rem Download jq if not available.
+if not exist "%jq_bin_path%" (
+ call :download "%jq_release_url%/jq-%os%-%arch%.exe" jq.exe
+)
+
+curl -s ^%latest_release_url% > %temp_input_dir%\latest-release
+
+if not %ERRORLEVEL% EQU 0 (
+ echo Failed to head release, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
+ goto :fatal
+)
+
+rem Head latest release
+call:jq .tag_name %temp_input_dir%\latest-release>%temp_input_dir%\version
+set /P latest_release_version=<%temp_input_dir%\version
+
+if %latest_release_version% EQU "" (
+ echo Failed to extract release version, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
+ goto :fatal
+)
+
+rem If local target binary version is different than the latest release version, download it again.
+"%bin_path%" "-version">%temp_input_dir%\bin_version
+set /P bin_version=<%temp_input_dir%\bin_version
+
+if %latest_release_version% NEQ "%bin_version%" (
+ call:jq -r ".assets[] | select(.name == \"master_%os%_%arch%\") | .browser_download_url" %temp_input_dir%\latest-release>%temp_input_dir%\download_url
+ set /P download_url=<%temp_input_dir%\download_url
+ call :download !download_url! master.exe
+)
+
+rem Run the binary.
+call:exec_bin
+
+REM %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+REM %%%%%%%%%%% FUNCTION DEFINITIONS %%%%%%%%%%%%
+REM %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+:fatal
+ echo %1
+ exit /b 1
+
+:jq
+ "%jq_bin_path%" %*
+ exit /b 0
+
+:exec_bin
+ call "%bin_path%"
+ exit /b 0
+
+:download
+ set url=%1
+ set bin_name=%2
+ if "%url%" EQU "" (
+ echo Failed to extract url, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
+ goto :fatal
+ )
+
+ echo Downloading %bin_name% ...
+ curl -L "%url%" -o "%install_dir%\%bin_name%"
+ exit /b 0
\ No newline at end of file
diff --git a/.github/get-node b/.github/get-node
index fcd34e3..52de491 100755
--- a/.github/get-node
+++ b/.github/get-node
@@ -1,8 +1,5 @@
#!/usr/bin/env bash
-# Save program arguments to later pass on init binary.
-init_args="$@"
-
# Common urls.
new_issue_url="https://github.com/guackamolly/zero-monitor/issues/new"
latest_release_url="https://api.github.com/repos/guackamolly/zero-monitor/releases/latest"
@@ -11,7 +8,6 @@ jq_release_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1"
# Installation directory and program paths.
install_dir="$HOME/.config/zero-monitor"
bin_path="$install_dir/node"
-init_bin_path="$install_dir/init"
jq_bin_path="$install_dir/jq"
if [ ! -d "$install_dir" ]; then
@@ -28,10 +24,6 @@ jq() {
}
exec_bin() {
- if [ ! -z "$init_args" ]; then
- "$init_bin_path" $init_args
- fi
-
exec "$bin_path"
}
@@ -79,20 +71,10 @@ if [ -z "$latest_release_version" ]; then
fatal "Failed to extract release version, please raise an issue to alert maintainers about this bug.\n$new_issue_url"
fi
-# If local init binary does not exist and no arguments have been passed, then bootstrap node.
-if [[ ! -f "$init_bin_path" && -z "$init_args" ]]; then
- init_args="--node="
-fi
-
# If local target binary version is different than the latest release version, download it again.
if ! [[ -f "$bin_path" && "$latest_release_version" != "$($bin_path -version)" ]]; then
download "$(echo $response | jq -r '.assets[] | select(.name == "node_'${os}'_'${arch}'") | .browser_download_url')"
fi
-# If local init binary version is different than the latest release version, download it again.
-if ! [[ -f "$init_bin_path " && "$latest_release_version" != "$($init_bin_path -version)" ]]; then
- download "$(echo $response | jq -r '.assets[] | select(.name == "init_'${os}'_'${arch}'") | .browser_download_url')"
-fi
-
# Run the binary.
exec_bin
diff --git a/.github/get-node.bat b/.github/get-node.bat
index 7c73005..840ce72 100644
--- a/.github/get-node.bat
+++ b/.github/get-node.bat
@@ -1,7 +1,4 @@
-@ECHO OFF
-
-rem Save program arguments to later pass on init binary.
-set init_args=%*
+@ECHO OFF & setlocal enabledelayedexpansion
rem Common urls.
set new_issue_url=https://github.com/guackamolly/zero-monitor/issues/new
@@ -9,82 +6,95 @@ set latest_release_url=https://api.github.com/repos/guackamolly/zero-monitor/rel
set jq_release_url=https://github.com/jqlang/jq/releases/download/jq-1.7.1
rem Installation directory and program paths.
-set install_dir=%~dp0\config\zero-monitor
+set install_dir=%APPDATA%\zero-monitor
+set temp_input_dir=%TEMP%\.zero-monitor
set bin_path=%install_dir%\node.exe
-set init_bin_path=%install_dir%\init.exe
set jq_bin_path=%install_dir%\jq.exe
+
if not exist "%install_dir%" (
mkdir "%install_dir%"
)
-:fatal
- echo %1
- exit /b 1
-
-:jq
- "%jq_bin_path%" %*
-
-:exec_bin
- if "%init_args%" NEQ "" (
- "%init_bin_path%" %init_args%
- )
- call "%bin_path%"
-
-:download
- set url=%1
- if "%url%" EQU "" (
- echo Failed to extract url, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
- goto :fatal
- )
- set bin_name=%url:~-10%
+if not exist "%temp_input_dir%" (
+ mkdir "%temp_input_dir%"
+)
- echo Downloading %bin_name% ...
- for /f "usebackq delims=\\" %%a in (`where wget`) do (
- %%a -O "%install_dir%\%bin_name%" "%url%"
- )
- attrib +x "%install_dir%\%bin_name%"
rem Query host OS
-set os=windos
+set os=windows
rem Query CPU architecture
-set arch=
-for /f "tokens=2 delims=:" %%a in ('wmic cpu get architecture /format:List') do set "arch=%%a"
-
-if "%arch%" EQU "x86_64" (
+set arch=%PROCESSOR_ARCHITECTURE%
+if "%arch%" EQU "AMD64" (
set arch=amd64
-) else if "%arch%" EQU "i686" (
+) else if "%arch%" EQU "X86" (
set arch=386
+) else if "%arch%" EQU "ARM64" (
+ set arch=arm64
) else (
- echo "%arch%" is not supported right now, please raise an issue to get support on this architecture. ^&%new_issue_url%
+ echo "%arch%" is not supported right now, please raise an issue to get support on this architecture. ^%new_issue_url%
goto :fatal
)
rem Download jq if not available.
if not exist "%jq_bin_path%" (
- call :download "%jq_release_url%\jq-%os%-%arch%"
+ call :download "%jq_release_url%/jq-%os%-%arch%.exe" jq.exe
)
-rem Head latest release
-for /f "usebackq delims=:" %%a in (`curl -s ^&%latest_release_url%`) do set "response=%%a"
-if not "!errorlevel!" EQU "0" (
+curl -s ^%latest_release_url% > %temp_input_dir%\latest-release
+
+if not %ERRORLEVEL% EQU 0 (
echo Failed to head release, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
goto :fatal
)
-set latest_release_version=%response:~10,1%
+rem Head latest release
+call:jq .tag_name %temp_input_dir%\latest-release>%temp_input_dir%\version
+set /P latest_release_version=<%temp_input_dir%\version
-if "%latest_release_version%" EQU "" (
+if %latest_release_version% EQU "" (
echo Failed to extract release version, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
goto :fatal
)
-rem If local init binary does not exist and no arguments have been passed, then bootstrap node.
-if not exist "%init_bin_path%" AND "%init_args%" EQU "" (
- set init_args="--node="
+rem If local target binary version is different than the latest release version, download it again.
+"%bin_path%" "-version">%temp_input_dir%\bin_version
+set /P bin_version=<%temp_input_dir%\bin_version
+
+if %latest_release_version% NEQ "%bin_version%" (
+ call:jq -r ".assets[] | select(.name == \"node_%os%_%arch%\") | .browser_download_url" %temp_input_dir%\latest-release>%temp_input_dir%\download_url
+ set /P download_url=<%temp_input_dir%\download_url
+ call :download !download_url! node.exe
)
-rem If local target binary version is different than the latest release version, download it again.
-if exist "%bin_path%" AND "%latest_release_version%" NEQ "%bin_path% -version%" (
- for /f "usebackq delims=:" %%a in (`echo ^&response% | jq -r '.assets[] | select(.name == "node_%os%_%arch%") | .browser_download_url'`)
\ No newline at end of file
+rem Run the binary.
+call:exec_bin
+
+REM %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+REM %%%%%%%%%%% FUNCTION DEFINITIONS %%%%%%%%%%%%
+REM %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+:fatal
+ echo %1
+ exit /b 1
+
+:jq
+ "%jq_bin_path%" %*
+ exit /b 0
+
+:exec_bin
+ call "%bin_path%"
+ exit /b 0
+
+:download
+ set url=%1
+ set bin_name=%2
+ if "%url%" EQU "" (
+ echo Failed to extract url, please raise an issue to alert maintainers about this bug. ^&%new_issue_url%
+ goto :fatal
+ )
+
+ echo Downloading %bin_name% ...
+ curl -L "%url%" -o "%install_dir%\%bin_name%"
+ exit /b 0
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2c5b0a1..4483db6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,7 +16,6 @@ jobs:
run: |
./tools/build master
./tools/build node
- ./tools/build init
- name: Prepare Version Tag
id: version_tag
diff --git a/README.md b/README.md
index d6626fd..2ec68dc 100644
--- a/README.md
+++ b/README.md
@@ -57,13 +57,13 @@ The simplest way to get started is by using the one-click deploy scripts provide
bash <(wget -qO- https://raw.githubusercontent.com/guackamolly/zero-monitor/refs/heads/master/.github/get-master)
```
-Running this command will download `master` + `init` binaries specifically for your OS + Architecture, generate a ready to use `.env` file and finally deploy `master`.
+Running this command will download `master` binary specifically for your OS + Architecture and then run it.
```bash
bash <(wget -qO- https://raw.githubusercontent.com/guackamolly/zero-monitor/refs/heads/master/.github/get-node)
```
-Running this command will download `node` + `init` binaries specifically for your OS + Architecture, generate a ready to use `.env` file and finally deploy `node`.
+Running this command will download `node` binary specifically for your OS + Architecture and then run it.
---
diff --git a/TODO.md b/TODO.md
index c6a98d5..32ef3d8 100644
--- a/TODO.md
+++ b/TODO.md
@@ -11,8 +11,5 @@ Looking to contribute to the project but you don't know how to start? The todo's
- Organize common template code (e.g., `error.gohtml`, websocket initialization, meta tags)
- Optimize network view template render
- Speedtests is not saved if client breaks Websocket connection (unbuffered channel not being consumed)
-- Remove depending on environment variables (100% portability)
-- Add initialization scripts for Windows
- Make Linux/macOS initialization scripts POSIX compliant (remove bash, use shell)
-- Cover comment //TODOs
-- Fix init not printing build version
\ No newline at end of file
+- Cover comment //TODOs
\ No newline at end of file
diff --git a/cmd/init/init.go b/cmd/init/init.go
deleted file mode 100644
index 08fce49..0000000
--- a/cmd/init/init.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package main
-
-import (
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "encoding/json"
- "encoding/pem"
- "flag"
- "fmt"
- "io"
- "log"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "reflect"
-
- build "github.com/guackamolly/zero-monitor/internal/build"
- "github.com/guackamolly/zero-monitor/internal/config"
- _http "github.com/guackamolly/zero-monitor/internal/http"
- "github.com/joho/godotenv"
-)
-
-const (
- BootstrapMaster Action = iota
- BootstrapNode
-)
-
-type Action int
-
-type NodeEnv struct {
- MessageQueueHost string `env:"mq_sub_host"`
- MessageQueuePort int `env:"mq_sub_port"`
- MessageQueueTransportPubKey string `env:"mq_transport_pub_key"`
- MessageQueueInviteCode string `env:"mq_invite_code"`
-}
-
-type MasterEnv struct {
- ServerHost string `env:"server_host"`
- ServerPort int `env:"server_port"`
- MessageQueueHost string `env:"mq_sub_host"`
- MessageQueuePort int `env:"mq_sub_port"`
- MessageQueueTransportPubKey string `env:"mq_transport_pub_key"`
- MessageQueueTransportPemKey string `env:"mq_transport_pem_key"`
- BoltDBPath string `env:"bolt_db_path"`
-}
-
-var action = BootstrapMaster
-
-var inviteLink *url.URL
-
-func init() {
- flag.BoolFunc("version", "prints build version", func(s string) error {
- println(build.Version())
-
- os.Exit(0)
- return nil
- })
-
- flag.Func("node", "configures the environment for starting a node", func(s string) error {
- if len(s) == 0 {
- println("Waiting for invite link... (press enter to resume)")
- fmt.Scanln(&s)
- }
-
- url, err := url.Parse(s)
- if err != nil {
- return err
- }
-
- inviteLink = url
- action = BootstrapNode
- return nil
- })
-
- flag.Parse()
-}
-
-func main() {
- switch action {
- case BootstrapMaster:
- bootstrapMaster()
- case BootstrapNode:
- bootstrapNode()
- }
-}
-
-func bootstrapMaster() {
- println("Bootstrapping master configuration")
- configPath := must(config.Dir())
-
- // Generate a RSA private key with 2048 bits
- privateKey := must(rsa.GenerateKey(rand.Reader, 2048))
-
- // Create a PEM-encoded block for the private key
- privBytes := x509.MarshalPKCS1PrivateKey(privateKey)
- privBlock := pem.Block{
- Type: "RSA PRIVATE KEY",
- Bytes: privBytes,
- }
-
- // Extract the public key from the private key
- publicKey := &privateKey.PublicKey
-
- // Create a PEM-encoded block for the public key
- pubBytes := must(x509.MarshalPKIXPublicKey(publicKey))
- pubBlock := pem.Block{
- Type: "RSA PUBLIC KEY",
- Bytes: pubBytes,
- }
-
- // Write the public key to a file
- pubFile := must(os.Create(filepath.Join(configPath, "master.pub")))
- defer pubFile.Close()
- must0(pem.Encode(pubFile, &pubBlock))
-
- // Write the private key to a file
- privFile := must(os.Create(filepath.Join(configPath, "master.pem")))
- defer privFile.Close()
- must0(pem.Encode(privFile, &privBlock))
-
- boltDbFile := must(os.Create(filepath.Join(configPath, "master.db")))
- defer boltDbFile.Close()
-
- env := MasterEnv{
- ServerHost: "0.0.0.0",
- MessageQueueHost: "0.0.0.0",
- ServerPort: 8080,
- MessageQueuePort: 36113,
- MessageQueueTransportPubKey: pubFile.Name(),
- MessageQueueTransportPemKey: privFile.Name(),
- BoltDBPath: boltDbFile.Name(),
- }
-
- envpath := fmt.Sprintf("%s/master.env", must(config.Dir()))
- writeEnv(env, envpath)
-
- println("> Generated private key on: %s", privFile.Name())
- println("> Generated public key on: %s", pubFile.Name())
- println("> Generated .env on: %s", envpath)
-}
-
-func bootstrapNode() {
- println("Bootstrapping node configuration using invite link: %s", inviteLink)
- configPath := must(config.Dir())
- inviteCode := inviteLink.Query().Get("join")
-
- v := downloadUnmarshal[_http.NetworkJoinView](inviteLink.String())
- pubKey := string(download(v.PublicKeyURL))
- endpoint := downloadUnmarshal[_http.NetworkConnectionEndpointView](v.ConnectionEndpointURL)
-
- pubFile := must(os.Create(filepath.Join(configPath, "node.pub")))
- defer pubFile.Close()
- io.WriteString(pubFile, pubKey)
-
- env := NodeEnv{
- MessageQueueHost: endpoint.Host,
- MessageQueuePort: endpoint.Port,
- MessageQueueTransportPubKey: pubFile.Name(),
- MessageQueueInviteCode: inviteCode,
- }
-
- envpath := fmt.Sprintf("%s/node.env", configPath)
- writeEnv(env, envpath)
-
- println("> Extracted invite code: %s", inviteCode)
- println("> Saved public key on: %s", pubFile.Name())
- println("> Generated .env on: %s", envpath)
-}
-
-// helper functions
-func writeEnv(env any, path string) {
- v := reflect.ValueOf(env)
- t := v.Type()
- m := map[string]string{}
- for i := 0; i < t.NumField(); i++ {
- f := t.Field(i)
- m[f.Tag.Get("env")] = fmt.Sprintf("%v", v.Field(i))
- }
-
- godotenv.Write(m, path)
-}
-
-func download(url string) []byte {
- println("> GET %s", url)
-
- resp := must(http.Get(url))
- if sc := resp.StatusCode; sc != 200 {
- panic(fmt.Sprintf("sc: %d", resp.StatusCode))
- }
-
- bs := must(io.ReadAll(resp.Body))
-
- return bs
-}
-
-func downloadUnmarshal[T any](url string) T {
- var v T
- must0(json.Unmarshal(download(url), &v))
-
- return v
-}
-
-func must[T any](t T, err error) T {
- if err != nil {
- panic(err)
- }
-
- return t
-}
-
-func must0(err error) {
- if err != nil {
- panic(err)
- }
-}
-
-func panic(v any) {
- log.Fatal(v)
-}
-
-func println(f any, v ...any) {
- if _, ok := f.(string); !ok || len(v) == 0 {
- fmt.Println(f)
- return
- }
-
- fmt.Printf("%s\n", fmt.Sprintf(f.(string), v))
-}
diff --git a/cmd/master/master.go b/cmd/master/master.go
index 49de7eb..0246172 100644
--- a/cmd/master/master.go
+++ b/cmd/master/master.go
@@ -8,11 +8,13 @@ import (
"syscall"
"time"
+ "github.com/guackamolly/zero-monitor/internal/bootstrap"
"github.com/guackamolly/zero-monitor/internal/config"
dbb "github.com/guackamolly/zero-monitor/internal/data/db"
dbbolt "github.com/guackamolly/zero-monitor/internal/data/db/db-bolt"
"github.com/guackamolly/zero-monitor/internal/data/models"
"github.com/guackamolly/zero-monitor/internal/data/repositories"
+ "github.com/guackamolly/zero-monitor/internal/env"
"github.com/guackamolly/zero-monitor/internal/event"
"github.com/guackamolly/zero-monitor/internal/http"
"github.com/guackamolly/zero-monitor/internal/logging"
@@ -23,7 +25,6 @@ import (
build "github.com/guackamolly/zero-monitor/internal/build"
flags "github.com/guackamolly/zero-monitor/internal/build/flags"
- _ "github.com/guackamolly/zero-monitor/internal/env"
)
func main() {
@@ -32,7 +33,8 @@ func main() {
}
logging.AddLogger(logging.NewConsoleLogger())
- // 1. Load config
+ // 1. Load env + config
+ env := loadEnv()
cfg := loadConfig()
// 2. Initialize DI.
@@ -42,19 +44,19 @@ func main() {
ctx = mq.InjectSubscribeContainer(ctx, suc)
// 3. Initialize sub server.
- loadCrypto()
+ loadCrypto(env.MessageQueueTransportPemKey)
- s := initializeSubServer(ctx)
+ s := initializeSubServer(ctx, env.MessageQueueHost, env.MessageQueuePort)
defer s.Close()
// 4. Initialize database.
- db := initializeDatabase()
+ db := initializeDatabase(env.BoltDBPath)
defer db.Close()
// 5. Initialize http server.
sc = updateServiceContainer(sc, &s, db)
ctx = http.InjectServiceContainer(ctx, sc)
- e := initializeHttpServer(ctx)
+ e := initializeHttpServer(ctx, env.ServerHost, env.ServerPort, env.ServerTLSCert, env.ServerTLSKey, env.ServerVirtualHost)
defer e.Close()
// 6. Await termination...
@@ -66,6 +68,15 @@ func main() {
saveConfig(sc.MasterConfiguration)
}
+func loadEnv() env.MasterEnv {
+ if env, err := env.Master(); err == nil {
+ return env
+ }
+
+ logging.LogDebug("couldn't lookup .env, bootstrapping configuration values...")
+ return bootstrap.Master()
+}
+
func loadConfig() config.Config {
cfg, err := config.Load()
if err != nil {
@@ -82,17 +93,18 @@ func saveConfig(s *service.MasterConfigurationService) {
}
}
-func loadCrypto() {
- err := mq.LoadAsymmetricBlock(false)
+func loadCrypto(keyfilepath string) {
+ err := mq.LoadAsymmetricBlock(keyfilepath)
if err != nil {
- logging.LogFatal("failed to load message queue private key, %v", err)
+ logging.LogError("failed to load message queue private key, %v", err)
+ logging.LogWarning("message queue sensitive data will not be encrypted!")
}
}
-func initializeSubServer(ctx context.Context) mq.Socket {
+func initializeSubServer(ctx context.Context, host, port string) mq.Socket {
s := mq.NewSubSocket(ctx)
s.RegisterSubscriptions()
- err := mq.ConnectSubscribe(s)
+ err := mq.ConnectSubscribe(s, host, port)
if err != nil {
s.Close()
log.Fatalf("coudln't open zeromq sub socket, %v\n", err)
@@ -102,7 +114,11 @@ func initializeSubServer(ctx context.Context) mq.Socket {
return s
}
-func initializeHttpServer(ctx context.Context) *echo.Echo {
+func initializeHttpServer(
+ ctx context.Context,
+ host, port, certfilepath, keyfilepath string,
+ virtualhost string,
+) *echo.Echo {
// Initialize echo framework.
e := echo.New()
@@ -111,17 +127,22 @@ func initializeHttpServer(ctx context.Context) *echo.Echo {
http.RegisterMiddlewares(e, ctx)
http.RegisterStaticFiles(e, public.FS)
http.RegisterTemplates(e, public.FS)
+ http.SetVirtualHost(virtualhost)
// Start server.
go func() {
- logging.LogFatal("server exit %v", http.Start(e))
+ if len(certfilepath) > 0 && len(keyfilepath) > 0 {
+ logging.LogFatal("server exit %v", http.StartTLS(e, host, port, certfilepath, keyfilepath))
+ }
+
+ logging.LogFatal("server exit %v", http.Start(e, host, port))
}()
return e
}
-func initializeDatabase() dbb.Database {
- db := dbbolt.NewBoltDatabase(dbbolt.Path())
+func initializeDatabase(dbpath string) dbb.Database {
+ db := dbbolt.NewBoltDatabase(dbpath)
err := db.Open()
if err != nil {
logging.LogFatal("couldn't initialize database, %v", db)
diff --git a/cmd/node/node.go b/cmd/node/node.go
index 40f26f8..dc16ce5 100644
--- a/cmd/node/node.go
+++ b/cmd/node/node.go
@@ -7,7 +7,9 @@ import (
"os/signal"
"syscall"
+ "github.com/guackamolly/zero-monitor/internal/bootstrap"
"github.com/guackamolly/zero-monitor/internal/data/repositories"
+ "github.com/guackamolly/zero-monitor/internal/env"
"github.com/guackamolly/zero-monitor/internal/logging"
"github.com/guackamolly/zero-monitor/internal/mq"
"github.com/guackamolly/zero-monitor/internal/service"
@@ -15,7 +17,6 @@ import (
build "github.com/guackamolly/zero-monitor/internal/build"
flags "github.com/guackamolly/zero-monitor/internal/build/flags"
- _ "github.com/guackamolly/zero-monitor/internal/env"
)
func main() {
@@ -24,33 +25,46 @@ func main() {
}
logging.AddLogger(logging.NewConsoleLogger())
- // 1. Initialize DI.
+ // 1. Load env
+ env := loadEnv()
+
+ // 2. Initialize DI.
pc := createPublishContainer()
ctx := context.Background()
ctx = mq.InjectPublishContainer(ctx, pc)
- // 2. Initialize pub server.
- loadCrypto()
+ // 3. Initialize pub server.
+ loadCrypto(env.MessageQueueTransportPubKey)
s := mq.NewPubSocket(ctx)
defer s.Close()
- err := mq.ConnectPublish(s)
+ err := mq.ConnectPublish(s, env.MessageQueueHost, env.MessageQueuePort)
if err != nil {
log.Fatalf("coudln't open zeromq pub socket, %v\n", err)
}
log.Printf("started zeromq pub socket on addr: %s\n", s.Addr())
- s.RegisterPublishers()
+ s.RegisterPublishers(env.MessageQueueInviteCode)
- // 3. Await termination...
+ // 4. Await termination...
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<-c
}
-func loadCrypto() {
- err := mq.LoadAsymmetricBlock(true)
+func loadEnv() env.NodeEnv {
+ if env, err := env.Node(); err == nil {
+ return env
+ }
+
+ logging.LogDebug("couldn't lookup .env, bootstrapping configuration values...")
+ return bootstrap.Node()
+}
+
+func loadCrypto(keyfilepath string) {
+ err := mq.LoadAsymmetricBlock(keyfilepath)
if err != nil {
- logging.LogFatal("failed to load message queue public key, %v", err)
+ logging.LogError("failed to load message queue public key, %v", err)
+ logging.LogWarning("message queue sensitive data will not be encrypted!")
}
}
diff --git a/docs/env.md b/docs/env.md
index 6b47b24..c4cc5bb 100644
--- a/docs/env.md
+++ b/docs/env.md
@@ -14,6 +14,7 @@ Master expects the .env file to be present in the working directory under the na
|`mq_sub_port`|Specifies the port to bind the ZeroMQ listener connection|`36113`|-|
|`mq_transport_pem_key`|Specifies the path of the `RSA 2048 PKCS 1` private key used for decrypting the communication during the key-exchange of node-master|`~/.config/zero-monitor/mq.pem`|`${CFG_DIR}/mq.pem`|
|`mq_transport_pub_key`|Specifies the path of the `RSA 2048 PKCS 1` public key used for encrypting the communication during the key-exchange of node-master|`~/.config/zero-monitor/mq.pub`|`${CFG_DIR}/mq.pub`|
+|`bolt_db_path`|Specifies the path of the Bolt in-memory database used to store speedtests results|`~/.config/zero-monitor/master.db`|`${WORKING_DIR}/master.db`|
---
@@ -21,7 +22,6 @@ Additionally you can set these variables to customize your experience while usin
|Environment Variable|Description|Example Value|Default Value|
|--------------------|-----------|-------------|-------------|
-|`bolt_db_path`|Specifies the path of the Bolt in-memory database used to store speedtests results|`~/.config/zero-monitor/master.db`|`${WORKING_DIR}/master.db`|
|`server_tls_crt_fp`|Specifies the path of the signed certificate to encrypt web-server communication (HTTPS)|`~/.config/zero-monitor/master.crt`|-|
|`server_tls_crt_key`|Specifies the path of the signed certificate private key to encrypt web-server communication (HTTPS)|`~/.config/zero-monitor/master.crt.key`|-|
|`server_virtual_host`|If you want to deploy the web server as a virtual path|`/zero-monitor`|-|
diff --git a/internal/bootstrap/boostrap.go b/internal/bootstrap/boostrap.go
new file mode 100644
index 0000000..d9f5350
--- /dev/null
+++ b/internal/bootstrap/boostrap.go
@@ -0,0 +1,89 @@
+package bootstrap
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "path/filepath"
+
+ "github.com/guackamolly/zero-monitor/internal/config"
+ "github.com/guackamolly/zero-monitor/internal/env"
+ "github.com/guackamolly/zero-monitor/internal/http"
+)
+
+const (
+ defaultHttpPort = "8080"
+ defaultMessageQueuePort = "36113"
+
+ defaultHttpHost = "0.0.0.0"
+ defaultMessageQueueHost = "0.0.0.0"
+)
+
+func Master() env.MasterEnv {
+ configPath := must(config.Dir())
+
+ // Generate a RSA private key with 2048 bits
+ privateKey := must(rsa.GenerateKey(rand.Reader, 2048))
+
+ // Create a PEM-encoded block for the private key
+ privBytes := x509.MarshalPKCS1PrivateKey(privateKey)
+ privBlock := pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: privBytes,
+ }
+
+ // Write the private key to a file
+ privFile := must(os.Create(filepath.Join(configPath, "master.pem")))
+ defer privFile.Close()
+ must0(pem.Encode(privFile, &privBlock))
+
+ boltDbFile := must(os.Create(filepath.Join(configPath, "master.db")))
+ defer boltDbFile.Close()
+
+ e := env.MasterEnv{
+ ServerHost: defaultHttpHost,
+ MessageQueueHost: defaultMessageQueueHost,
+ ServerPort: defaultHttpPort,
+ MessageQueuePort: defaultMessageQueuePort,
+ MessageQueueTransportPemKey: privFile.Name(),
+ BoltDBPath: boltDbFile.Name(),
+ }
+
+ // Save bootstrapped env.
+ return must(e, env.Save(e))
+}
+
+func Node() env.NodeEnv {
+ // Wait for user input regarding the network invite link.
+ var inviteLink string
+ println("Waiting for invite link... (press enter to resume)")
+ fmt.Scanln(&inviteLink)
+
+ configPath := must(config.Dir())
+ inviteCode := must(url.Parse(inviteLink)).Query().Get("join")
+
+ // Query connection information and public key for key exchange.
+ joinView := downloadUnmarshal[http.NetworkJoinView](inviteLink)
+ pubKey := string(download(joinView.PublicKeyURL))
+ endpoint := downloadUnmarshal[http.NetworkConnectionEndpointView](joinView.ConnectionEndpointURL)
+
+ // Write pub key to config folder.
+ pubFile := must(os.Create(filepath.Join(configPath, "node.pub")))
+ defer pubFile.Close()
+ io.WriteString(pubFile, pubKey)
+
+ e := env.NodeEnv{
+ MessageQueueHost: endpoint.Host,
+ MessageQueuePort: fmt.Sprintf("%d", endpoint.Port),
+ MessageQueueTransportPubKey: pubFile.Name(),
+ MessageQueueInviteCode: inviteCode,
+ }
+
+ // Save bootstrapped env.
+ return must(e, env.Save(e))
+}
diff --git a/internal/bootstrap/helper.go b/internal/bootstrap/helper.go
new file mode 100644
index 0000000..162be51
--- /dev/null
+++ b/internal/bootstrap/helper.go
@@ -0,0 +1,57 @@
+package bootstrap
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+)
+
+// helper functions
+func download(url string) []byte {
+ println("> GET %s", url)
+
+ resp := must(http.Get(url))
+ if sc := resp.StatusCode; sc != 200 {
+ panic(fmt.Sprintf("sc: %d", resp.StatusCode))
+ }
+
+ bs := must(io.ReadAll(resp.Body))
+
+ return bs
+}
+
+func downloadUnmarshal[T any](url string) T {
+ var v T
+ must0(json.Unmarshal(download(url), &v))
+
+ return v
+}
+
+func must[T any](t T, err error) T {
+ if err != nil {
+ panic(err)
+ }
+
+ return t
+}
+
+func must0(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+func panic(v any) {
+ log.Fatal(v)
+}
+
+func println(f any, v ...any) {
+ if _, ok := f.(string); !ok || len(v) == 0 {
+ fmt.Println(f)
+ return
+ }
+
+ fmt.Printf("%s\n", fmt.Sprintf(f.(string), v))
+}
diff --git a/internal/data/db/db-bolt/connection.go b/internal/data/db/db-bolt/connection.go
deleted file mode 100644
index 51b37db..0000000
--- a/internal/data/db/db-bolt/connection.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package dbbolt
-
-import "os"
-
-const (
- boltDbPathKey = "bolt_db_path"
-)
-
-var (
- boltDbPath = os.Getenv(boltDbPathKey)
-)
-
-func Path() string {
- if len(boltDbPath) == 0 {
- return "master.db"
- }
-
- return boltDbPath
-}
diff --git a/internal/data/db/db-bolt/connection_test.go b/internal/data/db/db-bolt/connection_test.go
deleted file mode 100644
index d5f3b35..0000000
--- a/internal/data/db/db-bolt/connection_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package dbbolt_test
-
-import (
- "testing"
-
- dbbolt "github.com/guackamolly/zero-monitor/internal/data/db/db-bolt"
-)
-
-func TestPathReturnsDefaultIfEnvKeyIsNotSet(t *testing.T) {
- def := "master.db"
- if dbbolt.Path() != def {
- t.Errorf("expected %s but got %s", def, dbbolt.Path())
- }
-}
diff --git a/internal/env/env.go b/internal/env/env.go
index 75f71a2..28587d4 100644
--- a/internal/env/env.go
+++ b/internal/env/env.go
@@ -1,33 +1,133 @@
package env
import (
- "log"
"os"
"path/filepath"
+ "reflect"
"github.com/guackamolly/zero-monitor/internal/config"
"github.com/joho/godotenv"
)
-func init() {
- if err := godotenv.Load(); err == nil {
- return
+type MasterEnv struct {
+ ServerHost string `env:"server_host"`
+ ServerPort string `env:"server_port"`
+ ServerTLSCert string `env:"server_tls_crt_fp"`
+ ServerTLSKey string `env:"server_tls_key_fp"`
+ ServerVirtualHost string `env:"server_virtual_host"`
+ MessageQueueHost string `env:"mq_sub_host"`
+ MessageQueuePort string `env:"mq_sub_port"`
+ MessageQueueTransportPemKey string `env:"mq_transport_pem_key"`
+ BoltDBPath string `env:"bolt_db_path"`
+}
+
+type NodeEnv struct {
+ MessageQueueHost string `env:"mq_sub_host"`
+ MessageQueuePort string `env:"mq_sub_port"`
+ MessageQueueTransportPubKey string `env:"mq_transport_pub_key"`
+ MessageQueueInviteCode string `env:"mq_invite_code"`
+}
+
+// If not nil, it means env has been already loaded.
+var nodeEnv *NodeEnv
+var masterEnv *MasterEnv
+
+// Loads environment variables for master server.
+// If error is not nil, it means neither .env on the working directory or ${CFG_DIR}/master.env could be lookup.
+func Master() (MasterEnv, error) {
+ if masterEnv != nil {
+ return *masterEnv, nil
+ }
+
+ err := loadEnv("master.env")
+ if err != nil {
+ return MasterEnv{}, err
}
+ masterEnv = fromEnv(MasterEnv{})
+ return *masterEnv, err
+}
+
+// Loads environment variables for node agent.
+// If error is not nil, it means neither .env on the working directory or ${CFG_DIR}/node.env could be lookup.
+func Node() (NodeEnv, error) {
+ if nodeEnv != nil {
+ return *nodeEnv, nil
+ }
+
+ err := loadEnv("node.env")
+ if err != nil {
+ return NodeEnv{}, err
+ }
+
+ nodeEnv = fromEnv(NodeEnv{})
+ return *nodeEnv, err
+}
+
+func Save[T MasterEnv | NodeEnv](
+ env T,
+) error {
d, err := config.Dir()
if err != nil {
- log.Fatal("couldn't lookup a .env file!")
+ return err
+ }
+
+ if reflect.TypeOf(env) == reflect.TypeFor[NodeEnv]() {
+ return setEnv(env, filepath.Join(d, "node.env"))
+ } else {
+ return setEnv(env, filepath.Join(d, "master.env"))
+ }
+}
+
+func loadEnv(
+ filename string,
+) error {
+ if err := godotenv.Load(); err == nil {
+ return nil
+ }
+
+ d, err := config.Dir()
+ if err == nil {
+ err = godotenv.Load(filepath.Join(d, filename))
}
- exe, err := os.Executable()
if err != nil {
- log.Fatal("couldn't lookup a .env file!")
+ return err
}
- envp := filepath.Join(d, filepath.Base(exe)+".env")
- if err = godotenv.Load(envp); err == nil {
- return
+ return nil
+}
+
+func fromEnv[T MasterEnv | NodeEnv](
+ env T,
+) *T {
+ v := reflect.ValueOf(env)
+ addr := reflect.ValueOf(&env).Elem()
+ t := v.Type()
+ for i := 0; i < t.NumField(); i++ {
+ f := t.Field(i)
+ k := f.Tag.Get("env")
+ addr.Field(i).SetString(os.Getenv(k))
+ }
+
+ return &env
+}
+
+func setEnv[T MasterEnv | NodeEnv](
+ env T,
+ path string,
+) error {
+
+ m := map[string]string{}
+ v := reflect.ValueOf(env)
+ t := v.Type()
+ for i := 0; i < t.NumField(); i++ {
+ f := t.Field(i)
+ k := f.Tag.Get("env")
+ v := v.Field(i)
+
+ m[k] = v.String()
}
- log.Fatalf("couldn't load .env or %s", envp)
+ return godotenv.Write(m, path)
}
diff --git a/internal/http/connection.go b/internal/http/connection.go
index 0440643..3fc3e08 100644
--- a/internal/http/connection.go
+++ b/internal/http/connection.go
@@ -2,35 +2,14 @@ package http
import (
"fmt"
- "os"
"github.com/labstack/echo/v4"
)
-const (
- serverHostEnvKey = "server_host"
- serverPortEnvKey = "server_port"
- serverTLSCertFilePathKey = "server_tls_crt_fp"
- serverTLSKeyFilePathKey = "server_tls_key_fp"
-)
-
-var (
- serverHost = os.Getenv(serverHostEnvKey)
- serverPort = os.Getenv(serverPortEnvKey)
- serverTLSCertFilePath = os.Getenv(serverTLSCertFilePathKey)
- serverTLSKeyFilePath = os.Getenv(serverTLSKeyFilePathKey)
-)
-
-func Start(e *echo.Echo) error {
- addr := ServerAddress()
-
- if len(serverTLSCertFilePath) == 0 {
- return e.Start(addr)
- }
-
- return e.StartTLS(addr, serverTLSCertFilePath, serverTLSKeyFilePath)
+func Start(e *echo.Echo, host string, port string) error {
+ return e.Start(fmt.Sprintf("[%s]:%s", host, port))
}
-func ServerAddress() string {
- return fmt.Sprintf("%s:%s", serverHost, serverPort)
+func StartTLS(e *echo.Echo, host string, port string, certfilepath string, keyfilepath string) error {
+ return e.StartTLS(fmt.Sprintf("[%s]:%s", host, port), certfilepath, keyfilepath)
}
diff --git a/internal/http/handler+dashboard.go b/internal/http/handler+dashboard.go
index b725179..44ce3a6 100644
--- a/internal/http/handler+dashboard.go
+++ b/internal/http/handler+dashboard.go
@@ -22,7 +22,7 @@ func dashboardFormHandler(ectx echo.Context) error {
return ectx.Redirect(301, ectx.Request().URL.Path)
}
- host := ServerAddress()
+ host := ServerAddress(ectx)
if IsReverseProxyRequest(ectx) {
host = net.JoinHostPort(ExtractReverseProxyIP(ectx), ExtractPort(ectx))
} else if IsBindToUnspecified(ectx) {
@@ -32,7 +32,8 @@ func dashboardFormHandler(ectx echo.Context) error {
return echo.ErrInternalServerError
}
- host = net.JoinHostPort(ip.String(), serverPort)
+ _, port, _ := net.SplitHostPort(host)
+ host = net.JoinHostPort(ip.String(), port)
}
code := sc.NodeManager.Code()
diff --git a/internal/http/handler+util.go b/internal/http/handler+util.go
index 3a806e0..9b4cbda 100644
--- a/internal/http/handler+util.go
+++ b/internal/http/handler+util.go
@@ -1,6 +1,7 @@
package http
import (
+ "fmt"
"net"
"net/url"
"strconv"
@@ -118,6 +119,11 @@ func RawURL(ectx echo.Context, host string, path string, query map[string]string
}
}
+func ServerAddress(ectx echo.Context) string {
+ addr := extractServerAddr(ectx)
+ return fmt.Sprintf("[%s]:%d", addr.IP, addr.Port)
+}
+
func IsLocalRequest(ectx echo.Context) bool {
ip := net.ParseIP(ectx.RealIP())
diff --git a/internal/http/virtual-host.go b/internal/http/virtual-host.go
index 3d25ccb..f676717 100644
--- a/internal/http/virtual-host.go
+++ b/internal/http/virtual-host.go
@@ -2,17 +2,14 @@ package http
import (
"fmt"
- "os"
"strings"
)
-const (
- serverVirtualHostEnvKey = "server_virtual_host"
-)
+var serverVirtualHost string
-var (
- serverVirtualHost = os.Getenv(serverVirtualHostEnvKey)
-)
+func SetVirtualHost(host string) {
+ serverVirtualHost = host
+}
func WithVirtualHost(path string) string {
if !strings.HasPrefix(serverVirtualHost, "/") {
diff --git a/internal/mq/authentication.go b/internal/mq/authentication.go
index 449751a..8a64dc3 100644
--- a/internal/mq/authentication.go
+++ b/internal/mq/authentication.go
@@ -1,28 +1,5 @@
package mq
-import (
- "fmt"
- "os"
-)
-
-const (
- mqInviteCodeEnvKey = "mq_invite_code"
-)
-
-var (
- mqInviteCode = os.Getenv(mqInviteCodeEnvKey)
-)
-
// This flag controls whether or not the current running node has already handshaked with the master node.
// It's used to disallow handshaking more than one time.
var handshaked = false
-
-// Returns the invite code passed through environment variables ([mqInviteCodeEnvKey]) or reads it from std input.
-func InviteCode() string {
- if len(mqInviteCode) == 0 {
- println("Waiting for invite code... (press enter to resume)")
- fmt.Scanln(&mqInviteCode)
- }
-
- return mqInviteCode
-}
diff --git a/internal/mq/connection.go b/internal/mq/connection.go
index 07dc338..6bd843f 100644
--- a/internal/mq/connection.go
+++ b/internal/mq/connection.go
@@ -3,50 +3,27 @@ package mq
import (
"fmt"
"net"
- "os"
"github.com/guackamolly/zero-monitor/internal/data/repositories"
"github.com/guackamolly/zero-monitor/internal/logging"
)
-const (
- mqSubHostEnvKey = "mq_sub_host"
- mqSubPortEnvKey = "mq_sub_port"
-)
-
-var (
- mqSubHost = os.Getenv(mqSubHostEnvKey)
- mqSubPort = os.Getenv(mqSubPortEnvKey)
-)
-
// Connects a socket for publishing messages to master node.
-func ConnectPublish(s Socket) error {
- if len(mqSubHost) > 0 && len(mqSubPort) > 0 {
- return s.Dial(fmt.Sprintf("tcp://[%s]:%s", mqSubHost, mqSubPort))
- }
-
- return fmt.Errorf("sub host and port haven't been provided")
+func ConnectPublish(s Socket, host string, port string) error {
+ return s.Dial(fmt.Sprintf("tcp://[%s]:%s", host, port))
}
// Connects a socket for subscribing messages from reporting nodes.
-func ConnectSubscribe(s Socket) error {
- ip := subHostIP()
-
- // if port is unspecified, default to 0 so go internals
- // choose a random available port
- if len(mqSubPort) == 0 {
- mqSubPort = "0"
- }
-
- return s.Listen(fmt.Sprintf("tcp://[%s]:%s", ip, mqSubPort))
+func ConnectSubscribe(s Socket, host string, port string) error {
+ return s.Listen(fmt.Sprintf("tcp://[%s]:%s", lookupHost(host), port))
}
func Close(s Socket) error {
return s.Close()
}
-func subHostIP() net.IP {
- if ip := net.ParseIP(mqSubHost); ip != nil {
+func lookupHost(host string) net.IP {
+ if ip := net.ParseIP(host); ip != nil {
return ip
}
diff --git a/internal/mq/crypto.go b/internal/mq/crypto.go
index b9bf945..6a77a4c 100644
--- a/internal/mq/crypto.go
+++ b/internal/mq/crypto.go
@@ -10,20 +10,6 @@ import (
"fmt"
"io"
"os"
- "path/filepath"
-
- "github.com/guackamolly/zero-monitor/internal/config"
- "github.com/guackamolly/zero-monitor/internal/logging"
-)
-
-const (
- mqTransportPrivateKeyFileEnvKey = "mq_transport_pem_key"
- mqTransportPublicKeyFileEnvKey = "mq_transport_pub_key"
-)
-
-var (
- mqTransportPrivateKeyFile = os.Getenv(mqTransportPrivateKeyFileEnvKey)
- mqTransportPublicKeyFile = os.Getenv(mqTransportPublicKeyFileEnvKey)
)
// Global map of blocks used to encrypt/decrypt sensitive
@@ -36,39 +22,11 @@ var cipherBlocks = map[string]cipher.Block{}
// the key exchange between nodes.
var blk *pem.Block
-func init() {
- peml := len(mqTransportPrivateKeyFile)
- publ := len(mqTransportPublicKeyFile)
-
- if peml > 0 && publ > 0 {
- return
- }
-
- d, err := config.Dir()
- if err != nil {
- logging.LogWarning("couldn't lookup pem/pub key files to encrypt message queue messages. either communication with master node fail OR it won't be encrypted")
- return
- }
-
- if peml == 0 {
- mqTransportPrivateKeyFile = filepath.Join(d, "mq.pem")
- }
-
- if publ == 0 {
- mqTransportPublicKeyFile = filepath.Join(d, "mq.pub")
- }
-}
-
// Loads the public/private key block to be used on encryption/decryption.
func LoadAsymmetricBlock(
- pub bool,
+ keyfilepath string,
) error {
- p := mqTransportPrivateKeyFile
- if pub {
- p = mqTransportPublicKeyFile
- }
-
- f, err := os.ReadFile(p)
+ f, err := os.ReadFile(keyfilepath)
if err != nil {
return err
}
@@ -178,5 +136,25 @@ func GenerateCipherKey() ([]byte, error) {
// TODO: derive from private key instead
func DerivePublicKey() ([]byte, error) {
- return os.ReadFile(mqTransportPublicKeyFile)
+ key, err := x509.ParsePKCS1PrivateKey(blk.Bytes)
+ if err != nil {
+ return nil, err
+ }
+
+ // Extract the public key from the private key
+ publicKey := &key.PublicKey
+
+ // Marshal the public key into DER format
+ publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
+ if err != nil {
+ return nil, err
+ }
+
+ // Encode the public key into a PEM block
+ publicKeyBlock := pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PUBLIC KEY",
+ Bytes: publicKeyBytes,
+ })
+
+ return publicKeyBlock, nil
}
diff --git a/internal/mq/crypto_int_test.go b/internal/mq/crypto_int_test.go
new file mode 100644
index 0000000..00c40c6
--- /dev/null
+++ b/internal/mq/crypto_int_test.go
@@ -0,0 +1,101 @@
+//go:build integration
+
+package mq_test
+
+import (
+ "os"
+ "slices"
+ "testing"
+
+ "github.com/guackamolly/zero-monitor/internal/mq"
+)
+
+var rsaPem = `
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAw358uANE26qvIs51rjjBRo3v58MJFLja9/padm7iM+ZSchAj
+LTNksmsJI1pbSRvA6gbmMxqqrvaWXu2TfQqponGc/iwBPzIa4dl8gHi1CLMME1Nl
+7yJw+uKK15XGo/Qtaz7KYdtHCpwRs5Gc+8Ww7Cq7jEYPu+KL/TrrqxXldUV/Hocv
+LRkydtRDeuSdefGjokTi2LEQspdpZ4HRbBncl/ntKLboN2R3RAomx1bWTqA/M0xx
+QqOiHO9zai9yrYuFYUuSmeyusqv3wH2mhLrEhPPQYzG/BLvq4rmZautJDnbZV613
+/WKzqg2hhMgwJtAzbm7yCMSlyTB69uNLHpVK9QIDAQABAoIBAADwsUsYMJ9lrnu3
+8rWihsgNvYxd1wjDhIayQDKXFMcxgacn8AU3+lO7pw0s5hylyQXtRPPFvMp9Awm+
+OU3KOhGhb2f5EuYnVPWwax9yl6GH9upTfowziIy5RJWZscSu8OoOeXHbRNpkGCYp
+XNnhSizPXhxkLHwyuidmSC7gpzJv6Ysy6+3PJubro1IyBIuMmQdiKtVbWL9Razk6
+IPhCMuOCknCHgZbOETh8NeqXh7Ztiragb58h5Fc8RSONdYsmkmfirWXFBArSs/Xw
+YNyYZGwfZ5Tyt2TggA2iFBF6oPJtQk9cOnyLOhjmCLc7w+TF6crOPxZRx6usOkP2
+ElduSBsCgYEA9gGloYDhK2oEmtU/3ZtI+NT9wEVW/W23aPaG4QqB7yI5XaW3zX9P
+Mq3pAFxR9xjOhvpW8ed+f9qg+5toc73fLLuMDWFuA5Pr34dxV0DeFdNZ8h8qPBtz
+j7kvUTtH2h+zTc4IvVpbcBKkqT2SJpr1wO3xThBajfOCh+8V/VdOwncCgYEAy2+J
+A2EuPAjHtybyJpcZp/lyMWzwN4c3N511e/PvJqT0TSQjRrM9M4TREmX4eQghE2Va
+QkKkm+RoN4mxFP7C6sjq7kmvFbZG4JC9kBtDqV79mmEGw8/IvaSTXXG0RL8tKGIg
+bul8Q0VVWlZgkx780bII9TMYOZRZP9FUSicb7PMCgYAsnN3ZrRKomeBd5+BeIuQX
+5CBkdu6wpO4HBfYt54bqxA0dM4lipfzJ1woTO6rNod0KU2njErU5IH/jQSqvGrbX
+WOesIYge8/tpnRlr1mKwGJUOOKKjJeNOJCo1lAeSwf71VDD3jeRZLbhYzMatY5q/
+syb4njSd25RHbI9TUzsAPwKBgDdy/C5uo5J7diwmsmPwVW7iX8y2+7a25UcEZQxX
+Db1Dws7v5amUmz7amb3hC1u56oIF4xciYQmYtQtGPX0Sf4BNKTOv48gQObtl2DVa
+KRQWLxuQDK78iKOgIwaaQl9mmGFkdaClhVg0orIPzxzqmlBxrV1gAt9W3wi0/ruD
+c2ofAoGBAL+GDfuCrJPbnp37C4ZEMgWiJ73Mgu7tFVh53irll1IXp4pqtn7YHRNL
+P7dplKCTY0RUyckE8WGqLl2sY5HrA3RrB+SzjFJcESsBrWxk56JIquEQPCY1kaFZ
++6J3fC5Nz8oA5jVUtyFGj156hP/A0grfWLQnauJpFNhOLS/VATBx
+-----END RSA PRIVATE KEY-----`
+
+var rsaPub = `
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw358uANE26qvIs51rjjB
+Ro3v58MJFLja9/padm7iM+ZSchAjLTNksmsJI1pbSRvA6gbmMxqqrvaWXu2TfQqp
+onGc/iwBPzIa4dl8gHi1CLMME1Nl7yJw+uKK15XGo/Qtaz7KYdtHCpwRs5Gc+8Ww
+7Cq7jEYPu+KL/TrrqxXldUV/HocvLRkydtRDeuSdefGjokTi2LEQspdpZ4HRbBnc
+l/ntKLboN2R3RAomx1bWTqA/M0xxQqOiHO9zai9yrYuFYUuSmeyusqv3wH2mhLrE
+hPPQYzG/BLvq4rmZautJDnbZV613/WKzqg2hhMgwJtAzbm7yCMSlyTB69uNLHpVK
+9QIDAQAB
+-----END PUBLIC KEY-----`
+
+var rsaPemKeypath string
+var rsaPubKeypath string
+
+func init() {
+ rsaPemFile, err := os.CreateTemp("", "")
+ if err != nil {
+ panic(err)
+ }
+ rsaPemFile.WriteString(rsaPem)
+ defer rsaPemFile.Close()
+
+ rsaPubFile, err := os.CreateTemp("", "")
+ if err != nil {
+ panic(err)
+ }
+ rsaPubFile.WriteString(rsaPub)
+ defer rsaPemFile.Close()
+
+ rsaPemKeypath = rsaPemFile.Name()
+ rsaPubKeypath = rsaPubFile.Name()
+}
+
+func TestEncryptAndDecryptAsymmetric(t *testing.T) {
+ data := []byte("zero-monitor")
+
+ err := mq.LoadAsymmetricBlock(rsaPubKeypath)
+ if err != nil {
+ t.Fatalf("didn't expect load public key block to fail, %v", err)
+ }
+
+ encrypted, err := mq.EncryptAsymmetric(data)
+ if err != nil {
+ t.Fatalf("didn't expect encrypt asymmetric to fail, %v", err)
+ }
+
+ err = mq.LoadAsymmetricBlock(rsaPemKeypath)
+ if err != nil {
+ t.Fatalf("didn't expect load private key block to fail, %v", err)
+ }
+
+ decrypted, err := mq.DecryptAsymmetric(encrypted)
+ if err != nil {
+ t.Fatalf("didn't expect decrypt asymmetric to fail, %v", err)
+ }
+
+ if !slices.Equal(decrypted, data) {
+ t.Errorf("expected decrypt to return %v, but got %v", data, decrypted)
+ }
+}
diff --git a/internal/mq/crypto_test.go b/internal/mq/crypto_test.go
index ae246df..cab9e5e 100644
--- a/internal/mq/crypto_test.go
+++ b/internal/mq/crypto_test.go
@@ -168,31 +168,3 @@ func TestEncryptAndDecryptCipher(t *testing.T) {
t.Errorf("expected decrypt to return %v, but got %v", data, plain)
}
}
-
-func TestEncryptAndDecryptAsymmetric(t *testing.T) {
- data := []byte("zero-monitor")
-
- err := mq.LoadAsymmetricBlock(true)
- if err != nil {
- t.Fatalf("didn't expect load public key block to fail, %v", err)
- }
-
- encrypted, err := mq.EncryptAsymmetric(data)
- if err != nil {
- t.Fatalf("didn't expect encrypt asymmetric to fail, %v", err)
- }
-
- err = mq.LoadAsymmetricBlock(false)
- if err != nil {
- t.Fatalf("didn't expect load private key block to fail, %v", err)
- }
-
- decrypted, err := mq.DecryptAsymmetric(encrypted)
- if err != nil {
- t.Fatalf("didn't expect decrypt asymmetric to fail, %v", err)
- }
-
- if !slices.Equal(decrypted, data) {
- t.Errorf("expected decrypt to return %v, but got %v", data, decrypted)
- }
-}
diff --git a/internal/mq/pub.go b/internal/mq/pub.go
index 079d15f..060a308 100644
--- a/internal/mq/pub.go
+++ b/internal/mq/pub.go
@@ -8,7 +8,8 @@ import (
"github.com/guackamolly/zero-monitor/internal/logging"
)
-func (s Socket) RegisterPublishers() {
+// Invite code is required if it's the first connection with master server.
+func (s Socket) RegisterPublishers(inviteCode string) {
pc := ExtractPublishContainer(s.ctx)
if pc == nil {
log.Fatalln("publish container hasn't been injected")
@@ -37,7 +38,7 @@ func (s Socket) RegisterPublishers() {
logging.LogDebug("(pub) handling topic: %d", topic)
switch topic {
case JoinNetwork:
- err = handleJoinNetworkResponse(s, m, pc.StartNodeStatsPolling, pc.GetCurrentNode)
+ err = handleJoinNetworkResponse(s, m, pc.StartNodeStatsPolling, pc.GetCurrentNode, inviteCode)
case AuthenticateNetwork:
err = handleAuthenticateNetworkResponse(s, m, pc.GetCurrentNode)
case UpdateNodeStatsPollDuration:
@@ -69,9 +70,10 @@ func handleJoinNetworkResponse(
m Msg,
start domain.StartNodeStatsPolling,
currentNode domain.GetCurrentNode,
+ inviteCode string,
) error {
if _, ok := m.Data.(RequiresAuthenticationResponse); ok {
- return handleRequiresAuthenticationResponse(s, currentNode)
+ return handleRequiresAuthenticationResponse(s, currentNode, inviteCode)
}
resp, ok := m.Data.(JoinNetworkResponse)
@@ -107,14 +109,20 @@ func handleAuthenticateNetworkResponse(
func handleRequiresAuthenticationResponse(
s Socket,
currentNode domain.GetCurrentNode,
+ inviteCode string,
) error {
// disallow handshaking more than once, otherwise both master and node will enter in a race condition like state
if handshaked {
logging.LogFatal("invalid state: already authenticated but master replied with
bash <(wget -qO- https://raw.githubusercontent.com/guackamolly/zero-monitor/refs/heads/master/.github/get-node) --node={{.InviteLink}}
- {{ else }}
- bash <(wget -qO- https://raw.githubusercontent.com/guackamolly/zero-monitor/refs/heads/master/.github/get-node) --node=
- {{ end }}
+ bash <(wget -qO- https://raw.githubusercontent.com/guackamolly/zero-monitor/refs/heads/master/.github/get-node)
+
+ curl -s -o %TEMP%\get-node.bat https://raw.githubusercontent.com/guackamolly/zero-monitor/refs/heads/master/.github/get-node.bat
"%TEMP%\get-node.bat"