From 3576c218efe1144e6dbb87bc144cd3e804d9fc07 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Sun, 8 Mar 2026 16:46:58 +0000 Subject: [PATCH 01/10] Remove localhost --- Makefile | 4 +- config/gateway.yaml | 66 +-- dataplane/main.go | 170 ++++---- docker-compose.yml | 8 +- go.mod | 50 +-- go.sum | 106 ++--- management/auth/auth.go | 276 ++++++------ management/handler/file_handler.go | 374 ++++++++-------- management/main.go | 86 ++-- management/static/auth.js | 4 +- management/static/config.js | 8 +- management/static/gateway.yaml | 53 ++- src/config/config.go | 434 +++++++++---------- src/config/db.go | 668 ++++++++++++++--------------- src/config/init.sql | 96 ++--- src/config/types.go | 56 +-- src/main.go | 150 +++---- src/semantics/semantic_analysis.go | 494 ++++++++++----------- src/utils/defaults.go | 28 +- src/watcher/watcher.go | 276 ++++++------ test/configs/gateway/gateway.yaml | 38 +- test/configs/nginx/nginx.conf | 2 +- 22 files changed, 1731 insertions(+), 1716 deletions(-) diff --git a/Makefile b/Makefile index 2516a72..9514e74 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ COMPOSE = $(DOCKER) compose PROJECT_DIR = cmd MAIN = main.go -HOST = http://localhost:8080 +HOST = http://54.75.125.2:8080 WAIT_RETRIES=5 WAIT_INTERVAL=3 @@ -35,7 +35,7 @@ docker-stop: wait: @echo "Waiting for $(SERVICE) on port $(PORT)..." @for i in $(shell seq 1 $(WAIT_RETRIES)); do \ - if curl -s http://localhost:$(PORT)/healthz >/dev/null 2>&1; then \ + if curl -s http://54.75.125.2:$(PORT)/healthz >/dev/null 2>&1; then \ echo "$(SERVICE) is ready"; \ break; \ fi; \ diff --git a/config/gateway.yaml b/config/gateway.yaml index 84577bc..09bffbe 100644 --- a/config/gateway.yaml +++ b/config/gateway.yaml @@ -1,33 +1,33 @@ -# -# Configuration file for API Gateway -# - -connections: - routes: - - path: /products - url: http://services:9001 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: /orders - url: http://services:9002 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: /protected - url: http://services:9003 - rate-limit: - zone: 10 - rate: 5 - auth: true - - - path: /external-weather - url: https://api.open-meteo.com/v1/forecast?latitude=51.898&longitude=-8.4706&hourly=temperature_2m/ - rate-limit: - zone: 10 - rate: 5 - auth: false +# +# Configuration file for API Gateway +# + +connections: + routes: + - path: /products + url: http://services:9001 + rate-limit: + zone: 10 + rate: 5 + auth: false + + - path: /orders + url: http://services:9002 + rate-limit: + zone: 10 + rate: 5 + auth: false + + - path: /protected + url: http://services:9003 + rate-limit: + zone: 10 + rate: 5 + auth: true + + - path: /external-weather + url: https://api.open-meteo.com/v1/forecast?latitude=51.898&longitude=-8.4706&hourly=temperature_2m/ + rate-limit: + zone: 10 + rate: 5 + auth: false diff --git a/dataplane/main.go b/dataplane/main.go index 047dde9..b46a6fb 100644 --- a/dataplane/main.go +++ b/dataplane/main.go @@ -1,85 +1,85 @@ -package main - -import ( - "encoding/json" - "fmt" - "log/slog" - "net/http" - "os" - "os/exec" - "path/filepath" -) - -type Response struct { - Filename string `json:"filename"` - Body []byte `json:"body"` -} - -func main() { - mux := http.NewServeMux() - - mux.HandleFunc("/api/handle-config", handleNewConfig) - err := http.ListenAndServe(":1000", mux) - if err != nil { - slog.Error("Error starting HTTP server", "error", err) - } -} - -func handleNewConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("handling config update") - res := &Response{} - - if r.Method != "POST" { - slog.Error("Invalid request method", "method", r.Method) - return - } - - err := json.NewDecoder(r.Body).Decode(&res) - if err != nil { - slog.Error("Error unmarshalling request body", "error", err) - return - } - - dir := filepath.Dir(res.Filename) - err = os.MkdirAll(dir, 0644) - if err != nil { - slog.Error("Error creating directory", "error", err) - return - } - - file, err := os.Create(res.Filename) - if err != nil { - slog.Error("Error creating file", "filename", res.Filename) - return - } - - _, err = file.Write(res.Body) - if err != nil { - slog.Error("Error writing to file", "filename", res.Filename) - return - } - - err = applyNginxConfig() - if err != nil { - slog.Error("Error applying nginx config", "error", err, "filename", res.Filename) - return - } -} - -func applyNginxConfig() error { - cmd := exec.Command("nginx", "-t") - - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("nginx config test failed: %s", string(output)) - } - - cmd = exec.Command("nginx", "-s", "reload") - - output, err = cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("nginx reload failed: %s", string(output)) - } - - return nil -} +package main + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "os/exec" + "path/filepath" +) + +type Response struct { + Filename string `json:"filename"` + Body []byte `json:"body"` +} + +func main() { + mux := http.NewServeMux() + + mux.HandleFunc("/api/handle-config", handleNewConfig) + err := http.ListenAndServe(":1000", mux) + if err != nil { + slog.Error("Error starting HTTP server", "error", err) + } +} + +func handleNewConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("handling config update") + res := &Response{} + + if r.Method != "POST" { + slog.Error("Invalid request method", "method", r.Method) + return + } + + err := json.NewDecoder(r.Body).Decode(&res) + if err != nil { + slog.Error("Error unmarshalling request body", "error", err) + return + } + + dir := filepath.Dir(res.Filename) + err = os.MkdirAll(dir, 0644) + if err != nil { + slog.Error("Error creating directory", "error", err) + return + } + + file, err := os.Create(res.Filename) + if err != nil { + slog.Error("Error creating file", "filename", res.Filename) + return + } + + _, err = file.Write(res.Body) + if err != nil { + slog.Error("Error writing to file", "filename", res.Filename) + return + } + + err = applyNginxConfig() + if err != nil { + slog.Error("Error applying nginx config", "error", err, "filename", res.Filename) + return + } +} + +func applyNginxConfig() error { + cmd := exec.Command("nginx", "-t") + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("nginx config test failed: %s", string(output)) + } + + cmd = exec.Command("nginx", "-s", "reload") + + output, err = cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("nginx reload failed: %s", string(output)) + } + + return nil +} diff --git a/docker-compose.yml b/docker-compose.yml index c9e568e..370b645 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,7 @@ services: depends_on: - minio healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://54.75.125.2:3100/ready || exit 1" ] interval: 10s timeout: 5s retries: 5 @@ -110,7 +110,7 @@ services: volumes: - ./src/collector/configs/loki-config.yaml:/etc/loki/config.yaml healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://54.75.125.2:3100/ready || exit 1" ] interval: 10s timeout: 5s retries: 5 @@ -151,7 +151,7 @@ services: volumes: - ./.data/minio:/data healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ] + test: [ "CMD", "curl", "-f", "http://54.75.125.2:9000/minio/health/live" ] interval: 15s timeout: 20s retries: 5 @@ -187,7 +187,7 @@ services: ports: - "3000:3000" healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://54.75.125.2:3000/api/health || exit 1" ] interval: 10s timeout: 5s retries: 5 diff --git a/go.mod b/go.mod index a99c1e9..c836fec 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,25 @@ -module fyp-api-gateway - -go 1.24.11 - -require ( - github.com/fsnotify/fsnotify v1.9.0 - github.com/stretchr/testify v1.11.1 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect -) +module fyp-api-gateway + +go 1.24.11 + +require ( + github.com/fsnotify/fsnotify v1.9.0 + github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum index e5324cd..a88f582 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,53 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/management/auth/auth.go b/management/auth/auth.go index 4e96257..b505e40 100644 --- a/management/auth/auth.go +++ b/management/auth/auth.go @@ -1,138 +1,138 @@ -package auth - -import ( - "bytes" - "encoding/json" - "log/slog" - "net/http" - "time" -) - -type UserInfo struct { - Name string `json:"name"` - Password string `json:"password"` -} - -/* -Receive the login details from auth.js -Decode them and format them as `UserInfo` to send to the control plane -Control plane validates the user is in the database and returns a cookie -*/ -func Login(w http.ResponseWriter, r *http.Request) { - slog.Info("received login request") - loginInfo := UserInfo{} - - if r.Method != "POST" { - slog.Error("method not allowed", "method", r.Method) - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - err := json.NewDecoder(r.Body).Decode(&loginInfo) - if err != nil { - slog.Error("error decoding login info", "error", err) - return - } - - body, err := json.Marshal(loginInfo) - if err != nil { - slog.Error("error encoding login info", "error", err) - return - } - - res, err := http.Post( - "http://control-plane:10000/verify-login", - "application/json", - bytes.NewBuffer(body), - ) - - if res.StatusCode != http.StatusOK { - slog.Error("error verifying login", "error", res.Status) - } else { - slog.Info("verified login") - } - - var resp struct { - SessionId string `json:"sessionId"` - } - err = json.NewDecoder(res.Body).Decode(&resp) - if err != nil { - slog.Error("error decoding response", "error", err) - return - } - - http.SetCookie(w, &http.Cookie{ - Name: "session", - Value: resp.SessionId, - HttpOnly: true, - Path: "/", - Expires: time.Now().Add(24 * time.Hour), - }) - - w.WriteHeader(http.StatusOK) -} - -func Signup(w http.ResponseWriter, r *http.Request) { - slog.Info("received signup request") - loginInfo := UserInfo{} - - if r.Method != "POST" { - slog.Error("method not allowed", "method", r.Method) - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - err := json.NewDecoder(r.Body).Decode(&loginInfo) - if err != nil { - slog.Error("error decoding signup info", "error", err) - return - } - - body, err := json.Marshal(loginInfo) - if err != nil { - slog.Error("error encoding signup info", "error", err) - return - } - - res, err := http.Post( - "http://control-plane:10000/verify-signup", - "application/json", - bytes.NewBuffer(body), - ) - - if res.StatusCode != http.StatusOK { - slog.Error("error verifying signup", "error", res.Status) - } else { - slog.Info("verified signup") - } - - w.WriteHeader(http.StatusOK) -} - -func RequireSession(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("error getting cookie", "error", err) - http.Redirect(w, r, "/auth", http.StatusSeeOther) - return - } - - req, _ := http.NewRequest( - "GET", - "http://control-plane:10000/validate-session", - nil, - ) - req.Header.Set("X-Session-ID", cookie.Value) - - res, err := http.DefaultClient.Do(req) - if err != nil || res.StatusCode != http.StatusOK { - slog.Error("error validating session", "error", err, "code", res.StatusCode) - http.Redirect(w, r, "/auth", http.StatusSeeOther) - return - } - - next(w, r) - } -} +package auth + +import ( + "bytes" + "encoding/json" + "log/slog" + "net/http" + "time" +) + +type UserInfo struct { + Name string `json:"name"` + Password string `json:"password"` +} + +/* +Receive the login details from auth.js +Decode them and format them as `UserInfo` to send to the control plane +Control plane validates the user is in the database and returns a cookie +*/ +func Login(w http.ResponseWriter, r *http.Request) { + slog.Info("received login request") + loginInfo := UserInfo{} + + if r.Method != "POST" { + slog.Error("method not allowed", "method", r.Method) + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + err := json.NewDecoder(r.Body).Decode(&loginInfo) + if err != nil { + slog.Error("error decoding login info", "error", err) + return + } + + body, err := json.Marshal(loginInfo) + if err != nil { + slog.Error("error encoding login info", "error", err) + return + } + + res, err := http.Post( + "http://control-plane:10000/verify-login", + "application/json", + bytes.NewBuffer(body), + ) + + if res.StatusCode != http.StatusOK { + slog.Error("error verifying login", "error", res.Status) + } else { + slog.Info("verified login") + } + + var resp struct { + SessionId string `json:"sessionId"` + } + err = json.NewDecoder(res.Body).Decode(&resp) + if err != nil { + slog.Error("error decoding response", "error", err) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: resp.SessionId, + HttpOnly: true, + Path: "/", + Expires: time.Now().Add(24 * time.Hour), + }) + + w.WriteHeader(http.StatusOK) +} + +func Signup(w http.ResponseWriter, r *http.Request) { + slog.Info("received signup request") + loginInfo := UserInfo{} + + if r.Method != "POST" { + slog.Error("method not allowed", "method", r.Method) + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + err := json.NewDecoder(r.Body).Decode(&loginInfo) + if err != nil { + slog.Error("error decoding signup info", "error", err) + return + } + + body, err := json.Marshal(loginInfo) + if err != nil { + slog.Error("error encoding signup info", "error", err) + return + } + + res, err := http.Post( + "http://control-plane:10000/verify-signup", + "application/json", + bytes.NewBuffer(body), + ) + + if res.StatusCode != http.StatusOK { + slog.Error("error verifying signup", "error", res.Status) + } else { + slog.Info("verified signup") + } + + w.WriteHeader(http.StatusOK) +} + +func RequireSession(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("error getting cookie", "error", err) + http.Redirect(w, r, "/auth", http.StatusSeeOther) + return + } + + req, _ := http.NewRequest( + "GET", + "http://control-plane:10000/validate-session", + nil, + ) + req.Header.Set("X-Session-ID", cookie.Value) + + res, err := http.DefaultClient.Do(req) + if err != nil || res.StatusCode != http.StatusOK { + slog.Error("error validating session", "error", err, "code", res.StatusCode) + http.Redirect(w, r, "/auth", http.StatusSeeOther) + return + } + + next(w, r) + } +} diff --git a/management/handler/file_handler.go b/management/handler/file_handler.go index 7f3f694..d3cff6f 100644 --- a/management/handler/file_handler.go +++ b/management/handler/file_handler.go @@ -1,187 +1,187 @@ -package handler - -import ( - "bytes" - "encoding/json" - "io" - "log/slog" - "net/http" -) - -type ConfigRequest struct { - Content string `json:"content"` -} -type FindingsResponse struct { - Errors []string `json:"errors"` - Updates []string `json:"updates"` -} - -var findings FindingsResponse // TODO: This is cowboy code - -func Gateway(w http.ResponseWriter, r *http.Request) { - // get the session so we know what user is requesting their config - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("Error getting session id", "error", err) - return - } - - // Create a request object, don't send yet - req, err := http.NewRequest( - "POST", - "http://control-plane:10000/api/gateway", - nil, - ) - if err != nil { - slog.Error("error creating request to send to control plane", "error", err) - return - } - - req.Header.Set("Cookie", "session="+cookie.Value) - - // send the request - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - slog.Error("error sending request to control plane", "error", err) - return - } - defer resp.Body.Close() - - // display the file - // set the content-type to the same as resp from control plane - w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) - w.WriteHeader(resp.StatusCode) - - _, err = io.Copy(w, resp.Body) - if err != nil { - slog.Error("error copying response body", "error", err) - return - } -} - -func HandleNewConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("request successfully reached file handler") - configRequest := ConfigRequest{} - - if r.Method != "POST" { - slog.Error("Method not allowed", "method", r.Method) - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - err := json.NewDecoder(r.Body).Decode(&configRequest) - if err != nil { - slog.Error("Error unmarshalling request body", "error", err) - return - } - - _, err = submitConfig([]byte(configRequest.Content), r) - if err != nil { - slog.Error("Error submitting config", "error", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } -} - -func submitConfig(cfg []byte, r *http.Request) (*http.Response, error) { - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("Error getting session id", "error", err) - return nil, err - } - - req, err := http.NewRequest( - "POST", - "http://control-plane:10000/analyse", - bytes.NewBuffer(cfg), - ) - if err != nil { - slog.Error("error creating request to send to control plane", "error", err) - return nil, err - } - - req.Header.Set("Cookie", "session="+cookie.Value) - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - slog.Error("error sending request to control plane", "error", err) - return nil, err - } - defer resp.Body.Close() - - return resp, nil -} - -func RecvFindings(w http.ResponseWriter, r *http.Request) { - slog.Info("received config from control plane") - - err := json.NewDecoder(r.Body).Decode(&findings) - if err != nil { - slog.Error("Error unmarshalling request body", "error", err) - return - } -} - -func Findings(w http.ResponseWriter, r *http.Request) { - // send the findings to the front end - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(findings) - if err != nil { - slog.Error("Error marshalling request body", "error", err) - return - } -} - -func HandleAcceptChanges(w http.ResponseWriter, r *http.Request) { - slog.Info("user has accepted the changes to the config file") - configRequest := ConfigRequest{} - - if r.Method != "POST" { - slog.Error("Method not allowed", "method", r.Method) - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("Error getting session id", "error", err) - return - } - - err = json.NewDecoder(r.Body).Decode(&configRequest) - if err != nil { - slog.Error("Error unmarshalling request body", "error", err) - return - } - - configRequestObj, err := json.Marshal(configRequest) - if err != nil { - slog.Error("Error marshalling request body", "error", err) - return - } - - req, err := http.NewRequest( - "POST", - "http://control-plane:10000/config/update", - bytes.NewBuffer(configRequestObj), - ) - if err != nil { - slog.Error("error creating request to send to control plane", "error", err) - return - } - - req.Header.Set("Cookie", "session="+cookie.Value) - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - slog.Error("error sending request to control plane", "error", err) - return - } - defer resp.Body.Close() - - w.WriteHeader(http.StatusOK) -} +package handler + +import ( + "bytes" + "encoding/json" + "io" + "log/slog" + "net/http" +) + +type ConfigRequest struct { + Content string `json:"content"` +} +type FindingsResponse struct { + Errors []string `json:"errors"` + Updates []string `json:"updates"` +} + +var findings FindingsResponse // TODO: This is cowboy code + +func Gateway(w http.ResponseWriter, r *http.Request) { + // get the session so we know what user is requesting their config + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("Error getting session id", "error", err) + return + } + + // Create a request object, don't send yet + req, err := http.NewRequest( + "POST", + "http://control-plane:10000/api/gateway", + nil, + ) + if err != nil { + slog.Error("error creating request to send to control plane", "error", err) + return + } + + req.Header.Set("Cookie", "session="+cookie.Value) + + // send the request + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + slog.Error("error sending request to control plane", "error", err) + return + } + defer resp.Body.Close() + + // display the file + // set the content-type to the same as resp from control plane + w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + w.WriteHeader(resp.StatusCode) + + _, err = io.Copy(w, resp.Body) + if err != nil { + slog.Error("error copying response body", "error", err) + return + } +} + +func HandleNewConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("request successfully reached file handler") + configRequest := ConfigRequest{} + + if r.Method != "POST" { + slog.Error("Method not allowed", "method", r.Method) + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + err := json.NewDecoder(r.Body).Decode(&configRequest) + if err != nil { + slog.Error("Error unmarshalling request body", "error", err) + return + } + + _, err = submitConfig([]byte(configRequest.Content), r) + if err != nil { + slog.Error("Error submitting config", "error", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } +} + +func submitConfig(cfg []byte, r *http.Request) (*http.Response, error) { + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("Error getting session id", "error", err) + return nil, err + } + + req, err := http.NewRequest( + "POST", + "http://control-plane:10000/analyse", + bytes.NewBuffer(cfg), + ) + if err != nil { + slog.Error("error creating request to send to control plane", "error", err) + return nil, err + } + + req.Header.Set("Cookie", "session="+cookie.Value) + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + slog.Error("error sending request to control plane", "error", err) + return nil, err + } + defer resp.Body.Close() + + return resp, nil +} + +func RecvFindings(w http.ResponseWriter, r *http.Request) { + slog.Info("received config from control plane") + + err := json.NewDecoder(r.Body).Decode(&findings) + if err != nil { + slog.Error("Error unmarshalling request body", "error", err) + return + } +} + +func Findings(w http.ResponseWriter, r *http.Request) { + // send the findings to the front end + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(findings) + if err != nil { + slog.Error("Error marshalling request body", "error", err) + return + } +} + +func HandleAcceptChanges(w http.ResponseWriter, r *http.Request) { + slog.Info("user has accepted the changes to the config file") + configRequest := ConfigRequest{} + + if r.Method != "POST" { + slog.Error("Method not allowed", "method", r.Method) + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("Error getting session id", "error", err) + return + } + + err = json.NewDecoder(r.Body).Decode(&configRequest) + if err != nil { + slog.Error("Error unmarshalling request body", "error", err) + return + } + + configRequestObj, err := json.Marshal(configRequest) + if err != nil { + slog.Error("Error marshalling request body", "error", err) + return + } + + req, err := http.NewRequest( + "POST", + "http://control-plane:10000/config/update", + bytes.NewBuffer(configRequestObj), + ) + if err != nil { + slog.Error("error creating request to send to control plane", "error", err) + return + } + + req.Header.Set("Cookie", "session="+cookie.Value) + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + slog.Error("error sending request to control plane", "error", err) + return + } + defer resp.Body.Close() + + w.WriteHeader(http.StatusOK) +} diff --git a/management/main.go b/management/main.go index c032a3d..42ae43c 100644 --- a/management/main.go +++ b/management/main.go @@ -1,43 +1,43 @@ -package main - -import ( - "fyp-api-gateway/management/auth" - "fyp-api-gateway/management/handler" - "log/slog" - "net/http" -) - -func main() { - mux := http.NewServeMux() - - // frontend routes - mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/auth", http.StatusSeeOther) - }) - mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/auth.html") - }) - mux.HandleFunc("/config", auth.RequireSession(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/config.html") - })) - mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/index.html") - }) - - // account initialisation routes - mux.HandleFunc("/api/signup", auth.Signup) - mux.HandleFunc("/api/login", auth.Login) - mux.HandleFunc("/file/gateway", auth.RequireSession(handler.Gateway)) - - // config routes - mux.HandleFunc("/file/upload", handler.HandleNewConfig) - mux.HandleFunc("/file/findings", handler.RecvFindings) - mux.HandleFunc("/file/retrieve", handler.Findings) - mux.HandleFunc("/file/accept", handler.HandleAcceptChanges) - - err := http.ListenAndServe(":80", mux) - if err != nil { - slog.Error("could not start management plane", "error", err) - } -} +package main + +import ( + "fyp-api-gateway/management/auth" + "fyp-api-gateway/management/handler" + "log/slog" + "net/http" +) + +func main() { + mux := http.NewServeMux() + + // frontend routes + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/auth", http.StatusSeeOther) + }) + mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/auth.html") + }) + mux.HandleFunc("/config", auth.RequireSession(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/config.html") + })) + mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/index.html") + }) + + // account initialisation routes + mux.HandleFunc("/api/signup", auth.Signup) + mux.HandleFunc("/api/login", auth.Login) + mux.HandleFunc("/file/gateway", auth.RequireSession(handler.Gateway)) + + // config routes + mux.HandleFunc("/file/upload", handler.HandleNewConfig) + mux.HandleFunc("/file/findings", handler.RecvFindings) + mux.HandleFunc("/file/retrieve", handler.Findings) + mux.HandleFunc("/file/accept", handler.HandleAcceptChanges) + + err := http.ListenAndServe(":80", mux) + if err != nil { + slog.Error("could not start management plane", "error", err) + } +} diff --git a/management/static/auth.js b/management/static/auth.js index 127ed86..5eff4b6 100644 --- a/management/static/auth.js +++ b/management/static/auth.js @@ -21,7 +21,7 @@ document.getElementById("loginForm").addEventListener("submit", function (e) { const name = document.getElementById("name").value; const password = document.getElementById("password").value; - sendLoginData(name, password, "http://localhost:80/api/login"); + sendLoginData(name, password, "http://54.75.125.2:80/api/login"); }); document.getElementById("signupForm").addEventListener("submit", function(e){ @@ -30,5 +30,5 @@ document.getElementById("signupForm").addEventListener("submit", function(e){ const name = document.getElementById("signupName").value; const password = document.getElementById("signupPassword").value; - sendLoginData(name, password, "http://localhost:80/api/signup"); + sendLoginData(name, password, "http://54.75.125.2:80/api/signup"); }); \ No newline at end of file diff --git a/management/static/config.js b/management/static/config.js index b8c9aaf..f0e5fd8 100644 --- a/management/static/config.js +++ b/management/static/config.js @@ -1,10 +1,10 @@ const editor = document.getElementById("editor"); const uploadBtn = document.getElementById("saveBtn"); -const FILE_URL = "http://localhost:80/file/gateway"; -const SAVE_URL = "http://localhost:80/file/upload"; -const FINDINGS_URL = "http://localhost:80/file/retrieve"; -const ACCEPT_URL = "http://localhost:80/file/accept"; +const FILE_URL = "http://54.75.125.2:80/file/gateway"; +const SAVE_URL = "http://54.75.125.2:80/file/upload"; +const FINDINGS_URL = "http://54.75.125.2:80/file/retrieve"; +const ACCEPT_URL = "http://54.75.125.2:80/file/accept"; window.addEventListener("DOMContentLoaded", async () => { try { diff --git a/management/static/gateway.yaml b/management/static/gateway.yaml index d84650f..09bffbe 100644 --- a/management/static/gateway.yaml +++ b/management/static/gateway.yaml @@ -3,32 +3,31 @@ # connections: - - host: localhost - port: 8080 - routes: - - path: /products - upstream: - name: product_service - port: 9001 - rate-limit: - zone: 10 - rate: 5 - auth: false + routes: + - path: /products + url: http://services:9001 + rate-limit: + zone: 10 + rate: 5 + auth: false - - path: /orders - upstream: - name: order_service - port: 9002 - rate-limit: - zone: 10 - rate: 5 - auth: false + - path: /orders + url: http://services:9002 + rate-limit: + zone: 10 + rate: 5 + auth: false - - path: /protected - upstream: - name: protected_service - port: 9003 - rate-limit: - zone: 10 - rate: 5 - auth: true \ No newline at end of file + - path: /protected + url: http://services:9003 + rate-limit: + zone: 10 + rate: 5 + auth: true + + - path: /external-weather + url: https://api.open-meteo.com/v1/forecast?latitude=51.898&longitude=-8.4706&hourly=temperature_2m/ + rate-limit: + zone: 10 + rate: 5 + auth: false diff --git a/src/config/config.go b/src/config/config.go index 7b268ae..23e2eaf 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -1,217 +1,217 @@ -package config - -import ( - "bytes" - "encoding/json" - "fyp-api-gateway/src/utils" - "io" - "log/slog" - "net/http" - "os" - "strings" - "text/template" - - "gopkg.in/yaml.v3" -) - -type ConfRequest struct { - Content string `json:"content"` -} - -func InitUserNGINX(username string) error { - // load the default config - gatewayConf, err := loadAndValidateGatewayConf(utils.DefaultConfigContent) - if err != nil { - slog.Error("failed to load and validate gateway config", "error", err) - return err - } - - nginxUserConfDir := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName - - // create new user nginx file - _, err = os.Stat(nginxUserConfDir) - if err == nil { - slog.Error("NGINX config file already exists") - return err - } - - err = os.MkdirAll(utils.NGINXDirName+"users/"+username, 0644) - if err != nil { - slog.Error("failed creating users directory", "error", err) - return err - } - - _, err = os.Create(nginxUserConfDir) - if err != nil { - slog.Error("failed creating users config", "error", err) - return err - } - - err = renderNginxTemplate(gatewayConf, nginxUserConfDir) - if err != nil { - slog.Error("failed rendering NGINX template", "error", err) - return err - } - - return nil -} - -func loadAndValidateGatewayConf(body string) (*GatewayConfig, error) { - var config GatewayConfig - - decoder := yaml.NewDecoder(bytes.NewReader([]byte(body))) - if err := decoder.Decode(&config); err != nil { - return nil, err - } - - return &config, nil -} - -func renderNginxTemplate(gatewayCfg *GatewayConfig, nginxUserConfDir string) error { - renderModel := buildRenderModel(gatewayCfg) - - // load the template file - tmpl, err := template.ParseFiles(utils.NGINXTemplateDirName + utils.NGINXTemplateFileName) - if err != nil { - return err - } - - // Create the updated NGINX config file and save it into the file containing the current config - var buf bytes.Buffer - if err = tmpl.Execute(&buf, renderModel); err != nil { - slog.Error("Error executing template: ", "error", err) - return err - } - nginxString := buf.String() - - // write the file - err = os.WriteFile(nginxUserConfDir, []byte(nginxString), 0644) - if err != nil { - return err - } - - return nil -} - -func buildRenderModel(gw *GatewayConfig) RenderModel { - model := RenderModel{} - - c := gw.Connections - conn := Connection{} - - for _, r := range c.Routes { - zoneName := strings.ReplaceAll(r.Path, "/", "_") - // if path is just "/", fallback - if zoneName == "" { - zoneName = "root" - } - - conn.Routes = append(conn.Routes, Routes{ - Path: r.Path, - Url: r.Url, - Auth: r.Auth, - RateLimit: r.RateLimit, - ZoneName: zoneName, - }) - } - - model.Connections = append(model.Connections, conn) - - return model -} - -func LoadNewConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("loading new gateway config file") - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("session cookie not found in request", "error", err) - http.Error(w, "cookie not found in request", http.StatusUnauthorized) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - slog.Error("failed to read request body", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer func() { - err = r.Body.Close() - if err != nil { - slog.Error("failed to close request body", "error", err) - http.Error(w, "failed to close request body", http.StatusInternalServerError) - return - } - }() - - var req ConfRequest - if err = json.Unmarshal(body, &req); err != nil { - slog.Error("failed to unmarshal request", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - err = InsertNewConfig(cookie.Value, req.Content) - if err != nil { - slog.Error("failed to insert new config", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - username := RetrieveUserBySessionId(cookie.Value) - nginxUserConfPath := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName - - gatewayConf, err := loadAndValidateGatewayConf(req.Content) - if err != nil { - slog.Error("failed to validate gateway config", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - err = renderNginxTemplate(gatewayConf, nginxUserConfPath) - if err != nil { - slog.Error("failed to render NGINX template", "error", err) - http.Error(w, "failed to render NGINX template", http.StatusInternalServerError) - return - } - - // Atomic writes - tempFile, err := os.CreateTemp(nginxUserConfPath, "nginx-*.conf") - if err != nil { - slog.Error("failed creating temp file", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - defer os.Remove(tempFile.Name()) - - if _, err = tempFile.Write(body); err != nil { - slog.Error("failed writing temp config", "error", err) - tempFile.Close() - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - if err = tempFile.Close(); err != nil { - slog.Error("failed closing temp file", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - // Atomic replace - if err := os.Rename(tempFile.Name(), nginxUserConfPath); err != nil { - slog.Error("failed replacing config file", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - slog.Info("gateway config updated successfully", "path", nginxUserConfPath) - - w.WriteHeader(http.StatusOK) -} +package config + +import ( + "bytes" + "encoding/json" + "fyp-api-gateway/src/utils" + "io" + "log/slog" + "net/http" + "os" + "strings" + "text/template" + + "gopkg.in/yaml.v3" +) + +type ConfRequest struct { + Content string `json:"content"` +} + +func InitUserNGINX(username string) error { + // load the default config + gatewayConf, err := loadAndValidateGatewayConf(utils.DefaultConfigContent) + if err != nil { + slog.Error("failed to load and validate gateway config", "error", err) + return err + } + + nginxUserConfDir := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName + + // create new user nginx file + _, err = os.Stat(nginxUserConfDir) + if err == nil { + slog.Error("NGINX config file already exists") + return err + } + + err = os.MkdirAll(utils.NGINXDirName+"users/"+username, 0644) + if err != nil { + slog.Error("failed creating users directory", "error", err) + return err + } + + _, err = os.Create(nginxUserConfDir) + if err != nil { + slog.Error("failed creating users config", "error", err) + return err + } + + err = renderNginxTemplate(gatewayConf, nginxUserConfDir) + if err != nil { + slog.Error("failed rendering NGINX template", "error", err) + return err + } + + return nil +} + +func loadAndValidateGatewayConf(body string) (*GatewayConfig, error) { + var config GatewayConfig + + decoder := yaml.NewDecoder(bytes.NewReader([]byte(body))) + if err := decoder.Decode(&config); err != nil { + return nil, err + } + + return &config, nil +} + +func renderNginxTemplate(gatewayCfg *GatewayConfig, nginxUserConfDir string) error { + renderModel := buildRenderModel(gatewayCfg) + + // load the template file + tmpl, err := template.ParseFiles(utils.NGINXTemplateDirName + utils.NGINXTemplateFileName) + if err != nil { + return err + } + + // Create the updated NGINX config file and save it into the file containing the current config + var buf bytes.Buffer + if err = tmpl.Execute(&buf, renderModel); err != nil { + slog.Error("Error executing template: ", "error", err) + return err + } + nginxString := buf.String() + + // write the file + err = os.WriteFile(nginxUserConfDir, []byte(nginxString), 0644) + if err != nil { + return err + } + + return nil +} + +func buildRenderModel(gw *GatewayConfig) RenderModel { + model := RenderModel{} + + c := gw.Connections + conn := Connection{} + + for _, r := range c.Routes { + zoneName := strings.ReplaceAll(r.Path, "/", "_") + // if path is just "/", fallback + if zoneName == "" { + zoneName = "root" + } + + conn.Routes = append(conn.Routes, Routes{ + Path: r.Path, + Url: r.Url, + Auth: r.Auth, + RateLimit: r.RateLimit, + ZoneName: zoneName, + }) + } + + model.Connections = append(model.Connections, conn) + + return model +} + +func LoadNewConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("loading new gateway config file") + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("session cookie not found in request", "error", err) + http.Error(w, "cookie not found in request", http.StatusUnauthorized) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + slog.Error("failed to read request body", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer func() { + err = r.Body.Close() + if err != nil { + slog.Error("failed to close request body", "error", err) + http.Error(w, "failed to close request body", http.StatusInternalServerError) + return + } + }() + + var req ConfRequest + if err = json.Unmarshal(body, &req); err != nil { + slog.Error("failed to unmarshal request", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + err = InsertNewConfig(cookie.Value, req.Content) + if err != nil { + slog.Error("failed to insert new config", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + username := RetrieveUserBySessionId(cookie.Value) + nginxUserConfPath := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName + + gatewayConf, err := loadAndValidateGatewayConf(req.Content) + if err != nil { + slog.Error("failed to validate gateway config", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + err = renderNginxTemplate(gatewayConf, nginxUserConfPath) + if err != nil { + slog.Error("failed to render NGINX template", "error", err) + http.Error(w, "failed to render NGINX template", http.StatusInternalServerError) + return + } + + // Atomic writes + tempFile, err := os.CreateTemp(nginxUserConfPath, "nginx-*.conf") + if err != nil { + slog.Error("failed creating temp file", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + defer os.Remove(tempFile.Name()) + + if _, err = tempFile.Write(body); err != nil { + slog.Error("failed writing temp config", "error", err) + tempFile.Close() + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + if err = tempFile.Close(); err != nil { + slog.Error("failed closing temp file", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Atomic replace + if err := os.Rename(tempFile.Name(), nginxUserConfPath); err != nil { + slog.Error("failed replacing config file", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + slog.Info("gateway config updated successfully", "path", nginxUserConfPath) + + w.WriteHeader(http.StatusOK) +} diff --git a/src/config/db.go b/src/config/db.go index e6c5dc7..62f7948 100644 --- a/src/config/db.go +++ b/src/config/db.go @@ -1,334 +1,334 @@ -package config - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fyp-api-gateway/src/utils" - "log/slog" - "net/http" - "os" - "time" - - uuid "github.com/google/uuid" - _ "github.com/jackc/pgx/v5/stdlib" -) - -type Database struct { - Conn *sql.DB -} - -type Server struct { - DB *Database -} - -type LoginInfo struct { - Name string `json:"name"` - Password string `json:"password"` -} - -func NewDatabase(dsn string) (*Database, error) { - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return nil, err - } - - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(25) - db.SetConnMaxLifetime(5 * time.Minute) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err = db.PingContext(ctx); err != nil { - return nil, err - } - - return &Database{Conn: db}, nil -} - -func (d *Database) StartDB(path string) error { - content, err := os.ReadFile(path) - if err != nil { - slog.Error("error reading database file", "error", err) - return err - } - - if _, err = d.Conn.Exec(string(content)); err != nil { - slog.Error("error executing database statement", "error", err) - return err - } - - return nil -} - -func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { - slog.Info("attempting to sign up new user...") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var id string - err := s.DB.Conn.QueryRow( - "SELECT id FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&id) - - if err == nil || id != "" { - slog.Error("error querying user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - // TODO: Store password hash - _, err = s.DB.Conn.Exec(` - INSERT INTO users (username, password, config_yaml) - VALUES ($1, $2, $3);`, - loginInfo.Name, loginInfo.Password, utils.DefaultConfigContent, - ) - if err != nil { - slog.Error("error inserting user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - err = InitUserNGINX(loginInfo.Name) - if err != nil { - slog.Error("error initializing user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -/* -Receive the login info from the management plane and decode it -Check the password used is the same as the one in the database, also check the username is there -If the user has no session, create one and send it back, otherwise return the existing session -*/ -func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { - slog.Info("validating login information") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var storedHash string - err := s.DB.Conn.QueryRow( - "SELECT password FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&storedHash) - - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "username", loginInfo.Name) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - //err = bcrypt.CompareHashAndPassword( - // []byte(storedHash), - // []byte(loginInfo.password), - //) - - // TODO: create a proper hashed password - if storedHash != loginInfo.Password { - err = errors.New("invalid credentials") - } - - if err != nil { - slog.Error("error verifying loginInfo", "error", err) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - // check is the user has a session already - var sessionId string - sessionId, isSession := s.sessionExists(loginInfo.Name) - - if !isSession { - sessionId, err = s.createSession(loginInfo.Name) - if err != nil { - slog.Error("error creating session", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "sessionId": sessionId, - }) -} - -func (s *Server) sessionExists(username string) (string, bool) { - var sessionId string - err := s.DB.Conn.QueryRow( - "SELECT id FROM sessions WHERE username=$1", - username, - ).Scan(&sessionId) - - if err != nil { - return "", false - } - return sessionId, true -} - -func (s *Server) createSession(name string) (string, error) { - sessionId := uuid.New().String() - expires := time.Now().Add(24 * time.Hour) - - _, err := s.DB.Conn.Exec( - "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", - sessionId, name, expires, - ) - - if err != nil { - slog.Error("error creating session", "error", err) - return "", err - } - - return sessionId, nil -} - -func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { - slog.Info("validating user session") - sessionId := r.Header.Get("X-Session-ID") - - var username string - err := s.DB.Conn.QueryRow( - "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", - sessionId, - ).Scan(&username) - - if err != nil { - slog.Error("error querying session", "error", err) - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("received request for user config") - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("failed getting session id", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sessionId := cookie.Value - - var gatewayCfg string - err = s.DB.Conn.QueryRow(` - SELECT u.config_yaml - FROM users AS u - JOIN sessions AS s ON u.username = s.username - WHERE s.id = $1 AND s.expires > NOW()`, - sessionId).Scan(&gatewayCfg) - - if err != nil { - slog.Error("error querying session", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/yaml") - w.WriteHeader(http.StatusOK) - _, err = w.Write([]byte(gatewayCfg)) - if err != nil { - slog.Error("error writing response", "error", err) - return - } -} - -func RetrieveUserBySessionId(sessionId string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var username string - err = db.QueryRow(` - SELECT username - FROM sessions - WHERE id = $1`, - sessionId).Scan(&username) - - return username -} - -func RetrieveUserConfig(username string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var gatewayCfg string - err = db.QueryRow(` - SELECT config_yaml - FROM users - WHERE username = $1`, - username, - ).Scan(&gatewayCfg) - - return gatewayCfg -} - -func InsertNewConfig(sessionId, gatewayCfg string) error { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - return err - } - - _, err = db.Exec(` - UPDATE users AS u - SET config_yaml = $1 - FROM sessions AS s - WHERE s.id = $2 - AND s.expires > NOW() - AND u.username = s.username`, - gatewayCfg, sessionId, - ) - slog.Info("inserted new config to database") - - if err != nil { - return err - } - - return nil -} +package config + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fyp-api-gateway/src/utils" + "log/slog" + "net/http" + "os" + "time" + + uuid "github.com/google/uuid" + _ "github.com/jackc/pgx/v5/stdlib" +) + +type Database struct { + Conn *sql.DB +} + +type Server struct { + DB *Database +} + +type LoginInfo struct { + Name string `json:"name"` + Password string `json:"password"` +} + +func NewDatabase(dsn string) (*Database, error) { + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return nil, err + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err = db.PingContext(ctx); err != nil { + return nil, err + } + + return &Database{Conn: db}, nil +} + +func (d *Database) StartDB(path string) error { + content, err := os.ReadFile(path) + if err != nil { + slog.Error("error reading database file", "error", err) + return err + } + + if _, err = d.Conn.Exec(string(content)); err != nil { + slog.Error("error executing database statement", "error", err) + return err + } + + return nil +} + +func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { + slog.Info("attempting to sign up new user...") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var id string + err := s.DB.Conn.QueryRow( + "SELECT id FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&id) + + if err == nil || id != "" { + slog.Error("error querying user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // TODO: Store password hash + _, err = s.DB.Conn.Exec(` + INSERT INTO users (username, password, config_yaml) + VALUES ($1, $2, $3);`, + loginInfo.Name, loginInfo.Password, utils.DefaultConfigContent, + ) + if err != nil { + slog.Error("error inserting user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + err = InitUserNGINX(loginInfo.Name) + if err != nil { + slog.Error("error initializing user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +/* +Receive the login info from the management plane and decode it +Check the password used is the same as the one in the database, also check the username is there +If the user has no session, create one and send it back, otherwise return the existing session +*/ +func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { + slog.Info("validating login information") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var storedHash string + err := s.DB.Conn.QueryRow( + "SELECT password FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&storedHash) + + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "username", loginInfo.Name) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + //err = bcrypt.CompareHashAndPassword( + // []byte(storedHash), + // []byte(loginInfo.password), + //) + + // TODO: create a proper hashed password + if storedHash != loginInfo.Password { + err = errors.New("invalid credentials") + } + + if err != nil { + slog.Error("error verifying loginInfo", "error", err) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + // check is the user has a session already + var sessionId string + sessionId, isSession := s.sessionExists(loginInfo.Name) + + if !isSession { + sessionId, err = s.createSession(loginInfo.Name) + if err != nil { + slog.Error("error creating session", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "sessionId": sessionId, + }) +} + +func (s *Server) sessionExists(username string) (string, bool) { + var sessionId string + err := s.DB.Conn.QueryRow( + "SELECT id FROM sessions WHERE username=$1", + username, + ).Scan(&sessionId) + + if err != nil { + return "", false + } + return sessionId, true +} + +func (s *Server) createSession(name string) (string, error) { + sessionId := uuid.New().String() + expires := time.Now().Add(24 * time.Hour) + + _, err := s.DB.Conn.Exec( + "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", + sessionId, name, expires, + ) + + if err != nil { + slog.Error("error creating session", "error", err) + return "", err + } + + return sessionId, nil +} + +func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { + slog.Info("validating user session") + sessionId := r.Header.Get("X-Session-ID") + + var username string + err := s.DB.Conn.QueryRow( + "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", + sessionId, + ).Scan(&username) + + if err != nil { + slog.Error("error querying session", "error", err) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("received request for user config") + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("failed getting session id", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sessionId := cookie.Value + + var gatewayCfg string + err = s.DB.Conn.QueryRow(` + SELECT u.config_yaml + FROM users AS u + JOIN sessions AS s ON u.username = s.username + WHERE s.id = $1 AND s.expires > NOW()`, + sessionId).Scan(&gatewayCfg) + + if err != nil { + slog.Error("error querying session", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/yaml") + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(gatewayCfg)) + if err != nil { + slog.Error("error writing response", "error", err) + return + } +} + +func RetrieveUserBySessionId(sessionId string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var username string + err = db.QueryRow(` + SELECT username + FROM sessions + WHERE id = $1`, + sessionId).Scan(&username) + + return username +} + +func RetrieveUserConfig(username string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var gatewayCfg string + err = db.QueryRow(` + SELECT config_yaml + FROM users + WHERE username = $1`, + username, + ).Scan(&gatewayCfg) + + return gatewayCfg +} + +func InsertNewConfig(sessionId, gatewayCfg string) error { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + return err + } + + _, err = db.Exec(` + UPDATE users AS u + SET config_yaml = $1 + FROM sessions AS s + WHERE s.id = $2 + AND s.expires > NOW() + AND u.username = s.username`, + gatewayCfg, sessionId, + ) + slog.Info("inserted new config to database") + + if err != nil { + return err + } + + return nil +} diff --git a/src/config/init.sql b/src/config/init.sql index b419c9a..12f17e7 100644 --- a/src/config/init.sql +++ b/src/config/init.sql @@ -1,49 +1,49 @@ -DROP TABLE IF EXISTS users; -CREATE TABLE IF NOT EXISTS users ( - id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - username VARCHAR(64) NOT NULL, - password VARCHAR(256) NOT NULL, - config_yaml TEXT NOT NULL -); - -INSERT INTO users(username, password, config_yaml) -VALUES ('admin', 'admin', '# -# Configuration file for API Gateway -# - -connections: - routes: - - path: /products - url: http://services:9001 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: /orders - url: http://services:9002 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: /protected - url: http://services:9003 - rate-limit: - zone: 10 - rate: 5 - auth: true - - - path: /external-weather - url: https://api.open-meteo.com - rate-limit: - zone: 10 - rate: 5 - auth: false'); - -DROP TABLE IF EXISTS sessions; -CREATE TABLE IF NOT EXISTS sessions ( - id UUID PRIMARY KEY, - username VARCHAR(64) NOT NULL, - expires TIMESTAMP NOT NULL +DROP TABLE IF EXISTS users; +CREATE TABLE IF NOT EXISTS users ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + username VARCHAR(64) NOT NULL, + password VARCHAR(256) NOT NULL, + config_yaml TEXT NOT NULL +); + +INSERT INTO users(username, password, config_yaml) +VALUES ('admin', 'admin', '# +# Configuration file for API Gateway +# + +connections: + routes: + - path: /products + url: http://services:9001 + rate-limit: + zone: 10 + rate: 5 + auth: false + + - path: /orders + url: http://services:9002 + rate-limit: + zone: 10 + rate: 5 + auth: false + + - path: /protected + url: http://services:9003 + rate-limit: + zone: 10 + rate: 5 + auth: true + + - path: /external-weather + url: https://api.open-meteo.com + rate-limit: + zone: 10 + rate: 5 + auth: false'); + +DROP TABLE IF EXISTS sessions; +CREATE TABLE IF NOT EXISTS sessions ( + id UUID PRIMARY KEY, + username VARCHAR(64) NOT NULL, + expires TIMESTAMP NOT NULL ); \ No newline at end of file diff --git a/src/config/types.go b/src/config/types.go index 53e9fc4..059a865 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -1,28 +1,28 @@ -package config - -type RenderModel struct { - Connections []Connection -} - -type ( - GatewayConfig struct { - Connections Connection `yaml:"connections" mapstructure:"connections"` - } - - Connection struct { - Routes []Routes `yaml:"routes" mapstructure:"routes"` - } - - Routes struct { - Path string `yaml:"path" mapstructure:"path"` - Url string `yaml:"url" mapstructure:"url"` - Auth bool `yaml:"auth" mapstructure:"auth"` - RateLimit RateLimit `yaml:"rate-limit" mapstructure:"rate_limit"` - ZoneName string `yaml:"zone-name" mapstructure:"zone-name"` - } - - RateLimit struct { - Zone int `yaml:"zone" mapstructure:"zone"` - Rate int `yaml:"rate" mapstructure:"rate"` - } -) +package config + +type RenderModel struct { + Connections []Connection +} + +type ( + GatewayConfig struct { + Connections Connection `yaml:"connections" mapstructure:"connections"` + } + + Connection struct { + Routes []Routes `yaml:"routes" mapstructure:"routes"` + } + + Routes struct { + Path string `yaml:"path" mapstructure:"path"` + Url string `yaml:"url" mapstructure:"url"` + Auth bool `yaml:"auth" mapstructure:"auth"` + RateLimit RateLimit `yaml:"rate-limit" mapstructure:"rate_limit"` + ZoneName string `yaml:"zone-name" mapstructure:"zone-name"` + } + + RateLimit struct { + Zone int `yaml:"zone" mapstructure:"zone"` + Rate int `yaml:"rate" mapstructure:"rate"` + } +) diff --git a/src/main.go b/src/main.go index cdff569..e50ef76 100644 --- a/src/main.go +++ b/src/main.go @@ -1,75 +1,75 @@ -package main - -import ( - "context" - "fyp-api-gateway/src/config" - "fyp-api-gateway/src/semantics" - "fyp-api-gateway/src/watcher" - "log/slog" - "net/http" - "os" - "os/signal" - "syscall" - "time" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - go func() { - select { - case <-sigChan: - slog.WarnContext(ctx, "failed to create main context") - cancel() - - time.Sleep(5 * time.Second) - os.Exit(1) - - case <-ctx.Done(): - } - }() - - go watcher.Watch() - - dsn := os.Getenv("DATABASE_URL") - if dsn == "" { - slog.Error("Error reading database connection", "error", "DATABASE_URL is not set") - } - - db, err := config.NewDatabase(dsn) - if err != nil { - slog.Error("Error initialising database", "error", err) - return - } - defer db.Conn.Close() - - if err = db.StartDB("/var/lib/init.sql"); err != nil { - slog.Error("Error running migration script", "error", err) - return - } - slog.Info("Initialised database") - - server := &config.Server{DB: db} - - mux := http.NewServeMux() - - // config handler routes - mux.HandleFunc("/analyse", semantics.RecvConfig) - mux.HandleFunc("/config/update", config.LoadNewConfig) - - // database routes - mux.HandleFunc("/verify-signup", server.Signup) - mux.HandleFunc("/verify-login", server.VerifyLoginInfo) - mux.HandleFunc("/validate-session", server.ValidateSession) - mux.HandleFunc("/api/gateway", server.UserConfig) - - slog.Info("Control plane listening on port 10000") - err = http.ListenAndServe(":10000", mux) - if err != nil { - slog.Error("Error starting server", "error", err) - cancel() - } - -} +package main + +import ( + "context" + "fyp-api-gateway/src/config" + "fyp-api-gateway/src/semantics" + "fyp-api-gateway/src/watcher" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + go func() { + select { + case <-sigChan: + slog.WarnContext(ctx, "failed to create main context") + cancel() + + time.Sleep(5 * time.Second) + os.Exit(1) + + case <-ctx.Done(): + } + }() + + go watcher.Watch() + + dsn := os.Getenv("DATABASE_URL") + if dsn == "" { + slog.Error("Error reading database connection", "error", "DATABASE_URL is not set") + } + + db, err := config.NewDatabase(dsn) + if err != nil { + slog.Error("Error initialising database", "error", err) + return + } + defer db.Conn.Close() + + if err = db.StartDB("/var/lib/init.sql"); err != nil { + slog.Error("Error running migration script", "error", err) + return + } + slog.Info("Initialised database") + + server := &config.Server{DB: db} + + mux := http.NewServeMux() + + // config handler routes + mux.HandleFunc("/analyse", semantics.RecvConfig) + mux.HandleFunc("/config/update", config.LoadNewConfig) + + // database routes + mux.HandleFunc("/verify-signup", server.Signup) + mux.HandleFunc("/verify-login", server.VerifyLoginInfo) + mux.HandleFunc("/validate-session", server.ValidateSession) + mux.HandleFunc("/api/gateway", server.UserConfig) + + slog.Info("Control plane listening on port 10000") + err = http.ListenAndServe(":10000", mux) + if err != nil { + slog.Error("Error starting server", "error", err) + cancel() + } + +} diff --git a/src/semantics/semantic_analysis.go b/src/semantics/semantic_analysis.go index e0eda92..5e8b77a 100644 --- a/src/semantics/semantic_analysis.go +++ b/src/semantics/semantic_analysis.go @@ -1,247 +1,247 @@ -package semantics - -import ( - "bytes" - "encoding/json" - "fmt" - "fyp-api-gateway/src/config" - "log/slog" - "net/http" - "strings" - - "gopkg.in/yaml.v3" -) - -type AnalysisResult struct { - Findings []string `json:"findings"` -} - -type RouteView struct { - Path string - Url string - Auth bool - RateLimit config.RateLimit - ZoneName string -} - -type FinalFindings struct { - Errors []string `json:"errors"` - Updates []string `json:"updates"` -} - -func RecvConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("Received config from management plane") - var newCfg config.GatewayConfig - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("error getting session cookie ", "error", err) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - username := config.RetrieveUserBySessionId(cookie.Value) - - if err = yaml.NewDecoder(r.Body).Decode(&newCfg); err != nil { - slog.Error("Error decoding config", "error", err) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - // Analyse the new config file - slog.Info("analysing new config file...") - findings, err := analyse(username, newCfg) - if err != nil { - slog.Error("Error analysing new config", "error", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - // send the findings back to the frontend - slog.Info("Sending data back to management plane") - _, err = http.Post("http://management-plane:80/file/findings", "application/json", bytes.NewBuffer(findings)) - if err != nil { - slog.Error("Error posting new config", "error", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } -} - -func analyse(username string, newCfg config.GatewayConfig) ([]byte, error) { - oldCfgStr := config.RetrieveUserConfig(username) - - var oldCfg config.GatewayConfig - err := yaml.Unmarshal([]byte(oldCfgStr), &oldCfg) - if err != nil { - slog.Error("Error decoding config", "error", err) - return nil, err - } - - oldConfig := flattenConfig(oldCfg) - newConfig := flattenConfig(newCfg) - foundErrors := validateConfigErrors(oldConfig, newConfig) - foundUpdates := explainDifferences(oldConfig, newConfig) - - finalFindings := FinalFindings{} - finalFindings.Errors = foundErrors - finalFindings.Updates = foundUpdates - findings, err := json.Marshal(finalFindings) - if err != nil { - slog.Error("Error marshalling findings map", "error", err) - return nil, err - } - - return findings, nil -} - -func validateConfigErrors(oldConf, newConf []RouteView) []string { - var findings []string - - // duplicate routes - paths := make(map[string]bool) - for _, r := range newConf { - if paths[r.Path] { - findings = append(findings, "Duplicate routes detected: "+r.Path) - } - paths[r.Path] = true - } - - // route shadowing - for i := 0; i < len(newConf); i++ { - for j := 0; j < len(oldConf); j++ { - if i == j { - continue - } - - r1 := newConf[i] - r2 := newConf[j] - - if pathShadows(r1.Path, r2.Path) { - findings = append(findings, "Route "+r2.Path+" may be shadowed by "+r1.Path) - } - } - } - - return findings -} - -func explainDifferences(oldConf, newConf []RouteView) []string { - var findings []string - - oldRoutes := indexRoutes(oldConf) - newRoutes := indexRoutes(newConf) - - // Detect Added Routes - for key, newRoute := range newRoutes { - _, exists := oldRoutes[key] - if !exists { - findings = append(findings, - "New route added: "+newRoute.Path+ - " on "+newRoute.Url) - - if !newRoute.Auth { - findings = append(findings, - "New public endpoint exposed at "+newRoute.Path) - } - } - } - - // Detect Removed Routes - for key, oldRoute := range oldRoutes { - _, exists := newRoutes[key] - if !exists { - findings = append(findings, - "Route removed: "+oldRoute.Path+ - " on "+oldRoute.Url) - - if oldRoute.Auth { - findings = append(findings, - "Previously protected route "+oldRoute.Path+ - " has been removed") - } - } - } - - // Detect Modified Routes - for key, newRoute := range newRoutes { - oldRoute, exists := oldRoutes[key] - if !exists { - continue - } - - // Auth Widening - if oldRoute.Auth && !newRoute.Auth { - findings = append(findings, - "Authentication removed from route "+newRoute.Path+ - " on "+newRoute.Url) - } - - // Auth Tightening - if !oldRoute.Auth && newRoute.Auth { - findings = append(findings, - "Authentication now required for route "+newRoute.Path) - } - - // Upstream Change - if oldRoute.Url != newRoute.Url { - findings = append(findings, - "Traffic for "+newRoute.Path+" will be routed from "+ - oldRoute.Url+" to "+newRoute.Url) - } - - // Rate Limit Tightening - if newRoute.RateLimit.Rate < oldRoute.RateLimit.Rate { - findings = append(findings, - "Rate limit tightened on "+newRoute.Path+ - " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ - " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") - } - - // Rate Limit Relaxed - if newRoute.RateLimit.Rate > oldRoute.RateLimit.Rate { - findings = append(findings, - "Rate limit relaxed on "+newRoute.Path+ - " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ - " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") - } - } - - return findings -} - -func flattenConfig(cfg config.GatewayConfig) []RouteView { - var routes []RouteView - - c := cfg.Connections - for _, r := range c.Routes { - zoneName := strings.ReplaceAll(r.Path, "/", "_") - if zoneName == "" { - zoneName = "root" - } - - routes = append(routes, RouteView{ - Path: r.Path, - Url: r.Url, - Auth: r.Auth, - RateLimit: r.RateLimit, - ZoneName: zoneName, - }) - } - - return routes -} - -func indexRoutes(routes []RouteView) map[string]RouteView { - index := make(map[string]RouteView) - for _, r := range routes { - index[r.Path] = r - } - return index -} - -func pathShadows(a, b string) bool { - if a == "/" { - return true - } - return len(b) > len(a) && b[:len(a)] == a -} +package semantics + +import ( + "bytes" + "encoding/json" + "fmt" + "fyp-api-gateway/src/config" + "log/slog" + "net/http" + "strings" + + "gopkg.in/yaml.v3" +) + +type AnalysisResult struct { + Findings []string `json:"findings"` +} + +type RouteView struct { + Path string + Url string + Auth bool + RateLimit config.RateLimit + ZoneName string +} + +type FinalFindings struct { + Errors []string `json:"errors"` + Updates []string `json:"updates"` +} + +func RecvConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("Received config from management plane") + var newCfg config.GatewayConfig + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("error getting session cookie ", "error", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + username := config.RetrieveUserBySessionId(cookie.Value) + + if err = yaml.NewDecoder(r.Body).Decode(&newCfg); err != nil { + slog.Error("Error decoding config", "error", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + // Analyse the new config file + slog.Info("analysing new config file...") + findings, err := analyse(username, newCfg) + if err != nil { + slog.Error("Error analysing new config", "error", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + // send the findings back to the frontend + slog.Info("Sending data back to management plane") + _, err = http.Post("http://management-plane:80/file/findings", "application/json", bytes.NewBuffer(findings)) + if err != nil { + slog.Error("Error posting new config", "error", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } +} + +func analyse(username string, newCfg config.GatewayConfig) ([]byte, error) { + oldCfgStr := config.RetrieveUserConfig(username) + + var oldCfg config.GatewayConfig + err := yaml.Unmarshal([]byte(oldCfgStr), &oldCfg) + if err != nil { + slog.Error("Error decoding config", "error", err) + return nil, err + } + + oldConfig := flattenConfig(oldCfg) + newConfig := flattenConfig(newCfg) + foundErrors := validateConfigErrors(oldConfig, newConfig) + foundUpdates := explainDifferences(oldConfig, newConfig) + + finalFindings := FinalFindings{} + finalFindings.Errors = foundErrors + finalFindings.Updates = foundUpdates + findings, err := json.Marshal(finalFindings) + if err != nil { + slog.Error("Error marshalling findings map", "error", err) + return nil, err + } + + return findings, nil +} + +func validateConfigErrors(oldConf, newConf []RouteView) []string { + var findings []string + + // duplicate routes + paths := make(map[string]bool) + for _, r := range newConf { + if paths[r.Path] { + findings = append(findings, "Duplicate routes detected: "+r.Path) + } + paths[r.Path] = true + } + + // route shadowing + for i := 0; i < len(newConf); i++ { + for j := 0; j < len(oldConf); j++ { + if i == j { + continue + } + + r1 := newConf[i] + r2 := newConf[j] + + if pathShadows(r1.Path, r2.Path) { + findings = append(findings, "Route "+r2.Path+" may be shadowed by "+r1.Path) + } + } + } + + return findings +} + +func explainDifferences(oldConf, newConf []RouteView) []string { + var findings []string + + oldRoutes := indexRoutes(oldConf) + newRoutes := indexRoutes(newConf) + + // Detect Added Routes + for key, newRoute := range newRoutes { + _, exists := oldRoutes[key] + if !exists { + findings = append(findings, + "New route added: "+newRoute.Path+ + " on "+newRoute.Url) + + if !newRoute.Auth { + findings = append(findings, + "New public endpoint exposed at "+newRoute.Path) + } + } + } + + // Detect Removed Routes + for key, oldRoute := range oldRoutes { + _, exists := newRoutes[key] + if !exists { + findings = append(findings, + "Route removed: "+oldRoute.Path+ + " on "+oldRoute.Url) + + if oldRoute.Auth { + findings = append(findings, + "Previously protected route "+oldRoute.Path+ + " has been removed") + } + } + } + + // Detect Modified Routes + for key, newRoute := range newRoutes { + oldRoute, exists := oldRoutes[key] + if !exists { + continue + } + + // Auth Widening + if oldRoute.Auth && !newRoute.Auth { + findings = append(findings, + "Authentication removed from route "+newRoute.Path+ + " on "+newRoute.Url) + } + + // Auth Tightening + if !oldRoute.Auth && newRoute.Auth { + findings = append(findings, + "Authentication now required for route "+newRoute.Path) + } + + // Upstream Change + if oldRoute.Url != newRoute.Url { + findings = append(findings, + "Traffic for "+newRoute.Path+" will be routed from "+ + oldRoute.Url+" to "+newRoute.Url) + } + + // Rate Limit Tightening + if newRoute.RateLimit.Rate < oldRoute.RateLimit.Rate { + findings = append(findings, + "Rate limit tightened on "+newRoute.Path+ + " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ + " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") + } + + // Rate Limit Relaxed + if newRoute.RateLimit.Rate > oldRoute.RateLimit.Rate { + findings = append(findings, + "Rate limit relaxed on "+newRoute.Path+ + " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ + " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") + } + } + + return findings +} + +func flattenConfig(cfg config.GatewayConfig) []RouteView { + var routes []RouteView + + c := cfg.Connections + for _, r := range c.Routes { + zoneName := strings.ReplaceAll(r.Path, "/", "_") + if zoneName == "" { + zoneName = "root" + } + + routes = append(routes, RouteView{ + Path: r.Path, + Url: r.Url, + Auth: r.Auth, + RateLimit: r.RateLimit, + ZoneName: zoneName, + }) + } + + return routes +} + +func indexRoutes(routes []RouteView) map[string]RouteView { + index := make(map[string]RouteView) + for _, r := range routes { + index[r.Path] = r + } + return index +} + +func pathShadows(a, b string) bool { + if a == "/" { + return true + } + return len(b) > len(a) && b[:len(a)] == a +} diff --git a/src/utils/defaults.go b/src/utils/defaults.go index c9a62f4..2e6cd63 100644 --- a/src/utils/defaults.go +++ b/src/utils/defaults.go @@ -1,14 +1,14 @@ -package utils - -var ( - GatewayConfigDirName = "/etc/config/" - GatewayConfigFileName = "gateway.yaml" - - NGINXDirName = "/etc/nginx/" - NGINXTemplateDirName = "/etc/nginx/" - NGINXConfigFileName = "nginx.conf" - NGINXTemplateFileName = "nginx.conf.tmpl" - NGINXUserDirName = "/etc/nginx/users/" - - DefaultConfigContent = "#\n# Configuration file for API Gateway\n#\n\nconnections:\n routes:\n - path: /products\n url: http://services:9001\n rate-limit:\n zone: 10\n rate: 5\n auth: false\n zone-name: root\n\n - path: /orders\n url: http://services:9002\n rate-limit:\n zone: 10\n rate: 5\n auth: false\n zone-name: root\n\n - path: /protected\n url: http://services:9003\n rate-limit:\n zone: 10\n rate: 5\n auth: true\n zone-name: root\n\n - path: /external-weather\n url: https://api.open-meteo.com/v1/forecast?latitude=51.898&longitude=-8.4706&hourly=temperature_2m/\n rate-limit:\n zone: 10\n rate: 5\n auth: false\n zone-name: root" -) +package utils + +var ( + GatewayConfigDirName = "/etc/config/" + GatewayConfigFileName = "gateway.yaml" + + NGINXDirName = "/etc/nginx/" + NGINXTemplateDirName = "/etc/nginx/" + NGINXConfigFileName = "nginx.conf" + NGINXTemplateFileName = "nginx.conf.tmpl" + NGINXUserDirName = "/etc/nginx/users/" + + DefaultConfigContent = "#\n# Configuration file for API Gateway\n#\n\nconnections:\n routes:\n - path: /products\n url: http://services:9001\n rate-limit:\n zone: 10\n rate: 5\n auth: false\n zone-name: root\n\n - path: /orders\n url: http://services:9002\n rate-limit:\n zone: 10\n rate: 5\n auth: false\n zone-name: root\n\n - path: /protected\n url: http://services:9003\n rate-limit:\n zone: 10\n rate: 5\n auth: true\n zone-name: root\n\n - path: /external-weather\n url: https://api.open-meteo.com/v1/forecast?latitude=51.898&longitude=-8.4706&hourly=temperature_2m/\n rate-limit:\n zone: 10\n rate: 5\n auth: false\n zone-name: root" +) diff --git a/src/watcher/watcher.go b/src/watcher/watcher.go index 9251372..e8cbceb 100644 --- a/src/watcher/watcher.go +++ b/src/watcher/watcher.go @@ -1,138 +1,138 @@ -package watcher - -import ( - "bytes" - "encoding/json" - "fyp-api-gateway/src/utils" - "log/slog" - "net/http" - "os" - "path/filepath" - - "github.com/fsnotify/fsnotify" -) - -func Watch() { - slog.Info("Starting file watcher") - watcher, err := fsnotify.NewWatcher() - if err != nil { - slog.Error("error creating watcher", "error", err) - } - defer watcher.Close() - - go func() { - slog.Info("File watcher started") - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - slog.Info("watcher event:", "event", event) - if event.Has(fsnotify.Create) { - info, err := os.Stat(event.Name) - if err == nil && info.IsDir() { - slog.Info("New user directory detected, adding watcher", "dir", event.Name) - if err = watcher.Add(event.Name); err != nil { - slog.Error("error adding new user directory to watcher", "error", err) - } - - confPath := filepath.Join(event.Name, "nginx.conf") - if _, err := os.Stat(confPath); err == nil { - sendNginxToDataplane(confPath) - } - continue - } - } - - if filepath.Base(event.Name) == "nginx.conf" { - if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) { - slog.Info("watcher detected modified file:", "file", event.Name) - - // send the config to the dataplane! - sendNginxToDataplane(event.Name) - - } - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - slog.Error("watcher error:", "error", err) - } - } - }() - - root := utils.NGINXUserDirName - err = addUserDirs(watcher, root) - if err != nil { - slog.Error("error adding watcher:", "error", err) - } - - err = watcher.Add(utils.NGINXUserDirName) - if err != nil { - slog.Error("error adding base watcher:", "error", err) - } - - <-make(chan struct{}) -} - -func addUserDirs(w *fsnotify.Watcher, root string) error { - entries, err := os.ReadDir(root) - if err != nil { - return err - } - - for _, entry := range entries { - if entry.IsDir() { - dirPath := filepath.Join(root, entry.Name()) - if err = w.Add(dirPath); err != nil { - return err - } - } - } - - return nil -} - -func sendNginxToDataplane(filename string) { - body, err := os.ReadFile(filename) - if err != nil { - slog.Error("error opening file", "filename", filename, "error", err) - return - } - - type SendData struct { - Filename string `json:"filename"` - Body []byte `json:"body"` - } - - sendData := SendData{ - Filename: filename, - Body: body, - } - - data, err := json.Marshal(sendData) - if err != nil { - slog.Error("error encoding file", "filename", filename, "error", err) - return - } - - req, err := http.NewRequest( - "POST", - "http://data-plane:1000/api/handle-config", - bytes.NewBuffer(data), - ) - if err != nil { - slog.Error("error creating request to send to control plane", "error", err) - return - } - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - slog.Error("error sending request to control plane", "error", err) - return - } - defer resp.Body.Close() -} +package watcher + +import ( + "bytes" + "encoding/json" + "fyp-api-gateway/src/utils" + "log/slog" + "net/http" + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" +) + +func Watch() { + slog.Info("Starting file watcher") + watcher, err := fsnotify.NewWatcher() + if err != nil { + slog.Error("error creating watcher", "error", err) + } + defer watcher.Close() + + go func() { + slog.Info("File watcher started") + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + slog.Info("watcher event:", "event", event) + if event.Has(fsnotify.Create) { + info, err := os.Stat(event.Name) + if err == nil && info.IsDir() { + slog.Info("New user directory detected, adding watcher", "dir", event.Name) + if err = watcher.Add(event.Name); err != nil { + slog.Error("error adding new user directory to watcher", "error", err) + } + + confPath := filepath.Join(event.Name, "nginx.conf") + if _, err := os.Stat(confPath); err == nil { + sendNginxToDataplane(confPath) + } + continue + } + } + + if filepath.Base(event.Name) == "nginx.conf" { + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) { + slog.Info("watcher detected modified file:", "file", event.Name) + + // send the config to the dataplane! + sendNginxToDataplane(event.Name) + + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + slog.Error("watcher error:", "error", err) + } + } + }() + + root := utils.NGINXUserDirName + err = addUserDirs(watcher, root) + if err != nil { + slog.Error("error adding watcher:", "error", err) + } + + err = watcher.Add(utils.NGINXUserDirName) + if err != nil { + slog.Error("error adding base watcher:", "error", err) + } + + <-make(chan struct{}) +} + +func addUserDirs(w *fsnotify.Watcher, root string) error { + entries, err := os.ReadDir(root) + if err != nil { + return err + } + + for _, entry := range entries { + if entry.IsDir() { + dirPath := filepath.Join(root, entry.Name()) + if err = w.Add(dirPath); err != nil { + return err + } + } + } + + return nil +} + +func sendNginxToDataplane(filename string) { + body, err := os.ReadFile(filename) + if err != nil { + slog.Error("error opening file", "filename", filename, "error", err) + return + } + + type SendData struct { + Filename string `json:"filename"` + Body []byte `json:"body"` + } + + sendData := SendData{ + Filename: filename, + Body: body, + } + + data, err := json.Marshal(sendData) + if err != nil { + slog.Error("error encoding file", "filename", filename, "error", err) + return + } + + req, err := http.NewRequest( + "POST", + "http://data-plane:1000/api/handle-config", + bytes.NewBuffer(data), + ) + if err != nil { + slog.Error("error creating request to send to control plane", "error", err) + return + } + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + slog.Error("error sending request to control plane", "error", err) + return + } + defer resp.Body.Close() +} diff --git a/test/configs/gateway/gateway.yaml b/test/configs/gateway/gateway.yaml index 31cda25..09bffbe 100644 --- a/test/configs/gateway/gateway.yaml +++ b/test/configs/gateway/gateway.yaml @@ -3,15 +3,31 @@ # connections: - - host: localhost - port: 8080 - routes: - - path: /products - upstream: - name: product_service - port: 9001 + routes: + - path: /products + url: http://services:9001 + rate-limit: + zone: 10 + rate: 5 + auth: false - - path: /orders - upstream: - name: order_service - port: 9002 + - path: /orders + url: http://services:9002 + rate-limit: + zone: 10 + rate: 5 + auth: false + + - path: /protected + url: http://services:9003 + rate-limit: + zone: 10 + rate: 5 + auth: true + + - path: /external-weather + url: https://api.open-meteo.com/v1/forecast?latitude=51.898&longitude=-8.4706&hourly=temperature_2m/ + rate-limit: + zone: 10 + rate: 5 + auth: false diff --git a/test/configs/nginx/nginx.conf b/test/configs/nginx/nginx.conf index 0e28b85..8b8acb6 100644 --- a/test/configs/nginx/nginx.conf +++ b/test/configs/nginx/nginx.conf @@ -28,7 +28,7 @@ http{ server { listen 8080; - server_name localhost; + server_name 54.75.125.2; location /products { proxy_pass http://product_service; From c5048886413327f479b3f8ca64dae86357defd73 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 8 Mar 2026 16:51:17 +0000 Subject: [PATCH 02/10] Refactor Makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9514e74..12402b3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ GORUN = $(GOCMD) run GOTEST = $(GOCMD) test DOCKER = docker -COMPOSE = $(DOCKER) compose +COMPOSE = $(DOCKER)-compose PROJECT_DIR = cmd MAIN = main.go @@ -51,4 +51,4 @@ test-routes: curl "${HOST}/products" curl "${HOST}/orders" curl "${HOST}/protected" - curl "${HOST}/external-weather" \ No newline at end of file + curl "${HOST}/external-weather" From 1ac577f6da833312b7197288ba62c63f3b655e15 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Sun, 8 Mar 2026 18:33:55 +0000 Subject: [PATCH 03/10] Correct the Port --- docker-compose.yml | 2 +- management/Dockerfile | 2 +- management/main.go | 86 ++--- management/static/auth.js | 4 +- management/static/config.js | 8 +- src/semantics/semantic_analysis.go | 494 ++++++++++++++--------------- 6 files changed, 298 insertions(+), 298 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 370b645..55821b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: dockerfile: management/Dockerfile container_name: management-plane ports: - - "80:80" + - "81:81" volumes: - "./management/static/gateway.conf:/etc/config/gateway.conf" restart: unless-stopped diff --git a/management/Dockerfile b/management/Dockerfile index f68374a..b0842cf 100644 --- a/management/Dockerfile +++ b/management/Dockerfile @@ -14,6 +14,6 @@ WORKDIR /app COPY --from=builder /app/main . COPY --from=builder /app/management/static ./static -EXPOSE 80 +EXPOSE 81 RUN chmod +x /app/main CMD ["./main"] diff --git a/management/main.go b/management/main.go index 42ae43c..2eccac1 100644 --- a/management/main.go +++ b/management/main.go @@ -1,43 +1,43 @@ -package main - -import ( - "fyp-api-gateway/management/auth" - "fyp-api-gateway/management/handler" - "log/slog" - "net/http" -) - -func main() { - mux := http.NewServeMux() - - // frontend routes - mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/auth", http.StatusSeeOther) - }) - mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/auth.html") - }) - mux.HandleFunc("/config", auth.RequireSession(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/config.html") - })) - mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./static/index.html") - }) - - // account initialisation routes - mux.HandleFunc("/api/signup", auth.Signup) - mux.HandleFunc("/api/login", auth.Login) - mux.HandleFunc("/file/gateway", auth.RequireSession(handler.Gateway)) - - // config routes - mux.HandleFunc("/file/upload", handler.HandleNewConfig) - mux.HandleFunc("/file/findings", handler.RecvFindings) - mux.HandleFunc("/file/retrieve", handler.Findings) - mux.HandleFunc("/file/accept", handler.HandleAcceptChanges) - - err := http.ListenAndServe(":80", mux) - if err != nil { - slog.Error("could not start management plane", "error", err) - } -} +package main + +import ( + "fyp-api-gateway/management/auth" + "fyp-api-gateway/management/handler" + "log/slog" + "net/http" +) + +func main() { + mux := http.NewServeMux() + + // frontend routes + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/auth", http.StatusSeeOther) + }) + mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/auth.html") + }) + mux.HandleFunc("/config", auth.RequireSession(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/config.html") + })) + mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/index.html") + }) + + // account initialisation routes + mux.HandleFunc("/api/signup", auth.Signup) + mux.HandleFunc("/api/login", auth.Login) + mux.HandleFunc("/file/gateway", auth.RequireSession(handler.Gateway)) + + // config routes + mux.HandleFunc("/file/upload", handler.HandleNewConfig) + mux.HandleFunc("/file/findings", handler.RecvFindings) + mux.HandleFunc("/file/retrieve", handler.Findings) + mux.HandleFunc("/file/accept", handler.HandleAcceptChanges) + + err := http.ListenAndServe(":81", mux) + if err != nil { + slog.Error("could not start management plane", "error", err) + } +} diff --git a/management/static/auth.js b/management/static/auth.js index 5eff4b6..6ee6343 100644 --- a/management/static/auth.js +++ b/management/static/auth.js @@ -21,7 +21,7 @@ document.getElementById("loginForm").addEventListener("submit", function (e) { const name = document.getElementById("name").value; const password = document.getElementById("password").value; - sendLoginData(name, password, "http://54.75.125.2:80/api/login"); + sendLoginData(name, password, "http://54.75.125.2:81/api/login"); }); document.getElementById("signupForm").addEventListener("submit", function(e){ @@ -30,5 +30,5 @@ document.getElementById("signupForm").addEventListener("submit", function(e){ const name = document.getElementById("signupName").value; const password = document.getElementById("signupPassword").value; - sendLoginData(name, password, "http://54.75.125.2:80/api/signup"); + sendLoginData(name, password, "http://54.75.125.2:81/api/signup"); }); \ No newline at end of file diff --git a/management/static/config.js b/management/static/config.js index f0e5fd8..ef42afb 100644 --- a/management/static/config.js +++ b/management/static/config.js @@ -1,10 +1,10 @@ const editor = document.getElementById("editor"); const uploadBtn = document.getElementById("saveBtn"); -const FILE_URL = "http://54.75.125.2:80/file/gateway"; -const SAVE_URL = "http://54.75.125.2:80/file/upload"; -const FINDINGS_URL = "http://54.75.125.2:80/file/retrieve"; -const ACCEPT_URL = "http://54.75.125.2:80/file/accept"; +const FILE_URL = "http://54.75.125.2:81/file/gateway"; +const SAVE_URL = "http://54.75.125.2:81/file/upload"; +const FINDINGS_URL = "http://54.75.125.2:81/file/retrieve"; +const ACCEPT_URL = "http://54.75.125.2:81/file/accept"; window.addEventListener("DOMContentLoaded", async () => { try { diff --git a/src/semantics/semantic_analysis.go b/src/semantics/semantic_analysis.go index 5e8b77a..7adddfd 100644 --- a/src/semantics/semantic_analysis.go +++ b/src/semantics/semantic_analysis.go @@ -1,247 +1,247 @@ -package semantics - -import ( - "bytes" - "encoding/json" - "fmt" - "fyp-api-gateway/src/config" - "log/slog" - "net/http" - "strings" - - "gopkg.in/yaml.v3" -) - -type AnalysisResult struct { - Findings []string `json:"findings"` -} - -type RouteView struct { - Path string - Url string - Auth bool - RateLimit config.RateLimit - ZoneName string -} - -type FinalFindings struct { - Errors []string `json:"errors"` - Updates []string `json:"updates"` -} - -func RecvConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("Received config from management plane") - var newCfg config.GatewayConfig - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("error getting session cookie ", "error", err) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - username := config.RetrieveUserBySessionId(cookie.Value) - - if err = yaml.NewDecoder(r.Body).Decode(&newCfg); err != nil { - slog.Error("Error decoding config", "error", err) - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - // Analyse the new config file - slog.Info("analysing new config file...") - findings, err := analyse(username, newCfg) - if err != nil { - slog.Error("Error analysing new config", "error", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - // send the findings back to the frontend - slog.Info("Sending data back to management plane") - _, err = http.Post("http://management-plane:80/file/findings", "application/json", bytes.NewBuffer(findings)) - if err != nil { - slog.Error("Error posting new config", "error", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } -} - -func analyse(username string, newCfg config.GatewayConfig) ([]byte, error) { - oldCfgStr := config.RetrieveUserConfig(username) - - var oldCfg config.GatewayConfig - err := yaml.Unmarshal([]byte(oldCfgStr), &oldCfg) - if err != nil { - slog.Error("Error decoding config", "error", err) - return nil, err - } - - oldConfig := flattenConfig(oldCfg) - newConfig := flattenConfig(newCfg) - foundErrors := validateConfigErrors(oldConfig, newConfig) - foundUpdates := explainDifferences(oldConfig, newConfig) - - finalFindings := FinalFindings{} - finalFindings.Errors = foundErrors - finalFindings.Updates = foundUpdates - findings, err := json.Marshal(finalFindings) - if err != nil { - slog.Error("Error marshalling findings map", "error", err) - return nil, err - } - - return findings, nil -} - -func validateConfigErrors(oldConf, newConf []RouteView) []string { - var findings []string - - // duplicate routes - paths := make(map[string]bool) - for _, r := range newConf { - if paths[r.Path] { - findings = append(findings, "Duplicate routes detected: "+r.Path) - } - paths[r.Path] = true - } - - // route shadowing - for i := 0; i < len(newConf); i++ { - for j := 0; j < len(oldConf); j++ { - if i == j { - continue - } - - r1 := newConf[i] - r2 := newConf[j] - - if pathShadows(r1.Path, r2.Path) { - findings = append(findings, "Route "+r2.Path+" may be shadowed by "+r1.Path) - } - } - } - - return findings -} - -func explainDifferences(oldConf, newConf []RouteView) []string { - var findings []string - - oldRoutes := indexRoutes(oldConf) - newRoutes := indexRoutes(newConf) - - // Detect Added Routes - for key, newRoute := range newRoutes { - _, exists := oldRoutes[key] - if !exists { - findings = append(findings, - "New route added: "+newRoute.Path+ - " on "+newRoute.Url) - - if !newRoute.Auth { - findings = append(findings, - "New public endpoint exposed at "+newRoute.Path) - } - } - } - - // Detect Removed Routes - for key, oldRoute := range oldRoutes { - _, exists := newRoutes[key] - if !exists { - findings = append(findings, - "Route removed: "+oldRoute.Path+ - " on "+oldRoute.Url) - - if oldRoute.Auth { - findings = append(findings, - "Previously protected route "+oldRoute.Path+ - " has been removed") - } - } - } - - // Detect Modified Routes - for key, newRoute := range newRoutes { - oldRoute, exists := oldRoutes[key] - if !exists { - continue - } - - // Auth Widening - if oldRoute.Auth && !newRoute.Auth { - findings = append(findings, - "Authentication removed from route "+newRoute.Path+ - " on "+newRoute.Url) - } - - // Auth Tightening - if !oldRoute.Auth && newRoute.Auth { - findings = append(findings, - "Authentication now required for route "+newRoute.Path) - } - - // Upstream Change - if oldRoute.Url != newRoute.Url { - findings = append(findings, - "Traffic for "+newRoute.Path+" will be routed from "+ - oldRoute.Url+" to "+newRoute.Url) - } - - // Rate Limit Tightening - if newRoute.RateLimit.Rate < oldRoute.RateLimit.Rate { - findings = append(findings, - "Rate limit tightened on "+newRoute.Path+ - " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ - " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") - } - - // Rate Limit Relaxed - if newRoute.RateLimit.Rate > oldRoute.RateLimit.Rate { - findings = append(findings, - "Rate limit relaxed on "+newRoute.Path+ - " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ - " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") - } - } - - return findings -} - -func flattenConfig(cfg config.GatewayConfig) []RouteView { - var routes []RouteView - - c := cfg.Connections - for _, r := range c.Routes { - zoneName := strings.ReplaceAll(r.Path, "/", "_") - if zoneName == "" { - zoneName = "root" - } - - routes = append(routes, RouteView{ - Path: r.Path, - Url: r.Url, - Auth: r.Auth, - RateLimit: r.RateLimit, - ZoneName: zoneName, - }) - } - - return routes -} - -func indexRoutes(routes []RouteView) map[string]RouteView { - index := make(map[string]RouteView) - for _, r := range routes { - index[r.Path] = r - } - return index -} - -func pathShadows(a, b string) bool { - if a == "/" { - return true - } - return len(b) > len(a) && b[:len(a)] == a -} +package semantics + +import ( + "bytes" + "encoding/json" + "fmt" + "fyp-api-gateway/src/config" + "log/slog" + "net/http" + "strings" + + "gopkg.in/yaml.v3" +) + +type AnalysisResult struct { + Findings []string `json:"findings"` +} + +type RouteView struct { + Path string + Url string + Auth bool + RateLimit config.RateLimit + ZoneName string +} + +type FinalFindings struct { + Errors []string `json:"errors"` + Updates []string `json:"updates"` +} + +func RecvConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("Received config from management plane") + var newCfg config.GatewayConfig + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("error getting session cookie ", "error", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + username := config.RetrieveUserBySessionId(cookie.Value) + + if err = yaml.NewDecoder(r.Body).Decode(&newCfg); err != nil { + slog.Error("Error decoding config", "error", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + // Analyse the new config file + slog.Info("analysing new config file...") + findings, err := analyse(username, newCfg) + if err != nil { + slog.Error("Error analysing new config", "error", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + // send the findings back to the frontend + slog.Info("Sending data back to management plane") + _, err = http.Post("http://management-plane:81/file/findings", "application/json", bytes.NewBuffer(findings)) + if err != nil { + slog.Error("Error posting new config", "error", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } +} + +func analyse(username string, newCfg config.GatewayConfig) ([]byte, error) { + oldCfgStr := config.RetrieveUserConfig(username) + + var oldCfg config.GatewayConfig + err := yaml.Unmarshal([]byte(oldCfgStr), &oldCfg) + if err != nil { + slog.Error("Error decoding config", "error", err) + return nil, err + } + + oldConfig := flattenConfig(oldCfg) + newConfig := flattenConfig(newCfg) + foundErrors := validateConfigErrors(oldConfig, newConfig) + foundUpdates := explainDifferences(oldConfig, newConfig) + + finalFindings := FinalFindings{} + finalFindings.Errors = foundErrors + finalFindings.Updates = foundUpdates + findings, err := json.Marshal(finalFindings) + if err != nil { + slog.Error("Error marshalling findings map", "error", err) + return nil, err + } + + return findings, nil +} + +func validateConfigErrors(oldConf, newConf []RouteView) []string { + var findings []string + + // duplicate routes + paths := make(map[string]bool) + for _, r := range newConf { + if paths[r.Path] { + findings = append(findings, "Duplicate routes detected: "+r.Path) + } + paths[r.Path] = true + } + + // route shadowing + for i := 0; i < len(newConf); i++ { + for j := 0; j < len(oldConf); j++ { + if i == j { + continue + } + + r1 := newConf[i] + r2 := newConf[j] + + if pathShadows(r1.Path, r2.Path) { + findings = append(findings, "Route "+r2.Path+" may be shadowed by "+r1.Path) + } + } + } + + return findings +} + +func explainDifferences(oldConf, newConf []RouteView) []string { + var findings []string + + oldRoutes := indexRoutes(oldConf) + newRoutes := indexRoutes(newConf) + + // Detect Added Routes + for key, newRoute := range newRoutes { + _, exists := oldRoutes[key] + if !exists { + findings = append(findings, + "New route added: "+newRoute.Path+ + " on "+newRoute.Url) + + if !newRoute.Auth { + findings = append(findings, + "New public endpoint exposed at "+newRoute.Path) + } + } + } + + // Detect Removed Routes + for key, oldRoute := range oldRoutes { + _, exists := newRoutes[key] + if !exists { + findings = append(findings, + "Route removed: "+oldRoute.Path+ + " on "+oldRoute.Url) + + if oldRoute.Auth { + findings = append(findings, + "Previously protected route "+oldRoute.Path+ + " has been removed") + } + } + } + + // Detect Modified Routes + for key, newRoute := range newRoutes { + oldRoute, exists := oldRoutes[key] + if !exists { + continue + } + + // Auth Widening + if oldRoute.Auth && !newRoute.Auth { + findings = append(findings, + "Authentication removed from route "+newRoute.Path+ + " on "+newRoute.Url) + } + + // Auth Tightening + if !oldRoute.Auth && newRoute.Auth { + findings = append(findings, + "Authentication now required for route "+newRoute.Path) + } + + // Upstream Change + if oldRoute.Url != newRoute.Url { + findings = append(findings, + "Traffic for "+newRoute.Path+" will be routed from "+ + oldRoute.Url+" to "+newRoute.Url) + } + + // Rate Limit Tightening + if newRoute.RateLimit.Rate < oldRoute.RateLimit.Rate { + findings = append(findings, + "Rate limit tightened on "+newRoute.Path+ + " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ + " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") + } + + // Rate Limit Relaxed + if newRoute.RateLimit.Rate > oldRoute.RateLimit.Rate { + findings = append(findings, + "Rate limit relaxed on "+newRoute.Path+ + " ("+fmt.Sprint(oldRoute.RateLimit.Rate)+ + " → "+fmt.Sprint(newRoute.RateLimit.Rate)+")") + } + } + + return findings +} + +func flattenConfig(cfg config.GatewayConfig) []RouteView { + var routes []RouteView + + c := cfg.Connections + for _, r := range c.Routes { + zoneName := strings.ReplaceAll(r.Path, "/", "_") + if zoneName == "" { + zoneName = "root" + } + + routes = append(routes, RouteView{ + Path: r.Path, + Url: r.Url, + Auth: r.Auth, + RateLimit: r.RateLimit, + ZoneName: zoneName, + }) + } + + return routes +} + +func indexRoutes(routes []RouteView) map[string]RouteView { + index := make(map[string]RouteView) + for _, r := range routes { + index[r.Path] = r + } + return index +} + +func pathShadows(a, b string) bool { + if a == "/" { + return true + } + return len(b) > len(a) && b[:len(a)] == a +} From 91b3041b48e0851eeb757e1c994cb30ae3630df0 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Sun, 8 Mar 2026 22:28:15 +0000 Subject: [PATCH 04/10] Fix static routes --- Makefile | 4 ++-- docker-compose.yml | 8 ++++---- management/static/auth.js | 4 ++-- management/static/config.js | 8 ++++---- test/configs/nginx/nginx.conf | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 9514e74..fa5708c 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ COMPOSE = $(DOCKER) compose PROJECT_DIR = cmd MAIN = main.go -HOST = http://54.75.125.2:8080 +HOST = http://127.0.0.1:8080 WAIT_RETRIES=5 WAIT_INTERVAL=3 @@ -35,7 +35,7 @@ docker-stop: wait: @echo "Waiting for $(SERVICE) on port $(PORT)..." @for i in $(shell seq 1 $(WAIT_RETRIES)); do \ - if curl -s http://54.75.125.2:$(PORT)/healthz >/dev/null 2>&1; then \ + if curl -s http://127.0.0.1:$(PORT)/healthz >/dev/null 2>&1; then \ echo "$(SERVICE) is ready"; \ break; \ fi; \ diff --git a/docker-compose.yml b/docker-compose.yml index 55821b6..aa1b4de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,7 @@ services: depends_on: - minio healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://54.75.125.2:3100/ready || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3100/ready || exit 1" ] interval: 10s timeout: 5s retries: 5 @@ -110,7 +110,7 @@ services: volumes: - ./src/collector/configs/loki-config.yaml:/etc/loki/config.yaml healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://54.75.125.2:3100/ready || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3100/ready || exit 1" ] interval: 10s timeout: 5s retries: 5 @@ -151,7 +151,7 @@ services: volumes: - ./.data/minio:/data healthcheck: - test: [ "CMD", "curl", "-f", "http://54.75.125.2:9000/minio/health/live" ] + test: [ "CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live" ] interval: 15s timeout: 20s retries: 5 @@ -187,7 +187,7 @@ services: ports: - "3000:3000" healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://54.75.125.2:3000/api/health || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1" ] interval: 10s timeout: 5s retries: 5 diff --git a/management/static/auth.js b/management/static/auth.js index 6ee6343..58d0910 100644 --- a/management/static/auth.js +++ b/management/static/auth.js @@ -21,7 +21,7 @@ document.getElementById("loginForm").addEventListener("submit", function (e) { const name = document.getElementById("name").value; const password = document.getElementById("password").value; - sendLoginData(name, password, "http://54.75.125.2:81/api/login"); + sendLoginData(name, password, "/api/login"); }); document.getElementById("signupForm").addEventListener("submit", function(e){ @@ -30,5 +30,5 @@ document.getElementById("signupForm").addEventListener("submit", function(e){ const name = document.getElementById("signupName").value; const password = document.getElementById("signupPassword").value; - sendLoginData(name, password, "http://54.75.125.2:81/api/signup"); + sendLoginData(name, password, "/api/signup"); }); \ No newline at end of file diff --git a/management/static/config.js b/management/static/config.js index ef42afb..719abdd 100644 --- a/management/static/config.js +++ b/management/static/config.js @@ -1,10 +1,10 @@ const editor = document.getElementById("editor"); const uploadBtn = document.getElementById("saveBtn"); -const FILE_URL = "http://54.75.125.2:81/file/gateway"; -const SAVE_URL = "http://54.75.125.2:81/file/upload"; -const FINDINGS_URL = "http://54.75.125.2:81/file/retrieve"; -const ACCEPT_URL = "http://54.75.125.2:81/file/accept"; +const FILE_URL = "/file/gateway"; +const SAVE_URL = "/file/upload"; +const FINDINGS_URL = "/file/retrieve"; +const ACCEPT_URL = "/file/accept"; window.addEventListener("DOMContentLoaded", async () => { try { diff --git a/test/configs/nginx/nginx.conf b/test/configs/nginx/nginx.conf index 8b8acb6..ceb9258 100644 --- a/test/configs/nginx/nginx.conf +++ b/test/configs/nginx/nginx.conf @@ -28,7 +28,7 @@ http{ server { listen 8080; - server_name 54.75.125.2; + server_name _; location /products { proxy_pass http://product_service; From 58538f3e707c3e28ce2b183871f26c049ca27451 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Tue, 10 Mar 2026 12:56:14 +0000 Subject: [PATCH 05/10] Fix new paths not loading --- src/config/config.go | 435 ++++++++++++++++++++++--------------------- 1 file changed, 218 insertions(+), 217 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 23e2eaf..840d42b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -1,217 +1,218 @@ -package config - -import ( - "bytes" - "encoding/json" - "fyp-api-gateway/src/utils" - "io" - "log/slog" - "net/http" - "os" - "strings" - "text/template" - - "gopkg.in/yaml.v3" -) - -type ConfRequest struct { - Content string `json:"content"` -} - -func InitUserNGINX(username string) error { - // load the default config - gatewayConf, err := loadAndValidateGatewayConf(utils.DefaultConfigContent) - if err != nil { - slog.Error("failed to load and validate gateway config", "error", err) - return err - } - - nginxUserConfDir := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName - - // create new user nginx file - _, err = os.Stat(nginxUserConfDir) - if err == nil { - slog.Error("NGINX config file already exists") - return err - } - - err = os.MkdirAll(utils.NGINXDirName+"users/"+username, 0644) - if err != nil { - slog.Error("failed creating users directory", "error", err) - return err - } - - _, err = os.Create(nginxUserConfDir) - if err != nil { - slog.Error("failed creating users config", "error", err) - return err - } - - err = renderNginxTemplate(gatewayConf, nginxUserConfDir) - if err != nil { - slog.Error("failed rendering NGINX template", "error", err) - return err - } - - return nil -} - -func loadAndValidateGatewayConf(body string) (*GatewayConfig, error) { - var config GatewayConfig - - decoder := yaml.NewDecoder(bytes.NewReader([]byte(body))) - if err := decoder.Decode(&config); err != nil { - return nil, err - } - - return &config, nil -} - -func renderNginxTemplate(gatewayCfg *GatewayConfig, nginxUserConfDir string) error { - renderModel := buildRenderModel(gatewayCfg) - - // load the template file - tmpl, err := template.ParseFiles(utils.NGINXTemplateDirName + utils.NGINXTemplateFileName) - if err != nil { - return err - } - - // Create the updated NGINX config file and save it into the file containing the current config - var buf bytes.Buffer - if err = tmpl.Execute(&buf, renderModel); err != nil { - slog.Error("Error executing template: ", "error", err) - return err - } - nginxString := buf.String() - - // write the file - err = os.WriteFile(nginxUserConfDir, []byte(nginxString), 0644) - if err != nil { - return err - } - - return nil -} - -func buildRenderModel(gw *GatewayConfig) RenderModel { - model := RenderModel{} - - c := gw.Connections - conn := Connection{} - - for _, r := range c.Routes { - zoneName := strings.ReplaceAll(r.Path, "/", "_") - // if path is just "/", fallback - if zoneName == "" { - zoneName = "root" - } - - conn.Routes = append(conn.Routes, Routes{ - Path: r.Path, - Url: r.Url, - Auth: r.Auth, - RateLimit: r.RateLimit, - ZoneName: zoneName, - }) - } - - model.Connections = append(model.Connections, conn) - - return model -} - -func LoadNewConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("loading new gateway config file") - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("session cookie not found in request", "error", err) - http.Error(w, "cookie not found in request", http.StatusUnauthorized) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - slog.Error("failed to read request body", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer func() { - err = r.Body.Close() - if err != nil { - slog.Error("failed to close request body", "error", err) - http.Error(w, "failed to close request body", http.StatusInternalServerError) - return - } - }() - - var req ConfRequest - if err = json.Unmarshal(body, &req); err != nil { - slog.Error("failed to unmarshal request", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - err = InsertNewConfig(cookie.Value, req.Content) - if err != nil { - slog.Error("failed to insert new config", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - username := RetrieveUserBySessionId(cookie.Value) - nginxUserConfPath := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName - - gatewayConf, err := loadAndValidateGatewayConf(req.Content) - if err != nil { - slog.Error("failed to validate gateway config", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - err = renderNginxTemplate(gatewayConf, nginxUserConfPath) - if err != nil { - slog.Error("failed to render NGINX template", "error", err) - http.Error(w, "failed to render NGINX template", http.StatusInternalServerError) - return - } - - // Atomic writes - tempFile, err := os.CreateTemp(nginxUserConfPath, "nginx-*.conf") - if err != nil { - slog.Error("failed creating temp file", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - defer os.Remove(tempFile.Name()) - - if _, err = tempFile.Write(body); err != nil { - slog.Error("failed writing temp config", "error", err) - tempFile.Close() - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - if err = tempFile.Close(); err != nil { - slog.Error("failed closing temp file", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - // Atomic replace - if err := os.Rename(tempFile.Name(), nginxUserConfPath); err != nil { - slog.Error("failed replacing config file", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - slog.Info("gateway config updated successfully", "path", nginxUserConfPath) - - w.WriteHeader(http.StatusOK) -} +package config + +import ( + "bytes" + "encoding/json" + "fyp-api-gateway/src/utils" + "io" + "log/slog" + "net/http" + "os" + "strings" + "text/template" + + "gopkg.in/yaml.v3" +) + +type ConfRequest struct { + Content string `json:"content"` +} + +func InitUserNGINX(username string) error { + // load the default config + gatewayConf, err := loadAndValidateGatewayConf(utils.DefaultConfigContent) + if err != nil { + slog.Error("failed to load and validate gateway config", "error", err) + return err + } + + nginxUserConfDir := utils.NGINXDirName + "users/" + username + "/" + utils.NGINXConfigFileName + + // create new user nginx file + _, err = os.Stat(nginxUserConfDir) + if err == nil { + slog.Error("NGINX config file already exists") + return err + } + + err = os.MkdirAll(utils.NGINXDirName+"users/"+username, 0644) + if err != nil { + slog.Error("failed creating users directory", "error", err) + return err + } + + _, err = os.Create(nginxUserConfDir) + if err != nil { + slog.Error("failed creating users config", "error", err) + return err + } + + err = renderNginxTemplate(gatewayConf, nginxUserConfDir) + if err != nil { + slog.Error("failed rendering NGINX template", "error", err) + return err + } + + return nil +} + +func loadAndValidateGatewayConf(body string) (*GatewayConfig, error) { + var config GatewayConfig + + decoder := yaml.NewDecoder(bytes.NewReader([]byte(body))) + if err := decoder.Decode(&config); err != nil { + return nil, err + } + + return &config, nil +} + +func renderNginxTemplate(gatewayCfg *GatewayConfig, nginxUserConfDir string) error { + renderModel := buildRenderModel(gatewayCfg) + + // load the template file + tmpl, err := template.ParseFiles(utils.NGINXTemplateDirName + utils.NGINXTemplateFileName) + if err != nil { + return err + } + + // Create the updated NGINX config file and save it into the file containing the current config + var buf bytes.Buffer + if err = tmpl.Execute(&buf, renderModel); err != nil { + slog.Error("Error executing template: ", "error", err) + return err + } + nginxString := buf.String() + + // write the file + err = os.WriteFile(nginxUserConfDir, []byte(nginxString), 0644) + if err != nil { + return err + } + + return nil +} + +func buildRenderModel(gw *GatewayConfig) RenderModel { + model := RenderModel{} + + c := gw.Connections + conn := Connection{} + + for _, r := range c.Routes { + zoneName := strings.ReplaceAll(r.Path, "/", "_") + // if path is just "/", fallback + if zoneName == "" { + zoneName = "root" + } + + conn.Routes = append(conn.Routes, Routes{ + Path: r.Path, + Url: r.Url, + Auth: r.Auth, + RateLimit: r.RateLimit, + ZoneName: zoneName, + }) + } + + model.Connections = append(model.Connections, conn) + + return model +} + +func LoadNewConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("loading new gateway config file") + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("session cookie not found in request", "error", err) + http.Error(w, "cookie not found in request", http.StatusUnauthorized) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + slog.Error("failed to read request body", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer func() { + err = r.Body.Close() + if err != nil { + slog.Error("failed to close request body", "error", err) + http.Error(w, "failed to close request body", http.StatusInternalServerError) + return + } + }() + + var req ConfRequest + if err = json.Unmarshal(body, &req); err != nil { + slog.Error("failed to unmarshal request", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + err = InsertNewConfig(cookie.Value, req.Content) + if err != nil { + slog.Error("failed to insert new config", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + username := RetrieveUserBySessionId(cookie.Value) + nginxUserConfDir := utils.NGINXDirName + "users/" + username + "/" + nginxUserConfPath := nginxUserConfDir + utils.NGINXConfigFileName + + gatewayConf, err := loadAndValidateGatewayConf(req.Content) + if err != nil { + slog.Error("failed to validate gateway config", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + err = renderNginxTemplate(gatewayConf, nginxUserConfPath) + if err != nil { + slog.Error("failed to render NGINX template", "error", err) + http.Error(w, "failed to render NGINX template", http.StatusInternalServerError) + return + } + + // Atomic writes + tempFile, err := os.CreateTemp(nginxUserConfDir, "nginx-*.conf") + if err != nil { + slog.Error("failed creating temp file", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + defer os.Remove(tempFile.Name()) + + if _, err = tempFile.Write(body); err != nil { + slog.Error("failed writing temp config", "error", err) + tempFile.Close() + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + if err = tempFile.Close(); err != nil { + slog.Error("failed closing temp file", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Atomic replace + if err := os.Rename(tempFile.Name(), nginxUserConfPath); err != nil { + slog.Error("failed replacing config file", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + slog.Info("gateway config updated successfully", "path", nginxUserConfPath) + + w.WriteHeader(http.StatusOK) +} From 7109280cd09f2fc993280041be1018858dab1480 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Tue, 10 Mar 2026 13:10:09 +0000 Subject: [PATCH 06/10] Remove cleaning database on startup --- src/config/init.sql | 74 ++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/config/init.sql b/src/config/init.sql index 12f17e7..f008aa6 100644 --- a/src/config/init.sql +++ b/src/config/init.sql @@ -1,47 +1,47 @@ -DROP TABLE IF EXISTS users; +-- DROP TABLE IF EXISTS users; CREATE TABLE IF NOT EXISTS users ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username VARCHAR(64) NOT NULL, password VARCHAR(256) NOT NULL, config_yaml TEXT NOT NULL ); +-- +-- INSERT INTO users(username, password, config_yaml) +-- VALUES ('admin', 'admin', '# +-- # Configuration file for API Gateway +-- # +-- +-- connections: +-- routes: +-- - path: /products +-- url: http://services:9001 +-- rate-limit: +-- zone: 10 +-- rate: 5 +-- auth: false +-- +-- - path: /orders +-- url: http://services:9002 +-- rate-limit: +-- zone: 10 +-- rate: 5 +-- auth: false +-- +-- - path: /protected +-- url: http://services:9003 +-- rate-limit: +-- zone: 10 +-- rate: 5 +-- auth: true +-- +-- - path: /external-weather +-- url: https://api.open-meteo.com +-- rate-limit: +-- zone: 10 +-- rate: 5 +-- auth: false'); -INSERT INTO users(username, password, config_yaml) -VALUES ('admin', 'admin', '# -# Configuration file for API Gateway -# - -connections: - routes: - - path: /products - url: http://services:9001 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: /orders - url: http://services:9002 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: /protected - url: http://services:9003 - rate-limit: - zone: 10 - rate: 5 - auth: true - - - path: /external-weather - url: https://api.open-meteo.com - rate-limit: - zone: 10 - rate: 5 - auth: false'); - -DROP TABLE IF EXISTS sessions; +-- DROP TABLE IF EXISTS sessions; CREATE TABLE IF NOT EXISTS sessions ( id UUID PRIMARY KEY, username VARCHAR(64) NOT NULL, From ca55b55c0cf218d10e943d53dc423b454bee8052 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Tue, 10 Mar 2026 13:41:27 +0000 Subject: [PATCH 07/10] Refactor atomic writes --- src/config/config.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 840d42b..2674465 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -184,6 +184,13 @@ func LoadNewConfig(w http.ResponseWriter, r *http.Request) { } // Atomic writes + renderedConfig, err := os.ReadFile(nginxUserConfPath) + if err != nil { + slog.Error("failed to read rendered NGINX template", "error", err) + http.Error(w, "failed to render NGINX template", http.StatusInternalServerError) + return + } + tempFile, err := os.CreateTemp(nginxUserConfDir, "nginx-*.conf") if err != nil { slog.Error("failed creating temp file", "error", err) @@ -192,7 +199,7 @@ func LoadNewConfig(w http.ResponseWriter, r *http.Request) { } defer os.Remove(tempFile.Name()) - if _, err = tempFile.Write(body); err != nil { + if _, err = tempFile.Write(renderedConfig); err != nil { slog.Error("failed writing temp config", "error", err) tempFile.Close() http.Error(w, "internal server error", http.StatusInternalServerError) @@ -206,7 +213,7 @@ func LoadNewConfig(w http.ResponseWriter, r *http.Request) { } // Atomic replace - if err := os.Rename(tempFile.Name(), nginxUserConfPath); err != nil { + if err = os.Rename(tempFile.Name(), nginxUserConfPath); err != nil { slog.Error("failed replacing config file", "error", err) http.Error(w, "internal server error", http.StatusInternalServerError) return From b7b421a4e58f5b523efad574c2afd515db63995b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 25 Mar 2026 17:15:17 +0000 Subject: [PATCH 08/10] Propagate changes from aws --- docker-compose.yml | 6 ++++-- management/static/auth.html | 3 ++- management/static/config.html | 3 ++- management/static/index.html | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8b74a07..d8b461a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -169,6 +169,8 @@ services: - GF_PATHS_PROVISIONING=/etc/grafana/provisioning - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_SERVER_ROOT_URL=http://synchro-accelerator.com/grafana/ + - GF_SERVER_SERVE_FROM_SUB_PATH=true depends_on: - gateway entrypoint: @@ -192,7 +194,7 @@ services: ports: - "3000:3000" healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1" ] + test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/grafana/api/health || exit 1" ] interval: 10s timeout: 5s retries: 5 @@ -285,4 +287,4 @@ services: image: mingrammer/flog command: -f json -d 200ms -l networks: - - loki \ No newline at end of file + - loki diff --git a/management/static/auth.html b/management/static/auth.html index 85cb3a8..16112a1 100644 --- a/management/static/auth.html +++ b/management/static/auth.html @@ -11,6 +11,7 @@ Home Signup/Login Config File + Dashboard @@ -44,4 +45,4 @@

