From 32e186d502bbbd781148535f622069fe5d028d45 Mon Sep 17 00:00:00 2001 From: "hex@zorgo" Date: Fri, 1 Aug 2025 18:24:13 +0200 Subject: [PATCH 1/3] feat: boxy integration (todo: test) --- .env.example | 11 +++++++++++ main.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 7a4a794..75a0c00 100644 --- a/.env.example +++ b/.env.example @@ -32,3 +32,14 @@ POSTGRES_DSN= # Random secret. Recommended length is 64 characters at minimum. JWT_SECRET= + +# URL to Boxy's API. +# e.g. http://localhost:8001 +# without ending forward slash (/) +BOXY_URL= + +# Address by which Boxy can reach us. If it is not set, it will be the address with which we'll reach Boxy's API. +BOXY_CLIENT_ADDRESS= + +BOXY_CLIENT_NAME= +BOXY_CLIENT_SECRET= diff --git a/main.go b/main.go index bf5eb73..f9b08dd 100644 --- a/main.go +++ b/main.go @@ -18,11 +18,15 @@ package main import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "log" + "net/http" "os" + "strconv" "github.com/gin-gonic/gin" "github.com/joho/godotenv" @@ -56,6 +60,9 @@ func main() { databaseType := getEnv("DATABASE_TYPE", "sqlite") sqliteFile := getEnv("SQLITE_FILE", "stereo.db") + boxyApiUrl := getEnv("BOXY_URL", "") + port := getEnv("PORT", "8081") + imagePath := getEnv("IMAGE_PATH", os.TempDir()) if _, err := os.Stat(imagePath); err != nil { @@ -82,7 +89,7 @@ func main() { Router: gin.Default(), MinioClient: minioClient, Bucket: requireEnv("S3_BUCKET"), - Context: context.Background(), + Context: context.Background(), ImagePath: imagePath, Client: client.New( requireEnv("REDIRECT_URI"), @@ -122,7 +129,40 @@ func main() { c.Database.AutoMigrate(&auth.User{}, &types.File{}) + if boxyApiUrl != "" { + boxyClientName := requireEnv("BOXY_CLIENT_NAME") + boxyClientSecret := requireEnv("BOXY_CLIENT_SECRET") + boxyClientAddress := getEnv("BOXY_CLIENT_ADDRESS", "") + + rawBody := map[string]any{ + "port": port, + "hostname": c.Domain, + } + if boxyClientAddress != "" { + rawBody["address"] = boxyClientAddress + } + + jBody, err := json.Marshal(rawBody) + + if err != nil { + log.Fatal(err) + } + + req, err := http.NewRequest("POST", boxyApiUrl+"/register", bytes.NewBuffer(jBody)) + + req.SetBasicAuth(boxyClientName, boxyClientSecret) + + http.DefaultClient.Do(req) + + if req.Response.StatusCode != 200 { + log.Fatal("Could not register with Boxy.\nStatus Code: " + strconv.Itoa(req.Response.StatusCode)) + return + } + + log.Println("Successfully registered with Boxy.") + } + api.Register(&c) - fmt.Printf("Running on port %s\n", getEnv("PORT", "8081")) + fmt.Printf("Running on port %s\n", port) c.Router.Run() } From cad2855220bef069956acc545d7c4a9da24a60dc Mon Sep 17 00:00:00 2001 From: hex Date: Fri, 1 Aug 2025 18:43:03 +0200 Subject: [PATCH 2/3] fix: port should actually be used, use port as int in api request --- .env.example | 5 +++-- main.go | 15 ++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 75a0c00..5d0a467 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,9 @@ REDIRECT_URI=http://localhost:8081/api/auth/callback CLIENT_ID= CLIENT_SECRET= -# Listening port -PORT= +# Listen address (if the IP address is empty, it will default to 127.0.0.1) +# e.g. 127.0.0.1:8081 or :8081 +LISTEN_ADDRESS=:8081 # S3 S3_ENDPOINT= diff --git a/main.go b/main.go index f9b08dd..b0006bc 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "net/http" "os" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/joho/godotenv" @@ -61,7 +62,7 @@ func main() { databaseType := getEnv("DATABASE_TYPE", "sqlite") sqliteFile := getEnv("SQLITE_FILE", "stereo.db") boxyApiUrl := getEnv("BOXY_URL", "") - port := getEnv("PORT", "8081") + listenAddr := getEnv("LISTEN_ADDRESS", ":8081") imagePath := getEnv("IMAGE_PATH", os.TempDir()) @@ -134,6 +135,10 @@ func main() { boxyClientSecret := requireEnv("BOXY_CLIENT_SECRET") boxyClientAddress := getEnv("BOXY_CLIENT_ADDRESS", "") + port, err := strconv.Atoi(strings.Split(listenAddr, ":")[1]) + + fmt.Printf("Using port %d\n", port) + rawBody := map[string]any{ "port": port, "hostname": c.Domain, @@ -152,9 +157,9 @@ func main() { req.SetBasicAuth(boxyClientName, boxyClientSecret) - http.DefaultClient.Do(req) + resp, _ := http.DefaultClient.Do(req) - if req.Response.StatusCode != 200 { + if resp.StatusCode != 200 { log.Fatal("Could not register with Boxy.\nStatus Code: " + strconv.Itoa(req.Response.StatusCode)) return } @@ -163,6 +168,6 @@ func main() { } api.Register(&c) - fmt.Printf("Running on port %s\n", port) - c.Router.Run() + fmt.Printf("Listening on %s\n", listenAddr) + c.Router.Run(listenAddr) } From 8522e60af3feac4e92d80dda3ce648a94e1a929f Mon Sep 17 00:00:00 2001 From: hex Date: Sun, 3 Aug 2025 14:22:37 +0200 Subject: [PATCH 3/3] feat: basic boxy client --- internal/boxy/boxy.go | 97 +++++++++++++++++++++++++++++++++++++++++ internal/types/types.go | 2 + main.go | 38 +++++----------- 3 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 internal/boxy/boxy.go diff --git a/internal/boxy/boxy.go b/internal/boxy/boxy.go new file mode 100644 index 0000000..4d5ea92 --- /dev/null +++ b/internal/boxy/boxy.go @@ -0,0 +1,97 @@ +package boxy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strconv" +) + +type BoxyClient struct { + Name string + Secret string + ApiUrl string + Hostnames []string + + endpointId int +} + +func (b *BoxyClient) Register(address string, port int) error { + rawBody := map[string]any{ + "port": port, + "hostname": b.Hostnames, + } + + if address != "" { + rawBody["address"] = address + } + + jBody, err := json.Marshal(rawBody) + + if err != nil { + return err + } + + req, err := http.NewRequest("POST", b.ApiUrl+"/register", bytes.NewBuffer(jBody)) + + if err != nil { + return err + } + + req.SetBasicAuth(b.Name, b.Secret) + + resp, _ := http.DefaultClient.Do(req) + + if resp.StatusCode != 200 { + log.Fatal("Could not register with Boxy.\nStatus Code: " + strconv.Itoa(req.Response.StatusCode)) + } + + body, err := io.ReadAll(resp.Body) + + if err != nil { + return err + } + + b.endpointId, err = strconv.Atoi(string(body)) + + if err != nil { + return err + } + + return nil +} + +func (b *BoxyClient) Unregister() error { + rawBody := map[string]any{} + + jBody, err := json.Marshal(rawBody) + + if err != nil { + return err + } + + req, err := http.NewRequest("DELETE", b.ApiUrl+"/endpoint/"+strconv.Itoa(b.endpointId), bytes.NewBuffer(jBody)) + + if err != nil { + return err + } + + req.SetBasicAuth(b.Name, b.Secret) + + resp, _ := http.DefaultClient.Do(req) + + body, err := io.ReadAll(resp.Body) + + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return fmt.Errorf("Could not delete endpoint.\nBody: %s", string(body)) + } + + return nil +} diff --git a/internal/types/types.go b/internal/types/types.go index 0395fcb..aef1f95 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -26,6 +26,7 @@ import ( "github.com/minio/minio-go/v7" "gorm.io/gorm" "stereo.cat/backend/internal/auth/client" + "stereo.cat/backend/internal/boxy" ) type Route struct { @@ -45,6 +46,7 @@ type StereoConfig struct { FrontendUri string Domain string Context context.Context + BoxyClient boxy.BoxyClient } type File struct { diff --git a/main.go b/main.go index b0006bc..ec52dc5 100644 --- a/main.go +++ b/main.go @@ -18,13 +18,10 @@ package main import ( - "bytes" "context" - "encoding/json" "errors" "fmt" "log" - "net/http" "os" "strconv" "strings" @@ -39,6 +36,7 @@ import ( "stereo.cat/backend/internal/api" "stereo.cat/backend/internal/auth" "stereo.cat/backend/internal/auth/client" + "stereo.cat/backend/internal/boxy" "stereo.cat/backend/internal/types" ) @@ -137,34 +135,22 @@ func main() { port, err := strconv.Atoi(strings.Split(listenAddr, ":")[1]) - fmt.Printf("Using port %d\n", port) - - rawBody := map[string]any{ - "port": port, - "hostname": c.Domain, - } - if boxyClientAddress != "" { - rawBody["address"] = boxyClientAddress + if err != nil { + panic(err) } - jBody, err := json.Marshal(rawBody) + c.BoxyClient = boxy.BoxyClient{ + Name: boxyClientName, + Secret: boxyClientSecret, + ApiUrl: boxyApiUrl, + Hostnames: []string{c.Domain}, + } + + err = c.BoxyClient.Register(boxyClientAddress, port) if err != nil { - log.Fatal(err) + panic(err) } - - req, err := http.NewRequest("POST", boxyApiUrl+"/register", bytes.NewBuffer(jBody)) - - req.SetBasicAuth(boxyClientName, boxyClientSecret) - - resp, _ := http.DefaultClient.Do(req) - - if resp.StatusCode != 200 { - log.Fatal("Could not register with Boxy.\nStatus Code: " + strconv.Itoa(req.Response.StatusCode)) - return - } - - log.Println("Successfully registered with Boxy.") } api.Register(&c)