From fe7c4fb45bfdc594bc56ef46f4c82a9c768f6af3 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:02:54 +0100 Subject: [PATCH 01/10] cleanup and refactor middlewares --- internals/proxy/middlewares/api.go | 95 ++-------- internals/proxy/middlewares/auth.go | 16 +- internals/proxy/middlewares/clientip.go | 11 +- internals/proxy/middlewares/common.go | 68 -------- internals/proxy/middlewares/endpoints.go | 5 +- internals/proxy/middlewares/hostname.go | 7 +- internals/proxy/middlewares/ipfilter.go | 9 +- internals/proxy/middlewares/log.go | 11 +- internals/proxy/middlewares/mapping.go | 5 +- internals/proxy/middlewares/message.go | 101 ----------- internals/proxy/middlewares/middleware.go | 2 +- internals/proxy/middlewares/policy.go | 21 +-- internals/proxy/middlewares/port.go | 8 +- internals/proxy/middlewares/proxy.go | 19 +- internals/proxy/middlewares/ratelimit.go | 13 +- internals/proxy/middlewares/template.go | 200 +--------------------- 16 files changed, 73 insertions(+), 518 deletions(-) delete mode 100644 internals/proxy/middlewares/common.go delete mode 100644 internals/proxy/middlewares/message.go diff --git a/internals/proxy/middlewares/api.go b/internals/proxy/middlewares/api.go index 4ee2edd5..86dd3f04 100644 --- a/internals/proxy/middlewares/api.go +++ b/internals/proxy/middlewares/api.go @@ -2,77 +2,19 @@ package middlewares import ( "net/http" - "net/url" - "os" - "regexp" - "strings" - "github.com/codeshelldev/gotl/pkg/logger" - "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" + e "github.com/codeshelldev/secured-signal-api/internals/proxy/endpoints" ) -var InternalAPI Middleware = Middleware{ - Name: "_Internal_API", - Use: internalAPIHandler, +var InternalInsecureAPI Middleware = Middleware{ + Name: "_Internal_Insecure_API", + Use: internalInsecureAPIHandler, } -func internalAPIHandler(next http.Handler) http.Handler { +func internalInsecureAPIHandler(next http.Handler) http.Handler { mux := http.NewServeMux() - const aboutEndpoint = "/v1/about" - mux.HandleFunc(aboutEndpoint, func(w http.ResponseWriter, req *http.Request) { - ChangeRequestDest(req, config.DEFAULT.API.URL.String() + aboutEndpoint) - - client := &http.Client{} - res, err := client.Do(req) - - if err != nil { - logger.Error("Error requesting backend: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - body, err := request.GetResBody(res) - - if err != nil { - logger.Error("Could not get Response Body: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - for key, values := range res.Header { - for _, value := range values { - w.Header().Add(key, value) - } - } - - if !body.Empty { - var version string - - if isValidSemver(os.Getenv("IMAGE_TAG")) { - version, _ = strings.CutPrefix(version, "v") - } - - payload := map[string]any{ - "version": version, - "auth_required": !config.ENV.INSECURE, - } - - body.Data["secured-signal-api"] = payload - - err := body.Write(w) - - if err != nil { - logger.Error("Could not write to Response Body: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - } - - w.WriteHeader(res.StatusCode) - }) - mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) { http.ServeFile(w, req, config.ENV.FAVICON_PATH) }) @@ -82,24 +24,19 @@ func internalAPIHandler(next http.Handler) http.Handler { return mux } -func isValidSemver(version string) bool { - re, err := regexp.Compile(`^v?([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$`) - - if err != nil { - return false - } - - return re.MatchString(version) +var InternalSecureAPI Middleware = Middleware{ + Name: "_Internal_Secure_API", + Use: internalSecureAPIHandler, } -func ChangeRequestDest(req *http.Request, newDest string) error { - newURL, err := url.Parse(newDest) - if err != nil { - return err - } +func internalSecureAPIHandler(next http.Handler) http.Handler { + mux := http.NewServeMux() + + e.AboutEndpoint.Use(mux) + e.SendEnpoint.Use(mux) + e.ScheduleEndpoint.Use(mux) - req.URL = newURL - req.Host = newURL.Host + mux.Handle("/", next) - return nil + return mux } \ No newline at end of file diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index f6655caf..2b532dac 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -12,6 +12,7 @@ import ( "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/deprecation" ) @@ -20,9 +21,6 @@ var Auth Middleware = Middleware{ Use: authHandler, } -const tokenKey contextKey = "token" -const isAuthKey contextKey = "isAuthenticated" - type AuthMethod struct { Name string Authenticate func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) @@ -273,15 +271,15 @@ func authHandler(next http.Handler) http.Handler { if token == "" { onUnauthorized(w) - req = setContext(req, isAuthKey, false) + req = SetContext(req, IsAuthKey, false) } else { - conf := getConfigWithoutDefault(token) + conf := GetConfigWithoutDefault(token) allowedMethods := conf.API.AUTH.METHODS.OptOrEmpty(config.DEFAULT.API.AUTH.METHODS) if isAuthMethodAllowed(method, token, conf.API.TOKENS, allowedMethods, conf.API.AUTH.TOKENS) { - req = setContext(req, isAuthKey, true) - req = setContext(req, tokenKey, token) + req = SetContext(req, IsAuthKey, true) + req = SetContext(req, TokenKey, token) } else { // BREAKING Query & Path auth disabled (default) if (method.Name == "Path" || method.Name == "Query") && len(*conf.API.AUTH.METHODS.Value) == 0 { @@ -297,7 +295,7 @@ func authHandler(next http.Handler) http.Handler { onUnauthorized(w) - req = setContext(req, isAuthKey, false) + req = SetContext(req, IsAuthKey, false) } } @@ -312,7 +310,7 @@ var InternalAuthRequirement Middleware = Middleware{ func authRequirementHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - isAuthenticated := getContext[bool](req, isAuthKey) + isAuthenticated := GetContext[bool](req, IsAuthKey) if !isAuthenticated { return diff --git a/internals/proxy/middlewares/clientip.go b/internals/proxy/middlewares/clientip.go index c1817368..41e02fef 100644 --- a/internals/proxy/middlewares/clientip.go +++ b/internals/proxy/middlewares/clientip.go @@ -6,6 +6,7 @@ import ( "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/netutils" ) @@ -14,17 +15,15 @@ var InternalClientIP Middleware = Middleware{ Use: clientIPHandler, } -var trustedClientKey contextKey = "isClientTrusted" - func clientIPHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) rawTrustedIPs := conf.SETTINGS.ACCESS.TRUSTED_IPS.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.TRUSTED_IPS) - ip := getContext[net.IP](req, clientIPKey) + ip := GetContext[net.IP](req, ClientIPKey) trustedIPs := parseIPsAndNets(rawTrustedIPs) trusted := netutils.IsIPIn(ip, trustedIPs) @@ -33,7 +32,7 @@ func clientIPHandler(next http.Handler) http.Handler { logger.Dev("Connection from trusted Client: ", ip.String()) } - req = setContext(req, trustedClientKey, trusted) + req = SetContext(req, TrustedClientKey, trusted) next.ServeHTTP(w, req) }) diff --git a/internals/proxy/middlewares/common.go b/internals/proxy/middlewares/common.go deleted file mode 100644 index 6e27653f..00000000 --- a/internals/proxy/middlewares/common.go +++ /dev/null @@ -1,68 +0,0 @@ -package middlewares - -import ( - "context" - "net/http" - - "github.com/codeshelldev/gotl/pkg/logger" - "github.com/codeshelldev/secured-signal-api/internals/config" - "github.com/codeshelldev/secured-signal-api/internals/config/structure" -) - -type Context struct { - Next http.Handler -} - -type contextKey string - -func setContext(req *http.Request, key, value any) *http.Request { - ctx := context.WithValue(req.Context(), key, value) - return req.WithContext(ctx) -} - -func getContext[T any](req *http.Request, key any) T { - value, ok := req.Context().Value(key).(T) - - if !ok { - var zero T - return zero - } - - return value -} - -func getLogger(req *http.Request) *logger.Logger { - return getContext[*logger.Logger](req, loggerKey) -} - -func getToken(req *http.Request) string { - return getContext[string](req, tokenKey) -} - -func getConfigByReq(req *http.Request) *structure.CONFIG { - return getConfig(getToken(req)) -} - -func getConfigWithoutDefaultByReq(req *http.Request) *structure.CONFIG { - return getConfigWithoutDefault(getToken(req)) -} - -func getConfigWithoutDefault(token string) *structure.CONFIG { - conf, exists := config.ENV.CONFIGS[token] - - if !exists { - return nil - } - - return conf -} - -func getConfig(token string) *structure.CONFIG { - conf := getConfigWithoutDefault(token) - - if conf == nil { - conf = config.DEFAULT - } - - return conf -} \ No newline at end of file diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index e1b9f38e..fef1a522 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -7,6 +7,7 @@ import ( "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" ) var Endpoints Middleware = Middleware{ @@ -16,9 +17,9 @@ var Endpoints Middleware = Middleware{ func endpointsHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) endpoints := conf.SETTINGS.ACCESS.ENDPOINTS.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.ENDPOINTS) diff --git a/internals/proxy/middlewares/hostname.go b/internals/proxy/middlewares/hostname.go index 19d3fde6..63168982 100644 --- a/internals/proxy/middlewares/hostname.go +++ b/internals/proxy/middlewares/hostname.go @@ -6,6 +6,7 @@ import ( "slices" "github.com/codeshelldev/secured-signal-api/internals/config" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" ) var Hostname Middleware = Middleware{ @@ -15,14 +16,14 @@ var Hostname Middleware = Middleware{ func hostnameHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) hostnames := conf.SERVICE.HOSTNAMES.OptOrEmpty(config.DEFAULT.SERVICE.HOSTNAMES) if len(hostnames) > 0 { - URL := getContext[*url.URL](req, originURLKey) + URL := GetContext[*url.URL](req, OriginURLKey) hostname := URL.Hostname() diff --git a/internals/proxy/middlewares/ipfilter.go b/internals/proxy/middlewares/ipfilter.go index 66331760..90605fd8 100644 --- a/internals/proxy/middlewares/ipfilter.go +++ b/internals/proxy/middlewares/ipfilter.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/codeshelldev/secured-signal-api/internals/config" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/netutils" ) @@ -15,15 +16,13 @@ var IPFilter Middleware = Middleware{ func ipFilterHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) ipFilter := conf.SETTINGS.ACCESS.IP_FILTER.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.IP_FILTER) - logger.Dev(conf.SETTINGS.ACCESS.IP_FILTER) - - ip := getContext[net.IP](req, clientIPKey) + ip := GetContext[net.IP](req, ClientIPKey) if isBlocked("", func(_, try string) bool { tryIP, err := netutils.ParseIPorNet(try) diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index 1c9e12f5..fd21f8ad 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -8,6 +8,7 @@ import ( "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" ) var RequestLogger Middleware = Middleware{ @@ -15,13 +16,11 @@ var RequestLogger Middleware = Middleware{ Use: loggingHandler, } -const loggerKey contextKey = "logger" - func loggingHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - ip := getContext[net.IP](req, clientIPKey) + ip := GetContext[net.IP](req, ClientIPKey) if !logger.IsDev() { logger.Info(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) @@ -46,7 +45,7 @@ var InternalMiddlewareLogger Middleware = Middleware{ func middlewareLoggerHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - conf := getConfigWithoutDefaultByReq(req) + conf := GetConfigWithoutDefaultByReq(req) var logLevel string @@ -64,7 +63,7 @@ func middlewareLoggerHandler(next http.Handler) http.Handler { }) } - req = setContext(req, loggerKey, l) + req = SetContext(req, LoggerKey, l) next.ServeHTTP(w, req) }) diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index fd877e7c..ec594b43 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -7,6 +7,7 @@ import ( request "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" ) var Mapping Middleware = Middleware{ @@ -16,9 +17,9 @@ var Mapping Middleware = Middleware{ func mappingHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) variables := conf.SETTINGS.MESSAGE.VARIABLES.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.VARIABLES) fieldMappings := conf.SETTINGS.MESSAGE.FIELD_MAPPINGS.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.FIELD_MAPPINGS) diff --git a/internals/proxy/middlewares/message.go b/internals/proxy/middlewares/message.go deleted file mode 100644 index fd29e2ee..00000000 --- a/internals/proxy/middlewares/message.go +++ /dev/null @@ -1,101 +0,0 @@ -package middlewares - -import ( - "net/http" - "path" - - request "github.com/codeshelldev/gotl/pkg/request" - "github.com/codeshelldev/secured-signal-api/internals/config" -) - -var Message Middleware = Middleware{ - Name: "Message", - Use: messageHandler, -} - -const templateMessageEndpoint = "/v2/send" - -func messageHandler(next http.Handler) http.Handler { - mux := http.NewServeMux() - - mux.HandleFunc(templateMessageEndpoint, func(w http.ResponseWriter, req *http.Request) { - if req.Method != "POST" { - next.ServeHTTP(w, req) - return - } - - logger := getLogger(req) - - conf := getConfigByReq(req) - - variables := conf.SETTINGS.MESSAGE.VARIABLES.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.VARIABLES) - messageTemplate := conf.SETTINGS.MESSAGE.TEMPLATE.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.TEMPLATE) - - body, err := request.GetReqBody(req) - - if err != nil { - logger.Error("Could not get Request Body: ", err.Error()) - http.Error(w, "Bad Request: invalid body", http.StatusBadRequest) - return - } - - bodyData := map[string]any{} - - var modifiedBody bool - - if !body.Empty && path.Clean(req.URL.Path) == templateMessageEndpoint { - bodyData = body.Data - - if messageTemplate != "" { - headerData := request.GetReqHeaders(req) - - newData, err := TemplateMessage(messageTemplate, bodyData, headerData, variables) - - if err != nil { - logger.Error("Error Templating Message: ", err.Error()) - } - - if newData["message"] != bodyData["message"] && newData["message"] != "" && newData["message"] != nil { - bodyData = newData - modifiedBody = true - } - } - } - - if modifiedBody { - body.Data = bodyData - - err := body.UpdateReq(req) - - if err != nil { - logger.Error("Could not write to Request Body: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - logger.Debug("Applied Message Templating: ", body.Data) - } - - next.ServeHTTP(w, req) - }) - - mux.Handle("/", next) - - return mux -} - -func TemplateMessage(template string, bodyData map[string]any, headerData map[string][]string, variables map[string]any) (map[string]any, error) { - bodyData["message_template"] = template - - data, _, err := TemplateBody(bodyData, headerData, variables) - - if err != nil || data == nil { - return bodyData, err - } - - data["message"] = data["message_template"] - - delete(data, "message_template") - - return data, nil -} diff --git a/internals/proxy/middlewares/middleware.go b/internals/proxy/middlewares/middleware.go index a6e76861..c2401c77 100644 --- a/internals/proxy/middlewares/middleware.go +++ b/internals/proxy/middlewares/middleware.go @@ -9,7 +9,7 @@ import ( type Middleware struct { Name string - Use func(http.Handler) http.Handler + Use func(next http.Handler) http.Handler } type Chain struct { diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 53a8db03..29c9be0b 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -9,6 +9,7 @@ import ( request "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/requestkeys" ) @@ -19,9 +20,9 @@ var Policy Middleware = Middleware{ func policyHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) policies := conf.SETTINGS.ACCESS.FIELD_POLICIES.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.FIELD_POLICIES) @@ -51,22 +52,6 @@ func policyHandler(next http.Handler) http.Handler { }) } -func getPolicies(policies []structure.FieldPolicy) ([]structure.FieldPolicy, []structure.FieldPolicy) { - blocked := []structure.FieldPolicy{} - allowed := []structure.FieldPolicy{} - - for _, policy := range policies { - switch policy.Action { - case "block": - blocked = append(blocked, policy) - case "allow": - allowed = append(allowed, policy) - } - } - - return allowed, blocked -} - func getField(key string, body map[string]any, headers map[string][]string) (any, error) { field := requestkeys.Parse(key) diff --git a/internals/proxy/middlewares/port.go b/internals/proxy/middlewares/port.go index fc708178..153215cc 100644 --- a/internals/proxy/middlewares/port.go +++ b/internals/proxy/middlewares/port.go @@ -5,6 +5,8 @@ import ( "net" "net/http" "strings" + + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" ) var Port Middleware = Middleware{ @@ -14,9 +16,9 @@ var Port Middleware = Middleware{ func portHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) allowedPort := conf.SERVICE.PORT @@ -29,7 +31,7 @@ func portHandler(next http.Handler) http.Handler { if err != nil { logger.Error("Could not get Port: ", err.Error()) - http.Error(w, "Bad Request", http.StatusBadRequest) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go index 74f44e5e..0d48b07a 100644 --- a/internals/proxy/middlewares/proxy.go +++ b/internals/proxy/middlewares/proxy.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/codeshelldev/secured-signal-api/internals/config" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/netutils" ) @@ -15,10 +16,6 @@ var InternalProxy Middleware = Middleware{ Use: proxyHandler, } -const trustedProxyKey contextKey = "isProxyTrusted" -const clientIPKey contextKey = "clientIP" -const originURLKey contextKey = "originURL" - type ForwardedEntry struct { For string Host string @@ -33,9 +30,9 @@ type OriginInfo struct { func proxyHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) rawTrustedProxies := conf.SETTINGS.ACCESS.TRUSTED_PROXIES.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.TRUSTED_PROXIES) @@ -72,15 +69,15 @@ func proxyHandler(next http.Handler) http.Handler { originURL, err := url.Parse(originUrl) if err != nil { - logger.Error("Could not parse Url: ", originUrl) - http.Error(w, "Bad Request: invalid Url", http.StatusBadRequest) + logger.Error("Could not parse url: ", originUrl) + http.Error(w, "Bad Request: invalid url", http.StatusBadRequest) return } - req = setContext(req, trustedProxyKey, trusted) - req = setContext(req, originURLKey, originURL) + req = SetContext(req, TrustedProxyKey, trusted) + req = SetContext(req, OriginURLKey, originURL) - req = setContext(req, clientIPKey, ip) + req = SetContext(req, ClientIPKey, ip) next.ServeHTTP(w, req) }) diff --git a/internals/proxy/middlewares/ratelimit.go b/internals/proxy/middlewares/ratelimit.go index 6310e340..2c4054b1 100644 --- a/internals/proxy/middlewares/ratelimit.go +++ b/internals/proxy/middlewares/ratelimit.go @@ -5,6 +5,7 @@ import ( "time" "github.com/codeshelldev/secured-signal-api/internals/config" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "golang.org/x/time/rate" ) @@ -33,25 +34,21 @@ var tokenLimiters = map[string]*TokenLimiter{} func ratelimitHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - trusted := getContext[bool](req, trustedClientKey) + trusted := GetContext[bool](req, TrustedClientKey) if trusted { next.ServeHTTP(w, req) return } - conf := getConfigByReq(req) + conf := GetConfigByReq(req) rateLimiting := conf.SETTINGS.ACCESS.RATE_LIMITING.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.RATE_LIMITING) - logger.Dev(config.DEFAULT.SETTINGS.ACCESS.RATE_LIMITING.Value.Period) - if rateLimiting.Period.Duration != 0 && rateLimiting.Limit != 0 { - token := getToken(req) - - logger.Dev(time.Duration(config.DEFAULT.SETTINGS.ACCESS.RATE_LIMITING.Value.Period.Duration).String()) + token := GetToken(req) tokenLimiter, exists := tokenLimiters[token] diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index 4cb0294d..2e74f7c4 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -1,19 +1,11 @@ package middlewares import ( - "maps" "net/http" - "net/url" - "regexp" - "strings" - jsonutils "github.com/codeshelldev/gotl/pkg/jsonutils" - query "github.com/codeshelldev/gotl/pkg/query" request "github.com/codeshelldev/gotl/pkg/request" - "github.com/codeshelldev/gotl/pkg/stringutils" - templating "github.com/codeshelldev/gotl/pkg/templating" "github.com/codeshelldev/secured-signal-api/internals/config" - "github.com/codeshelldev/secured-signal-api/utils/requestkeys" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" ) var Template Middleware = Middleware{ @@ -23,9 +15,9 @@ var Template Middleware = Middleware{ func templateHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - logger := getLogger(req) + logger := GetLogger(req) - conf := getConfigByReq(req) + conf := GetConfigByReq(req) variables := conf.SETTINGS.MESSAGE.VARIABLES.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.VARIABLES) @@ -106,188 +98,4 @@ func templateHandler(next http.Handler) http.Handler { next.ServeHTTP(w, req) }) -} - -func normalizeData(fromPrefix, toPrefix string, data map[string]any) (map[string]any, error) { - jsonStr := jsonutils.ToJson(data) - - if jsonStr != "" { - toVar, err := templating.TransformTemplateKeys(jsonStr, fromPrefix, func(re *regexp.Regexp, match string) string { - return re.ReplaceAllStringFunc(match, func(varMatch string) string { - varName := re.ReplaceAllString(varMatch, "$1") - - return "." + toPrefix + varName - }) - }) - - if err != nil { - return data, err - } - - jsonStr = toVar - - normalizedData, err := jsonutils.GetJsonSafe[map[string]any](jsonStr) - - if err == nil { - data = normalizedData - } - } - - return data, nil -} - -func prefixData(prefix string, data map[string]any) map[string]any { - res := map[string]any{} - - for key, value := range data { - res[prefix+key] = value - } - - return res -} - -func cleanHeaders(headers map[string][]string) map[string][]string { - cleanedHeaders := map[string][]string{} - - for key, value := range headers { - cleanedKey := strings.ReplaceAll(key, "-", "_") - - cleanedHeaders[cleanedKey] = value - } - - authHeader, ok := cleanedHeaders["Authorization"] - - if !ok { - authHeader = []string{"UNKNOWN REDACTED"} - } - - cleanedHeaders["Authorization"] = []string{strings.Split(authHeader[0], ` `)[0] + " REDACTED"} - - return cleanedHeaders -} - -func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES map[string]any) (map[string]any, bool, error) { - var modified bool - - headers = cleanHeaders(headers) - - // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to "".header_key_Var" and ".body_key_Var" - normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "body_key_", body) - - if err != nil { - return body, false, err - } - - normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "header_key_", normalizedBody) - - if err != nil { - return body, false, err - } - - // Prefix Body Data with body_key_ - prefixedBody := prefixData("body_key_", normalizedBody) - - // Prefix Header Data with header_key_ - prefixedHeaders := prefixData("header_key_", request.ParseHeaders(headers)) - - variables := map[string]any{} - - maps.Copy(variables, VARIABLES) - - maps.Copy(variables, prefixedBody) - maps.Copy(variables, prefixedHeaders) - - templatedData, err := templating.RenderJSON(normalizedBody, variables) - - if err != nil { - return body, false, err - } - - beforeStr := jsonutils.ToJson(body) - afterStr := jsonutils.ToJson(templatedData) - - modified = beforeStr != afterStr - - return templatedData, modified, nil -} - -func TemplatePath(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, bool, error) { - var modified bool - var modifiedBody bool - - reqPath, err := url.PathUnescape(reqUrl.Path) - - if err != nil { - return reqUrl.Path, data, false, false, err - } - - reqPath, err = templating.RenderNormalizedTemplate("path", reqPath, VARIABLES) - - if err != nil { - return reqUrl.Path, data, false, false, err - } - - parts := strings.Split(reqPath, "/") - newParts := []string{} - - for _, part := range parts { - newParts = append(newParts, part) - - keyValuePair := strings.SplitN(part, "=", 2) - - if len(keyValuePair) != 2 { - continue - } - - keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], "@") - - if !match { - continue - } - - value := stringutils.ToType(keyValuePair[1]) - - data[keyWithoutPrefix] = value - modifiedBody = true - - newParts = newParts[:len(newParts) - 1] - } - - reqPath = strings.Join(newParts, "/") - - if reqUrl.Path != reqPath { - modified = true - } - - return reqPath, data, modified, modifiedBody, nil -} - -func TemplateQuery(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, error) { - var modified bool - - decodedQuery, _ := url.QueryUnescape(reqUrl.RawQuery) - - templatedQuery, _ := templating.RenderNormalizedTemplate("query", decodedQuery, VARIABLES) - - originalQueryData := reqUrl.Query() - - addedData, _ := query.ParseTypedQuery(templatedQuery) - - for key, val := range addedData { - keyWithoutPrefix, match := strings.CutPrefix(key, "@") - - if !match { - continue - } - - data[keyWithoutPrefix] = val - - originalQueryData.Del(key) - - modified = true - } - - reqRawQuery := originalQueryData.Encode() - - return reqRawQuery, data, modified, nil -} +} \ No newline at end of file From d7d4d8d9f44e75b26b4c60f7e4350ee05b2ff891 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:03:47 +0100 Subject: [PATCH 02/10] added endpoints instead as better solution instead of middlewares --- internals/proxy/common/common.go | 88 +++++++++++ internals/proxy/common/context.go | 14 ++ internals/proxy/common/template.go | 197 +++++++++++++++++++++++++ internals/proxy/endpoints/about.go | 87 +++++++++++ internals/proxy/endpoints/endpoints.go | 14 ++ internals/proxy/endpoints/schedule.go | 109 ++++++++++++++ internals/proxy/proxy.go | 4 +- 7 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 internals/proxy/common/common.go create mode 100644 internals/proxy/common/context.go create mode 100644 internals/proxy/common/template.go create mode 100644 internals/proxy/endpoints/about.go create mode 100644 internals/proxy/endpoints/endpoints.go create mode 100644 internals/proxy/endpoints/schedule.go diff --git a/internals/proxy/common/common.go b/internals/proxy/common/common.go new file mode 100644 index 00000000..3500afff --- /dev/null +++ b/internals/proxy/common/common.go @@ -0,0 +1,88 @@ +package common + +import ( + "context" + "net/http" + "net/url" + + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/secured-signal-api/internals/config" + "github.com/codeshelldev/secured-signal-api/internals/config/structure" +) + +func SetContext(req *http.Request, key, value any) *http.Request { + ctx := context.WithValue(req.Context(), key, value) + return req.WithContext(ctx) +} + +func GetContext[T any](req *http.Request, key any) T { + value, ok := req.Context().Value(key).(T) + + if !ok { + var zero T + return zero + } + + return value +} + +func GetLogger(req *http.Request) *logger.Logger { + return GetContext[*logger.Logger](req, LoggerKey) +} + +func GetToken(req *http.Request) string { + return GetContext[string](req, TokenKey) +} + +func GetConfigByReq(req *http.Request) *structure.CONFIG { + return GetConfig(GetToken(req)) +} + +func GetConfigWithoutDefaultByReq(req *http.Request) *structure.CONFIG { + return GetConfigWithoutDefault(GetToken(req)) +} + +func GetConfigWithoutDefault(token string) *structure.CONFIG { + conf, exists := config.ENV.CONFIGS[token] + + if !exists { + return nil + } + + return conf +} + +func GetConfig(token string) *structure.CONFIG { + conf := GetConfigWithoutDefault(token) + + if conf == nil { + conf = config.DEFAULT + } + + return conf +} + + +func ChangeRequestDest(req *http.Request, newDest string) error { + newURL, err := url.Parse(newDest) + if err != nil { + return err + } + + req.URL = newURL + req.Host = newURL.Host + + return nil +} + +func WriteError(w http.ResponseWriter, status int, msg string) { + res := request.Body{ + Data: map[string]any{ + "error": msg, + }, + } + + w.WriteHeader(status) + res.Write(w) +} \ No newline at end of file diff --git a/internals/proxy/common/context.go b/internals/proxy/common/context.go new file mode 100644 index 00000000..3e7811cc --- /dev/null +++ b/internals/proxy/common/context.go @@ -0,0 +1,14 @@ +package common + +type contextKey string + +const TokenKey contextKey = "token" +const IsAuthKey contextKey = "isAuthenticated" + +const LoggerKey contextKey = "logger" + +const TrustedClientKey contextKey = "isClientTrusted" + +const TrustedProxyKey contextKey = "isProxyTrusted" +const ClientIPKey contextKey = "clientIP" +const OriginURLKey contextKey = "originURL" \ No newline at end of file diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go new file mode 100644 index 00000000..6ef9ab21 --- /dev/null +++ b/internals/proxy/common/template.go @@ -0,0 +1,197 @@ +package common + +import ( + "net/url" + "regexp" + "strings" + + "github.com/codeshelldev/gotl/pkg/jsonutils" + "github.com/codeshelldev/gotl/pkg/query" + "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/gotl/pkg/stringutils" + "github.com/codeshelldev/gotl/pkg/templating" + "github.com/codeshelldev/secured-signal-api/utils/requestkeys" +) + +func normalizeData(fromPrefix, toPrefix string, data map[string]any) (map[string]any, error) { + jsonStr := jsonutils.ToJson(data) + + if jsonStr != "" { + toVar, err := templating.TransformTemplateKeys(jsonStr, fromPrefix, func(re *regexp.Regexp, match string) string { + return re.ReplaceAllStringFunc(match, func(varMatch string) string { + varName := re.ReplaceAllString(varMatch, "$1") + + return "." + toPrefix + varName + }) + }) + + if err != nil { + return data, err + } + + jsonStr = toVar + + normalizedData, err := jsonutils.GetJsonSafe[map[string]any](jsonStr) + + if err == nil { + data = normalizedData + } + } + + return data, nil +} + +func prefixData(prefix string, data map[string]any) map[string]any { + res := map[string]any{} + + for key, value := range data { + res[prefix+key] = value + } + + return res +} + +func cleanHeaders(headers map[string][]string) map[string][]string { + cleanedHeaders := map[string][]string{} + + for key, value := range headers { + cleanedKey := strings.ReplaceAll(key, "-", "_") + + cleanedHeaders[cleanedKey] = value + } + + authHeader, ok := cleanedHeaders["Authorization"] + + if !ok { + authHeader = []string{"UNKNOWN REDACTED"} + } + + cleanedHeaders["Authorization"] = []string{strings.Split(authHeader[0], ` `)[0] + " REDACTED"} + + return cleanedHeaders +} + +func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES map[string]any) (map[string]any, bool, error) { + var modified bool + + headers = cleanHeaders(headers) + + // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to "".header_key_Var" and ".body_key_Var" + normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "body_key_", body) + + if err != nil { + return body, false, err + } + + normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "header_key_", normalizedBody) + + if err != nil { + return body, false, err + } + + // Prefix Body Data with body_key_ + prefixedBody := prefixData("body_key_", normalizedBody) + + // Prefix Header Data with header_key_ + prefixedHeaders := prefixData("header_key_", request.ParseHeaders(headers)) + + variables := map[string]any{} + + request.CopyMap(variables, VARIABLES) + request.CopyMap(variables, prefixedBody) + request.CopyMap(variables, prefixedHeaders) + + templatedData, err := templating.RenderJSON(normalizedBody, variables) + + if err != nil { + return body, false, err + } + + beforeStr := jsonutils.ToJson(body) + afterStr := jsonutils.ToJson(templatedData) + + modified = beforeStr != afterStr + + return templatedData, modified, nil +} + +func TemplatePath(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, bool, error) { + var modified bool + var modifiedBody bool + + reqPath, err := url.PathUnescape(reqUrl.Path) + + if err != nil { + return reqUrl.Path, data, false, false, err + } + + reqPath, err = templating.RenderNormalizedTemplate("path", reqPath, VARIABLES) + + if err != nil { + return reqUrl.Path, data, false, false, err + } + + parts := strings.Split(reqPath, "/") + newParts := []string{} + + for _, part := range parts { + newParts = append(newParts, part) + + keyValuePair := strings.SplitN(part, "=", 2) + + if len(keyValuePair) != 2 { + continue + } + + keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], "@") + + if !match { + continue + } + + value := stringutils.ToType(keyValuePair[1]) + + data[keyWithoutPrefix] = value + modifiedBody = true + + newParts = newParts[:len(newParts) - 1] + } + + reqPath = strings.Join(newParts, "/") + + if reqUrl.Path != reqPath { + modified = true + } + + return reqPath, data, modified, modifiedBody, nil +} + +func TemplateQuery(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, error) { + var modified bool + + decodedQuery, _ := url.QueryUnescape(reqUrl.RawQuery) + + templatedQuery, _ := templating.RenderNormalizedTemplate("query", decodedQuery, VARIABLES) + + originalQueryData := reqUrl.Query() + + addedData, _ := query.ParseTypedQuery(templatedQuery) + + for key, val := range addedData { + keyWithoutPrefix, match := strings.CutPrefix(key, "@") + + if !match { + continue + } + + data[keyWithoutPrefix] = val + + originalQueryData.Del(key) + + modified = true + } + + reqRawQuery := originalQueryData.Encode() + + return reqRawQuery, data, modified, nil +} diff --git a/internals/proxy/endpoints/about.go b/internals/proxy/endpoints/about.go new file mode 100644 index 00000000..687ad608 --- /dev/null +++ b/internals/proxy/endpoints/about.go @@ -0,0 +1,87 @@ +package endpoints + +import ( + "net/http" + "os" + "regexp" + "strings" + + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/secured-signal-api/internals/config" + "github.com/codeshelldev/secured-signal-api/internals/proxy/common" +) + +var AboutEndpoint = Endpoint{ + Name: "About", + Handler: aboutHandler, +} + +func aboutHandler(mux *http.ServeMux) *http.ServeMux { + mux.HandleFunc("GET /v1/about", func(w http.ResponseWriter, req *http.Request) { + common.ChangeRequestDest(req, config.DEFAULT.API.URL.String() + "/v1/about") + + client := &http.Client{} + res, err := client.Do(req) + + if err != nil { + logger.Error("Error requesting backend: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + body, err := request.GetResBody(res) + + if err != nil { + logger.Error("Could not get Response Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + for key, values := range res.Header { + for _, value := range values { + w.Header().Add(key, value) + } + } + + if !body.Empty { + var version string + + if isValidSemver(os.Getenv("IMAGE_TAG")) { + version, _ = strings.CutPrefix(version, "v") + } + + payload := map[string]any{ + "version": version, + "auth_required": !config.ENV.INSECURE, + "capabilities": map[string]any{ + "v2/send": []string{ + "scheduling", + }, + }, + } + + body.Data["secured-signal-api"] = payload + + err := body.Write(w) + + if err != nil { + logger.Error("Could not write to Response Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } + }) + + return mux +} + +func isValidSemver(version string) bool { + re, err := regexp.Compile(`^v?([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$`) + + if err != nil { + return false + } + + return re.MatchString(version) +} \ No newline at end of file diff --git a/internals/proxy/endpoints/endpoints.go b/internals/proxy/endpoints/endpoints.go new file mode 100644 index 00000000..c7077248 --- /dev/null +++ b/internals/proxy/endpoints/endpoints.go @@ -0,0 +1,14 @@ +package endpoints + +import ( + "net/http" +) + +type Endpoint struct { + Name string + Handler func(mux *http.ServeMux) *http.ServeMux +} + +func (endpoint Endpoint) Use(mux *http.ServeMux) *http.ServeMux { + return endpoint.Handler(mux) +} \ No newline at end of file diff --git a/internals/proxy/endpoints/schedule.go b/internals/proxy/endpoints/schedule.go new file mode 100644 index 00000000..4f9dd285 --- /dev/null +++ b/internals/proxy/endpoints/schedule.go @@ -0,0 +1,109 @@ +package endpoints + +import ( + "net/http" + "strconv" + + "github.com/codeshelldev/gotl/pkg/jsonutils" + "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/secured-signal-api/internals/db" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" +) + +var ScheduleEndpoint = Endpoint{ + Name: "Schedule", + Handler: scheduleHandler, +} + +func scheduleHandler(mux *http.ServeMux) *http.ServeMux { + mux.HandleFunc("DELETE /v1/schedule/{id}", func(w http.ResponseWriter, req *http.Request) { + id := req.PathValue("id") + + rsdb := db.NewRequestSchedulerDB() + err := rsdb.DeleteByID(id) + + if err != nil { + WriteError(w, http.StatusBadRequest, "request not found") + return + } + + w.WriteHeader(http.StatusNoContent) + }) + + mux.HandleFunc("GET /v1/schedule/{id}", func(w http.ResponseWriter, req *http.Request) { + logger := GetLogger(req) + + id := req.PathValue("id") + + rsdb := db.NewRequestSchedulerDB() + entry, err := rsdb.GetByID(id) + + if err != nil { + WriteError(w, http.StatusBadRequest, "request not found") + return + } + + body, _ := request.CreateBody(map[string]any{ + "status": string(entry.Status), + + "method": entry.Method, + "url": entry.URL, + + "created_at": strconv.Itoa(int(entry.CreatedAt.Unix())), + "run_at": strconv.Itoa(int(entry.RunAt.Unix())), + }) + + if entry.Status != db.STATUS_DONE && entry.Status != db.STATUS_FAILED { + body.Write(w) + return + } + + var finishedAt *string + if entry.FinishedAt != nil { + finished := entry.FinishedAt.Unix() + + tm := strconv.Itoa(int(finished)) + finishedAt = &tm + } + + body.Data["finished_at"] = finishedAt + body.Data["response_status_code"] = entry.ResponseStatusCode + + if entry.Status == db.STATUS_FAILED { + body.Data["error"] = entry.LastError + body.Write(w) + return + } + + var resBody *map[string]any + if entry.ResponseBody != nil { + resBody, err = jsonutils.GetJsonSafe[*map[string]any](string(*entry.ResponseBody)) + } + + if err != nil { + logger.Error("Error parsing json string: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if resBody != nil { + body.Data["response_body"] = resBody + } + + if entry.Headers != nil { + body.Data["response_headers"] = entry.ResponseHeaders + } + + err = body.Write(w) + + if err != nil { + logger.Error("Could not write to Response Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + rsdb.DeleteByID(id) + }) + + return mux +} \ No newline at end of file diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index cb4b51e4..ad40639b 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -47,7 +47,7 @@ func Create(targetUrl *url.URL) Proxy { func (proxy Proxy) Init() http.Handler { handler := m.NewChain(). - Use(m.InternalAPI). + Use(m.InternalInsecureAPI). Use(m.Auth). Use(m.InternalMiddlewareLogger). Use(m.InternalProxy). @@ -62,7 +62,7 @@ func (proxy Proxy) Init() http.Handler { Use(m.Endpoints). Use(m.Mapping). Use(m.Policy). - Use(m.Message). + Use(m.InternalSecureAPI). Then(proxy.Use()) return handler From 0be3ba43ef57cbe4803e8a60d89f9423e96eeeca Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:04:25 +0100 Subject: [PATCH 03/10] fixed toupper'ing in stdlogs --- utils/stdlog/log.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/stdlog/log.go b/utils/stdlog/log.go index 94704a40..c9604356 100644 --- a/utils/stdlog/log.go +++ b/utils/stdlog/log.go @@ -29,7 +29,7 @@ const ( func normalizeMessage(msg string) string { msg = strings.TrimSuffix(msg, "\n") - msg = strings.ToLower(msg[:1]) + msg[1:] + msg = strings.ToUpper(msg[:1]) + msg[1:] return msg } @@ -42,11 +42,11 @@ var writer = &ioutils.InterceptWriter{ return } - msg = normalizeMessage(msg) - level, _ := strconv.Atoi(msg[len(logLevelPrefix):len(logLevelPrefix) + 1]) msg = msg[len(logLevelPrefix) + 1:] + msg = normalizeMessage(msg) + switch (logLevel(level)) { case FATAL: logger.Fatal(msg) From 7742a4e4ef21db81a6d80ff3c9e8d91abfa99749 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:04:45 +0100 Subject: [PATCH 04/10] added db --- Dockerfile | 4 + dev-env.sh | 3 + internals/config/loader.go | 1 + internals/config/structure/structure.go | 24 +-- internals/db/db.go | 73 +++++++ internals/db/reqdb.go | 259 ++++++++++++++++++++++++ internals/db/schema.sql | 30 +++ internals/scheduler/reqscheduler.go | 180 ++++++++++++++++ internals/scheduler/scheduler.go | 23 +++ main.go | 7 + 10 files changed, 592 insertions(+), 12 deletions(-) create mode 100644 internals/db/db.go create mode 100644 internals/db/reqdb.go create mode 100644 internals/db/schema.sql create mode 100644 internals/scheduler/reqscheduler.go create mode 100644 internals/scheduler/scheduler.go diff --git a/Dockerfile b/Dockerfile index 4d3bc6c0..8de1c8b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,10 @@ ENV FAVICON_PATH=/app/data/favicon.ico ENV CONFIG_PATH=/config/config.yml ENV TOKENS_DIR=/config/tokens +ENV DB_PATH=/db/db.sqlite3 + +ENV CGO_ENABLED=1 + ARG TARGETOS ARG TARGETARCH diff --git a/dev-env.sh b/dev-env.sh index 087776e0..54192048 100644 --- a/dev-env.sh +++ b/dev-env.sh @@ -41,6 +41,9 @@ export DEFAULTS_PATH=$DIR/data/defaults.yml export FAVICON_PATH=$DIR/data/favicon.ico export CONFIG_PATH=$DIR/.dev/config.yml export TOKENS_DIR=$DIR/.dev/tokens +export DB_PATH=$DIR/.dev/db/db.sqlite3 + +export CGO_ENABLED=1 export API_URL=http://127.0.0.1:8881 diff --git a/internals/config/loader.go b/internals/config/loader.go index 92af4b6d..98f55ce5 100644 --- a/internals/config/loader.go +++ b/internals/config/loader.go @@ -18,6 +18,7 @@ var ENV *structure.ENV = &structure.ENV{ DEFAULTS_PATH: os.Getenv("DEFAULTS_PATH"), TOKENS_DIR: os.Getenv("TOKENS_DIR"), FAVICON_PATH: os.Getenv("FAVICON_PATH"), + DB_PATH: os.Getenv("DB_PATH"), INSECURE: false, CONFIGS: map[string]*structure.CONFIG{}, diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 01837f53..0873a1d3 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -1,7 +1,7 @@ package structure import ( - . "github.com/codeshelldev/gotl/pkg/configutils/types" + t "github.com/codeshelldev/gotl/pkg/configutils/types" ) type ENV struct { @@ -37,7 +37,7 @@ const ( ) type SERVICE struct { - HOSTNAMES Opt[[]string] `koanf:"hostnames" env>aliases:".hostnames"` + HOSTNAMES t.Opt[[]string] `koanf:"hostnames" env>aliases:".hostnames"` PORT string `koanf:"port" env>aliases:".port"` LOG_LEVEL string `koanf:"loglevel" env>aliases:".loglevel"` } @@ -51,7 +51,7 @@ type API struct { } type AUTH struct { - METHODS Opt[[]string] `koanf:"methods" env>aliases:".authmethods"` + METHODS t.Opt[[]string] `koanf:"methods" env>aliases:".authmethods"` // DEPRECATION auth.token => auth.tokens TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,fg=yellow}\x60{s}api.auth.token{/}\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` } @@ -67,9 +67,9 @@ type SETTINGS struct { } type MESSAGE struct { - VARIABLES Opt[map[string]any] `koanf:"variables" childtransform:"upper"` - FIELD_MAPPINGS Opt[map[string][]FieldMapping]`koanf:"fieldmappings" childtransform:"default"` - TEMPLATE Opt[string] `koanf:"template"` + VARIABLES t.Opt[map[string]any] `koanf:"variables" childtransform:"upper"` + FIELD_MAPPINGS t.Opt[map[string][]FieldMapping]`koanf:"fieldmappings" childtransform:"default"` + TEMPLATE t.Opt[string] `koanf:"template"` } type FieldMapping struct { @@ -78,12 +78,12 @@ type FieldMapping struct { } type ACCESS struct { - ENDPOINTS Opt[AllowBlockSlice] `koanf:"endpoints"` - FIELD_POLICIES Opt[map[string]FieldPolicies]`koanf:"fieldpolicies" childtransform:"default"` - RATE_LIMITING Opt[RateLimiting] `koanf:"ratelimiting"` - IP_FILTER Opt[AllowBlockSlice] `koanf:"ipfilter"` - TRUSTED_IPS Opt[[]IPOrNet] `koanf:"trustedips"` - TRUSTED_PROXIES Opt[[]IPOrNet] `koanf:"trustedproxies"` + ENDPOINTS t.Opt[AllowBlockSlice] `koanf:"endpoints"` + FIELD_POLICIES t.Opt[map[string]FieldPolicies]`koanf:"fieldpolicies" childtransform:"default"` + RATE_LIMITING t.Opt[RateLimiting] `koanf:"ratelimiting"` + IP_FILTER t.Opt[AllowBlockSlice] `koanf:"ipfilter"` + TRUSTED_IPS t.Opt[[]IPOrNet] `koanf:"trustedips"` + TRUSTED_PROXIES t.Opt[[]IPOrNet] `koanf:"trustedproxies"` } type FieldPolicy struct { diff --git a/internals/db/db.go b/internals/db/db.go new file mode 100644 index 00000000..783ed359 --- /dev/null +++ b/internals/db/db.go @@ -0,0 +1,73 @@ +package db + +import ( + "bytes" + "database/sql" + "encoding/gob" + + _ "embed" + + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/secured-signal-api/internals/config" + _ "github.com/mattn/go-sqlite3" +) + +var db *sql.DB + +//go:embed schema.sql +var schema string + +func Init() { + var err error + + db, err = sql.Open("sqlite3", config.ENV.DB_PATH) + + if err != nil { + logger.Fatal("Error opening database: ", err.Error()) + return + } + + db.SetMaxOpenConns(1) + + err = db.Ping() + + if err != nil { + logger.Fatal("Error opening database connection: ", err.Error()) + return + } + + _, err = db.Exec(schema) + + if err != nil { + logger.Fatal("Could not apply database schema: ", err.Error()) + return + } + + logger.Debug("Successfully opened database") +} + +func Close() { + ShutdownRequestDB() + + db.Close() +} + +func Serialize(value any) []byte { + var valueBytes bytes.Buffer + + enc := gob.NewEncoder(&valueBytes) + enc.Encode(value) + + return valueBytes.Bytes() +} + +func Deserialize[T any](valueBytes []byte) T { + var out T + + buf := bytes.NewBuffer(valueBytes) + dec := gob.NewDecoder(buf) + + dec.Decode(&out) + + return out +} \ No newline at end of file diff --git a/internals/db/reqdb.go b/internals/db/reqdb.go new file mode 100644 index 00000000..d027d8b6 --- /dev/null +++ b/internals/db/reqdb.go @@ -0,0 +1,259 @@ +package db + +import ( + "database/sql" + "time" +) + +type ScheduledRequest struct { + ID string + Method string + URL string + Headers map[string][]string + Body []byte + RunAt time.Time + CreatedAt time.Time +} + +type RequestResult struct { + RunAt time.Time + FinishedAt *time.Time + LastError *string + Status *int + Headers *map[string][]string + Body *[]byte +} + +type ScheduledRequestEntry struct { + Status ScheduledRequestStatus + ID string + Method string + URL string + Headers map[string][]string + Body []byte + RunAt time.Time + CreatedAt time.Time + + FinishedAt *time.Time + LastError *string + ResponseStatusCode *int + ResponseBody *[]byte + ResponseHeaders *map[string][]string +} + +type ScheduledRequestStatus string + +const ( + STATUS_PENDING ScheduledRequestStatus = "pending" + STATUS_QUEUED ScheduledRequestStatus = "queued" + STATUS_DONE ScheduledRequestStatus = "done" + STATUS_FAILED ScheduledRequestStatus = "failed" + STATUS_RUNNING ScheduledRequestStatus = "running" +) + +type RequestSchedulerDB struct { + db *sql.DB +} + +func NewRequestSchedulerDB() *RequestSchedulerDB { + return &RequestSchedulerDB{db: db} +} + +func ShutdownRequestDB() { + NewRequestSchedulerDB().RecoverStales(0) +} + +func (s *RequestSchedulerDB) Insert(req *ScheduledRequest) error { + _, err := s.db.Exec(` + INSERT INTO scheduled_requests (id, status, method, url, created_at, run_at, request_headers, request_body) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + req.ID, + STATUS_PENDING, + req.Method, + req.URL, + time.Now().Unix(), + req.RunAt.Unix(), + Serialize(req.Headers), + req.Body, + ) + + return err +} + +func (s *RequestSchedulerDB) SetStatus(id string, status ScheduledRequestStatus) error { + _, err := s.db.Exec(` + UPDATE scheduled_requests + SET status = ? + WHERE id = ?`, + string(status), + id, + ) + + return err +} + +func (s *RequestSchedulerDB) SetResponse(id string, e error, res RequestResult) error { + var errMsg string + + if e != nil { + errMsg = e.Error() + } + + var finishedAt *int64 + + if res.FinishedAt != nil { + tm := res.FinishedAt.Unix() + + finishedAt = &tm + } + + _, err := s.db.Exec(` + UPDATE scheduled_requests + SET finished_at = ?, last_error = ?, response_status_code = ?, response_headers = ?, response_body = ? + WHERE id = ?`, + finishedAt, + errMsg, + res.Status, + Serialize(res.Headers), + res.Body, + id, + ) + + return err +} + +func (s *RequestSchedulerDB) GetByID(id string) (*ScheduledRequestEntry, error) { + row := s.db.QueryRow(` + SELECT status, id, method, url, created_at, run_at, request_headers, request_body, finished_at, last_error, response_status_code, response_headers, response_body + FROM scheduled_requests + WHERE id = ?`, + id, + ) + + res := &ScheduledRequestEntry{} + + var requestHeaderBytes []byte + var responseHeaderBytes *[]byte + + var createdAt, runAt int64 + var finishedAt *int64 + + err := row.Scan(&res.Status, &res.ID, &res.Method, &res.URL, &createdAt, &runAt, &requestHeaderBytes, &res.Body, &finishedAt, &res.LastError, &res.ResponseStatusCode, &responseHeaderBytes, &res.ResponseBody) + + if err != nil { + return nil, err + } + + res.Headers = Deserialize[map[string][]string](requestHeaderBytes) + + if responseHeaderBytes != nil { + res.ResponseHeaders = Deserialize[*map[string][]string](*responseHeaderBytes) + } + + res.CreatedAt = time.Unix(createdAt, 0) + res.RunAt = time.Unix(runAt, 0) + + if finishedAt != nil { + finished := time.Unix(*finishedAt, 0) + res.FinishedAt = &finished + } + + return res, nil +} + +func (s *RequestSchedulerDB) DeleteByID(id string) error { + _, err := s.db.Exec(` + DELETE FROM scheduled_requests + WHERE id = ?`, + id, + ) + + return err +} + +func (s *RequestSchedulerDB) Claim(id string) error { + _, err := s.db.Exec(` + UPDATE scheduled_requests + SET claimed_at = ? + WHERE id = ?`, + time.Now().Unix(), + id, + ) + + return err +} + +func (s *RequestSchedulerDB) RecoverStales(threshold time.Duration) error { + minClaimedAt := time.Now().Add(-threshold).Unix() + + _, err := s.db.Exec(` + UPDATE scheduled_requests + SET status = ?, claimed_at = null + WHERE status != ? AND (claimed_at IS NULL OR claimed_at <= ?)`, + STATUS_PENDING, + STATUS_DONE, + minClaimedAt, + ) + + return err +} + +func (s *RequestSchedulerDB) CleanupDones(threshold time.Duration) error { + minFinishedAt := time.Now().Add(-threshold).Unix() + + _, err := s.db.Exec(` + DELETE FROM scheduled_requests + WHERE status = ? AND finished_at <= ?`, + STATUS_DONE, + minFinishedAt, + ) + + return err +} + +func (s *RequestSchedulerDB) FetchNext(amount int, within time.Duration) ([]*ScheduledRequest, error) { + minRunAt := time.Now().Add(within).Unix() + + rows, err := s.db.Query(` + SELECT id, method, url, created_at, run_at, request_headers, request_body + FROM scheduled_requests + WHERE status = ? AND run_at <= ? + ORDER BY run_at ASC + LIMIT ?`, + STATUS_PENDING, + minRunAt, + amount, + ) + + if err != nil { + return nil, err + } + defer rows.Close() + + var requests []*ScheduledRequest + + for rows.Next() { + req := &ScheduledRequest{} + + var headerBytes []byte + var createdAt, runAt int64 + + err := rows.Scan(&req.ID, &req.Method, &req.URL, &createdAt, &runAt, &headerBytes, &req.Body) + if err != nil { + return nil, err + } + + req.Headers = Deserialize[map[string][]string](headerBytes) + req.CreatedAt = time.Unix(createdAt, 0) + req.RunAt = time.Unix(runAt, 0) + + requests = append(requests, req) + } + + err = rows.Err() + if err != nil { + return nil, err + } + + return requests, nil +} \ No newline at end of file diff --git a/internals/db/schema.sql b/internals/db/schema.sql new file mode 100644 index 00000000..81a3da62 --- /dev/null +++ b/internals/db/schema.sql @@ -0,0 +1,30 @@ +PRAGMA journal_mode = WAL; +PRAGMA synchronous = NORMAL; +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS scheduled_requests ( + id TEXT PRIMARY KEY, + status TEXT NOT NULL, + + method TEXT NOT NULL, + url TEXT NOT NULL, + + created_at INTEGER NOT NULL, + claimed_at INTEGER, + run_at INTEGER NOT NULL, + + /* before run */ + request_headers BLOB NOT NULL, + request_body BLOB NOT NULL, + + /* after run */ + finished_at INTEGER, + last_error TEXT, + response_status_code INTEGER, + response_headers BLOB, + response_body BLOB +); + +CREATE INDEX IF NOT EXISTS idx_scheduled_requests_run_at +ON scheduled_requests (run_at) +WHERE status = 'pending'; diff --git a/internals/scheduler/reqscheduler.go b/internals/scheduler/reqscheduler.go new file mode 100644 index 00000000..f126d9ab --- /dev/null +++ b/internals/scheduler/reqscheduler.go @@ -0,0 +1,180 @@ +package scheduler + +import ( + "bytes" + "errors" + "io" + "net/http" + "net/url" + "time" + + "github.com/codeshelldev/gotl/pkg/jsonutils" + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/secured-signal-api/internals/db" + "github.com/google/uuid" +) + +var rsdb *db.RequestSchedulerDB + +const limit = 5 +const withinTime = 5 * time.Minute + +const recoveryThreshold = 10 * time.Minute + +const doneStaleThreshold = 24 * time.Hour + +func StartRequestScheduler() { + rsdb = db.NewRequestSchedulerDB() + + rsdb.CleanupDones(doneStaleThreshold) + + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for range ticker.C { + rsdb.RecoverStales(recoveryThreshold) + + if scheduler.Len() < limit { + UpdateQueue() + } + } + }() +} + +func UpdateQueue() { + requests, _ := rsdb.FetchNext(limit, withinTime) + + for _, req := range requests { + AddToQueue(req) + } +} + +func AddToQueue(req *db.ScheduledRequest) { + rsdb.SetStatus(req.ID, db.STATUS_QUEUED) + rsdb.Claim(req.ID) + + scheduler.AddAt(req.RunAt, func() { + HandleScheduledRequest(req) + }) +} + +func OnRequestScheduled(req *db.ScheduledRequest) { + next, exists := scheduler.PeekTime() + + if exists { + if req.RunAt.Before(next) { + // add earliest job (current) + AddToQueue(req) + + // remove latest job + scheduler.Pop() + + rsdb.SetStatus(req.ID, db.STATUS_PENDING) + } + } +} + +func ScheduleRequest(tm time.Time, req *http.Request) (string, error) { + if tm.Before(time.Now()) { + return "", errors.New("time lies in the past") + } + + body, err := io.ReadAll(req.Body) + + if err != nil { + return "", err + } + + id := uuid.NewString() + + scheduledReq := &db.ScheduledRequest{ + ID: id, + Method: req.Method, + URL: req.URL.String(), + Headers: req.Header, + Body: body, + RunAt: tm, + CreatedAt: time.Now(), + } + + err = rsdb.Insert(scheduledReq) + + if err != nil { + return "", err + } + + OnRequestScheduled(scheduledReq) + + return id, nil +} + +func HandleScheduledRequest(req *db.ScheduledRequest) { + rsdb.SetStatus(req.ID, db.STATUS_RUNNING) + + res, err := fireScheduledRequest(req) + result := db.RequestResult{} + + now := time.Now() + result.FinishedAt = &now + + if err != nil { + rsdb.SetStatus(req.ID, db.STATUS_FAILED) + rsdb.SetResponse(req.ID, err, result) + + logger.Error("Could not send scheduled request: ", err.Error()) + return + } + + body, err := request.GetResBody(res) + + if err != nil { + body.Raw = nil + } + + result.Status = &res.StatusCode + + headers := map[string][]string{} + request.CopyHeaders(headers, res.Header) + + result.Headers = &headers + + bodyCopy := append([]byte(nil), body.Raw...) + result.Body = &bodyCopy + + rsdb.SetStatus(req.ID, db.STATUS_DONE) + rsdb.SetResponse(req.ID, nil, result) + + URL, _ := url.Parse(req.URL) + + if !logger.IsDev() { + logger.Info("Fired request", + " from ", req.CreatedAt.Local().Format("02.01.06 15:04:05"), ": ", + req.Method, " ", URL.Path, " ", URL.RawQuery, + ) + } else { + if len(req.Body) != 0{ + logger.Dev("Fired request", + " from ", req.CreatedAt.Local().Format("02.01.06 15:04:05"), ": ", + req.Method, " ", URL.Path, " ", URL.RawQuery, + jsonutils.GetJson[map[string]any](string(req.Body)), + ) + } else { + logger.Dev("Fired request", + " from ", req.CreatedAt.Local().Format("02.01.06 15:04:05"), ": ", + req.Method, " ", URL.Path, " ", URL.RawQuery, + ) + } + } +} + +func fireScheduledRequest(req *db.ScheduledRequest) (*http.Response, error) { + httpReq, _ := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body)) + + request.CopyHeaders(httpReq.Header, req.Headers) + + client := &http.Client{} + + return client.Do(httpReq) +} \ No newline at end of file diff --git a/internals/scheduler/scheduler.go b/internals/scheduler/scheduler.go new file mode 100644 index 00000000..a645bc71 --- /dev/null +++ b/internals/scheduler/scheduler.go @@ -0,0 +1,23 @@ +package scheduler + +import ( + "context" + + scheduling "github.com/codeshelldev/gotl/pkg/scheduler" +) + +var scheduler = scheduling.New() +var cancel context.CancelFunc + +func Start() { + var ctx context.Context + ctx, cancel = context.WithCancel(context.Background()) + + go scheduler.Run(ctx) + + StartRequestScheduler() +} + +func Stop() { + cancel() +} \ No newline at end of file diff --git a/main.go b/main.go index 1f58f6c5..1ad25c5e 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,9 @@ import ( "github.com/codeshelldev/gotl/pkg/logger" httpserver "github.com/codeshelldev/gotl/pkg/server/http" config "github.com/codeshelldev/secured-signal-api/internals/config" + "github.com/codeshelldev/secured-signal-api/internals/db" reverseProxy "github.com/codeshelldev/secured-signal-api/internals/proxy" + "github.com/codeshelldev/secured-signal-api/internals/scheduler" docker "github.com/codeshelldev/secured-signal-api/utils/docker" "github.com/codeshelldev/secured-signal-api/utils/stdlog" ) @@ -37,6 +39,10 @@ func main() { config.Log() + db.Init() + + scheduler.Start() + proxy = reverseProxy.Create(config.DEFAULT.API.URL.URL) handler := proxy.Init() @@ -70,5 +76,6 @@ func main() { <-stop + db.Close() docker.Shutdown(server) } From a4d152f94a721e9a9b680adf831119ccd8441926 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:05:02 +0100 Subject: [PATCH 05/10] added `send_at` for scheduling messages to `/v2/send` --- go.mod | 26 ++--- go.sum | 48 +++++----- internals/proxy/endpoints/send.go | 153 ++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 37 deletions(-) create mode 100644 internals/proxy/endpoints/send.go diff --git a/go.mod b/go.mod index 3f78f293..8715c3fc 100644 --- a/go.mod +++ b/go.mod @@ -3,28 +3,27 @@ module github.com/codeshelldev/secured-signal-api go 1.25.6 require ( - github.com/codeshelldev/gotl/pkg/configutils v0.0.15-16 + github.com/codeshelldev/gotl/pkg/configutils v0.0.16 github.com/codeshelldev/gotl/pkg/docker v0.0.2 + github.com/codeshelldev/gotl/pkg/ioutils v0.0.2 github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 - github.com/codeshelldev/gotl/pkg/logger v0.0.6 - github.com/codeshelldev/gotl/pkg/pretty v0.0.9 - github.com/codeshelldev/gotl/pkg/query v0.0.3 - github.com/codeshelldev/gotl/pkg/request v0.0.6 - github.com/codeshelldev/gotl/pkg/scheduler v0.0.2 - github.com/codeshelldev/gotl/pkg/server/http v0.0.2 - github.com/codeshelldev/gotl/pkg/stringutils v0.0.3 - github.com/codeshelldev/gotl/pkg/templating v0.0.3 + github.com/codeshelldev/gotl/pkg/logger v0.0.8 + github.com/codeshelldev/gotl/pkg/pretty v0.0.10 + github.com/codeshelldev/gotl/pkg/query v0.0.4 + github.com/codeshelldev/gotl/pkg/request v0.0.8 + github.com/codeshelldev/gotl/pkg/scheduler v0.0.7 + github.com/codeshelldev/gotl/pkg/server/http v0.0.3 + github.com/codeshelldev/gotl/pkg/stringutils v0.0.8 + github.com/codeshelldev/gotl/pkg/templating v0.0.4 ) require ( - github.com/codeshelldev/gotl/pkg/ioutils v0.0.2 github.com/knadh/koanf/parsers/yaml v1.1.0 golang.org/x/time v0.14.0 ) require ( - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.4.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 @@ -34,10 +33,11 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 // indirect github.com/knadh/koanf/v2 v2.3.2 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.34 github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.41.0 // indirect ) diff --git a/go.sum b/go.sum index bc9f39c4..2cb90715 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,29 @@ -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g= -github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/codeshelldev/gotl/pkg/configutils v0.0.15-16 h1:cKJFxNj1k0QpQrFhfbKR32oL0Wgsai4eLMeCtLhHekY= -github.com/codeshelldev/gotl/pkg/configutils v0.0.15-16/go.mod h1:X7wY6s44k0Cxx2oMmJChogB1n1+oR+RqbzmQCCqnN5I= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/codeshelldev/gotl/pkg/configutils v0.0.16 h1:iS/Yw2ruYnRGf2zZphI+JFIGkQCECh+AFbJAi0uyXLA= +github.com/codeshelldev/gotl/pkg/configutils v0.0.16/go.mod h1:Tiu27XQ7D08fcwCHp5tZsDIQwIo6q626+l77k2dse7k= github.com/codeshelldev/gotl/pkg/docker v0.0.2 h1:kpseReocEBoSzWe/tOhUrIrOYeAR/inw3EF2/d+N078= github.com/codeshelldev/gotl/pkg/docker v0.0.2/go.mod h1:odNnlRw4aO1n2hSkDZIaiuSXIoFoVeatmXtF64Yd33U= github.com/codeshelldev/gotl/pkg/ioutils v0.0.2 h1:IRcN2M6H4v59iodw1k7gFX9lirhbVy6RZ4yRtKNcFYg= github.com/codeshelldev/gotl/pkg/ioutils v0.0.2/go.mod h1:WPQYglNqThBatoGaQK0OGx2bwzto1oi0zb1fB9gsaUU= github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 h1:ERsjkaWVrsyUZoEunCEeNYDXhuaIvoSetB8e/zI4Tqo= github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2/go.mod h1:oxgKaAoMu6iYVHfgR7AhkK22xbYx4K0KCkyVEfYVoWs= -github.com/codeshelldev/gotl/pkg/logger v0.0.6 h1:heo6z6yZm5PpX78vxud9HJNfVU9J46HVlW8T4EOy9nQ= -github.com/codeshelldev/gotl/pkg/logger v0.0.6/go.mod h1:pL/I7KYxbGHhyedallZlCkBvoalv9gAWNEYVXbF9BoM= -github.com/codeshelldev/gotl/pkg/pretty v0.0.9 h1:YgAZ0QpV+cUCKeLz5T0XFVByM8BXrJuz5KKLE0a6o1Y= -github.com/codeshelldev/gotl/pkg/pretty v0.0.9/go.mod h1:2Gk6UBrtkIME2RCSNytS/RJ5lHXYL/MDx0rYRpknobM= -github.com/codeshelldev/gotl/pkg/query v0.0.3 h1:Zy8k0R5HcJS00OMPRHybgFEiwMg7ceLyv6bA0G7NOfs= -github.com/codeshelldev/gotl/pkg/query v0.0.3/go.mod h1:kKaPOKXluIid3qeS7xzrmfq3NxIa8/PhKYHM6GRbwJw= -github.com/codeshelldev/gotl/pkg/request v0.0.6 h1:NySoIrdWv/5RUUufyXTodGmT6mc71VbjlQ+Wcthixp8= -github.com/codeshelldev/gotl/pkg/request v0.0.6/go.mod h1:vCXIZ2n/XxvEVInBQv9eIh0kQ2353V+WymL8kZ9yrOU= -github.com/codeshelldev/gotl/pkg/scheduler v0.0.2 h1:qofwNcdfiPz+5tscodsHpWSyY9QVc9TUAnchvACe7xI= -github.com/codeshelldev/gotl/pkg/scheduler v0.0.2/go.mod h1:sXEpRxbDc/JAN8WDxxq5+UxJf2dOQpKJIZyvORjIJGM= -github.com/codeshelldev/gotl/pkg/server/http v0.0.2 h1:Vo8PZYHEvw1lfQtS/Mc5gU7Jx5VyHjbXOwfSFyFcjss= -github.com/codeshelldev/gotl/pkg/server/http v0.0.2/go.mod h1:/asx7ViJtwlBvLgObjI/tejm6lNDN1/B+/6BPImqDfc= -github.com/codeshelldev/gotl/pkg/stringutils v0.0.3 h1:7k/HMnX7me8Kchm41I/M6dp3wXI0XORI3oyS87O0Viw= -github.com/codeshelldev/gotl/pkg/stringutils v0.0.3/go.mod h1:/dWlzYoTj23LmpFs+Bpal4tfUDbOVeApIgkLv9gTgUE= -github.com/codeshelldev/gotl/pkg/templating v0.0.3 h1:PAz6VN8yGBuZIdR/sDM+TmW1OFykl+I7/Zwa07uMgYA= -github.com/codeshelldev/gotl/pkg/templating v0.0.3/go.mod h1:D+wxgsPSiq9HShEzv1mhYAjGJyasWgPoIu+nRk4TPqY= +github.com/codeshelldev/gotl/pkg/logger v0.0.8 h1:mt8dLt3aEgzCTOLbJ+KuAghwnP6Iv7/VR8tHXxsXuTA= +github.com/codeshelldev/gotl/pkg/logger v0.0.8/go.mod h1:AFO/snEIfF8YB3+TH6XtFMlhRCAJxItSfyc4ndbwc8E= +github.com/codeshelldev/gotl/pkg/pretty v0.0.10 h1:efoRJfkrk26c5j26qiwCXWPzeG/TfFK9V55Q6Rn+1CM= +github.com/codeshelldev/gotl/pkg/pretty v0.0.10/go.mod h1:SkyfcVnQp37jV3SMTtnIFc1fyVvorvSskJxOmYvfIHU= +github.com/codeshelldev/gotl/pkg/query v0.0.4 h1:o2Oagx/s1wfNMqkh6GfR6wpsIVOFSDPIbxe8ABRIXDw= +github.com/codeshelldev/gotl/pkg/query v0.0.4/go.mod h1:Bg3tFzFq9xButTw0BSfGQhSmfAnFDrJamOcnX6Io4m4= +github.com/codeshelldev/gotl/pkg/request v0.0.8 h1:sVVt2ADOTgZrna7RsqThwMKxYCuxlBE80s7kV90rARg= +github.com/codeshelldev/gotl/pkg/request v0.0.8/go.mod h1:ngE6/OksRIclheFGfqJ6/2lBpzCm9sPe4p5JfGIg5kg= +github.com/codeshelldev/gotl/pkg/scheduler v0.0.7 h1:6D16m1/DndhkIvoYMc26ebc9SySy1UQMc7W4QifdvvM= +github.com/codeshelldev/gotl/pkg/scheduler v0.0.7/go.mod h1:sXEpRxbDc/JAN8WDxxq5+UxJf2dOQpKJIZyvORjIJGM= +github.com/codeshelldev/gotl/pkg/server/http v0.0.3 h1:3232uPB2CljzUJadyrME7p0DaOCGz+vPVfPjnS788SE= +github.com/codeshelldev/gotl/pkg/server/http v0.0.3/go.mod h1:/asx7ViJtwlBvLgObjI/tejm6lNDN1/B+/6BPImqDfc= +github.com/codeshelldev/gotl/pkg/stringutils v0.0.8 h1:VKIuEYLJARDmHyhAbcMy1TsdxPdzsKlbQvgr1G4QE7s= +github.com/codeshelldev/gotl/pkg/stringutils v0.0.8/go.mod h1:892bcYDpOf0sTpXtABQ3m+9MACpWHCVpN3f/mcPr7qo= +github.com/codeshelldev/gotl/pkg/templating v0.0.4 h1:qIWiqRtkSt/784lOlL7yi29lXx1eGXdacWDIV6euLKI= +github.com/codeshelldev/gotl/pkg/templating v0.0.4/go.mod h1:J1MfmzI5Smhqtz3+lkMM+vrF1sXiypKRUmFE77JSifU= 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= @@ -52,6 +50,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -68,8 +68,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internals/proxy/endpoints/send.go b/internals/proxy/endpoints/send.go new file mode 100644 index 00000000..afa18d75 --- /dev/null +++ b/internals/proxy/endpoints/send.go @@ -0,0 +1,153 @@ +package endpoints + +import ( + "errors" + "net/http" + "strconv" + "time" + + request "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/secured-signal-api/internals/config" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" + "github.com/codeshelldev/secured-signal-api/internals/scheduler" +) + +var SendEnpoint = Endpoint{ + Name: "Send", + Handler: sendHandler, +} + +const messageField = "message" +const sendAtField = "send_at" + +func sendHandler(mux *http.ServeMux) *http.ServeMux { + mux.HandleFunc("POST /v2/send", func(w http.ResponseWriter, req *http.Request) { + logger := GetLogger(req) + + conf := GetConfigByReq(req) + + variables := conf.SETTINGS.MESSAGE.VARIABLES.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.VARIABLES) + messageTemplate := conf.SETTINGS.MESSAGE.TEMPLATE.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.TEMPLATE) + + body, err := request.GetReqBody(req) + + if err != nil { + logger.Error("Could not get Request Body: ", err.Error()) + http.Error(w, "Bad Request: invalid body", http.StatusBadRequest) + return + } + + bodyData := map[string]any{} + + var modifiedBody bool + + if !body.Empty { + bodyData = body.Data + + if messageTemplate != "" { + headerData := request.GetReqHeaders(req) + + newData, err := TemplateMessage(messageTemplate, bodyData, headerData, variables) + + if err != nil { + logger.Error("Error Templating Message: ", err.Error()) + } + + if newData[messageField] != bodyData[messageField] && newData[messageField] != "" && newData[messageField] != nil { + bodyData = newData + modifiedBody = true + } + } + } + + if modifiedBody { + body.Data = bodyData + + err := body.UpdateReq(req) + + if err != nil { + logger.Error("Could not write to Request Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + logger.Debug("Applied Message Templating: ", body.Data) + } + + sendAtStr, ok := bodyData[sendAtField].(string) + + if ok && bodyData[messageField] != "" && bodyData[messageField] != nil { + delete(bodyData, sendAtField) + + body.Data = bodyData + + body.UpdateReq(req) + + tm, err := handleScheduledMessage(sendAtStr, w, req) + + if err != nil { + logger.Warn("Could not schedule request: ", err.Error()) + return + } + + logger.Debug("Scheduled message for ", tm.Local().Format("02.01.06 15:04:05")) + + return + } + }) + + return mux +} + +func handleScheduledMessage(sendAtStr string, w http.ResponseWriter, req *http.Request) (time.Time, error) { + sendAt, err := strconv.Atoi(sendAtStr) + + if err != nil { + WriteError(w, http.StatusBadRequest, "invalid timestamp: invalid unix number string") + return time.Time{}, errors.New("invalid timestamp") + } + + tm := time.Unix(int64(sendAt), 0) + + if tm.Before(time.Now()) { + WriteError(w, http.StatusBadRequest, "invalid timestamp: time lies in the past") + return time.Time{}, errors.New("timestamp expired") + } + + ChangeRequestDest(req, config.DEFAULT.API.URL.String() + req.URL.Path) + + reqID, err := scheduler.ScheduleRequest(tm, req) + + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return time.Time{}, err + } + + res := request.Body{ + Data: map[string]any{ + "id": reqID, + }, + } + + w.WriteHeader(http.StatusAccepted) + + res.Write(w) + + return tm, nil +} + +func TemplateMessage(template string, bodyData map[string]any, headerData map[string][]string, variables map[string]any) (map[string]any, error) { + bodyData["message_template"] = template + + data, _, err := TemplateBody(bodyData, headerData, variables) + + if err != nil || data == nil { + return bodyData, err + } + + data[messageField] = data["message_template"] + + delete(data, "message_template") + + return data, nil +} From b4413055b8b863b418d7c709714f82f63d8b5a1d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:15:52 +0100 Subject: [PATCH 06/10] fix unallowed req.RequestURI --- internals/proxy/endpoints/about.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internals/proxy/endpoints/about.go b/internals/proxy/endpoints/about.go index 687ad608..a24fcc69 100644 --- a/internals/proxy/endpoints/about.go +++ b/internals/proxy/endpoints/about.go @@ -19,6 +19,7 @@ var AboutEndpoint = Endpoint{ func aboutHandler(mux *http.ServeMux) *http.ServeMux { mux.HandleFunc("GET /v1/about", func(w http.ResponseWriter, req *http.Request) { + req.RequestURI = "" common.ChangeRequestDest(req, config.DEFAULT.API.URL.String() + "/v1/about") client := &http.Client{} From 506a57421751ad87cf09900448e6d69932e3d398 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:16:15 +0100 Subject: [PATCH 07/10] remove auth censoring, since they are deleted at auth middleware level --- internals/proxy/common/template.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 6ef9ab21..1f68feb7 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -60,14 +60,6 @@ func cleanHeaders(headers map[string][]string) map[string][]string { cleanedHeaders[cleanedKey] = value } - authHeader, ok := cleanedHeaders["Authorization"] - - if !ok { - authHeader = []string{"UNKNOWN REDACTED"} - } - - cleanedHeaders["Authorization"] = []string{strings.Split(authHeader[0], ` `)[0] + " REDACTED"} - return cleanedHeaders } From 0da04f218e2e0dd55d10242bc831e5f1677810e0 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:16:31 +0100 Subject: [PATCH 08/10] fix error when auth method is not even set --- internals/proxy/middlewares/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 2b532dac..334c7ff3 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -282,7 +282,7 @@ func authHandler(next http.Handler) http.Handler { req = SetContext(req, TokenKey, token) } else { // BREAKING Query & Path auth disabled (default) - if (method.Name == "Path" || method.Name == "Query") && len(*conf.API.AUTH.METHODS.Value) == 0 { + if (method.Name == "Path" || method.Name == "Query") && conf.API.AUTH.METHODS.Value == nil { deprecation.Error(method.Name, deprecation.DeprecationMessage{ Message: "{b}Query{/} and {b}Path{/} auth are {u}disabled{/} by default\nTo be able to use them they must first be enabled", Fix: "\n{b}Add{/} {b,fg=green}`" + strings.ToLower(method.Name) + "`{/} to {i}`api.auth.methods`{/}:" + From d95740d31bedd40a363a800b4944e20030f2835d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:25:03 +0100 Subject: [PATCH 09/10] use `Body_XYZ` and `Header_XYZ` if `@/#XYZ` does not work --- internals/proxy/common/template.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 1f68feb7..07e1db6a 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -68,24 +68,24 @@ func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES ma headers = cleanHeaders(headers) - // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to "".header_key_Var" and ".body_key_Var" - normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "body_key_", body) + // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to "".Header_Var" and ".Body_Var" + normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "Body_", body) if err != nil { return body, false, err } - normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "header_key_", normalizedBody) + normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "Header_", normalizedBody) if err != nil { return body, false, err } - // Prefix Body Data with body_key_ - prefixedBody := prefixData("body_key_", normalizedBody) + // Prefix Body Data with Body_ + prefixedBody := prefixData("Body_", normalizedBody) - // Prefix Header Data with header_key_ - prefixedHeaders := prefixData("header_key_", request.ParseHeaders(headers)) + // Prefix Header Data with Header_ + prefixedHeaders := prefixData("Header_", request.ParseHeaders(headers)) variables := map[string]any{} From 51397b79fd1b9feafd99f4d6f67197a71e246ea4 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:07:16 +0100 Subject: [PATCH 10/10] typo fix --- internals/proxy/common/template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 07e1db6a..6f3a0ee8 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -68,7 +68,7 @@ func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES ma headers = cleanHeaders(headers) - // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to "".Header_Var" and ".Body_Var" + // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".Header_Var" and ".Body_Var" normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "Body_", body) if err != nil {