Login

- \ No newline at end of file + diff --git a/management/static/config.html b/management/static/config.html index 3bad495..cfd0be4 100644 --- a/management/static/config.html +++ b/management/static/config.html @@ -11,6 +11,7 @@ Home Signup/Login Config File + Dashboard @@ -23,4 +24,4 @@

Edit your Config

- \ No newline at end of file + diff --git a/management/static/index.html b/management/static/index.html index 677b794..bf2830f 100644 --- a/management/static/index.html +++ b/management/static/index.html @@ -11,7 +11,7 @@ Home Signup/Login Config File - Dashboard + Dashboard @@ -40,4 +40,4 @@

Live Deployment

- \ No newline at end of file + From 4b49d616ceaa8a1b20c5a3d49d1144f3cbd758fe Mon Sep 17 00:00:00 2001 From: Ubuntu <122345776@umail.ucc.ie> Date: Thu, 26 Mar 2026 12:26:23 +0000 Subject: [PATCH 09/10] fix --- dataplane/main.go | 1 + src/templates/nginx.conf.tmpl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dataplane/main.go b/dataplane/main.go index e07ed80..1682b4a 100644 --- a/dataplane/main.go +++ b/dataplane/main.go @@ -75,6 +75,7 @@ func handleNewConfig(w http.ResponseWriter, r *http.Request) { } func applyNginxConfig() error { + slog.Info("attempting to reload nginx config") cmd := exec.Command("nginx", "-t") output, err := cmd.CombinedOutput() diff --git a/src/templates/nginx.conf.tmpl b/src/templates/nginx.conf.tmpl index a74a844..c5fda32 100644 --- a/src/templates/nginx.conf.tmpl +++ b/src/templates/nginx.conf.tmpl @@ -7,7 +7,7 @@ limit_req_zone $binary_remote_addr zone={{ $.Username }}_{{ .ZoneName }}:{{ .Rat {{/* Server blocks */}} server { listen 8080; - server_name _; + server_name synchro-accelerator.com; access_log /var/log/nginx/users/{{ .Username }}/access.log; error_log /var/log/nginx/users/{{ .Username }}/error.log; @@ -35,4 +35,4 @@ server { proxy_pass {{ .Url }}; } {{- end }} -} \ No newline at end of file +} From bf49e4424ee5af912e31ea7b8d2f6aece466e402 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Fri, 3 Apr 2026 11:39:27 +0100 Subject: [PATCH 10/10] Switch password hash to Argon2id --- config/gateway.yaml | 14 - src/config/db.go | 713 ++++++++++++++++++++++++-------------------- 2 files changed, 387 insertions(+), 340 deletions(-) diff --git a/config/gateway.yaml b/config/gateway.yaml index 2d8b791..2732a77 100644 --- a/config/gateway.yaml +++ b/config/gateway.yaml @@ -4,20 +4,6 @@ connections: routes: - - path: products - url: http://services:9001 - rate-limit: - zone: 10 - rate: 5 - auth: false - - - path: orders - url: http://services:9002 - rate-limit: - zone: 10 - rate: 5 - auth: false - - path: protected url: http://services:9003 rate-limit: diff --git a/src/config/db.go b/src/config/db.go index 53eeeb6..850c9d0 100644 --- a/src/config/db.go +++ b/src/config/db.go @@ -1,326 +1,387 @@ -package config - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fyp-api-gateway/src/utils" - "log/slog" - "net/http" - "os" - "time" - - uuid "github.com/google/uuid" - _ "github.com/jackc/pgx/v5/stdlib" - "golang.org/x/crypto/bcrypt" -) - -type Database struct { - Conn *sql.DB -} - -type Server struct { - DB *Database -} - -type LoginInfo struct { - Name string `json:"name"` - Password string `json:"password"` -} - -func NewDatabase(dsn string) (*Database, error) { - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return nil, err - } - - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(25) - db.SetConnMaxLifetime(5 * time.Minute) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err = db.PingContext(ctx); err != nil { - return nil, err - } - - return &Database{Conn: db}, nil -} - -func (d *Database) StartDB(path string) error { - content, err := os.ReadFile(path) - if err != nil { - slog.Error("error reading database file", "error", err) - return err - } - - if _, err = d.Conn.Exec(string(content)); err != nil { - slog.Error("error executing database statement", "error", err) - return err - } - - return nil -} - -func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { - slog.Info("attempting to sign up new user...") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var id string - err := s.DB.Conn.QueryRow( - "SELECT id FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&id) - - if err == nil || id != "" { - slog.Error("error querying user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - hashed_password, err := bcrypt.GenerateFromPassword([]byte(loginInfo.Password), 14) - _, err = s.DB.Conn.Exec(` - INSERT INTO users (username, password, config_yaml) - VALUES ($1, $2, $3);`, - loginInfo.Name, string(hashed_password), utils.DefaultConfigContent, - ) - if err != nil { - slog.Error("error inserting user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - err = InitUserNGINX(loginInfo.Name) - if err != nil { - slog.Error("error initializing user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -/* -Receive the login info from the management plane and decode it -Check the password used is the same as the one in the database, also check the username is there -If the user has no session, create one and send it back, otherwise return the existing session -*/ -func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { - slog.Info("validating login information") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var storedHash string - err := s.DB.Conn.QueryRow( - "SELECT password FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&storedHash) - - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "username", loginInfo.Name) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(loginInfo.Password)) - if err != nil { - slog.Error("error verifying loginInfo", "error", err) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - // check is the user has a session already - var sessionId string - sessionId, isSession := s.sessionExists(loginInfo.Name) - - if !isSession { - sessionId, err = s.createSession(loginInfo.Name) - if err != nil { - slog.Error("error creating session", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "sessionId": sessionId, - }) -} - -func (s *Server) sessionExists(username string) (string, bool) { - var sessionId string - err := s.DB.Conn.QueryRow( - "SELECT id FROM sessions WHERE username=$1", - username, - ).Scan(&sessionId) - - if err != nil { - return "", false - } - return sessionId, true -} - -func (s *Server) createSession(name string) (string, error) { - sessionId := uuid.New().String() - expires := time.Now().Add(24 * time.Hour) - - _, err := s.DB.Conn.Exec( - "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", - sessionId, name, expires, - ) - - if err != nil { - slog.Error("error creating session", "error", err) - return "", err - } - - return sessionId, nil -} - -func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { - slog.Info("validating user session") - sessionId := r.Header.Get("X-Session-ID") - - var username string - err := s.DB.Conn.QueryRow( - "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", - sessionId, - ).Scan(&username) - - if err != nil { - slog.Error("error querying session", "error", err) - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("received request for user config") - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("failed getting session id", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sessionId := cookie.Value - - var gatewayCfg string - err = s.DB.Conn.QueryRow(` - SELECT u.config_yaml - FROM users AS u - JOIN sessions AS s ON u.username = s.username - WHERE s.id = $1 AND s.expires > NOW()`, - sessionId).Scan(&gatewayCfg) - - if err != nil { - slog.Error("error querying session", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/yaml") - w.WriteHeader(http.StatusOK) - _, err = w.Write([]byte(gatewayCfg)) - if err != nil { - slog.Error("error writing response", "error", err) - return - } -} - -func RetrieveUserBySessionId(sessionId string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var username string - err = db.QueryRow(` - SELECT username - FROM sessions - WHERE id = $1`, - sessionId).Scan(&username) - - return username -} - -func RetrieveUserConfig(username string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var gatewayCfg string - err = db.QueryRow(` - SELECT config_yaml - FROM users - WHERE username = $1`, - username, - ).Scan(&gatewayCfg) - - return gatewayCfg -} - -func InsertNewConfig(sessionId, gatewayCfg string) error { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - return err - } - - _, err = db.Exec(` - UPDATE users AS u - SET config_yaml = $1 - FROM sessions AS s - WHERE s.id = $2 - AND s.expires > NOW() - AND u.username = s.username`, - gatewayCfg, sessionId, - ) - slog.Info("inserted new config to database") - - if err != nil { - return err - } - - return nil -} +package config + +import ( + "context" + "crypto/rand" + "crypto/subtle" + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "fyp-api-gateway/src/utils" + "log/slog" + "net/http" + "os" + "strings" + "time" + + uuid "github.com/google/uuid" + _ "github.com/jackc/pgx/v5/stdlib" + "golang.org/x/crypto/argon2" +) + +type Database struct { + Conn *sql.DB +} + +type Server struct { + DB *Database +} + +type LoginInfo struct { + Name string `json:"name"` + Password string `json:"password"` +} + +func NewDatabase(dsn string) (*Database, error) { + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return nil, err + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err = db.PingContext(ctx); err != nil { + return nil, err + } + + return &Database{Conn: db}, nil +} + +func (d *Database) StartDB(path string) error { + content, err := os.ReadFile(path) + if err != nil { + slog.Error("error reading database file", "error", err) + return err + } + + if _, err = d.Conn.Exec(string(content)); err != nil { + slog.Error("error executing database statement", "error", err) + return err + } + + return nil +} + +func hashPassword(password string) (string, error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return "", err + } + + hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32) + + encoded := fmt.Sprintf("$argon2id$v=19$m=65536,t=1,p=4$%s$%s", + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(hash), + ) + return encoded, nil +} + +func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { + slog.Info("attempting to sign up new user...") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var id string + err := s.DB.Conn.QueryRow( + "SELECT id FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&id) + + if err == nil || id != "" { + slog.Error("error querying user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + hashed_password, err := hashPassword(loginInfo.Password) + if err != nil { + slog.Error("error hashing password", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + _, err = s.DB.Conn.Exec(` + INSERT INTO users (username, password, config_yaml) + VALUES ($1, $2, $3);`, + loginInfo.Name, string(hashed_password), utils.DefaultConfigContent, + ) + if err != nil { + slog.Error("error inserting user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + err = InitUserNGINX(loginInfo.Name) + if err != nil { + slog.Error("error initializing user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +/* Code for this method supplied by https://www.alexedwards.net/blog/how-to-hash-and-verify-passwords-with-argon2-in-go */ +func verifyPassword(password, hash string) (bool, error) { + var m, t, p uint32 + var saltB64, hashB64 string + _, err := fmt.Sscanf(hash, "$argon2id$v=19$m=%d,t=%d,p=%d$%s", + &m, &t, &p, &saltB64) + if err != nil { + return false, err + } + + parts := strings.Split(hash, "$") + if len(parts) != 6 { + return false, fmt.Errorf("invalid hash format") + } + saltB64 = parts[4] + hashB64 = parts[5] + + salt, err := base64.RawStdEncoding.DecodeString(saltB64) + if err != nil { + return false, err + } + storedHash, err := base64.RawStdEncoding.DecodeString(hashB64) + if err != nil { + return false, err + } + + keyLen := uint32(len(storedHash)) + computed := argon2.IDKey([]byte(password), salt, t, m, uint8(p), keyLen) + + if subtle.ConstantTimeCompare(computed, storedHash) != 1 { + return false, nil + } + return true, nil +} + +/* +Receive the login info from the management plane and decode it +Check the password used is the same as the one in the database, also check the username is there +If the user has no session, create one and send it back, otherwise return the existing session +*/ +func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { + slog.Info("validating login information") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var storedHash string + err := s.DB.Conn.QueryRow( + "SELECT password FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&storedHash) + + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "username", loginInfo.Name) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + isCorrect, err := verifyPassword(loginInfo.Password, storedHash) + if err != nil || !isCorrect { + slog.Error("error verifying loginInfo", "error", err) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + // check is the user has a session already + var sessionId string + sessionId, isSession := s.sessionExists(loginInfo.Name) + + if !isSession { + sessionId, err = s.createSession(loginInfo.Name) + if err != nil { + slog.Error("error creating session", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "sessionId": sessionId, + }) +} + +func (s *Server) sessionExists(username string) (string, bool) { + var sessionId string + err := s.DB.Conn.QueryRow( + "SELECT id FROM sessions WHERE username=$1", + username, + ).Scan(&sessionId) + + if err != nil { + return "", false + } + return sessionId, true +} + +func (s *Server) createSession(name string) (string, error) { + sessionId := uuid.New().String() + expires := time.Now().Add(24 * time.Hour) + + _, err := s.DB.Conn.Exec( + "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", + sessionId, name, expires, + ) + + if err != nil { + slog.Error("error creating session", "error", err) + return "", err + } + + return sessionId, nil +} + +func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { + slog.Info("validating user session") + sessionId := r.Header.Get("X-Session-ID") + + var username string + err := s.DB.Conn.QueryRow( + "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", + sessionId, + ).Scan(&username) + + if err != nil { + slog.Error("error querying session", "error", err) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("received request for user config") + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("failed getting session id", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sessionId := cookie.Value + + var gatewayCfg string + err = s.DB.Conn.QueryRow(` + SELECT u.config_yaml + FROM users AS u + JOIN sessions AS s ON u.username = s.username + WHERE s.id = $1 AND s.expires > NOW()`, + sessionId).Scan(&gatewayCfg) + + if err != nil { + slog.Error("error querying session", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/yaml") + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(gatewayCfg)) + if err != nil { + slog.Error("error writing response", "error", err) + return + } +} + +func RetrieveUserBySessionId(sessionId string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var username string + err = db.QueryRow(` + SELECT username + FROM sessions + WHERE id = $1`, + sessionId).Scan(&username) + + return username +} + +func RetrieveUserConfig(username string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var gatewayCfg string + err = db.QueryRow(` + SELECT config_yaml + FROM users + WHERE username = $1`, + username, + ).Scan(&gatewayCfg) + + return gatewayCfg +} + +func InsertNewConfig(sessionId, gatewayCfg string) error { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + return err + } + + _, err = db.Exec(` + UPDATE users AS u + SET config_yaml = $1 + FROM sessions AS s + WHERE s.id = $2 + AND s.expires > NOW() + AND u.username = s.username`, + gatewayCfg, sessionId, + ) + slog.Info("inserted new config to database") + + if err != nil { + return err + } + + return nil +}