From 9f5dce9d6ae991c43fd9b05316f980ed8bdb2741 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 21 Jun 2024 20:07:24 +0200 Subject: [PATCH] Add server stats (#15) 3. **Server Stats**: Shows real-time statistics about server usage: - Server Uptime - Download/Upload Counts (since start and last 24 hours) - HTTP Error Counts - Rate Limit Hit Count 4. **Rate Limit Stats**: Provides information about rate limiting, if applicable. --- README.md | 29 ++++ httphandler/config.go | 2 + httphandler/deleteHandler.go | 1 + httphandler/downloadHandler.go | 9 ++ httphandler/keyPairHandler.go | 1 + httphandler/uploadHandler.go | 3 + main.go | 53 +++--- main_router_test.go | 286 +++++++++++---------------------- middleware/config.go | 3 + middleware/limitRequestSize.go | 1 + middleware/ratelimit.go | 11 +- static/index.html | 28 ++++ stats/stats.go | 143 +++++++++++++++++ 13 files changed, 356 insertions(+), 214 deletions(-) create mode 100644 stats/stats.go diff --git a/README.md b/README.md index 18309fd..0cab73e 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,35 @@ This project provides a simple HTTP server that offers ephemeral storage for IoT - Data is stored for a configurable duration before being deleted. - The server can be run on a local network or in the cloud. +## Self-Hosted Info Website + +The iot-ephemeral-value-store server includes a self-hosted info website that provides valuable information about the server's status, usage, and API. This website is automatically available when you run the server and can be accessed at the root URL (e.g., `http://127.0.0.1:8088/`). + +### Features of the Info Website + +1. **Getting Started Guide**: Provides example URLs for uploading, downloading, and deleting data, customized with a generated key pair for immediate use. + +2. **Server Settings**: Displays important server configuration information, including: + - Software Version + - Software Build Time + - Data Retention Period + +3. **Server Stats**: Shows real-time statistics about server usage: + - Server Uptime + - Download/Upload Counts (since start and last 24 hours) + - HTTP Error Counts + - Rate Limit Hit Count + +4. **Rate Limit Stats**: Provides information about rate limiting, if applicable. + +5. **API Usage Guide**: Offers a quick reference for using the server's API, including: + - Creating Key Pairs + - Uploading Data + - Downloading Data (JSON and Plain Text) + - Advanced Patch Usage + +This self-hosted info website serves as a dashboard and quick-start guide, making it easier for users to understand and interact with the iot-ephemeral-value-store server. + ## Diagrams ### Simple diff --git a/httphandler/config.go b/httphandler/config.go index ccec3b4..bf9b293 100644 --- a/httphandler/config.go +++ b/httphandler/config.go @@ -5,11 +5,13 @@ import ( "html" "net/http" + "github.com/dhcgn/iot-ephemeral-value-store/stats" "github.com/dhcgn/iot-ephemeral-value-store/storage" ) type Config struct { StorageInstance storage.Storage + StatsInstance *stats.Stats } func sanitizeInput(input string) string { diff --git a/httphandler/deleteHandler.go b/httphandler/deleteHandler.go index 2e330ac..1535766 100644 --- a/httphandler/deleteHandler.go +++ b/httphandler/deleteHandler.go @@ -13,6 +13,7 @@ func (c Config) DeleteHandler(w http.ResponseWriter, r *http.Request) { downloadKey, err := domain.DeriveDownloadKey(uploadKey) if err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Error deriving download key", http.StatusInternalServerError) return } diff --git a/httphandler/downloadHandler.go b/httphandler/downloadHandler.go index f52e2c1..149a66a 100644 --- a/httphandler/downloadHandler.go +++ b/httphandler/downloadHandler.go @@ -16,6 +16,7 @@ func (c Config) PlainDownloadHandler(w http.ResponseWriter, r *http.Request) { jsonData, err := c.StorageInstance.GetJSON(downloadKey) if err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Invalid download key or database error", http.StatusNotFound) return } @@ -23,6 +24,7 @@ func (c Config) PlainDownloadHandler(w http.ResponseWriter, r *http.Request) { // Parse the JSON data to retrieve the specific parameter paramMap := make(map[string]interface{}) if err := json.Unmarshal(jsonData, ¶mMap); err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Error decoding JSON", http.StatusInternalServerError) return } @@ -36,15 +38,19 @@ func (c Config) PlainDownloadHandler(w http.ResponseWriter, r *http.Request) { if m, ok := value.(map[string]interface{}); ok { value, ok = m[key] if !ok { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Parameter not found", http.StatusNotFound) return } } else { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Invalid parameter path", http.StatusBadRequest) return } } + c.StatsInstance.IncrementDownloads() + // Return the value as plain text w.Header().Set("Content-Type", "text/plain") fmt.Fprintln(w, value) @@ -56,10 +62,13 @@ func (c Config) DownloadHandler(w http.ResponseWriter, r *http.Request) { jsonData, err := c.StorageInstance.GetJSON(downloadKey) if err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Invalid download key or database error", http.StatusNotFound) return } + c.StatsInstance.IncrementDownloads() + // Set header and write the JSON data to the response writer w.Header().Set("Content-Type", "application/json") w.Write(jsonData) diff --git a/httphandler/keyPairHandler.go b/httphandler/keyPairHandler.go index 45ec506..3ad5288 100644 --- a/httphandler/keyPairHandler.go +++ b/httphandler/keyPairHandler.go @@ -10,6 +10,7 @@ func (c Config) KeyPairHandler(w http.ResponseWriter, r *http.Request) { uploadKey := domain.GenerateRandomKey() downloadKey, err := domain.DeriveDownloadKey(uploadKey) if err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Error deriving download key", http.StatusInternalServerError) return } diff --git a/httphandler/uploadHandler.go b/httphandler/uploadHandler.go index 1890e85..9333ac3 100644 --- a/httphandler/uploadHandler.go +++ b/httphandler/uploadHandler.go @@ -33,6 +33,7 @@ func (c Config) handleUpload(w http.ResponseWriter, r *http.Request, uploadKey, // Derive download key downloadKey, err := domain.DeriveDownloadKey(uploadKey) if err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Error deriving download key", http.StatusInternalServerError) return } @@ -51,6 +52,8 @@ func (c Config) handleUpload(w http.ResponseWriter, r *http.Request, uploadKey, } c.StorageInstance.Store(downloadKey, data) + c.StatsInstance.IncrementUploads() + // Construct and return response constructAndReturnResponse(w, r, downloadKey, paramMap) } diff --git a/main.go b/main.go index 2ed575a..14573e6 100644 --- a/main.go +++ b/main.go @@ -11,10 +11,10 @@ import ( "text/template" "time" - "github.com/dgraph-io/badger/v3" "github.com/dhcgn/iot-ephemeral-value-store/domain" "github.com/dhcgn/iot-ephemeral-value-store/httphandler" "github.com/dhcgn/iot-ephemeral-value-store/middleware" + "github.com/dhcgn/iot-ephemeral-value-store/stats" "github.com/dhcgn/iot-ephemeral-value-store/storage" "github.com/gorilla/mux" ) @@ -22,8 +22,8 @@ import ( const ( // Server configuration MaxRequestSize = 1024 * 10 // 10 KB for request size limit - RateLimitPerSecond = 10 // Requests per second - RateLimitBurst = 5 // Burst capability + RateLimitPerSecond = 100 // Requests per second + RateLimitBurst = 10 // Burst capability // Database and server paths DefaultStorePath = "./data" @@ -44,7 +44,6 @@ var ( persistDurationString string storePath string port int - db *badger.DB ) // Set in build time @@ -70,20 +69,24 @@ func main() { log.Fatalf("Failed to parse duration: %v", err) } + stats := stats.NewStats() + storage := storage.NewPersistentStorage(storePath, persistDuration) defer storage.Db.Close() httphandlerConfig := httphandler.Config{ StorageInstance: storage, + StatsInstance: stats, } middlewareConfig := middleware.Config{ RateLimitPerSecond: RateLimitPerSecond, RateLimitBurst: RateLimitBurst, MaxRequestSize: MaxRequestSize, + StatsInstance: stats, } - r := createRouter(httphandlerConfig, middlewareConfig) + r := createRouter(httphandlerConfig, middlewareConfig, stats) serverAddress := fmt.Sprintf("127.0.0.1:%d", port) srv := &http.Server{ @@ -99,7 +102,7 @@ func main() { } } -func createRouter(hhc httphandler.Config, mc middleware.Config) *mux.Router { +func createRouter(hhc httphandler.Config, mc middleware.Config, stats *stats.Stats) *mux.Router { // Template parsing tmpl, err := template.ParseFS(staticFiles, "static/index.html") if err != nil { @@ -134,7 +137,22 @@ func createRouter(hhc httphandler.Config, mc middleware.Config) *mux.Router { r.HandleFunc("/delete/{uploadKey}", hhc.DeleteHandler).Methods("GET") r.HandleFunc("/delete/{uploadKey}/", hhc.DeleteHandler).Methods("GET") - r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc("/", templateHandler(tmpl, stats)) + + // Not Found handler + r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 Not Found")) + }) + + staticSubFS, _ := fs.Sub(staticFiles, "static") + r.PathPrefix("/").Handler(http.FileServer(http.FS(staticSubFS))) + + return r +} + +func templateHandler(tmpl *template.Template, stats *stats.Stats) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { key := domain.GenerateRandomKey() key_down, err := domain.DeriveDownloadKey(key) if err != nil { @@ -147,20 +165,13 @@ func createRouter(hhc httphandler.Config, mc middleware.Config) *mux.Router { DataRetention: persistDurationString, Version: Version, BuildTime: BuildTime, - } - tmpl.Execute(w, data) - }) - // Not Found handler - r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 Not Found")) - }) + Uptime: stats.GetUptime(), - staticSubFS, _ := fs.Sub(staticFiles, "static") - r.PathPrefix("/").Handler(http.FileServer(http.FS(staticSubFS))) - - return r + StateData: stats.GetCurrentStats(), + } + tmpl.Execute(w, data) + } } type PageData struct { @@ -169,4 +180,8 @@ type PageData struct { DataRetention string Version string BuildTime string + + Uptime time.Duration + + StateData stats.StatsData } diff --git a/main_router_test.go b/main_router_test.go index a02510f..1b5bb24 100644 --- a/main_router_test.go +++ b/main_router_test.go @@ -1,256 +1,160 @@ package main import ( + "fmt" "net/http" "net/http/httptest" "testing" "github.com/dhcgn/iot-ephemeral-value-store/httphandler" "github.com/dhcgn/iot-ephemeral-value-store/middleware" + "github.com/dhcgn/iot-ephemeral-value-store/stats" "github.com/dhcgn/iot-ephemeral-value-store/storage" "github.com/stretchr/testify/assert" ) -func createTestEnvireonment(t *testing.T) (httphandler.Config, middleware.Config) { +const ( + keyUp = "8e88f1b62b946dd3fccfd8eaf54c9a2e5e27747c3662f2e20645073e4626d7c5" + keyDown = "fcbbda7c04eba41d060b70d1bf7fde8c4a148a087729017d22fc54037c9eb11b" +) + +type testCase struct { + name string + url string + expectedStatusCode int + checkBody bool + bodyContains string + bodyNotContains string +} + +func createTestEnvironment(t *testing.T) (*stats.Stats, httphandler.Config, middleware.Config) { + stats := stats.NewStats() storageInMemory := storage.NewInMemoryStorage() - var httphandlerConfig = httphandler.Config{ + httphandlerConfig := httphandler.Config{ StorageInstance: storageInMemory, + StatsInstance: stats, } - var middlewareConfig = middleware.Config{ + middlewareConfig := middleware.Config{ RateLimitPerSecond: RateLimitPerSecond, RateLimitBurst: RateLimitBurst, MaxRequestSize: MaxRequestSize, + StatsInstance: stats, } - return httphandlerConfig, middlewareConfig + return stats, httphandlerConfig, middlewareConfig } -var key_up = "8e88f1b62b946dd3fccfd8eaf54c9a2e5e27747c3662f2e20645073e4626d7c5" -var key_down = "fcbbda7c04eba41d060b70d1bf7fde8c4a148a087729017d22fc54037c9eb11b" - -func TestCreateRouter(t *testing.T) { - httphandlerConfig, middlewareConfig := createTestEnvireonment(t) - - router := createRouter(httphandlerConfig, middlewareConfig) - - tests := []struct { - name string - method string - url string - expectedStatusCode int - }{ - {"GET /", "GET", "/", http.StatusOK}, - {"GET /kp", "GET", "/kp", http.StatusOK}, - {"wrong upload key", "GET", "/wrong_upload_key", http.StatusBadRequest}, - } - +func runTests(t *testing.T, router http.Handler, tests []testCase) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest(tt.method, tt.url, nil) - if err != nil { - t.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, tt.url, nil) + assert.NoError(t, err) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) assert.Equal(t, tt.expectedStatusCode, rr.Code) + if tt.checkBody { + if tt.bodyContains != "" { + assert.Contains(t, rr.Body.String(), tt.bodyContains) + } + if tt.bodyNotContains != "" { + assert.NotContains(t, rr.Body.String(), tt.bodyNotContains) + } + } }) } } -func TestLegacyRoutes(t *testing.T) { - httphandlerConfig, middlewareConfig := createTestEnvireonment(t) +func buildURL(format string, a ...interface{}) string { + return fmt.Sprintf(format, a...) +} - router := createRouter(httphandlerConfig, middlewareConfig) +func TestCreateRouter(t *testing.T) { + stats, httphandlerConfig, middlewareConfig := createTestEnvironment(t) + router := createRouter(httphandlerConfig, middlewareConfig, stats) - tests := []struct { - name string - method string - url string - expectedStatusCode int - checkBody bool - bodyContains string - }{ - {"Upload", "GET", "/" + key_up + "/" + "?value=8923423", http.StatusOK, true, "Data uploaded successfully"}, - {"Download Plain", "GET", "/" + key_down + "/" + "plain/value", http.StatusOK, true, "8923423\n"}, - {"Downlaod JSON", "GET", "/" + key_down + "/" + "json", http.StatusOK, true, "\"value\":\"8923423\""}, + tests := []testCase{ + {"GET /", "/", http.StatusOK, false, "", ""}, + {"GET /kp", "/kp", http.StatusOK, false, "", ""}, + {"wrong upload key", "/wrong_upload_key", http.StatusBadRequest, false, "", ""}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest(tt.method, tt.url, nil) - if err != nil { - t.Fatal(err) - } + runTests(t, router, tests) +} - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) +func TestLegacyRoutes(t *testing.T) { + stats, httphandlerConfig, middlewareConfig := createTestEnvironment(t) + router := createRouter(httphandlerConfig, middlewareConfig, stats) - assert.Equal(t, tt.expectedStatusCode, rr.Code) - if tt.checkBody { - assert.Contains(t, rr.Body.String(), tt.bodyContains) - } - }) + tests := []testCase{ + {"Upload", buildURL("/%s/?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Download Plain", buildURL("/%s/plain/value", keyDown), http.StatusOK, true, "8923423\n", ""}, + {"Download JSON", buildURL("/%s/json", keyDown), http.StatusOK, true, "\"value\":\"8923423\"", ""}, } + + runTests(t, router, tests) } func TestRoutesUploadDownload(t *testing.T) { - httphandlerConfig, middlewareConfig := createTestEnvireonment(t) + stats, httphandlerConfig, middlewareConfig := createTestEnvironment(t) + router := createRouter(httphandlerConfig, middlewareConfig, stats) - router := createRouter(httphandlerConfig, middlewareConfig) - - tests := []struct { - name string - method string - url string - expectedStatusCode int - checkBody bool - bodyContains string - }{ - {"GET /", "GET", "/u/" + key_up + "/" + "?value=8923423", http.StatusOK, true, "Data uploaded successfully"}, - {"GET /", "GET", "/d/" + key_down + "/" + "plain/value", http.StatusOK, true, "8923423\n"}, - {"GET /", "GET", "/d/" + key_down + "/" + "json", http.StatusOK, true, "\"value\":\"8923423\""}, + tests := []testCase{ + {"Upload", buildURL("/u/%s/?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Download Plain", buildURL("/d/%s/plain/value", keyDown), http.StatusOK, true, "8923423\n", ""}, + {"Download JSON", buildURL("/d/%s/json", keyDown), http.StatusOK, true, "\"value\":\"8923423\"", ""}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest(tt.method, tt.url, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - assert.Equal(t, tt.expectedStatusCode, rr.Code) - if tt.checkBody { - assert.Contains(t, rr.Body.String(), tt.bodyContains) - } - }) - } + runTests(t, router, tests) } func TestRoutesUploadDownloadDelete(t *testing.T) { - httphandlerConfig, middlewareConfig := createTestEnvireonment(t) - - router := createRouter(httphandlerConfig, middlewareConfig) - - tests := []struct { - name string - url string - expectedStatusCode int - bodyContains string - }{ - {"Upload", "/u/" + key_up + "/" + "?value=8923423", http.StatusOK, "Data uploaded successfully"}, - {"Download plain", "/d/" + key_down + "/" + "plain/value", http.StatusOK, "8923423\n"}, - {"Download json", "/d/" + key_down + "/" + "json", http.StatusOK, "\"value\":\"8923423\""}, - {"Delete", "/delete/" + key_up + "/", http.StatusOK, "OK"}, - {"Download after delete plain", "/d/" + key_down + "/" + "plain/value", http.StatusNotFound, ""}, - {"Download after delete json", "/d/" + key_down + "/" + "json", http.StatusNotFound, ""}, + stats, httphandlerConfig, middlewareConfig := createTestEnvironment(t) + router := createRouter(httphandlerConfig, middlewareConfig, stats) + + tests := []testCase{ + {"Upload", buildURL("/u/%s/?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Download plain", buildURL("/d/%s/plain/value", keyDown), http.StatusOK, true, "8923423\n", ""}, + {"Download json", buildURL("/d/%s/json", keyDown), http.StatusOK, true, "\"value\":\"8923423\"", ""}, + {"Delete", buildURL("/delete/%s/", keyUp), http.StatusOK, true, "OK", ""}, + {"Download after delete plain", buildURL("/d/%s/plain/value", keyDown), http.StatusNotFound, false, "", ""}, + {"Download after delete json", buildURL("/d/%s/json", keyDown), http.StatusNotFound, false, "", ""}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest("GET", tt.url, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - assert.Equal(t, tt.expectedStatusCode, rr.Code) - if tt.bodyContains != "" { - assert.Contains(t, rr.Body.String(), tt.bodyContains) - } - }) - } + runTests(t, router, tests) } func TestLegacyRoutesWithDifferentPathEndings(t *testing.T) { - httphandlerConfig, middlewareConfig := createTestEnvireonment(t) - - router := createRouter(httphandlerConfig, middlewareConfig) - - tests := []struct { - name string - method string - url string - expectedStatusCode int - bodyContains string - }{ - {"Upload Legacy with ending /", "GET", "/" + key_up + "/" + "?value=8923423", http.StatusOK, "Data uploaded successfully"}, - {"Upload Legacy without ending /", "GET", "/" + key_up + "?value=8923423", http.StatusOK, "Data uploaded successfully"}, - - {"Upload with ending /", "GET", "/u/" + key_up + "/" + "?value=8923423", http.StatusOK, "Data uploaded successfully"}, - {"Upload without ending /", "GET", "/u/" + key_up + "?value=8923423", http.StatusOK, "Data uploaded successfully"}, + stats, httphandlerConfig, middlewareConfig := createTestEnvironment(t) + router := createRouter(httphandlerConfig, middlewareConfig, stats) + + tests := []testCase{ + {"Upload Legacy with ending /", buildURL("/%s/?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Upload Legacy without ending /", buildURL("/%s?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Upload with ending /", buildURL("/u/%s/?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Upload without ending /", buildURL("/u/%s?value=8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest(tt.method, tt.url, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - assert.Equal(t, tt.expectedStatusCode, rr.Code) - if tt.bodyContains != "" { - assert.Contains(t, rr.Body.String(), tt.bodyContains) - } - }) - } + runTests(t, router, tests) } func TestRoutesPatchDownload(t *testing.T) { - httphandlerConfig, middlewareConfig := createTestEnvireonment(t) - - router := createRouter(httphandlerConfig, middlewareConfig) - - tests := []struct { - name string - method string - url string - expectedStatusCode int - checkBody bool - bodyContains string - bodyNotContains string - }{ - {"Upload patch level 0", "GET", "/patch/" + key_up + "/" + "?value=1_4324232", http.StatusOK, true, "Data uploaded successfully", ""}, - {"Upload patch level 0 no /", "GET", "/patch/" + key_up + "?value_temp=1_4324232", http.StatusOK, true, "Data uploaded successfully", ""}, - {"Upload patch level 2", "GET", "/patch/" + key_up + "/1/2" + "?value=2_8923423", http.StatusOK, true, "Data uploaded successfully", ""}, - - {"Download plain level 0", "GET", "/d/" + key_down + "/" + "plain/value", http.StatusOK, true, "1_4324232\n", ""}, - {"Download plain level 2", "GET", "/d/" + key_down + "/" + "plain/1/2/value", http.StatusOK, true, "2_8923423\n", ""}, - - {"Download json level 0", "GET", "/d/" + key_down + "/" + "json", http.StatusOK, true, "\"value\":\"1_4324232\"", ""}, - {"Download json level 2", "GET", "/d/" + key_down + "/" + "json", http.StatusOK, true, "\"value\":\"2_8923423\"", ""}, - {"Download not contains empty key", "GET", "/d/" + key_down + "/" + "json", http.StatusOK, false, "", "\"\""}, + stats, httphandlerConfig, middlewareConfig := createTestEnvironment(t) + router := createRouter(httphandlerConfig, middlewareConfig, stats) + + tests := []testCase{ + {"Upload patch level 0", buildURL("/patch/%s/?value=1_4324232", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Upload patch level 0 no /", buildURL("/patch/%s?value_temp=1_4324232", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Upload patch level 2", buildURL("/patch/%s/1/2?value=2_8923423", keyUp), http.StatusOK, true, "Data uploaded successfully", ""}, + {"Download plain level 0", buildURL("/d/%s/plain/value", keyDown), http.StatusOK, true, "1_4324232\n", ""}, + {"Download plain level 2", buildURL("/d/%s/plain/1/2/value", keyDown), http.StatusOK, true, "2_8923423\n", ""}, + {"Download json level 0", buildURL("/d/%s/json", keyDown), http.StatusOK, true, "\"value\":\"1_4324232\"", ""}, + {"Download json level 2", buildURL("/d/%s/json", keyDown), http.StatusOK, true, "\"value\":\"2_8923423\"", ""}, + {"Download not contains empty key", buildURL("/d/%s/json", keyDown), http.StatusOK, false, "", "\"\""}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest(tt.method, tt.url, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - assert.Equal(t, tt.expectedStatusCode, rr.Code) - if tt.checkBody { - assert.Contains(t, rr.Body.String(), tt.bodyContains) - } - - if tt.bodyNotContains != "" { - assert.NotContains(t, rr.Body.String(), tt.bodyNotContains) - } - }) - } + runTests(t, router, tests) } diff --git a/middleware/config.go b/middleware/config.go index a2e8dab..7d2c800 100644 --- a/middleware/config.go +++ b/middleware/config.go @@ -1,7 +1,10 @@ package middleware +import "github.com/dhcgn/iot-ephemeral-value-store/stats" + type Config struct { MaxRequestSize int64 RateLimitPerSecond float64 RateLimitBurst int + StatsInstance *stats.Stats } diff --git a/middleware/limitRequestSize.go b/middleware/limitRequestSize.go index 049b4cc..d894bb6 100644 --- a/middleware/limitRequestSize.go +++ b/middleware/limitRequestSize.go @@ -8,6 +8,7 @@ func (c Config) LimitRequestSize(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check if the request size is too large if r.ContentLength > c.MaxRequestSize { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Request size is too large", http.StatusRequestEntityTooLarge) return } diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go index 1531189..5281748 100644 --- a/middleware/ratelimit.go +++ b/middleware/ratelimit.go @@ -20,18 +20,21 @@ func (c Config) RateLimit(next http.Handler) http.Handler { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { + c.StatsInstance.IncrementHTTPErrors() http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // Exclude local IP addresses from rate limiting - if ip == "127.0.0.1" || ip == "::1" { - next.ServeHTTP(w, r) - return - } + // if ip == "127.0.0.1" || ip == "::1" { + // next.ServeHTTP(w, r) + // return + // } limiter := c.getLimiter(ip) if !limiter.Allow() { + c.StatsInstance.IncrementHTTPErrors() + c.StatsInstance.RecordRateLimitHit(ip) http.Error(w, "Too Many Requests", http.StatusTooManyRequests) return } diff --git a/static/index.html b/static/index.html index 65d952f..4e47325 100644 --- a/static/index.html +++ b/static/index.html @@ -102,6 +102,34 @@

Server Settings

  • Data retention: {{.DataRetention}}
  • +

    Server Stats

    + + +

    Rate Limit Stats

    + {{if len .StateData.RateLimitedIPs}} + + + + + + {{range .StateData.RateLimitedIPs}} + + + + + {{end}} +
    IPRequest Count
    {{.IP}}{{.RequestCount}}
    + {{else}} +

    No records

    + {{end}} +

    API Usage