From 6085616bf463bcb017c071c81aaa3cab6453df9b Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:19:44 +0100 Subject: [PATCH 01/26] make send_at int --- internals/proxy/endpoints/send.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/internals/proxy/endpoints/send.go b/internals/proxy/endpoints/send.go index af67bca3..7ec2844f 100644 --- a/internals/proxy/endpoints/send.go +++ b/internals/proxy/endpoints/send.go @@ -3,7 +3,6 @@ package endpoints import ( "errors" "net/http" - "strconv" "time" request "github.com/codeshelldev/gotl/pkg/request" @@ -77,7 +76,7 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { logger.Debug("Applied Message Templating: ", body.Data) } - sendAtStr, ok := bodyData[sendAtField].(string) + sendAt, ok := bodyData[sendAtField].(float64) if ok && bodyData[messageField] != "" && bodyData[messageField] != nil { delete(bodyData, sendAtField) @@ -92,7 +91,7 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { return } - tm, err := parseTimestamp(sendAtStr) + tm, err := parseTimestamp(int(sendAt)) if err != nil { logger.Warn("Could not parse timestamp: ", err.Error()) @@ -136,13 +135,7 @@ func getSendCapabilities(conf *structure.CONFIG) []string { return out } -func parseTimestamp(str string) (time.Time, error) { - sendAt, err := strconv.Atoi(str) - - if err != nil { - return time.Time{}, errors.New("invalid number string") - } - +func parseTimestamp(sendAt int) (time.Time, error) { tm := time.Unix(int64(sendAt), 0) if tm.Before(time.Now()) { From 88ea793a04e5c004dd473b983bc1e92998ea6eb4 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:54:47 +0100 Subject: [PATCH 02/26] added options to disable templating / injection * offtopic: removed deprecation warnings * made all "{Action}XYZ" functions mutate the input map and all "GetXYZ" functions work on a copy and return independent map (template related funcs) --- internals/config/structure/customtypes.go | 4 +- internals/config/structure/structure.go | 35 +++++++---- internals/proxy/common/template.go | 76 +++++++++++------------ internals/proxy/endpoints/send.go | 47 ++++++-------- internals/proxy/middlewares/auth.go | 25 -------- internals/proxy/middlewares/mapping.go | 15 ++--- internals/proxy/middlewares/policy.go | 10 +-- internals/proxy/middlewares/template.go | 65 ++++++++++++------- 8 files changed, 132 insertions(+), 145 deletions(-) diff --git a/internals/config/structure/customtypes.go b/internals/config/structure/customtypes.go index c23836fd..98f4e617 100644 --- a/internals/config/structure/customtypes.go +++ b/internals/config/structure/customtypes.go @@ -39,12 +39,12 @@ func (splitter *AllowBlockSlice) UnmarshalMapstructure(raw any) error { return nil } -type FieldPolicies struct{ +type FPolicies struct{ Allow []FieldPolicy Block []FieldPolicy } -func (splitter *FieldPolicies) UnmarshalMapstructure(raw any) error { +func (splitter *FPolicies) UnmarshalMapstructure(raw any) error { slice, ok := raw.([]any) if !ok { diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 5291ed48..b3002144 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -25,8 +25,7 @@ type CONFIG struct { NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` - // DEPRECATION overrides in Token Config - SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,fg=yellow}\x60{s}overrides{/}\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` + SETTINGS SETTINGS `koanf:"settings"` } type ConfigType string @@ -44,16 +43,13 @@ type SERVICE struct { type API struct { URL URL `koanf:"url" env>aliases:".apiurl"` - // DEPRECATION token, tokens in Token Config - // DEPRECATION api.token => api.tokens - TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,fg=yellow}\x60{s}tokens{/}\x60{/} and {b,fg=yellow}\x60{s}token{/}\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,fg=yellow}\x60{s}api.token{/}\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens"` AUTH AUTH `koanf:"auth"` } type AUTH struct { 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"` + TOKENS []Token `koanf:"tokens"` } type Token struct { @@ -68,9 +64,26 @@ type SETTINGS struct { type MESSAGE struct { 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"` + FIELD_MAPPINGS t.Opt[map[string][]FMapping]`koanf:"fieldmappings" childtransform:"default"` + TEMPLATING t.Opt[Templating] `koanf:"templating"` SCHEDULING t.Opt[Scheduling] `koanf:"scheduling"` + INJECTING t.Opt[Injecting] `koanf:"injecting"` +} + +type Injecting struct { + URLToBody t.Opt[URLToBody] `koanf:"urltobody"` +} + +type URLToBody struct { + Path bool `koanf:"path"` + Query bool `koanf:"query"` +} + +type Templating struct { + MessageTemplate string `koanf:"messagetemplate"` + Body bool `koanf:"body"` + Query bool `koanf:"query"` + Path bool `koanf:"path"` } type Scheduling struct { @@ -78,14 +91,14 @@ type Scheduling struct { MaxHorizon t.Opt[TimeDuration] `koanf:"maxhorizon"` } -type FieldMapping struct { +type FMapping struct { Field string `koanf:"field"` Score int `koanf:"score"` } type ACCESS struct { ENDPOINTS t.Opt[AllowBlockSlice] `koanf:"endpoints"` - FIELD_POLICIES t.Opt[map[string]FieldPolicies]`koanf:"fieldpolicies" childtransform:"default"` + FIELD_POLICIES t.Opt[map[string]FPolicies] `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"` diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 6f3a0ee8..0f58f314 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -6,7 +6,6 @@ import ( "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" @@ -63,29 +62,35 @@ func cleanHeaders(headers map[string][]string) map[string][]string { return cleanedHeaders } -func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES map[string]any) (map[string]any, bool, error) { +func GetTemplatedBody(body map[string]any, headers map[string][]string, VARIABLES map[string]any) (map[string]any, bool, error) { var modified bool - headers = cleanHeaders(headers) + var headersCopy map[string][]string + var bodyCopy map[string]any + + request.CopyHeaders(headersCopy, headers) + request.CopyMap(bodyCopy, body) + + headersCopy = cleanHeaders(headersCopy) // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".Header_Var" and ".Body_Var" - normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "Body_", body) + normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "Body_", bodyCopy) if err != nil { - return body, false, err + return bodyCopy, false, err } normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "Header_", normalizedBody) if err != nil { - return body, false, err + return bodyCopy, false, err } // Prefix Body Data with Body_ prefixedBody := prefixData("Body_", normalizedBody) // Prefix Header Data with Header_ - prefixedHeaders := prefixData("Header_", request.ParseHeaders(headers)) + prefixedHeaders := prefixData("Header_", request.ParseHeaders(headersCopy)) variables := map[string]any{} @@ -96,10 +101,10 @@ func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES ma templatedData, err := templating.RenderJSON(normalizedBody, variables) if err != nil { - return body, false, err + return bodyCopy, false, err } - beforeStr := jsonutils.ToJson(body) + beforeStr := jsonutils.ToJson(bodyCopy) afterStr := jsonutils.ToJson(templatedData) modified = beforeStr != afterStr @@ -107,23 +112,26 @@ func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES ma 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) +func TemplatePath(path string, VARIABLES any) (string, error) { + reqPath, err := url.PathUnescape(path) if err != nil { - return reqUrl.Path, data, false, false, err + return path, err } reqPath, err = templating.RenderNormalizedTemplate("path", reqPath, VARIABLES) if err != nil { - return reqUrl.Path, data, false, false, err + return path, err } - parts := strings.Split(reqPath, "/") + return reqPath, nil +} + +func InjectPathIntoBody(path string, data map[string]any) (string, bool) { + var modified bool + + parts := strings.Split(path, "/") newParts := []string{} for _, part := range parts { @@ -144,32 +152,26 @@ func TemplatePath(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, value := stringutils.ToType(keyValuePair[1]) data[keyWithoutPrefix] = value - modifiedBody = true + modified = true newParts = newParts[:len(newParts) - 1] } - reqPath = strings.Join(newParts, "/") - - if reqUrl.Path != reqPath { - modified = true - } - - return reqPath, data, modified, modifiedBody, nil + return strings.Join(newParts, "/"), modified } -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) +func TemplateQuery(rawQuery string, VARIABLES any) (string, error) { + decodedQuery, _ := url.QueryUnescape(rawQuery) - templatedQuery, _ := templating.RenderNormalizedTemplate("query", decodedQuery, VARIABLES) + templatedQuery, err := templating.RenderNormalizedTemplate("query", decodedQuery, VARIABLES) - originalQueryData := reqUrl.Query() + return templatedQuery, err +} - addedData, _ := query.ParseTypedQuery(templatedQuery) +func InjectQueryIntoBody(query url.Values, data map[string]any) bool { + var modified bool - for key, val := range addedData { + for key, val := range data { keyWithoutPrefix, match := strings.CutPrefix(key, "@") if !match { @@ -178,12 +180,10 @@ func TemplateQuery(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, data[keyWithoutPrefix] = val - originalQueryData.Del(key) + query.Del(key) modified = true } - reqRawQuery := originalQueryData.Encode() - - return reqRawQuery, data, modified, nil -} + return modified +} \ No newline at end of file diff --git a/internals/proxy/endpoints/send.go b/internals/proxy/endpoints/send.go index 7ec2844f..e87bc83a 100644 --- a/internals/proxy/endpoints/send.go +++ b/internals/proxy/endpoints/send.go @@ -27,7 +27,7 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { 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) + templating := conf.SETTINGS.MESSAGE.TEMPLATING.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.TEMPLATING) scheduling := conf.SETTINGS.MESSAGE.SCHEDULING.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.SCHEDULING) @@ -39,32 +39,29 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { return } - bodyData := map[string]any{} - var modifiedBody bool if !body.Empty { - bodyData = body.Data - - if messageTemplate != "" { - headerData := request.GetReqHeaders(req) + if templating.MessageTemplate != "" { + headers := request.GetReqHeaders(req) - newData, err := TemplateMessage(messageTemplate, bodyData, headerData, variables) + templatedMessage, err := GetTemplatedMessage(templating.MessageTemplate, body.Data, headers, variables) if err != nil { logger.Error("Error Templating Message: ", err.Error()) } - if newData[messageField] != bodyData[messageField] && newData[messageField] != "" && newData[messageField] != nil { - bodyData = newData + if templatedMessage != body.Data[messageField] && templatedMessage != "" { + body.Data[messageField] = templatedMessage + + logger.Debug("Applied Message Templating: \n", templatedMessage) + modifiedBody = true } } } if modifiedBody { - body.Data = bodyData - err := body.UpdateReq(req) if err != nil { @@ -72,16 +69,12 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - - logger.Debug("Applied Message Templating: ", body.Data) } - sendAt, ok := bodyData[sendAtField].(float64) - - if ok && bodyData[messageField] != "" && bodyData[messageField] != nil { - delete(bodyData, sendAtField) + sendAt, ok := body.Data[sendAtField].(float64) - body.Data = bodyData + if ok && body.Data[messageField] != "" && body.Data[messageField] != nil { + delete(body.Data, sendAtField) body.UpdateReq(req) @@ -168,18 +161,16 @@ func handleScheduledMessage(tm time.Time, w http.ResponseWriter, req *http.Reque return 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 +func GetTemplatedMessage(template string, body map[string]any, headers map[string][]string, VARIABLES map[string]any) (string, error) { + var bodyCopy map[string]any + + request.CopyMap(bodyCopy, body) - data, _, err := TemplateBody(bodyData, headerData, variables) + data, _, err := GetTemplatedBody(bodyCopy, headers, VARIABLES) if err != nil || data == nil { - return bodyData, err + return "", err } - data[messageField] = data["message_template"] - - delete(data, "message_template") - - return data, nil + return data["message_template"].(string), nil } diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 334c7ff3..c4b3bb88 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -13,7 +13,6 @@ 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/deprecation" ) var Auth Middleware = Middleware{ @@ -141,20 +140,6 @@ var QueryAuth = AuthMethod{ auth := req.URL.Query().Get("@" + authQuery) - // BREAKING @authorization Query - const oldAuthQuery = "authorization" - - if req.URL.Query().Has("@" + oldAuthQuery) { - fullURL, _ := request.ParseReqURL(req) - urlWithNewAuthQuery := strings.Replace(fullURL.String(), "@" + oldAuthQuery, "@{s,fg=bright_red}" + oldAuthQuery + "{/}{b,fg=green}" + authQuery + "{/}", 1) - - deprecation.Error(req.URL.String(), deprecation.DeprecationMessage{ - Using: "{b,i,bg=red}`@authorization`{/} in the query", - Message: "{b,fg=red}`/?@{s}authorization{/}`{/} has been renamed to {b,fg=green}`/?@auth`{}", - Fix: "\nChange the {b}url{/} to:\n`" + urlWithNewAuthQuery + "`", - }) - } - if strings.TrimSpace(auth) == "" { return "", nil } @@ -281,16 +266,6 @@ func authHandler(next http.Handler) http.Handler { req = SetContext(req, IsAuthKey, true) req = SetContext(req, TokenKey, token) } else { - // BREAKING Query & Path auth disabled (default) - 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`{/}:" + - "\napi.auth.methods: [" + strings.Join(append(allowedMethods, "{b,fg=green}" + strings.ToLower(method.Name) + "{/}"), ", ") + "]", - Note: "\n{i}Let us know what you think about this change at\n{i}{u,fg=blue}https://github.com/CodeShellDev/secured-signal-api/discussions/221{/}{/}", - }) - } - logger.Warn("Client tried using disabled auth method: ", method.Name) onUnauthorized(w) diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index ec594b43..1953a8d2 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -33,12 +33,9 @@ func mappingHandler(next http.Handler) http.Handler { } var modifiedBody bool - var bodyData map[string]any if !body.Empty { - bodyData = body.Data - - aliasData := processFieldMappings(fieldMappings, bodyData) + aliasData := processFieldMappings(fieldMappings, body.Data) for key, value := range aliasData { prefix := key[:1] @@ -47,7 +44,7 @@ func mappingHandler(next http.Handler) http.Handler { switch prefix { case "@": - bodyData[keyWithoutPrefix] = value + body.Data[keyWithoutPrefix] = value modifiedBody = true case ".": variables[keyWithoutPrefix] = value @@ -56,8 +53,6 @@ func mappingHandler(next http.Handler) http.Handler { } if modifiedBody { - body.Data = bodyData - err := body.UpdateReq(req) if err != nil { @@ -73,7 +68,7 @@ func mappingHandler(next http.Handler) http.Handler { }) } -func processFieldMappings(aliases map[string][]structure.FieldMapping, data map[string]any) map[string]any { +func processFieldMappings(aliases map[string][]structure.FMapping, data map[string]any) map[string]any { aliasData := map[string]any{} for key, alias := range aliases { @@ -87,7 +82,7 @@ func processFieldMappings(aliases map[string][]structure.FieldMapping, data map[ return aliasData } -func getData(key string, aliases []structure.FieldMapping, data map[string]any) (string, any) { +func getData(key string, aliases []structure.FMapping, data map[string]any) (string, any) { var best int var value any @@ -106,7 +101,7 @@ func getData(key string, aliases []structure.FieldMapping, data map[string]any) return key, value } -func processFieldMapping(alias structure.FieldMapping, data map[string]any) (any, int, bool) { +func processFieldMapping(alias structure.FMapping, data map[string]any) (any, int, bool) { aliasKey := alias.Field value, ok := jsonutils.GetByPath(aliasKey, data) diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 29c9be0b..9abafc78 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -34,13 +34,9 @@ func policyHandler(next http.Handler) http.Handler { return } - if body.Empty { - body.Data = map[string]any{} - } - - headerData := request.GetReqHeaders(req) + headers := request.GetReqHeaders(req) - shouldBlock, field := isBlockedByPolicy(body.Data, headerData, policies) + shouldBlock, field := isBlockedByPolicy(body.Data, headers, policies) if shouldBlock { logger.Warn("Client tried to use blocked field: ", field) @@ -120,7 +116,7 @@ func doPoliciesApply(key string, body map[string]any, headers map[string][]strin return false, "" } -func isBlockedByPolicy(body map[string]any, headers map[string][]string, policies map[string]structure.FieldPolicies) (bool, string) { +func isBlockedByPolicy(body map[string]any, headers map[string][]string, policies map[string]structure.FPolicies) (bool, string) { if len(policies) == 0 || policies == nil { // default: allow all return false, "" diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index 2e74f7c4..7dcaf706 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -19,8 +19,13 @@ func templateHandler(next http.Handler) http.Handler { conf := GetConfigByReq(req) + templating := conf.SETTINGS.MESSAGE.TEMPLATING.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.TEMPLATING) + injecting := conf.SETTINGS.MESSAGE.INJECTING.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.INJECTING) + variables := conf.SETTINGS.MESSAGE.VARIABLES.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.VARIABLES) + urlToBody := injecting.URLToBody.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.INJECTING.Value.URLToBody) + body, err := request.GetReqBody(req) if err != nil { @@ -29,19 +34,17 @@ func templateHandler(next http.Handler) http.Handler { return } - bodyData := map[string]any{} - var modifiedBody bool - if !body.Empty { + if !body.Empty && templating.Body { var modified bool - headerData := request.GetReqHeaders(req) + headers := request.GetReqHeaders(req) - bodyData, modified, err = TemplateBody(body.Data, headerData, variables) + body.Data, modified, err = GetTemplatedBody(body.Data, headers, variables) if err != nil { - logger.Error("Error Templating JSON: ", err.Error()) + logger.Error("Error Templating Body: ", err.Error()) } if modified { @@ -50,41 +53,55 @@ func templateHandler(next http.Handler) http.Handler { } if req.URL.RawQuery != "" { - var modified bool + if templating.Query { + oldRawQuery := req.URL.RawQuery - req.URL.RawQuery, bodyData, modified, err = TemplateQuery(req.URL, bodyData, variables) + req.URL.RawQuery, err = TemplateQuery(req.URL.RawQuery, variables) - if err != nil { - logger.Error("Error Templating Query: ", err.Error()) + if err != nil { + logger.Error("Error Templating Query: ", err.Error()) + } + + if req.URL.RawQuery != oldRawQuery { + logger.Debug("Applied Query Templating: ", req.URL.RawQuery) + } } - if modified { - modifiedBody = true + if urlToBody.Query { + modified := InjectQueryIntoBody(req.URL.Query(), body.Data) + + if modified { + modifiedBody = true + } } } if req.URL.Path != "" { - var modified bool - var templated bool + if templating.Path { + oldPath := req.URL.Path - req.URL.Path, bodyData, modified, templated, err = TemplatePath(req.URL, bodyData, variables) + req.URL.Path, err = TemplatePath(req.URL.Path, variables) - if err != nil { - logger.Error("Error Templating Path: ", err.Error()) - } + if err != nil { + logger.Error("Error Templating Path: ", err.Error()) + } - if modified { - logger.Debug("Applied Path Templating: ", req.URL.Path) + if req.URL.Path != oldPath { + logger.Debug("Applied Path Templating: ", req.URL.Path) + } } - if templated { - modifiedBody = true + if urlToBody.Path { + var modified bool + req.URL.Path, modified = InjectPathIntoBody(req.URL.Path, body.Data) + + if modified { + modifiedBody = true + } } } if modifiedBody { - body.Data = bodyData - err := body.UpdateReq(req) if err != nil { From d4b36deb8a56f53e843ed813977cdbef7c1faf64 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:16:15 +0100 Subject: [PATCH 03/26] enable templating by default --- data/defaults.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/data/defaults.yml b/data/defaults.yml index 2848aef0..f39138f7 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -29,9 +29,15 @@ settings: { field: data, score: 1 }, ] - variables: - recipients: ${RECIPIENTS} - number: ${NUMBER} + templating: + body: true + path: true + query: true + + injecting: + urlToBody: + query: true + path: true access: endpoints: From a1d6c7dfbe0dac57239c8fae55856e675a054242 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:16:31 +0100 Subject: [PATCH 04/26] add breaking for struct tags --- internals/config/parser.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internals/config/parser.go b/internals/config/parser.go index db4b9d45..b7c4adc0 100644 --- a/internals/config/parser.go +++ b/internals/config/parser.go @@ -30,10 +30,13 @@ var onUseFuncs = map[string]func(source string, target configutils.TransformTarg "deprecated": func(source string, target configutils.TransformTarget) { deprecationHandler(source, target) }, + "broken": func(source string, target configutils.TransformTarget) { + deprecationHandler(source, target) + }, } func deprecationHandler(source string, target configutils.TransformTarget) { - msgMap := configutils.ParseTag(target.Source.Tag.Get("deprecation")) + msgMap := configutils.ParseTag(target.Source.Tag.Get("breaking")) message := configutils.GetValueWithSource(source, target.Parent, msgMap) @@ -46,10 +49,10 @@ func deprecationHandler(source string, target configutils.TransformTarget) { usingSuffix = " (at root)" } - deprecation.Warn(source, deprecation.DeprecationMessage{ + deprecation.Error(source, deprecation.DeprecationMessage{ Using: "{b,fg=bright_white}" + usingPrefix + "{/}{b,i,bg=red}`" + source + "`{/}" + usingSuffix, Message: message, Fix: "", - Note: "\n{i}Update your config before the next update,{/}\n{i}where it will be removed for good{/}", + Note: "\n{i}Update your config {b,fg=red}NOW!{/} {/}", }) } \ No newline at end of file From 0b5483c9c5968ab7030bd9fabfcef8a63b89c088 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:16:43 +0100 Subject: [PATCH 05/26] add breaking error for message templates --- internals/config/structure/structure.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index b3002144..97b4cd3a 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -36,19 +36,19 @@ const ( ) type SERVICE struct { - HOSTNAMES t.Opt[[]string] `koanf:"hostnames" env>aliases:".hostnames"` - PORT string `koanf:"port" env>aliases:".port"` - LOG_LEVEL string `koanf:"loglevel" env>aliases:".loglevel"` + HOSTNAMES t.Opt[[]string] `koanf:"hostnames" env>aliases:".hostnames"` + PORT string `koanf:"port" env>aliases:".port"` + LOG_LEVEL string `koanf:"loglevel" env>aliases:".loglevel"` } type API struct { - URL URL `koanf:"url" env>aliases:".apiurl"` - TOKENS []string `koanf:"tokens" env>aliases:".apitokens"` + URL URL `koanf:"url" env>aliases:".apiurl"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens"` AUTH AUTH `koanf:"auth"` } type AUTH struct { - METHODS t.Opt[[]string] `koanf:"methods" env>aliases:".authmethods"` + METHODS t.Opt[[]string] `koanf:"methods" env>aliases:".authmethods"` TOKENS []Token `koanf:"tokens"` } @@ -63,9 +63,9 @@ type SETTINGS struct { } type MESSAGE struct { - VARIABLES t.Opt[map[string]any] `koanf:"variables" childtransform:"upper"` - FIELD_MAPPINGS t.Opt[map[string][]FMapping]`koanf:"fieldmappings" childtransform:"default"` - TEMPLATING t.Opt[Templating] `koanf:"templating"` + VARIABLES t.Opt[map[string]any] `koanf:"variables" childtransform:"upper"` + FIELD_MAPPINGS t.Opt[map[string][]FMapping]`koanf:"fieldmappings" childtransform:"default"` + TEMPLATING t.Opt[Templating] `koanf:"templating" aliases:"template" onuse:"template>>broken" breaking:"{b,fg=red}\x60{s}settings.message.template{/}\x60{/} has been moved\n Use {b,fg=green}\x60settings.message.templating.messageTemplate\x60{/} instead"` SCHEDULING t.Opt[Scheduling] `koanf:"scheduling"` INJECTING t.Opt[Injecting] `koanf:"injecting"` } @@ -98,7 +98,7 @@ type FMapping struct { type ACCESS struct { ENDPOINTS t.Opt[AllowBlockSlice] `koanf:"endpoints"` - FIELD_POLICIES t.Opt[map[string]FPolicies] `koanf:"fieldpolicies" childtransform:"default"` + FIELD_POLICIES t.Opt[map[string]FPolicies] `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"` From d2247f4ee5a9483571eb6cce383685a2e672eade Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:19:22 +0100 Subject: [PATCH 06/26] allow for using body/header.var instead of @/#var --- internals/proxy/common/template.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 0f58f314..660b8187 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -44,7 +44,7 @@ func prefixData(prefix string, data map[string]any) map[string]any { res := map[string]any{} for key, value := range data { - res[prefix+key] = value + res[prefix + key] = value } return res @@ -73,24 +73,28 @@ func GetTemplatedBody(body map[string]any, headers map[string][]string, VARIABLE headersCopy = cleanHeaders(headersCopy) - // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".Header_Var" and ".Body_Var" - normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "Body_", bodyCopy) + // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".header.Var" and ".body.Var" + normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "body.", bodyCopy) if err != nil { return bodyCopy, false, err } - normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "Header_", normalizedBody) + normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "header.", normalizedBody) if err != nil { return bodyCopy, false, err } // Prefix Body Data with Body_ - prefixedBody := prefixData("Body_", normalizedBody) + prefixedBody := map[string]any{ + "body": normalizedBody, + } // Prefix Header Data with Header_ - prefixedHeaders := prefixData("Header_", request.ParseHeaders(headersCopy)) + prefixedHeaders := map[string]any{ + "header": request.ParseHeaders(headersCopy), + } variables := map[string]any{} From 86d56e8c7d93af46686fa1d13f83e6b80789712e Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:00:30 +0100 Subject: [PATCH 07/26] fix endpoints not redirecting back to middleware chain --- internals/proxy/common/template.go | 15 ++++++++++----- internals/proxy/endpoints/about.go | 4 +++- internals/proxy/endpoints/endpoints.go | 6 +++--- internals/proxy/endpoints/schedule.go | 2 +- internals/proxy/endpoints/send.go | 14 +++++++++++--- internals/proxy/middlewares/api.go | 6 +++--- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 660b8187..1bbf6e61 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/codeshelldev/gotl/pkg/jsonutils" + queryutils "github.com/codeshelldev/gotl/pkg/query" "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/gotl/pkg/stringutils" "github.com/codeshelldev/gotl/pkg/templating" @@ -65,8 +66,8 @@ func cleanHeaders(headers map[string][]string) map[string][]string { func GetTemplatedBody(body map[string]any, headers map[string][]string, VARIABLES map[string]any) (map[string]any, bool, error) { var modified bool - var headersCopy map[string][]string - var bodyCopy map[string]any + bodyCopy := map[string]any{} + headersCopy := map[string][]string{} request.CopyHeaders(headersCopy, headers) request.CopyMap(bodyCopy, body) @@ -147,7 +148,7 @@ func InjectPathIntoBody(path string, data map[string]any) (string, bool) { continue } - keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], "@") + keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], requestkeys.BodyPrefix) if !match { continue @@ -175,8 +176,12 @@ func TemplateQuery(rawQuery string, VARIABLES any) (string, error) { func InjectQueryIntoBody(query url.Values, data map[string]any) bool { var modified bool - for key, val := range data { - keyWithoutPrefix, match := strings.CutPrefix(key, "@") + decodedQuery, _ := url.QueryUnescape(query.Encode()) + + parsedQuery, _ := queryutils.ParseTypedQuery(decodedQuery) + + for key, val := range parsedQuery { + keyWithoutPrefix, match := strings.CutPrefix(key, requestkeys.BodyPrefix) if !match { continue diff --git a/internals/proxy/endpoints/about.go b/internals/proxy/endpoints/about.go index fa42f627..efe86c3c 100644 --- a/internals/proxy/endpoints/about.go +++ b/internals/proxy/endpoints/about.go @@ -17,7 +17,7 @@ var AboutEndpoint = Endpoint{ Handler: aboutHandler, } -func aboutHandler(mux *http.ServeMux) *http.ServeMux { +func aboutHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { mux.HandleFunc("GET /v1/about", func(w http.ResponseWriter, req *http.Request) { req.RequestURI = "" ChangeRequestDest(req, config.DEFAULT.API.URL.String() + "/v1/about") @@ -41,6 +41,8 @@ func aboutHandler(mux *http.ServeMux) *http.ServeMux { return } + body.EnsureNotNil() + for key, values := range res.Header { for _, value := range values { w.Header().Add(key, value) diff --git a/internals/proxy/endpoints/endpoints.go b/internals/proxy/endpoints/endpoints.go index c7077248..ba367278 100644 --- a/internals/proxy/endpoints/endpoints.go +++ b/internals/proxy/endpoints/endpoints.go @@ -6,9 +6,9 @@ import ( type Endpoint struct { Name string - Handler func(mux *http.ServeMux) *http.ServeMux + Handler func(mux *http.ServeMux, next http.Handler) *http.ServeMux } -func (endpoint Endpoint) Use(mux *http.ServeMux) *http.ServeMux { - return endpoint.Handler(mux) +func (endpoint Endpoint) Use(mux *http.ServeMux, next http.Handler) *http.ServeMux { + return endpoint.Handler(mux, next) } \ No newline at end of file diff --git a/internals/proxy/endpoints/schedule.go b/internals/proxy/endpoints/schedule.go index 4f9dd285..ff5a4275 100644 --- a/internals/proxy/endpoints/schedule.go +++ b/internals/proxy/endpoints/schedule.go @@ -15,7 +15,7 @@ var ScheduleEndpoint = Endpoint{ Handler: scheduleHandler, } -func scheduleHandler(mux *http.ServeMux) *http.ServeMux { +func scheduleHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { mux.HandleFunc("DELETE /v1/schedule/{id}", func(w http.ResponseWriter, req *http.Request) { id := req.PathValue("id") diff --git a/internals/proxy/endpoints/send.go b/internals/proxy/endpoints/send.go index e87bc83a..e92078b9 100644 --- a/internals/proxy/endpoints/send.go +++ b/internals/proxy/endpoints/send.go @@ -20,7 +20,7 @@ var SendEnpoint = Endpoint{ const messageField = "message" const sendAtField = "send_at" -func sendHandler(mux *http.ServeMux) *http.ServeMux { +func sendHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { mux.HandleFunc("POST /v2/send", func(w http.ResponseWriter, req *http.Request) { logger := GetLogger(req) @@ -39,6 +39,8 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { return } + body.EnsureNotNil() + var modifiedBody bool if !body.Empty { @@ -111,6 +113,8 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { return } + + next.ServeHTTP(w, req) }) return mux @@ -162,7 +166,11 @@ func handleScheduledMessage(tm time.Time, w http.ResponseWriter, req *http.Reque } func GetTemplatedMessage(template string, body map[string]any, headers map[string][]string, VARIABLES map[string]any) (string, error) { - var bodyCopy map[string]any + const templatedSuffix = "_template" + + bodyCopy := map[string]any{ + messageField + templatedSuffix: template, + } request.CopyMap(bodyCopy, body) @@ -172,5 +180,5 @@ func GetTemplatedMessage(template string, body map[string]any, headers map[strin return "", err } - return data["message_template"].(string), nil + return data[messageField + templatedSuffix].(string), nil } diff --git a/internals/proxy/middlewares/api.go b/internals/proxy/middlewares/api.go index 86dd3f04..15a2222f 100644 --- a/internals/proxy/middlewares/api.go +++ b/internals/proxy/middlewares/api.go @@ -32,9 +32,9 @@ var InternalSecureAPI Middleware = Middleware{ func internalSecureAPIHandler(next http.Handler) http.Handler { mux := http.NewServeMux() - e.AboutEndpoint.Use(mux) - e.SendEnpoint.Use(mux) - e.ScheduleEndpoint.Use(mux) + e.AboutEndpoint.Use(mux, next) + e.SendEnpoint.Use(mux, next) + e.ScheduleEndpoint.Use(mux, next) mux.Handle("/", next) From 67ff62a8aa93bf2302a09d3e3b5b3056b1aac126 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:01:09 +0100 Subject: [PATCH 08/26] make sure maps are not nil before copying to --- internals/proxy/middlewares/auth.go | 11 ++++++----- internals/proxy/middlewares/mapping.go | 5 ++++- internals/proxy/middlewares/policy.go | 2 ++ internals/proxy/middlewares/template.go | 2 ++ internals/scheduler/reqscheduler.go | 6 +++--- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index c4b3bb88..94879c78 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -13,6 +13,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/requestkeys" ) var Auth Middleware = Middleware{ @@ -36,7 +37,7 @@ var BearerAuth = AuthMethod{ return "", nil } - if strings.ToLower(headerParts[0]) == "bearer" { + if strings.EqualFold(headerParts[0], "bearer") { req.Header.Del("Authorization") if isValidToken(tokens, headerParts[1]) { @@ -65,7 +66,7 @@ var BasicAuth = AuthMethod{ return "", nil } - if strings.ToLower(headerParts[0]) == "basic" { + if strings.EqualFold(headerParts[0], "basic") { req.Header.Del("Authorization") base64Bytes, err := base64.StdEncoding.DecodeString(headerParts[1]) @@ -83,7 +84,7 @@ var BasicAuth = AuthMethod{ user, password := parts[0], parts[1] - if strings.ToLower(user) == "api" && isValidToken(tokens, password) { + if strings.EqualFold(user, "api") && isValidToken(tokens, password) { return password, nil } @@ -138,7 +139,7 @@ var QueryAuth = AuthMethod{ Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { const authQuery = "auth" - auth := req.URL.Query().Get("@" + authQuery) + auth := req.URL.Query().Get(requestkeys.BodyPrefix + authQuery) if strings.TrimSpace(auth) == "" { return "", nil @@ -147,7 +148,7 @@ var QueryAuth = AuthMethod{ if isValidToken(tokens, auth) { query := req.URL.Query() - query.Del("@" + authQuery) + query.Del(requestkeys.BodyPrefix + authQuery) req.URL.RawQuery = query.Encode() diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index 1953a8d2..1206cb1d 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -8,6 +8,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/requestkeys" ) var Mapping Middleware = Middleware{ @@ -32,6 +33,8 @@ func mappingHandler(next http.Handler) http.Handler { return } + body.EnsureNotNil() + var modifiedBody bool if !body.Empty { @@ -43,7 +46,7 @@ func mappingHandler(next http.Handler) http.Handler { keyWithoutPrefix := key[1:] switch prefix { - case "@": + case requestkeys.BodyPrefix: body.Data[keyWithoutPrefix] = value modifiedBody = true case ".": diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 9abafc78..96d8509e 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -34,6 +34,8 @@ func policyHandler(next http.Handler) http.Handler { return } + body.EnsureNotNil() + headers := request.GetReqHeaders(req) shouldBlock, field := isBlockedByPolicy(body.Data, headers, policies) diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index 7dcaf706..63d35b8d 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -34,6 +34,8 @@ func templateHandler(next http.Handler) http.Handler { return } + body.EnsureNotNil() + var modifiedBody bool if !body.Empty && templating.Body { diff --git a/internals/scheduler/reqscheduler.go b/internals/scheduler/reqscheduler.go index 5b5a1e47..08a84108 100644 --- a/internals/scheduler/reqscheduler.go +++ b/internals/scheduler/reqscheduler.go @@ -161,10 +161,10 @@ func HandleScheduledRequest(req *db.ScheduledRequest) { result.Status = &res.StatusCode - headers := map[string][]string{} - request.CopyHeaders(headers, res.Header) + headersCopy := map[string][]string{} + request.CopyHeaders(headersCopy, res.Header) - result.Headers = &headers + result.Headers = &headersCopy bodyCopy := append([]byte(nil), body.Raw...) result.Body = &bodyCopy From 7901f9d4c55a81dfbf747e3757ed85d61a73dd4c Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:01:43 +0100 Subject: [PATCH 09/26] added global redact transform and improved stdlog for httpserver, etc. --- Dockerfile | 2 + go.mod | 11 ++-- go.sum | 18 +++---- internals/config/loader.go | 3 ++ internals/config/structure/structure.go | 1 + internals/proxy/middlewares/log.go | 6 ++- internals/proxy/proxy.go | 38 ++++++------- main.go | 14 ++--- utils/logging/logging.go | 70 ++++++++++++++++++++++++ utils/stdlog/log.go | 72 ------------------------- 10 files changed, 118 insertions(+), 117 deletions(-) create mode 100644 utils/logging/logging.go delete mode 100644 utils/stdlog/log.go diff --git a/Dockerfile b/Dockerfile index 8de1c8b2..d1992aab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ ENV DB_PATH=/db/db.sqlite3 ENV CGO_ENABLED=1 +ENV REDACT_TOKENS=true + ARG TARGETOS ARG TARGETARCH diff --git a/go.mod b/go.mod index 13885370..fb572f02 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,15 @@ module github.com/codeshelldev/secured-signal-api -go 1.25.6 +go 1.26.0 require ( 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.8 + github.com/codeshelldev/gotl/pkg/logger v0.0.18 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/request v0.0.10 github.com/codeshelldev/gotl/pkg/scheduler v0.0.9 github.com/codeshelldev/gotl/pkg/server/http v0.0.3 github.com/codeshelldev/gotl/pkg/stringutils v0.0.8 @@ -23,7 +22,7 @@ require ( ) require ( - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.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 @@ -32,7 +31,7 @@ require ( github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect 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-runewidth v0.0.20 // 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 diff --git a/go.sum b/go.sum index 980dee82..9af88879 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,19 @@ -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/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 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.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/logger v0.0.18 h1:eQ9F8UXJbdKQ31759VN3tY0UoaczFRjKuMGEhQOZ7D0= +github.com/codeshelldev/gotl/pkg/logger v0.0.18/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/request v0.0.10 h1:wIotjU0pBgmd5lE0OSqhejCn+Ld7DnYd+nzzb3f/ApU= +github.com/codeshelldev/gotl/pkg/request v0.0.10/go.mod h1:ngE6/OksRIclheFGfqJ6/2lBpzCm9sPe4p5JfGIg5kg= github.com/codeshelldev/gotl/pkg/scheduler v0.0.9 h1:8IFSPmyQehogwwo9vo3aAo3NxVY5aR8Hzrb8E+hMrDU= github.com/codeshelldev/gotl/pkg/scheduler v0.0.9/go.mod h1:sXEpRxbDc/JAN8WDxxq5+UxJf2dOQpKJIZyvORjIJGM= github.com/codeshelldev/gotl/pkg/server/http v0.0.3 h1:3232uPB2CljzUJadyrME7p0DaOCGz+vPVfPjnS788SE= @@ -48,8 +46,8 @@ github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/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= diff --git a/internals/config/loader.go b/internals/config/loader.go index 98f55ce5..0b2881fd 100644 --- a/internals/config/loader.go +++ b/internals/config/loader.go @@ -19,8 +19,11 @@ var ENV *structure.ENV = &structure.ENV{ TOKENS_DIR: os.Getenv("TOKENS_DIR"), FAVICON_PATH: os.Getenv("FAVICON_PATH"), DB_PATH: os.Getenv("DB_PATH"), + INSECURE: false, + REDACT_TOKENS: strings.EqualFold(os.Getenv("REDACT_TOKENS"), "true") || os.Getenv("REDACT_TOKENS") == "1", + CONFIGS: map[string]*structure.CONFIG{}, } diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 97b4cd3a..27156d71 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,6 +14,7 @@ type ENV struct { DB_PATH string INSECURE bool + REDACT_TOKENS bool TOKENS []string diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index fd21f8ad..6b11cd0d 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -9,6 +9,7 @@ import ( "github.com/codeshelldev/gotl/pkg/request" "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/logging" ) var RequestLogger Middleware = Middleware{ @@ -58,9 +59,12 @@ func middlewareLoggerHandler(next http.Handler) http.Handler { if strings.TrimSpace(logLevel) != "" { l = logger.Get().Sub(logLevel) - l.SetTransform(func(content string) string { + transforms := logging.DefaultTransforms() + transforms = append(transforms, func(content string) string { return conf.NAME + "\t" + content }) + + l.SetTransform(logging.Apply(transforms...)) } req = SetContext(req, LoggerKey, l) diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index ad40639b..ba9e3c3b 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -7,7 +7,6 @@ import ( "github.com/codeshelldev/gotl/pkg/logger" m "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares" - "github.com/codeshelldev/secured-signal-api/utils/stdlog" ) type Proxy struct { @@ -20,26 +19,23 @@ func Create(targetUrl *url.URL) Proxy { return Proxy{Use: func() *httputil.ReverseProxy {return nil}} } - proxy := httputil.NewSingleHostReverseProxy(targetUrl) - proxy.ModifyResponse = func(res *http.Response) error { - res.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, private, proxy-revalidate") - res.Header.Set("Pragma", "no-cache") - res.Header.Set("Expires", "0") - res.Header.Set("Vary", "*") - res.Header.Set("Referrer-Policy", "no-referrer") - - return nil - } - - proxy.ErrorLog = stdlog.ErrorLog - - director := proxy.Director - - proxy.Director = func(req *http.Request) { - director(req) - - req.Header.Add("X-Forwarded-Host", req.Host) - req.Host = targetUrl.Host + proxy := &httputil.ReverseProxy{ + Rewrite: func(req *httputil.ProxyRequest) { + req.Out.URL.Scheme = targetUrl.Scheme + req.Out.URL.Host = targetUrl.Host + req.Out.Host = targetUrl.Host + + req.SetXForwarded() + }, + ModifyResponse: func(res *http.Response) error { + res.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, private, proxy-revalidate") + res.Header.Set("Pragma", "no-cache") + res.Header.Set("Expires", "0") + res.Header.Set("Vary", "*") + res.Header.Set("Referrer-Policy", "no-referrer") + return nil + }, + ErrorLog: logger.StdError(), } return Proxy{Use: func() *httputil.ReverseProxy {return proxy}} diff --git a/main.go b/main.go index 1ad25c5e..30307d7a 100644 --- a/main.go +++ b/main.go @@ -12,26 +12,26 @@ import ( 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" + "github.com/codeshelldev/secured-signal-api/utils/logging" ) var proxy reverseProxy.Proxy func main() { - logLevel := os.Getenv("LOG_LEVEL") - - logger.Init(logLevel) + logging.Init(os.Getenv("LOG_LEVEL")) docker.Init() config.Load() if config.DEFAULT.SERVICE.LOG_LEVEL != logger.Level() { - logger.Init(config.DEFAULT.SERVICE.LOG_LEVEL) + logging.Init(config.DEFAULT.SERVICE.LOG_LEVEL) } logger.Info("Initialized Logger with Level of ", logger.Level()) + logging.Setup() + if logger.Level() == "dev" { logger.Dev("Welcome back, Developer!") logger.Dev("CTRL+S config to Print to Console") @@ -61,8 +61,8 @@ func main() { server := httpserver.Create(handler, "0.0.0.0", ports...) - server.ErrorLog = stdlog.ErrorLog - server.InfoLog = stdlog.DebugLog + server.ErrorLog = logger.StdError() + server.InfoLog = logger.StdInfo() stop := docker.Run(func() { if logger.IsDebug() && len(ports) > 1 { diff --git a/utils/logging/logging.go b/utils/logging/logging.go new file mode 100644 index 00000000..3ccfb6a4 --- /dev/null +++ b/utils/logging/logging.go @@ -0,0 +1,70 @@ +package logging + +import ( + "strings" + + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/secured-signal-api/internals/config" +) + +func DefaultTransforms() []func(string)string { + transforms := []func(string)string{} + + transforms = append(transforms, BeginWithCapital) + + if config.ENV.REDACT_TOKENS { + transforms = append(transforms, RedactTokens()) + } + + return transforms +} + +func Init(level string) { + options := logger.DefaultOptions() + + logger.InitWith(level, options) + logger.InitStdLoggerWith(level, options) +} + +func Setup() { + transform := Apply(DefaultTransforms()...) + + logger.Get().SetTransform(transform) + logger.GetStdLogger().SetTransform(transform) +} + +func RedactTokens() func(string) string { + return RedactWords('*', config.ENV.TOKENS...) +} + +func Apply(transforms ...func(content string) string) func(string) string { + return func(content string) string { + for _, fn := range transforms { + content = fn(content) + } + + return content + } +} + +func BeginWithCapital(content string) string { + return strings.ToUpper(content[:1]) + content[1:] +} + +func Redact(redact string) string { + if len(redact) <= 4 { + return strings.Repeat("*", len(redact)) + } + + return string(redact[0]) + strings.Repeat("*", len(redact) - 2) + string(redact[len(redact) - 1]) +} + +func RedactWords(replaceBy rune, words ...string) func(string) string { + return func(content string) string { + for _, word := range words { + content = strings.ReplaceAll(content, word, "[" + Redact(word) + "]") + } + + return content + } +} \ No newline at end of file diff --git a/utils/stdlog/log.go b/utils/stdlog/log.go deleted file mode 100644 index c9604356..00000000 --- a/utils/stdlog/log.go +++ /dev/null @@ -1,72 +0,0 @@ -package stdlog - -import ( - "bytes" - "log" - "strconv" - "strings" - - "github.com/codeshelldev/gotl/pkg/ioutils" - "github.com/codeshelldev/gotl/pkg/logger" -) - -type logLevel int - -const logLevelPrefix = "logLevel." - -func (l logLevel) String() string { - return logLevelPrefix + strconv.Itoa(int(l)) -} - -const ( - FATAL logLevel = iota - ERROR - WARN - INFO - DEBUG -) - -func normalizeMessage(msg string) string { - msg = strings.TrimSuffix(msg, "\n") - - msg = strings.ToUpper(msg[:1]) + msg[1:] - - return msg -} - -var writer = &ioutils.InterceptWriter{ - Writer: &bytes.Buffer{}, - Hook: func(bytes []byte) { - msg := string(bytes) - if len(msg) == 0 { - return - } - - 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) - case ERROR: - logger.Error(msg) - case WARN: - logger.Warn(msg) - case INFO: - logger.Info(msg) - case DEBUG: - logger.Debug(msg) - default: - logger.Info(msg) - } - }, -} - -var FatalLog *log.Logger = log.New(writer, FATAL.String(), 0) -var ErrorLog *log.Logger = log.New(writer, ERROR.String(), 0) -var WarnLog *log.Logger = log.New(writer, WARN.String(), 0) - -var InfoLog *log.Logger = log.New(writer, INFO.String(), 0) -var DebugLog *log.Logger = log.New(writer, DEBUG.String(), 0) From af6b50d2b4c9fd506454110e9ec4c4ba98471908 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:21:25 +0100 Subject: [PATCH 10/26] feat: Improved Regex handling and Custom type compilers (#268) --- data/defaults.yml | 22 +- go.mod | 4 +- go.sum | 4 +- .../config/structure/custom/fieldpolicies.go | 124 ++++++++ internals/config/structure/customtypes.go | 74 ++--- internals/config/structure/generics.go | 84 ------ .../config/structure/generics/generics.go | 107 +++++++ .../config/structure/generics/matchrule.go | 271 ++++++++++++++++++ internals/config/structure/structure.go | 38 ++- .../endpoints/{endpoints.go => endpoint.go} | 0 internals/proxy/endpoints/send.go | 2 +- internals/proxy/middlewares/clientip.go | 7 +- internals/proxy/middlewares/endpoints.go | 53 ++-- internals/proxy/middlewares/ipfilter.go | 25 +- internals/proxy/middlewares/policy.go | 104 ++----- internals/proxy/middlewares/ratelimit.go | 4 +- main.go | 6 +- utils/deprecation/deprecation.go | 3 + utils/runtime/tests.go | 78 +++++ 19 files changed, 744 insertions(+), 266 deletions(-) create mode 100644 internals/config/structure/custom/fieldpolicies.go delete mode 100644 internals/config/structure/generics.go create mode 100644 internals/config/structure/generics/generics.go create mode 100644 internals/config/structure/generics/matchrule.go rename internals/proxy/endpoints/{endpoints.go => endpoint.go} (100%) create mode 100644 utils/runtime/tests.go diff --git a/data/defaults.yml b/data/defaults.yml index f39138f7..f872f25a 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -41,10 +41,18 @@ settings: access: endpoints: - - "!/v1/configuration" - - "!/v1/devices" - - "!/v1/register" - - "!/v1/unregister" - - "!/v1/qrcodelink" - - "!/v1/accounts" - - "!/v1/contacts" + blocked: + - pattern: /v1/configuration + matchType: prefix + - pattern: /v1/devices + matchType: prefix + - pattern: /v1/register + matchType: prefix + - pattern: /v1/unregister + matchType: prefix + - pattern: /v1/qrcodelink + matchType: prefix + - pattern: /v1/accounts + matchType: prefix + - pattern: /v1/contacts + matchType: prefix diff --git a/go.mod b/go.mod index fb572f02..1a16be26 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/codeshelldev/secured-signal-api go 1.26.0 require ( - github.com/codeshelldev/gotl/pkg/configutils v0.0.16 + github.com/codeshelldev/gotl/pkg/configutils v0.0.17 github.com/codeshelldev/gotl/pkg/docker v0.0.2 github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 github.com/codeshelldev/gotl/pkg/logger v0.0.18 @@ -17,6 +17,7 @@ require ( ) require ( + github.com/go-viper/mapstructure/v2 v2.5.0 github.com/knadh/koanf/parsers/yaml v1.1.0 golang.org/x/time v0.14.0 ) @@ -24,7 +25,6 @@ require ( require ( github.com/clipperhouse/uax29/v2 v2.7.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 github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/providers/confmap v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9af88879..3e5384cc 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -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/configutils v0.0.17 h1:jm9sO+BTp9BHLRXjpdKCvFVc9YRzvGEoTEnkSYDZUw8= +github.com/codeshelldev/gotl/pkg/configutils v0.0.17/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/jsonutils v0.0.2 h1:ERsjkaWVrsyUZoEunCEeNYDXhuaIvoSetB8e/zI4Tqo= diff --git a/internals/config/structure/custom/fieldpolicies.go b/internals/config/structure/custom/fieldpolicies.go new file mode 100644 index 00000000..3b2f4311 --- /dev/null +++ b/internals/config/structure/custom/fieldpolicies.go @@ -0,0 +1,124 @@ +package custom + +import ( + "errors" + "strings" + + g "github.com/codeshelldev/secured-signal-api/internals/config/structure/generics" + "github.com/go-viper/mapstructure/v2" +) + +type FPolicyAction int + +const ( + FPolicyActionBlock = iota + FPolicyActionAllow +) + +func (m FPolicyAction) ParseEnum(str string) (FPolicyAction, bool) { + str = strings.TrimSpace(str) + str = strings.ToLower(str) + + switch str { + case "block": + return FPolicyActionBlock, true + case "allow": + return FPolicyActionAllow, true + default: + return -1, false + } +} + +func (m FPolicyAction) String() string { + switch m { + case FPolicyActionBlock: + return "block" + case FPolicyActionAllow: + return "allow" + default: + return "" + } +} + +type RFPolicy struct { + Action g.Enum[FPolicyAction] `koanf:"action"` + Value any `koanf:"value"` + MatchType g.Enum[g.MatchType] `koanf:"matchtype"` +} + +type RFieldPolicies map[string][]RFPolicy + +func (r *RFieldPolicies) UnmarshalMapstructure(raw any) error { + rawMap, ok := raw.(map[string]any) + + if !ok { + return errors.New("expected map input") + } + + result := make(RFieldPolicies, len(rawMap)) + + for key, val := range rawMap { + var policies []RFPolicy + + err := mapstructure.Decode(val, &policies) + + if err != nil { + return err + } + + result[key] = policies + } + + *r = result + + return nil +} + + +type FPolicy struct { + Action FPolicyAction + MatchRule g.MatchRule[any] +} + +type FPolicies struct { + Allowed []FPolicy + Blocked []FPolicy +} + +type FieldPolicies map[string]FPolicies + +func (r RFPolicy) Compile() FPolicy { + return FPolicy{ + Action: r.Action.Value, + MatchRule: g.MatchRule[any]{ + MatchType: r.MatchType, + Pattern: r.Value, + }, + } +} + +func (r RFieldPolicies) Compile() FieldPolicies { + out := make(FieldPolicies) + + for field, policies := range r { + var allowed []FPolicy + var blocked []FPolicy + + for _, p := range policies { + fp := p.Compile() + + if fp.Action == FPolicyActionAllow { + allowed = append(allowed, fp) + } else { + blocked = append(blocked, fp) + } + } + + out[field] = FPolicies{ + Allowed: allowed, + Blocked: blocked, + } + } + + return out +} \ No newline at end of file diff --git a/internals/config/structure/customtypes.go b/internals/config/structure/customtypes.go index 98f4e617..7de2454f 100644 --- a/internals/config/structure/customtypes.go +++ b/internals/config/structure/customtypes.go @@ -2,70 +2,44 @@ package structure import ( "errors" - "fmt" - "reflect" - "strings" -) - -type AllowBlockSlice struct{ - Allow []string - Block []string -} -func (splitter *AllowBlockSlice) UnmarshalMapstructure(raw any) error { - slice, ok := raw.([]any) - - if !ok { - fmt.Println(raw) - return errors.New("expected []string, got " + reflect.TypeOf(raw).String()) - } + g "github.com/codeshelldev/secured-signal-api/internals/config/structure/generics" +) - for _, item := range slice { - str, ok := item.(string) +type StringMatchList []g.StringMatchRule - if !ok { - return errors.New("expected string, got " + reflect.TypeOf(item).String()) - } +func (m StringMatchList) TestRules() error { + var errs []error - str, block := strings.CutPrefix(str, "!") + for _, rule := range m { + err := rule.Test() - if block { - splitter.Block = append(splitter.Block, str) - } else { - splitter.Allow = append(splitter.Allow, str) + if err != nil { + errs = append(errs, err) } } - return nil + return errors.Join(errs...) } -type FPolicies struct{ - Allow []FieldPolicy - Block []FieldPolicy -} +func (m StringMatchList) FindMatchRule(str string) (g.StringMatchRule, error) { + for _, rule := range m { + ok, err := rule.Match(str) -func (splitter *FPolicies) UnmarshalMapstructure(raw any) error { - slice, ok := raw.([]any) - - if !ok { - fmt.Println(raw) - return errors.New("expected []FieldPolicy, got " + reflect.TypeOf(raw).String()) - } + if ok { + return rule, err + } + } - for _, item := range slice { - policy, ok := item.(FieldPolicy) + return g.StringMatchRule{}, nil +} - if !ok { - return errors.New("expected string, got " + reflect.TypeOf(item).String()) - } +func (m StringMatchList) Match(str string) (bool, error) { + rule, err := m.FindMatchRule(str) - switch strings.ToLower(policy.Action) { - case "block": - splitter.Block = append(splitter.Block, policy) - case "allow": - splitter.Allow = append(splitter.Allow, policy) - } + if err != nil { + return false, err } - return nil + return rule.Pattern != "", nil } \ No newline at end of file diff --git a/internals/config/structure/generics.go b/internals/config/structure/generics.go deleted file mode 100644 index 2204a82d..00000000 --- a/internals/config/structure/generics.go +++ /dev/null @@ -1,84 +0,0 @@ -package structure - -import ( - "errors" - "net" - "net/url" - "reflect" - "time" - - "github.com/codeshelldev/secured-signal-api/utils/netutils" -) - -// TimeDuration is a wrapper struct used to parse string durations using time.ParseDuration() -type TimeDuration struct { - Duration time.Duration -} - -func (timeDuration *TimeDuration) UnmarshalMapstructure(raw any) error { - str, ok := raw.(string) - - if !ok { - return errors.New("expected string, got " + reflect.TypeOf(raw).String()) - } - - d, err := time.ParseDuration(str) - - if err != nil { - return err - } - - timeDuration.Duration = d - - return nil -} - -// IPOrNet is a wrapper struct used to parse 1.2.3.4 and 1.2.3.4/24 into net.IPNet (IPs are converted into A.B.C.D/32) -type IPOrNet struct { - IPNet *net.IPNet -} - -func (ipNet *IPOrNet) UnmarshalMapstructure(raw any) error { - str, ok := raw.(string) - - if !ok { - return errors.New("expected string, got " + reflect.TypeOf(raw).String()) - } - - ip, err := netutils.ParseIPorNet(str) - - if err != nil { - return err - } - - ipNet.IPNet = ip - - return nil -} - -// URL is a wrapper struct used to parse string URLs with url.Parse() -type URL struct { - URL *url.URL -} - -func (Url *URL) UnmarshalMapstructure(raw any) error { - str, ok := raw.(string) - - if !ok { - return errors.New("expected string, got " + reflect.TypeOf(raw).String()) - } - - u, err := url.Parse(str) - - if err != nil { - return err - } - - Url.URL = u - - return nil -} - -func (Url URL) String() string { - return Url.URL.String() -} \ No newline at end of file diff --git a/internals/config/structure/generics/generics.go b/internals/config/structure/generics/generics.go new file mode 100644 index 00000000..88703212 --- /dev/null +++ b/internals/config/structure/generics/generics.go @@ -0,0 +1,107 @@ +package generics + +import ( + "errors" + "net" + "net/url" + "reflect" + "time" + + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/secured-signal-api/utils/netutils" +) + +// TimeDuration is a wrapper for parsing string durations using time.ParseDuration() +type TimeDuration time.Duration + +func (timeDuration *TimeDuration) UnmarshalMapstructure(raw any) error { + str, ok := raw.(string) + + if !ok { + return errors.New("expected string, got " + reflect.TypeOf(raw).String()) + } + + d, err := time.ParseDuration(str) + + if err != nil { + logger.Fatal("Invalid duration ", str, ": ", err.Error()) + return err + } + + *timeDuration = TimeDuration(d) + + return nil +} + +// IPOrNet is a wrapper for parsing 1.2.3.4 and 1.2.3.4/24 into net.IPNet (IPs are converted into A.B.C.D/32) +type IPOrNet net.IPNet + +func (ipNet *IPOrNet) UnmarshalMapstructure(raw any) error { + str, ok := raw.(string) + + if !ok { + return errors.New("expected string, got " + reflect.TypeOf(raw).String()) + } + + ip, err := netutils.ParseIPorNet(str) + + if err != nil { + logger.Fatal("Invalid IP ", str, ": ", err.Error()) + return err + } + + *ipNet = IPOrNet(*ip) + + return nil +} + +// URL is a wrapper for parsing string URLs with url.Parse() +type URL url.URL + +func (Url *URL) UnmarshalMapstructure(raw any) error { + str, ok := raw.(string) + + if !ok { + return errors.New("expected string, got " + reflect.TypeOf(raw).String()) + } + + u, err := url.Parse(str) + + if err != nil { + logger.Fatal("Invalid URL ", str, ": ", err.Error()) + return err + } + + *Url = URL(*u) + + return nil +} + +func (Url URL) String() string { + return Url.String() +} + +// Enum is a wrapper for enum types +type Enum[T interface{ ParseEnum(string) (T, bool) }] struct { + Value T +} + +func (e *Enum[T]) UnmarshalMapstructure(raw any) error { + str, ok := raw.(string) + + if !ok { + return errors.New("expected string, got " + reflect.TypeOf(raw).String()) + } + + var zero T + value, found := zero.ParseEnum(str) + + if !found { + logger.Fatal("Invalid enum: ", str) + return errors.New("unsupported enum value: " + str) + } + + e.Value = value + + return nil +} \ No newline at end of file diff --git a/internals/config/structure/generics/matchrule.go b/internals/config/structure/generics/matchrule.go new file mode 100644 index 00000000..c284bd2a --- /dev/null +++ b/internals/config/structure/generics/matchrule.go @@ -0,0 +1,271 @@ +package generics + +import ( + "errors" + "path" + "reflect" + "regexp" + "strings" +) + +type MatchRule[T any] struct { + Pattern T `koanf:"value"` + MatchType Enum[MatchType] `koanf:"matchtype"` +} + +type StringMatchRule struct { + Pattern string `koanf:"pattern"` + MatchType Enum[MatchType] `koanf:"matchtype"` +} + +type MatchType int + +const ( + MatchExact MatchType = iota + MatchEquals + MatchRegex + MatchGlob + MatchContains + MatchIncludes + MatchHas + MatchPrefix + MatchSuffix +) + +func (m MatchType) ParseEnum(str string) (MatchType, bool) { + str = strings.TrimSpace(str) + str = strings.ToLower(str) + + switch str { + case "exact": + return MatchExact, true + case "equals": + return MatchEquals, true + case "regex": + return MatchRegex, true + case "glob": + return MatchGlob, true + case "contains": + return MatchContains, true + case "includes": + return MatchIncludes, true + case "has": + return MatchHas, true + case "prefix": + return MatchPrefix, true + case "suffix": + return MatchSuffix, true + default: + return -1, false + } +} + +func (m MatchType) String() string { + switch m { + case MatchExact: + return "exact" + case MatchEquals: + return "equals" + case MatchRegex: + return "regex" + case MatchGlob: + return "glob" + case MatchContains: + return "contains" + case MatchIncludes: + return "includes" + case MatchHas: + return "has" + case MatchPrefix: + return "prefix" + case MatchSuffix: + return "suffix" + default: + return "" + } +} + +func (r StringMatchRule) Match(str string) (bool, error) { + rule := MatchRule[string]{ + Pattern: r.Pattern, + MatchType: r.MatchType, + } + + return rule.Match(str) +} + +func (r StringMatchRule) Test() error { + rule := MatchRule[string]{ + Pattern: r.Pattern, + MatchType: r.MatchType, + } + + return rule.Test() +} + +func (r MatchRule[T]) Test() error { + p := any(r.Pattern) + + switch r.MatchType.Value { + case MatchEquals: + _, ok := p.(string) + + if !ok { + return errors.New("pattern must be string to be able to use match type " + r.MatchType.Value.String()) + } + case MatchContains: + _, ok := p.(string) + + if !ok { + return errors.New("pattern must be string to be able to use match type " + r.MatchType.Value.String()) + } + case MatchPrefix: + _, ok := p.(string) + + if !ok { + return errors.New("pattern must be string to be able to use match type " + r.MatchType.Value.String()) + } + case MatchSuffix: + _, ok := p.(string) + + if !ok { + return errors.New("pattern must be string to be able to use match type " + r.MatchType.Value.String()) + } + case MatchRegex: + pStr, ok := p.(string) + + if !ok { + return errors.New("pattern must be string to be able to use match type " + r.MatchType.Value.String()) + } + + _, err := regexp.Compile(pStr) + + if err != nil { + return errors.New("could not compile " + pStr + " as regex: " + err.Error()) + } + case MatchGlob: + pStr, ok := p.(string) + + if !ok { + return errors.New("pattern must be string to be able to use match type " + r.MatchType.Value.String()) + } + + _, err := path.Match(pStr, " ") + + if err != nil { + return errors.New("could not compile " + pStr + " as glob-style pattern: " + err.Error()) + } + } + + return nil +} + +func (r MatchRule[T]) Match(value T) (bool, error) { + v := any(value) + p := any(r.Pattern) + + switch r.MatchType.Value { + case MatchExact: + return reflect.DeepEqual(v, p), nil + case MatchEquals: + vStr, ok1 := v.(string) + pStr, ok2 := p.(string) + + if !ok1 || !ok2 { + return false, errors.New("match type equals is only allowed for strings") + } + + return strings.EqualFold(vStr, pStr), nil + case MatchContains: + vStr, ok1 := v.(string) + pStr, ok2 := p.(string) + + if !ok1 || !ok2 { + return false, errors.New("match type contains is only allowed for strings") + } + + return strings.Contains(strings.ToLower(vStr), strings.ToLower(pStr)), nil + case MatchIncludes: + vVal := reflect.ValueOf(v) + + if vVal.Kind() == reflect.Slice || vVal.Kind() == reflect.Array { + pVal := reflect.ValueOf(p) + + for i := 0; i < vVal.Len(); i++ { + if reflect.DeepEqual(vVal.Index(i).Interface(), pVal.Interface()) { + return true, nil + } + } + + return false, nil + } + + return false, errors.New("match type includes is not supported for type " + vVal.Kind().String()) + case MatchHas: + vVal := reflect.ValueOf(v) + + if vVal.Kind() == reflect.Map { + pVal := reflect.ValueOf(p) + + for _, key := range vVal.MapKeys() { + if reflect.DeepEqual(key.Interface(), pVal.Interface()) { + return true, nil + } + } + + return false, nil + } + + return false, errors.New("match type has is only supported for maps") + case MatchPrefix: + vStr, ok1 := v.(string) + pStr, ok2 := p.(string) + + if !ok1 || !ok2 { + return false, errors.New("match type prefix is only supported for strings") + } + + return strings.HasPrefix(strings.ToLower(vStr), strings.ToLower(pStr)), nil + case MatchSuffix: + vStr, ok1 := v.(string) + pStr, ok2 := p.(string) + + if !ok1 || !ok2 { + return false, errors.New("match type suffix is only supported for strings") + } + + return strings.HasSuffix(strings.ToLower(vStr), strings.ToLower(pStr)), nil + case MatchRegex: + vStr, ok1 := v.(string) + pStr, ok2 := p.(string) + + if !ok1 || !ok2 { + return false, errors.New("match type regex is only supported for strings") + } + + re, err := regexp.Compile(pStr) + + if err != nil { + return false, errors.New("error during regex compilation of " + pStr + ": " + err.Error()) + } + + return re.MatchString(vStr), nil + case MatchGlob: + vStr, ok1 := v.(string) + pStr, ok2 := p.(string) + + if !ok1 || !ok2 { + return false, errors.New("match type glob is only supported for strings") + } + + match, err := path.Match(pStr, vStr) + + if err != nil { + return false, errors.New("error during glob-style pattern compilation of " + pStr + ": " + err.Error()) + } + + return match, nil + default: + return false, errors.New("unsupported match type") + } +} \ No newline at end of file diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 27156d71..f0a2474d 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -2,6 +2,8 @@ package structure import ( t "github.com/codeshelldev/gotl/pkg/configutils/types" + c "github.com/codeshelldev/secured-signal-api/internals/config/structure/custom" + g "github.com/codeshelldev/secured-signal-api/internals/config/structure/generics" ) type ENV struct { @@ -43,7 +45,7 @@ type SERVICE struct { } type API struct { - URL URL `koanf:"url" env>aliases:".apiurl"` + URL *g.URL `koanf:"url" env>aliases:".apiurl"` TOKENS []string `koanf:"tokens" env>aliases:".apitokens"` AUTH AUTH `koanf:"auth"` } @@ -65,12 +67,14 @@ type SETTINGS struct { type MESSAGE struct { VARIABLES t.Opt[map[string]any] `koanf:"variables" childtransform:"upper"` - FIELD_MAPPINGS t.Opt[map[string][]FMapping]`koanf:"fieldmappings" childtransform:"default"` + FIELD_MAPPINGS t.Opt[FieldMappings] `koanf:"fieldmappings" childtransform:"default"` TEMPLATING t.Opt[Templating] `koanf:"templating" aliases:"template" onuse:"template>>broken" breaking:"{b,fg=red}\x60{s}settings.message.template{/}\x60{/} has been moved\n Use {b,fg=green}\x60settings.message.templating.messageTemplate\x60{/} instead"` SCHEDULING t.Opt[Scheduling] `koanf:"scheduling"` INJECTING t.Opt[Injecting] `koanf:"injecting"` } +type FieldMappings = map[string][]FMapping + type Injecting struct { URLToBody t.Opt[URLToBody] `koanf:"urltobody"` } @@ -89,7 +93,7 @@ type Templating struct { type Scheduling struct { Enabled bool `koanf:"enabled"` - MaxHorizon t.Opt[TimeDuration] `koanf:"maxhorizon"` + MaxHorizon t.Opt[g.TimeDuration] `koanf:"maxhorizon"` } type FMapping struct { @@ -98,20 +102,32 @@ type FMapping struct { } type ACCESS struct { - ENDPOINTS t.Opt[AllowBlockSlice] `koanf:"endpoints"` - FIELD_POLICIES t.Opt[map[string]FPolicies] `koanf:"fieldpolicies" childtransform:"default"` + ENDPOINTS t.Opt[Endpoints] `koanf:"endpoints"` + FIELD_POLICIES t.Opt[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"` + IP_FILTER t.Opt[IPFilter] `koanf:"ipfilter"` + TRUSTED_IPS t.Opt[[]g.IPOrNet] `koanf:"trustedips"` + TRUSTED_PROXIES t.Opt[[]g.IPOrNet] `koanf:"trustedproxies"` +} + +type FieldPolicies = *t.Comp[c.RFieldPolicies, c.FieldPolicies] + +type Endpoints struct { + Allowed []g.StringMatchRule `koanf:"allowed"` + Blocked []g.StringMatchRule `koanf:"blocked"` +} + +type IPFilter struct { + Allowed []g.IPOrNet `koanf:"allowed"` + Blocked []g.IPOrNet `koanf:"blocked"` } -type FieldPolicy struct { - Value any `koanf:"value"` +type FPolicy struct { + Match g.MatchRule[any] `koanf:"match"` Action string `koanf:"action"` } type RateLimiting struct { Limit int `koanf:"limit"` - Period TimeDuration `koanf:"period"` + Period g.TimeDuration `koanf:"period"` } \ No newline at end of file diff --git a/internals/proxy/endpoints/endpoints.go b/internals/proxy/endpoints/endpoint.go similarity index 100% rename from internals/proxy/endpoints/endpoints.go rename to internals/proxy/endpoints/endpoint.go diff --git a/internals/proxy/endpoints/send.go b/internals/proxy/endpoints/send.go index e92078b9..e3d6a76b 100644 --- a/internals/proxy/endpoints/send.go +++ b/internals/proxy/endpoints/send.go @@ -95,7 +95,7 @@ func sendHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { } if scheduling.MaxHorizon.Set { - if tm.After(time.Now().Add(scheduling.MaxHorizon.Value.Duration)) { + if tm.After(time.Now().Add(time.Duration(*scheduling.MaxHorizon.Value))) { logger.Warn("Request scheduled too far in the future: ", time.Until(tm).String()) WriteError(w, http.StatusBadRequest, "invalid timestamp: " + "timestamp to far in the future") return diff --git a/internals/proxy/middlewares/clientip.go b/internals/proxy/middlewares/clientip.go index 41e02fef..cc8b047f 100644 --- a/internals/proxy/middlewares/clientip.go +++ b/internals/proxy/middlewares/clientip.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/codeshelldev/secured-signal-api/internals/config" - "github.com/codeshelldev/secured-signal-api/internals/config/structure" + "github.com/codeshelldev/secured-signal-api/internals/config/structure/generics" . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/netutils" ) @@ -38,11 +38,12 @@ func clientIPHandler(next http.Handler) http.Handler { }) } -func parseIPsAndNets(ipNets []structure.IPOrNet) []*net.IPNet { +func parseIPsAndNets(ipNets []generics.IPOrNet) []*net.IPNet { out := []*net.IPNet{} for _, ipNet := range ipNets { - out = append(out, ipNet.IPNet) + n := net.IPNet(ipNet) + out = append(out, &n) } return out diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index fef1a522..bd4ddf0d 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -2,8 +2,6 @@ package middlewares import ( "net/http" - "regexp" - "slices" "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" @@ -25,7 +23,15 @@ func endpointsHandler(next http.Handler) http.Handler { reqPath := req.URL.Path - if isBlocked(reqPath, matchesPattern, endpoints) { + blocked, err := isEndpointBlocked(reqPath, endpoints.Allowed, endpoints.Blocked) + + if err != nil { + logger.Error("Error during blocked endpoint check: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if blocked { logger.Warn("Client tried to access blocked endpoint: ", reqPath) http.Error(w, "Forbidden", http.StatusForbidden) return @@ -35,48 +41,47 @@ func endpointsHandler(next http.Handler) http.Handler { }) } -func matchesPattern(endpoint, pattern string) bool { - re, err := regexp.Compile(pattern) +func isEndpointBlocked(endpoint string, allowed structure.StringMatchList, blocked structure.StringMatchList) (bool, error) { + isExplicitlyAllowed, err := allowed.Match(endpoint) + + if err != nil { + return true, err + } + + isExplicitlyBlocked, err := blocked.Match(endpoint) if err != nil { - return endpoint == pattern + return true, err } - return re.MatchString(endpoint) + return checkBlockLogic(isExplicitlyAllowed, isExplicitlyBlocked, allowed, blocked), nil } -func isBlocked(test string, matchFunc func(test, try string) bool, allowBlockSlice structure.AllowBlockSlice) bool { - if len(allowBlockSlice.Allow) == 0 && len(allowBlockSlice.Block) == 0 { +func checkBlockLogic[T any](explicitlyAllowed, explicitlyBlocked bool, allowed, blocked []T) bool { + if len(allowed) == 0 && len(blocked) == 0 { // default: allow all return false } - isExplicitlyAllowed := slices.ContainsFunc(allowBlockSlice.Allow, func(try string) bool { - return matchFunc(test, try) - }) - isExplicitlyBlocked := slices.ContainsFunc(allowBlockSlice.Block, func(try string) bool { - return matchFunc(test, try) - }) - // explicit allow > block - if isExplicitlyAllowed { + if explicitlyAllowed { return false } - - if isExplicitlyBlocked { + + if explicitlyBlocked { return true } - // allows -> default deny - if len(allowBlockSlice.Allow) > 0 { + // allows exist -> default deny + if len(allowed) > 0 { return true } - + // only blocks -> default allow - if len(allowBlockSlice.Block) > 0 { + if len(blocked) > 0 { return false } // safety net -> block return true -} +} \ No newline at end of file diff --git a/internals/proxy/middlewares/ipfilter.go b/internals/proxy/middlewares/ipfilter.go index 90605fd8..479383c3 100644 --- a/internals/proxy/middlewares/ipfilter.go +++ b/internals/proxy/middlewares/ipfilter.go @@ -3,10 +3,11 @@ package middlewares import ( "net" "net/http" + "slices" "github.com/codeshelldev/secured-signal-api/internals/config" + "github.com/codeshelldev/secured-signal-api/internals/config/structure/generics" . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" - "github.com/codeshelldev/secured-signal-api/utils/netutils" ) var IPFilter Middleware = Middleware{ @@ -24,11 +25,7 @@ func ipFilterHandler(next http.Handler) http.Handler { ip := GetContext[net.IP](req, ClientIPKey) - if isBlocked("", func(_, try string) bool { - tryIP, err := netutils.ParseIPorNet(try) - - return tryIP.Contains(ip) && err == nil - }, ipFilter) { + if isIPBlocked(ip, ipFilter.Allowed, ipFilter.Blocked) { logger.Warn("Client IP is blocked by filter: ", ip.String()) http.Error(w, "Forbidden", http.StatusForbidden) return @@ -36,4 +33,20 @@ func ipFilterHandler(next http.Handler) http.Handler { next.ServeHTTP(w, req) }) +} + +func isIPBlocked(ip net.IP, allowed []generics.IPOrNet, blocked []generics.IPOrNet) bool { + isExplicitlyAllowed := slices.ContainsFunc(allowed, func(try generics.IPOrNet) bool { + tryIP := net.IPNet(try) + + return tryIP.Contains(ip) + }) + + isExplicitlyBlocked := slices.ContainsFunc(blocked, func(try generics.IPOrNet) bool { + tryIP := net.IPNet(try) + + return tryIP.Contains(ip) + }) + + return checkBlockLogic(isExplicitlyAllowed, isExplicitlyBlocked, allowed, blocked) } \ No newline at end of file diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 96d8509e..67a41558 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -3,12 +3,11 @@ package middlewares import ( "errors" "net/http" - "reflect" - "regexp" 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/config/structure/custom" + c "github.com/codeshelldev/secured-signal-api/internals/config/structure/custom" . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" "github.com/codeshelldev/secured-signal-api/utils/requestkeys" ) @@ -38,7 +37,13 @@ func policyHandler(next http.Handler) http.Handler { headers := request.GetReqHeaders(req) - shouldBlock, field := isBlockedByPolicy(body.Data, headers, policies) + shouldBlock, field, err := isBlockedByPolicy(body.Data, headers, policies.Compile()) + + if err != nil { + logger.Error("Could not perform policy checks: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } if shouldBlock { logger.Warn("Client tried to use blocked field: ", field) @@ -62,70 +67,34 @@ func getField(key string, body map[string]any, headers map[string][]string) (any return nil, errors.New("field not found") } -func doPoliciesApply(key string, body map[string]any, headers map[string][]string, policies []structure.FieldPolicy) (bool, string) { +func doPoliciesApply(key string, body map[string]any, headers map[string][]string, policies []custom.FPolicy) (bool, string, error) { value, err := getField(key, body, headers) if err != nil { - return false, "" + return false, "", nil } for _, policy := range policies { - switch asserted := value.(type) { - case string: - policyValue, ok := policy.Value.(string) - - re, err := regexp.Compile(policyValue) - - if err == nil { - if re.MatchString(asserted) { - return true, key - } - continue - } - - if ok && asserted == policyValue { - return true, key - } - case int: - policyValue, ok := policy.Value.(int) - - if ok && asserted == policyValue { - return true, key - } - case float64: - var policyValue float64 - - // needed for json - switch assertedValue := policy.Value.(type) { - case int: - policyValue = float64(assertedValue) - case float64: - policyValue = assertedValue - default: - continue - } - - if asserted == policyValue { - return true, key - } - default: - if reflect.DeepEqual(value, policy.Value) { - return true, key - } + ok, err := policy.MatchRule.Match(value) + + if ok { + return true, key, err } } - return false, "" + return false, "", nil } -func isBlockedByPolicy(body map[string]any, headers map[string][]string, policies map[string]structure.FPolicies) (bool, string) { - if len(policies) == 0 || policies == nil { +func isBlockedByPolicy(body map[string]any, headers map[string][]string, p c.FieldPolicies) (bool, string, error) { + policies := map[string]custom.FPolicies(p) + + if len(policies) == 0 { // default: allow all - return false, "" + return false, "", nil } for field, policy := range policies { - if len(policy.Allow) == 0 || len(policy.Block) == 0 { + if len(policy.Allowed) == 0 && len(policy.Blocked) == 0 { continue } @@ -135,32 +104,21 @@ func isBlockedByPolicy(body map[string]any, headers map[string][]string, policie continue } - isExplicitlyAllowed, cause := doPoliciesApply(field, body, headers, policy.Allow) - isExplicitlyBlocked, cause := doPoliciesApply(field, body, headers, policy.Block) + isExplicitlyAllowed, cause, err := doPoliciesApply(field, body, headers, policy.Allowed) - // explicit allow > block - if isExplicitlyAllowed { - return false, cause - } - - if isExplicitlyBlocked { - return true, cause + if err != nil { + return true, "", err } - // allow rules -> default deny - if len(policy.Allow) > 0 { - return true, cause - } - - // only block rules -> default allow - if len(policy.Block) > 0 { - return false, cause + isExplicitlyBlocked, cause, err := doPoliciesApply(field, body, headers, policy.Blocked) + + if err != nil { + return true, "", err } - // safety net -> block - return true, "safety net" + return checkBlockLogic(isExplicitlyAllowed, isExplicitlyBlocked, policy.Allowed, policy.Blocked), cause, nil } // default: allow all - return false, "" + return false, "", nil } diff --git a/internals/proxy/middlewares/ratelimit.go b/internals/proxy/middlewares/ratelimit.go index 2c4054b1..8ef1cc97 100644 --- a/internals/proxy/middlewares/ratelimit.go +++ b/internals/proxy/middlewares/ratelimit.go @@ -47,13 +47,13 @@ func ratelimitHandler(next http.Handler) http.Handler { rateLimiting := conf.SETTINGS.ACCESS.RATE_LIMITING.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.RATE_LIMITING) - if rateLimiting.Period.Duration != 0 && rateLimiting.Limit != 0 { + if rateLimiting.Period != 0 && rateLimiting.Limit != 0 { token := GetToken(req) tokenLimiter, exists := tokenLimiters[token] if !exists { - tokenLimiter = NewTokenLimiter(rateLimiting.Limit, time.Duration(rateLimiting.Period.Duration)) + tokenLimiter = NewTokenLimiter(rateLimiting.Limit, time.Duration(rateLimiting.Period)) tokenLimiters[token] = tokenLimiter } diff --git a/main.go b/main.go index 30307d7a..c79e3258 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "net/url" "os" "slices" "strings" @@ -13,6 +14,7 @@ import ( "github.com/codeshelldev/secured-signal-api/internals/scheduler" docker "github.com/codeshelldev/secured-signal-api/utils/docker" "github.com/codeshelldev/secured-signal-api/utils/logging" + runtime "github.com/codeshelldev/secured-signal-api/utils/runtime" ) var proxy reverseProxy.Proxy @@ -24,6 +26,8 @@ func main() { config.Load() + runtime.Test() + if config.DEFAULT.SERVICE.LOG_LEVEL != logger.Level() { logging.Init(config.DEFAULT.SERVICE.LOG_LEVEL) } @@ -43,7 +47,7 @@ func main() { scheduler.Start() - proxy = reverseProxy.Create(config.DEFAULT.API.URL.URL) + proxy = reverseProxy.Create((*url.URL)(config.DEFAULT.API.URL)) handler := proxy.Init() diff --git a/utils/deprecation/deprecation.go b/utils/deprecation/deprecation.go index 46c086ef..d39dd68a 100644 --- a/utils/deprecation/deprecation.go +++ b/utils/deprecation/deprecation.go @@ -2,6 +2,7 @@ package deprecation import ( "fmt" + "os" "github.com/codeshelldev/gotl/pkg/pretty" ) @@ -157,4 +158,6 @@ func Error(id string, msg DeprecationMessage) { }, msg, ) + + os.Exit(1) } \ No newline at end of file diff --git a/utils/runtime/tests.go b/utils/runtime/tests.go new file mode 100644 index 00000000..bfd78354 --- /dev/null +++ b/utils/runtime/tests.go @@ -0,0 +1,78 @@ +package runtime + +import ( + "errors" + + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/secured-signal-api/internals/config" + "github.com/codeshelldev/secured-signal-api/internals/config/structure" + "github.com/codeshelldev/secured-signal-api/internals/config/structure/custom" +) + +func Test() { + for _, conf := range config.ENV.CONFIGS { + err, obj := TestEndpointRules(*conf) + + if err != nil { + logger.Fatal("Error in endpoint rules: ", err.Error(), obj) + } + + err, obj = TestFieldPolicyRules(*conf) + + if err != nil { + logger.Fatal("Error in field policy rules: ", err.Error(), obj) + } + } +} + +func TestEndpointRules(conf structure.CONFIG) (error, any) { + if !conf.SETTINGS.ACCESS.ENDPOINTS.Set { + return nil, nil + } + + endpoints := conf.SETTINGS.ACCESS.ENDPOINTS.Value + + err := structure.StringMatchList(endpoints.Allowed).TestRules() + + if err != nil { + return err, endpoints.Allowed + } + + err = structure.StringMatchList(endpoints.Blocked).TestRules() + + if err != nil { + return err, endpoints.Blocked + } + + return nil, nil +} + +func TestFieldPolicyRules(conf structure.CONFIG) (error, any) { + if !conf.SETTINGS.ACCESS.FIELD_POLICIES.Set { + return nil, nil + } + + p := *conf.SETTINGS.ACCESS.FIELD_POLICIES.Value + + policies := map[string]custom.FPolicies(p.Compile()) + + for field, policy := range policies { + for _, item := range policy.Allowed { + err := item.MatchRule.Test() + + if err != nil { + return errors.New(field + ": " + err.Error()), item + } + } + + for _, item := range policy.Blocked { + err := item.MatchRule.Test() + + if err != nil { + return errors.New(field + ": " + err.Error()), item + } + } + } + + return nil, nil +} \ No newline at end of file From e6fe072b30888c31d9723f2aade139c332151d70 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:18:35 +0100 Subject: [PATCH 11/26] feat: Improved Placeholders in Configs (#278) --- go.mod | 6 +- go.sum | 12 ++-- internals/config/loader.go | 29 +++++++-- internals/config/tokens.go | 60 +++++++------------ internals/proxy/common/template.go | 96 ++++++++++++++++++------------ utils/runtime/tests.go | 20 +++++-- 6 files changed, 128 insertions(+), 95 deletions(-) diff --git a/go.mod b/go.mod index 1a16be26..f3c944cd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/codeshelldev/secured-signal-api go 1.26.0 require ( - github.com/codeshelldev/gotl/pkg/configutils v0.0.17 + github.com/codeshelldev/gotl/pkg/configutils v0.0.22 github.com/codeshelldev/gotl/pkg/docker v0.0.2 github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 github.com/codeshelldev/gotl/pkg/logger v0.0.18 @@ -13,7 +13,7 @@ require ( github.com/codeshelldev/gotl/pkg/scheduler v0.0.9 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 + github.com/codeshelldev/gotl/pkg/templating v0.0.16 ) require ( @@ -30,7 +30,7 @@ require ( github.com/knadh/koanf/providers/confmap v1.0.0 // indirect github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect github.com/knadh/koanf/providers/file v1.2.1 // indirect - github.com/knadh/koanf/v2 v2.3.2 // indirect + github.com/knadh/koanf/v2 v2.3.3 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.34 github.com/mitchellh/copystructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index 3e5384cc..ae1f0156 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codeshelldev/gotl/pkg/configutils v0.0.17 h1:jm9sO+BTp9BHLRXjpdKCvFVc9YRzvGEoTEnkSYDZUw8= -github.com/codeshelldev/gotl/pkg/configutils v0.0.17/go.mod h1:Tiu27XQ7D08fcwCHp5tZsDIQwIo6q626+l77k2dse7k= +github.com/codeshelldev/gotl/pkg/configutils v0.0.22 h1:6M10jRjTxF+5mVrt+OlXG75O9NTCjJ2dL0OY6U/RPqk= +github.com/codeshelldev/gotl/pkg/configutils v0.0.22/go.mod h1:2o+D0+pbb7xaNGeESpL0/eWZLmDk6o8yhbaoTK06gMs= 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/jsonutils v0.0.2 h1:ERsjkaWVrsyUZoEunCEeNYDXhuaIvoSetB8e/zI4Tqo= @@ -20,8 +20,8 @@ github.com/codeshelldev/gotl/pkg/server/http v0.0.3 h1:3232uPB2CljzUJadyrME7p0Da 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/codeshelldev/gotl/pkg/templating v0.0.16 h1:0dl/NEApCtlm4kyEscQPknx4DwUtl1bsgl3Iyv6jPkM= +github.com/codeshelldev/gotl/pkg/templating v0.0.16/go.mod h1:MHM4ouEsLNKXRYO+fS9qqpS1SFlL4Z6Q/0kxtS+auLk= 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= @@ -40,8 +40,8 @@ github.com/knadh/koanf/providers/env/v2 v2.0.0 h1:Ad5H3eun722u+FvchiIcEIJZsZ2M6o github.com/knadh/koanf/providers/env/v2 v2.0.0/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc= github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= -github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.3 h1:jLJC8XCRfLC7n4F+ZKKdBsbq1bfXTpuFhf4L7t94D94= +github.com/knadh/koanf/v2 v2.3.3/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/internals/config/loader.go b/internals/config/loader.go index 0b2881fd..1991df1f 100644 --- a/internals/config/loader.go +++ b/internals/config/loader.go @@ -58,8 +58,6 @@ func Load() { mainConf.MergeLayers(defaultsConf.Layer, userConf.Layer) - mainConf.TemplateConfig() - NormalizeTokens() InitConfig() @@ -143,6 +141,9 @@ func InitReload() { func InitConfig() { var config structure.CONFIG + templateConfigWithVariables(mainConf) + + // after templating reunmarshal mainConf.Unmarshal("", &config) config.TYPE = structure.MAIN @@ -168,8 +169,8 @@ func LoadConfig() { if err != nil { _, fsErr := os.Stat(ENV.CONFIG_PATH) - // Config File doesn't exist - // => User is using Environment + // config file doesn't exist + // => user is using environment if errors.Is(fsErr, fs.ErrNotExist) { return } @@ -185,3 +186,23 @@ func normalizeEnv(key string, value string) (string, any) { return key, stringutils.ToType(value) } + +func templateConfigWithVariables(config *configutils.Config) { + var configData structure.CONFIG + + err := config.Unmarshal("", &configData) + + if err != nil { + return + } + + var variables map[string]any + + if configData.SETTINGS.MESSAGE.VARIABLES.Set { + variables = *configData.SETTINGS.MESSAGE.VARIABLES.Value + } else if DEFAULT != nil { + variables = DEFAULT.SETTINGS.MESSAGE.VARIABLES.ValueOrFallback(map[string]any{}) + } + + config.TemplateConfig(variables) +} \ No newline at end of file diff --git a/internals/config/tokens.go b/internals/config/tokens.go index 9fdcab19..4053561e 100644 --- a/internals/config/tokens.go +++ b/internals/config/tokens.go @@ -2,7 +2,6 @@ package config import ( "path/filepath" - "reflect" "strconv" "strings" @@ -12,22 +11,22 @@ import ( "github.com/knadh/koanf/parsers/yaml" ) +const tokenConfigsPath = "tokenconfigs" + func LoadTokens() { logger.Debug("Loading Configs in ", ENV.TOKENS_DIR) - err := tokenConf.LoadDir("tokenconfigs", ENV.TOKENS_DIR, ".yml", yaml.Parser(), setTokenConfigName) + err := tokenConf.LoadDir(tokenConfigsPath, ENV.TOKENS_DIR, ".yml", yaml.Parser(), setTokenConfigName) if err != nil { logger.Error("Could not Load Configs in ", ENV.TOKENS_DIR, ": ", err.Error()) } - - tokenConf.TemplateConfig() } func NormalizeTokens() { - data := []map[string]any{} + data := []any{} - for _, config := range tokenConf.Layer.Slices("tokenconfigs") { + for _, config := range tokenConf.Layer.Slices(tokenConfigsPath) { tmpConf := configutils.New() tmpConf.Load(config.Raw(), "") @@ -37,7 +36,7 @@ func NormalizeTokens() { } // Merge token configs together into new temporary config - tokenConf.Load(data, "tokenconfigs") + tokenConf.Layer.Set(tokenConfigsPath, data) } func InitTokens() { @@ -47,13 +46,9 @@ func InitTokens() { ENV.CONFIGS[token] = DEFAULT } - var tokenConfigs []structure.CONFIG - - tokenConf.Unmarshal("tokenconfigs", &tokenConfigs) + configs := parseTokenConfigs(tokenConf) - config := parseTokenConfigs(tokenConfigs) - - for token, config := range config { + for token, config := range configs { apiTokens = append(apiTokens, token) config.TYPE = structure.TOKEN @@ -80,13 +75,23 @@ func InitTokens() { ENV.TOKENS = apiTokens } -func parseTokenConfigs(configArray []structure.CONFIG) map[string]structure.CONFIG { +func parseTokenConfigs(config *configutils.Config) map[string]structure.CONFIG { configs := map[string]structure.CONFIG{} - for _, config := range configArray { - tokens := parseAuthTokens(config) + for _, c := range config.Layer.Slices(tokenConfigsPath) { + tmpConf := configutils.New() + tmpConf.Load(c.Raw(), "") + + templateConfigWithVariables(tmpConf) + + var configData structure.CONFIG + + tmpConf.Unmarshal("", &configData) + + tokens := parseAuthTokens(configData) + for _, token := range tokens { - configs[token] = config + configs[token] = configData } } @@ -103,31 +108,12 @@ func parseAuthTokens(config structure.CONFIG) []string { return tokens } -func getSchemeTagByPointer(config any, tag string, fieldPointer any) string { - v := reflect.ValueOf(config) - if v.Kind() == reflect.Pointer { - v = v.Elem() - } - - fieldValue := reflect.ValueOf(fieldPointer).Elem() - - for i := 0; i < v.NumField(); i++ { - if v.Field(i).Addr().Interface() == fieldValue.Addr().Interface() { - field := v.Type().Field(i) - - return field.Tag.Get(tag) - } - } - - return "" -} - func setTokenConfigName(config *configutils.Config, p string) { schema := structure.CONFIG{ NAME: "", } - nameField := getSchemeTagByPointer(&schema, "koanf", &schema.NAME) + nameField := configutils.GetSchemeTagByFieldPointer(&schema, "koanf", &schema.NAME) filename := filepath.Base(p) filenameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename)) diff --git a/internals/proxy/common/template.go b/internals/proxy/common/template.go index 1bbf6e61..c906d4a2 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -13,25 +13,37 @@ import ( "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) +func transformPrefixInto(tmplStr string, prefix string, to string) string { + re, err := regexp.Compile(`{{([^{}]+)}}`) - 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") + if err != nil { + return tmplStr + } + + varRe, err := regexp.Compile(string(prefix) + `("?[a-zA-Z0-9_.]+"?)`) + + if err != nil { + return tmplStr + } - return "." + toPrefix + varName - }) + transformed := re.ReplaceAllStringFunc(tmplStr, func(match string) string { + return varRe.ReplaceAllStringFunc(match, func(varMatch string) string { + varName := varRe.ReplaceAllString(varMatch, "$1") + + return "." + to + "." + varName }) + }) - if err != nil { - return data, err - } + return transformed +} + +func normalizeData(fromPrefix, toPrefix string, data map[string]any) (map[string]any, error) { + jsonStr := jsonutils.ToJson(data) - jsonStr = toVar + if jsonStr != "" { + normalizedTemplate := transformPrefixInto(jsonStr, fromPrefix, toPrefix) - normalizedData, err := jsonutils.GetJsonSafe[map[string]any](jsonStr) + normalizedData, err := jsonutils.GetJsonSafe[map[string]any](normalizedTemplate) if err == nil { data = normalizedData @@ -41,17 +53,7 @@ func normalizeData(fromPrefix, toPrefix string, data map[string]any) (map[string 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 { +func normalizeHeaders(headers map[string][]string) map[string][]string { cleanedHeaders := map[string][]string{} for key, value := range headers { @@ -72,38 +74,38 @@ func GetTemplatedBody(body map[string]any, headers map[string][]string, VARIABLE request.CopyHeaders(headersCopy, headers) request.CopyMap(bodyCopy, body) - headersCopy = cleanHeaders(headersCopy) + headersCopy = normalizeHeaders(headersCopy) - // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".header.Var" and ".body.Var" - normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "body.", bodyCopy) + // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".headers.Var" and ".body.Var" + normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "body", bodyCopy) if err != nil { return bodyCopy, false, err } - normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "header.", normalizedBody) + normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "headers", normalizedBody) if err != nil { return bodyCopy, false, err } // Prefix Body Data with Body_ - prefixedBody := map[string]any{ + nestedBody := map[string]any{ "body": normalizedBody, } // Prefix Header Data with Header_ - prefixedHeaders := map[string]any{ - "header": request.ParseHeaders(headersCopy), + nestedHeaders := map[string]any{ + "headers": request.ParseHeaders(headersCopy), } variables := map[string]any{} request.CopyMap(variables, VARIABLES) - request.CopyMap(variables, prefixedBody) - request.CopyMap(variables, prefixedHeaders) + request.CopyMap(variables, nestedBody) + request.CopyMap(variables, nestedHeaders) - templatedData, err := templating.RenderJSON(normalizedBody, variables) + templatedData, err := templating.TemplateData(normalizedBody, variables) if err != nil { return bodyCopy, false, err @@ -114,7 +116,7 @@ func GetTemplatedBody(body map[string]any, headers map[string][]string, VARIABLE modified = beforeStr != afterStr - return templatedData, modified, nil + return templatedData.(map[string]any), modified, nil } func TemplatePath(path string, VARIABLES any) (string, error) { @@ -124,13 +126,19 @@ func TemplatePath(path string, VARIABLES any) (string, error) { return path, err } - reqPath, err = templating.RenderNormalizedTemplate("path", reqPath, VARIABLES) + templt, err := templating.CreateNormalizedTemplateFromString("path", reqPath) + + if err != nil { + return path, err + } + + templated, err := templating.ExecuteTemplate(templt, VARIABLES) if err != nil { return path, err } - return reqPath, nil + return templated, nil } func InjectPathIntoBody(path string, data map[string]any) (string, bool) { @@ -168,9 +176,19 @@ func InjectPathIntoBody(path string, data map[string]any) (string, bool) { func TemplateQuery(rawQuery string, VARIABLES any) (string, error) { decodedQuery, _ := url.QueryUnescape(rawQuery) - templatedQuery, err := templating.RenderNormalizedTemplate("query", decodedQuery, VARIABLES) + templt, err := templating.CreateNormalizedTemplateFromString("query", decodedQuery) + + if err != nil { + return rawQuery, err + } + + templated, err := templating.ExecuteTemplate(templt, VARIABLES) + + if err != nil { + return rawQuery, err + } - return templatedQuery, err + return templated, err } func InjectQueryIntoBody(query url.Values, data map[string]any) bool { diff --git a/utils/runtime/tests.go b/utils/runtime/tests.go index bfd78354..12ce837d 100644 --- a/utils/runtime/tests.go +++ b/utils/runtime/tests.go @@ -32,16 +32,24 @@ func TestEndpointRules(conf structure.CONFIG) (error, any) { endpoints := conf.SETTINGS.ACCESS.ENDPOINTS.Value - err := structure.StringMatchList(endpoints.Allowed).TestRules() + if endpoints == nil { + return nil, nil + } - if err != nil { - return err, endpoints.Allowed + if len(endpoints.Allowed) != 0 { + err := structure.StringMatchList(endpoints.Allowed).TestRules() + + if err != nil { + return err, endpoints.Allowed + } } - err = structure.StringMatchList(endpoints.Blocked).TestRules() + if len(endpoints.Blocked) != 0 { + err := structure.StringMatchList(endpoints.Blocked).TestRules() - if err != nil { - return err, endpoints.Blocked + if err != nil { + return err, endpoints.Blocked + } } return nil, nil From 61b9d3011a28ad13efd3377ff5724c6b735cad07 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:05:35 +0100 Subject: [PATCH 12/26] semver + note for pre-releases --- internals/proxy/endpoints/about.go | 17 +---- utils/docker/docker.go | 79 +++++++++++++++++++++++- utils/semver/semver.go | 99 ++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 utils/semver/semver.go diff --git a/internals/proxy/endpoints/about.go b/internals/proxy/endpoints/about.go index efe86c3c..5ccd77d0 100644 --- a/internals/proxy/endpoints/about.go +++ b/internals/proxy/endpoints/about.go @@ -2,14 +2,13 @@ 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" + "github.com/codeshelldev/secured-signal-api/utils/docker" ) var AboutEndpoint = Endpoint{ @@ -52,8 +51,8 @@ func aboutHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { if !body.Empty { var version string - if isValidSemver(os.Getenv("IMAGE_TAG")) { - version, _ = strings.CutPrefix(version, "v") + if docker.VERSION != nil { + version, _ = strings.CutPrefix(docker.VERSION.String(), "v") } payload := map[string]any{ @@ -77,14 +76,4 @@ func aboutHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { }) 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/utils/docker/docker.go b/utils/docker/docker.go index 558fc396..5dc8c7ce 100644 --- a/utils/docker/docker.go +++ b/utils/docker/docker.go @@ -2,16 +2,93 @@ package docker import ( "context" + "fmt" "os" "time" "github.com/codeshelldev/gotl/pkg/docker" "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/pretty" httpserver "github.com/codeshelldev/gotl/pkg/server/http" + "github.com/codeshelldev/secured-signal-api/utils/semver" ) +var VERSION *semver.Version + func Init() { - logger.Info("Running ", os.Getenv("IMAGE_TAG"), " Image") + imageTag := os.Getenv("IMAGE_TAG") + + if imageTag == "" { + return + } + + if semver.IsValid(imageTag) { + v := semver.ParseSemver(imageTag) + + VERSION = &v + + logger.Info("Running ", VERSION.String(), " Image") + + if VERSION.Type != semver.FULL_RELEASE { + box := pretty.NewAutoBox() + + box.Border.Style.Color = pretty.Basic(pretty.BrightBlue) + box.Border.Chars = pretty.BorderChars{ + TopLeft: '+', + BottomLeft: '+', + TopRight: '+', + BottomRight: '+', + Horizontal: '─', + Vertical: '│', + } + + box.MinWidth = 60 + box.PaddingX = 2 + box.PaddingY = 1 + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.TextBlockSegment{ + Text: "🛠️ Pre-release Version 🛠️", + Style: pretty.Style{ + Bold: true, + }, + }, + pretty.InlineSegment{}, + }, + }) + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.StyledTextBlockSegment{ + Raw: "This is a" + + func() string { if VERSION.Type == semver.ALPHA_RELEASE { return "n" } else { return "" } }() + + " {i,b}" + string(VERSION.Type.Long()) + "{/}" + + func() string { if VERSION.Type != semver.RC_RELEASE { return " release" } else { return "" } }() + ", it may contain {b,fg=red}bugs{/} and ", + }, + pretty.StyledTextBlockSegment{ + Raw: "some features may be {b,fg=bright_black}incomplete{/} or {b,fg=bright_yellow}unstable{/}", + }, + }, + }) + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.InlineSegment{}, + pretty.StyledTextBlockSegment{ + Raw: "Encounter {b,fg=blue}issues{/}? Please {b,fg=blue}Report{/} them here:\n{b,u,fg=cyan}https://github.com/codeshelldev/secured-signal-api/issues{/}", + }, + }, + }) + + fmt.Println(box.Render()) + } + } else { + logger.Info("Running custom ", imageTag, " Image") + } } func Run(main func()) chan os.Signal { diff --git a/utils/semver/semver.go b/utils/semver/semver.go new file mode 100644 index 00000000..77cb8572 --- /dev/null +++ b/utils/semver/semver.go @@ -0,0 +1,99 @@ +package semver + +import ( + "regexp" + "strconv" + "strings" +) + +type Version struct { + Major int + Minor int + Patch int + Type ReleaseType + Count int +} + +type ReleaseType string + +const ( + FULL_RELEASE = "" + RC_RELEASE = "rc" + BETA_RELEASE = "beta" + ALPHA_RELEASE = "alpha" + DEV_RELEASE = "dev" +) + +var semverRegex = regexp.MustCompile( + `^v?` + // optional v as prefix + `(0|[1-9]\d*)\.` + // major + `(0|[1-9]\d*)\.` + // minor + `(0|[1-9]\d*)` + // patch + `(?:-([0-9A-Za-z-]+?)(\d*)` + // release type + optional numeric suffix + `(?:\.[0-9A-Za-z-]+)*)?$`, // allow dots in release type +) + +func (t ReleaseType) Long() string { + switch (t) { + case RC_RELEASE: + return "release candidate" + case DEV_RELEASE: + return "development" + case "": + return "full" + default: + return "" + } +} + +func (v Version) String() string { + res := "v" + strings.Join([]string{strconv.Itoa(v.Major), strconv.Itoa(v.Minor), strconv.Itoa(v.Patch)}, ".") + + if v.Type != "" { + res += "-" + string(v.Type) + strconv.Itoa(v.Count) + } + + return res +} + +func ParseSemver(str string) Version { + matches := semverRegex.FindStringSubmatch(str) + + if len(matches) == 0 { + return Version{} + } + + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + patch, _ := strconv.Atoi(matches[3]) + count, _ := strconv.Atoi(matches[5]) + + return Version{ + Major: major, + Minor: minor, + Patch: patch, + Type: ParseReleaseType(matches[4]), + Count: count, + } +} + +func IsValid(str string) bool { + return semverRegex.MatchString(str) +} + +func ParseReleaseType(str string) ReleaseType { + switch (str) { + case "rc": + return RC_RELEASE + case "beta": + return BETA_RELEASE + case "alpha": + return ALPHA_RELEASE + case "dev": + return DEV_RELEASE + case "": + return FULL_RELEASE + default: + return "" + } +} \ No newline at end of file From 7c485d9bd7e0a2ee37d8158a3213c18f4064aa44 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:19:17 +0100 Subject: [PATCH 13/26] update report link --- utils/docker/docker.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/utils/docker/docker.go b/utils/docker/docker.go index 5dc8c7ce..8551e130 100644 --- a/utils/docker/docker.go +++ b/utils/docker/docker.go @@ -33,14 +33,6 @@ func Init() { box := pretty.NewAutoBox() box.Border.Style.Color = pretty.Basic(pretty.BrightBlue) - box.Border.Chars = pretty.BorderChars{ - TopLeft: '+', - BottomLeft: '+', - TopRight: '+', - BottomRight: '+', - Horizontal: '─', - Vertical: '│', - } box.MinWidth = 60 box.PaddingX = 2 @@ -50,7 +42,7 @@ func Init() { Align: pretty.AlignCenter, Segments: []pretty.Segment{ pretty.TextBlockSegment{ - Text: "🛠️ Pre-release Version 🛠️", + Text: "🔬 Pre-Release 🔬", Style: pretty.Style{ Bold: true, }, @@ -79,7 +71,7 @@ func Init() { Segments: []pretty.Segment{ pretty.InlineSegment{}, pretty.StyledTextBlockSegment{ - Raw: "Encounter {b,fg=blue}issues{/}? Please {b,fg=blue}Report{/} them here:\n{b,u,fg=cyan}https://github.com/codeshelldev/secured-signal-api/issues{/}", + Raw: "Encounter {b,fg=blue}issues{/}? Please {b,fg=blue}Report{/} them here:\n\n{b,u,fg=cyan}https://codeshelldev.github.io/secured-signal-api/bug?v=" + VERSION.String() + "{/}", }, }, }) From 5f9387962ec26221a045133b7c15d50e2968682f Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:16:14 +0100 Subject: [PATCH 14/26] return timestamps as integers --- internals/proxy/endpoints/schedule.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internals/proxy/endpoints/schedule.go b/internals/proxy/endpoints/schedule.go index ff5a4275..38070f91 100644 --- a/internals/proxy/endpoints/schedule.go +++ b/internals/proxy/endpoints/schedule.go @@ -2,7 +2,6 @@ package endpoints import ( "net/http" - "strconv" "github.com/codeshelldev/gotl/pkg/jsonutils" "github.com/codeshelldev/gotl/pkg/request" @@ -49,8 +48,8 @@ func scheduleHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { "method": entry.Method, "url": entry.URL, - "created_at": strconv.Itoa(int(entry.CreatedAt.Unix())), - "run_at": strconv.Itoa(int(entry.RunAt.Unix())), + "created_at": int(entry.CreatedAt.Unix()), + "run_at": int(entry.RunAt.Unix()), }) if entry.Status != db.STATUS_DONE && entry.Status != db.STATUS_FAILED { @@ -58,11 +57,11 @@ func scheduleHandler(mux *http.ServeMux, next http.Handler) *http.ServeMux { return } - var finishedAt *string + var finishedAt *int if entry.FinishedAt != nil { finished := entry.FinishedAt.Unix() - tm := strconv.Itoa(int(finished)) + tm := int(finished) finishedAt = &tm } From fd982503218c4587eab36dc6521a524603fc8115 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:19:44 +0100 Subject: [PATCH 15/26] enable scheduling by default --- data/defaults.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/defaults.yml b/data/defaults.yml index f872f25a..042b55b3 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -29,6 +29,9 @@ settings: { field: data, score: 1 }, ] + scheduling: + enabled: true + templating: body: true path: true From 2dca1ec57d0445940b3a3fc766c35fd04a7a1def Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:23:03 +0100 Subject: [PATCH 16/26] fix url string conversion loop --- internals/config/structure/generics/generics.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internals/config/structure/generics/generics.go b/internals/config/structure/generics/generics.go index 88703212..3362e7cf 100644 --- a/internals/config/structure/generics/generics.go +++ b/internals/config/structure/generics/generics.go @@ -78,7 +78,8 @@ func (Url *URL) UnmarshalMapstructure(raw any) error { } func (Url URL) String() string { - return Url.String() + URL := url.URL(Url) + return URL.String() } // Enum is a wrapper for enum types From 9fb2159897a887b01bc9dd33943fbfb44bcd3483 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:47:39 +0100 Subject: [PATCH 17/26] feat: custom response headers (#295) --- internals/config/structure/structure.go | 5 +++ internals/proxy/middlewares/middleware.go | 39 +++++++++++++++++++ .../proxy/middlewares/responseheaders.go | 33 ++++++++++++++++ internals/proxy/proxy.go | 13 +++---- 4 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 internals/proxy/middlewares/responseheaders.go diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index f0a2474d..d705b651 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -63,6 +63,11 @@ type Token struct { type SETTINGS struct { ACCESS ACCESS `koanf:"access"` MESSAGE MESSAGE `koanf:"message"` + HTTP HTTP `koanf:"http"` +} + +type HTTP struct { + RESPONSE_HEADERS t.Opt[map[string]string] `koanf:"responseheaders"` } type MESSAGE struct { diff --git a/internals/proxy/middlewares/middleware.go b/internals/proxy/middlewares/middleware.go index c2401c77..a9c3e319 100644 --- a/internals/proxy/middlewares/middleware.go +++ b/internals/proxy/middlewares/middleware.go @@ -41,4 +41,43 @@ func (chain *Chain) Then(final http.Handler) http.Handler { } return handler +} + +type ResponseMiddleware struct { + Name string + Use func(res *http.Response) error +} + +type ResponseChain struct { + middlewares []ResponseMiddleware +} + +func NewResponseChain() *ResponseChain { + return &ResponseChain{} +} + +func (chain *ResponseChain) Use(middleware ResponseMiddleware) *ResponseChain { + chain.middlewares = append(chain.middlewares, middleware) + + if strings.HasPrefix(middleware.Name, "_") { + logger.Dev("Registered ", middleware.Name, " response middleware") + } else { + logger.Debug("Registered ", middleware.Name, " response middleware") + } + + return chain +} + +func (chain *ResponseChain) Then() func(*http.Response) error { + return func(resp *http.Response) error { + for _, middleware := range chain.middlewares { + err := middleware.Use(resp) + + if err != nil { + return err + } + } + + return nil + } } \ No newline at end of file diff --git a/internals/proxy/middlewares/responseheaders.go b/internals/proxy/middlewares/responseheaders.go new file mode 100644 index 00000000..740a2618 --- /dev/null +++ b/internals/proxy/middlewares/responseheaders.go @@ -0,0 +1,33 @@ +package middlewares + +import ( + "net/http" + + "github.com/codeshelldev/secured-signal-api/internals/config" + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" +) + +var InternalResponseHeaders = ResponseMiddleware{ + Name: "_Response_Headers", + Use: responseHandler, +} + +func responseHandler(res *http.Response) error { + conf := GetConfigByReq(res.Request) + + resHeaders := conf.SETTINGS.HTTP.RESPONSE_HEADERS.OptOrEmpty(config.DEFAULT.SETTINGS.HTTP.RESPONSE_HEADERS) + + if len(resHeaders) != 0 { + for k, v := range resHeaders { + res.Header.Set(k, v) + } + } + + res.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, private, proxy-revalidate") + res.Header.Set("Pragma", "no-cache") + res.Header.Set("Expires", "0") + res.Header.Set("Vary", "*") + res.Header.Set("Referrer-Policy", "no-referrer") + + return nil +} \ No newline at end of file diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index ba9e3c3b..a2eae453 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -19,6 +19,10 @@ func Create(targetUrl *url.URL) Proxy { return Proxy{Use: func() *httputil.ReverseProxy {return nil}} } + modifyResponse := m.NewResponseChain(). + Use(m.InternalResponseHeaders). + Then() + proxy := &httputil.ReverseProxy{ Rewrite: func(req *httputil.ProxyRequest) { req.Out.URL.Scheme = targetUrl.Scheme @@ -27,15 +31,8 @@ func Create(targetUrl *url.URL) Proxy { req.SetXForwarded() }, - ModifyResponse: func(res *http.Response) error { - res.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, private, proxy-revalidate") - res.Header.Set("Pragma", "no-cache") - res.Header.Set("Expires", "0") - res.Header.Set("Vary", "*") - res.Header.Set("Referrer-Policy", "no-referrer") - return nil - }, ErrorLog: logger.StdError(), + ModifyResponse: modifyResponse, } return Proxy{Use: func() *httputil.ReverseProxy {return proxy}} From b25e13ac3195529cd38dae243cf1fda7f6c09acd Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:19:12 +0100 Subject: [PATCH 18/26] feat: CORS (#296) --- .dockerignore | 35 +++++----- .github/workflows/docker-image-dev.yml | 7 +- .github/workflows/docker-image.yml | 2 +- .gitignore | 27 +------- Dockerfile | 29 +++++--- data/defaults.yml | 5 ++ dev-env.sh | 2 - go.mod | 16 ++++- go.sum | 53 ++++++++++++-- internals/config/structure/structure.go | 14 ++++ internals/db/db.go | 8 ++- internals/proxy/common/common.go | 12 ++++ internals/proxy/common/context.go | 4 +- internals/proxy/middlewares/auth.go | 32 +++------ internals/proxy/middlewares/cors.go | 69 +++++++++++++++++++ internals/proxy/middlewares/middleware.go | 39 ----------- .../middlewares/{proxy.go => proxies.go} | 8 +-- .../headers.go} | 6 +- internals/proxy/middlewares/response/hooks.go | 26 +++++++ .../proxy/middlewares/response/middleware.go | 47 +++++++++++++ internals/proxy/proxy.go | 9 ++- utils/urlutils/urlutils.go | 16 +++++ 22 files changed, 327 insertions(+), 139 deletions(-) create mode 100644 internals/proxy/middlewares/cors.go rename internals/proxy/middlewares/{proxy.go => proxies.go} (96%) rename internals/proxy/middlewares/{responseheaders.go => response/headers.go} (89%) create mode 100644 internals/proxy/middlewares/response/hooks.go create mode 100644 internals/proxy/middlewares/response/middleware.go create mode 100644 utils/urlutils/urlutils.go diff --git a/.dockerignore b/.dockerignore index d65b70ba..92127676 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,32 +1,33 @@ -# Ignore all -* +# Env files +**/*.env -# env file -*.env +# Editor/IDE +# .idea/ +.vscode/ # Git folders .git* -.github +!.github + +# Docusaurus +.docusaurus +node_modules # Dev folders .dev +dev-env.sh *.local.* -# Ignore yml files -*.yaml -*.yml +# Editor config +.editorconfig # Markdown files -*.md +**/*.md + +!LICENSE # Include data/ !data/* -# Ignore source files -*.go -go.mod -go.sum - -# Include build -!app -!dist/* \ No newline at end of file +# Include source files +!*.go \ No newline at end of file diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index b1a44ae6..c7a3cedb 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -21,9 +21,10 @@ on: - beta - alpha - dev + - feat env: - TYPES: "rc,beta,alpha,dev" + TYPES: "rc,beta,alpha,dev,feat" TYPE: ${{ inputs.type }} BASE_TAG: ${{ inputs.base-tag }} @@ -113,7 +114,7 @@ jobs: echo "COUNT=$COUNT" >> "$GITHUB_ENV" - name: Login to Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -181,7 +182,7 @@ jobs: update: needs: resolve-tag - uses: codeshelldev/gh-actions/.github/workflows/docker-image-go.yml@main + uses: codeshelldev/gh-actions/.github/workflows/docker-image.yml@main name: Development Image with: registry: ghcr.io diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 116e3baa..e846b071 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -39,7 +39,7 @@ jobs: update: needs: check if: needs.check.outputs.continue == 'true' - uses: codeshelldev/gh-actions/.github/workflows/docker-image-go.yml@main + uses: codeshelldev/gh-actions/.github/workflows/docker-image.yml@main name: Stable Image with: registry: ghcr.io diff --git a/.gitignore b/.gitignore index 37dd0660..4fd453c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,5 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Code coverage profiles and other test artifacts -*.out -coverage.* -*.coverprofile -profile.cov - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work -go.work.sum - -# env file -*.env +# Env files +**/*.env # Editor/IDE # .idea/ diff --git a/Dockerfile b/Dockerfile index d1992aab..6a695d42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,26 @@ +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.26-alpine AS builder + +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -ldflags="-s -w" -o /app/app . + FROM alpine:3.22 + RUN apk --no-cache add ca-certificates ARG IMAGE_TAG ENV IMAGE_TAG=$IMAGE_TAG LABEL org.opencontainers.image.version=$IMAGE_TAG -ENV SERVICE__PORT=8880 - ENV DEFAULTS_PATH=/app/data/defaults.yml ENV FAVICON_PATH=/app/data/favicon.ico @@ -15,19 +29,12 @@ ENV TOKENS_DIR=/config/tokens ENV DB_PATH=/db/db.sqlite3 -ENV CGO_ENABLED=1 - ENV REDACT_TOKENS=true -ARG TARGETOS -ARG TARGETARCH - WORKDIR /app -COPY . . - -COPY dist/${TARGETOS}/${TARGETARCH}/app . +COPY --from=builder /app/app . -RUN rm dist/ -r +COPY data/ /app/data/ CMD ["./app"] diff --git a/data/defaults.yml b/data/defaults.yml index 042b55b3..0e2226ca 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -59,3 +59,8 @@ settings: matchType: prefix - pattern: /v1/contacts matchType: prefix + + cors: + methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS] + headers: + ["Content-Type", "Authorization", "Accept", "Accept-Language", "Origin"] diff --git a/dev-env.sh b/dev-env.sh index 54192048..4a672d36 100644 --- a/dev-env.sh +++ b/dev-env.sh @@ -43,8 +43,6 @@ 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 export LOG_LEVEL=dev diff --git a/go.mod b/go.mod index f3c944cd..bee006ad 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/codeshelldev/secured-signal-api -go 1.26.0 +go 1.26.1 require ( github.com/codeshelldev/gotl/pkg/configutils v0.0.22 @@ -20,6 +20,17 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/knadh/koanf/parsers/yaml v1.1.0 golang.org/x/time v0.14.0 + modernc.org/sqlite v1.47.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) require ( @@ -32,11 +43,10 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 // indirect github.com/knadh/koanf/v2 v2.3.3 // indirect github.com/mattn/go-runewidth v0.0.20 // 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.41.0 // indirect + golang.org/x/sys v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index ae1f0156..8ee69ed7 100644 --- a/go.sum +++ b/go.sum @@ -24,12 +24,18 @@ github.com/codeshelldev/gotl/pkg/templating v0.0.16 h1:0dl/NEApCtlm4kyEscQPknx4D github.com/codeshelldev/gotl/pkg/templating v0.0.16/go.mod h1:MHM4ouEsLNKXRYO+fS9qqpS1SFlL4Z6Q/0kxtS+auLk= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= @@ -46,16 +52,20 @@ github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= github.com/mattn/go-runewidth v0.0.20/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= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -66,12 +76,47 @@ 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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index d705b651..629e7a2d 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -97,6 +97,7 @@ type Templating struct { } type Scheduling struct { + // Enabled is needed because this isn't a data-driven setting, but rather a toggle Enabled bool `koanf:"enabled"` MaxHorizon t.Opt[g.TimeDuration] `koanf:"maxhorizon"` } @@ -113,6 +114,19 @@ type ACCESS struct { IP_FILTER t.Opt[IPFilter] `koanf:"ipfilter"` TRUSTED_IPS t.Opt[[]g.IPOrNet] `koanf:"trustedips"` TRUSTED_PROXIES t.Opt[[]g.IPOrNet] `koanf:"trustedproxies"` + CORS t.Opt[Cors] `koanf:"cors"` +} + +type Cors struct { + Origins []Origin `koanf:"origins"` + Methods t.Opt[[]string] `koanf:"methods"` + Headers t.Opt[[]string] `koanf:"headers"` +} + +type Origin struct { + URL g.URL `koanf:"url"` + Methods t.Opt[[]string] `koanf:"methods"` + Headers t.Opt[[]string] `koanf:"headers"` } type FieldPolicies = *t.Comp[c.RFieldPolicies, c.FieldPolicies] diff --git a/internals/db/db.go b/internals/db/db.go index 783ed359..86e57306 100644 --- a/internals/db/db.go +++ b/internals/db/db.go @@ -4,12 +4,14 @@ import ( "bytes" "database/sql" "encoding/gob" + "os" + "path/filepath" _ "embed" "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/secured-signal-api/internals/config" - _ "github.com/mattn/go-sqlite3" + _ "modernc.org/sqlite" ) var db *sql.DB @@ -20,7 +22,9 @@ var schema string func Init() { var err error - db, err = sql.Open("sqlite3", config.ENV.DB_PATH) + os.MkdirAll(filepath.Dir(config.ENV.DB_PATH), 0755) + + db, err = sql.Open("sqlite", config.ENV.DB_PATH) if err != nil { logger.Fatal("Error opening database: ", err.Error()) diff --git a/internals/proxy/common/common.go b/internals/proxy/common/common.go index 3500afff..51adc3a7 100644 --- a/internals/proxy/common/common.go +++ b/internals/proxy/common/common.go @@ -85,4 +85,16 @@ func WriteError(w http.ResponseWriter, status int, msg string) { w.WriteHeader(status) res.Write(w) +} + +func AddResponseHook(req *http.Request, hook func(res *http.Response) error) *http.Request { + hooks := GetResponseHooks(req) + + hooks = append(hooks, hook) + + return SetContext(req, ResponseHooksKey, hooks) +} + +func GetResponseHooks(req *http.Request) []func(*http.Response) error { + return GetContext[[]func(*http.Response) error](req, ResponseHooksKey) } \ No newline at end of file diff --git a/internals/proxy/common/context.go b/internals/proxy/common/context.go index 3e7811cc..45b6ac0c 100644 --- a/internals/proxy/common/context.go +++ b/internals/proxy/common/context.go @@ -11,4 +11,6 @@ const TrustedClientKey contextKey = "isClientTrusted" const TrustedProxyKey contextKey = "isProxyTrusted" const ClientIPKey contextKey = "clientIP" -const OriginURLKey contextKey = "originURL" \ No newline at end of file +const OriginURLKey contextKey = "originURL" + +const ResponseHooksKey contextKey = "responseHooks" \ No newline at end of file diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 94879c78..04b8bf95 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -263,7 +263,7 @@ func authHandler(next http.Handler) http.Handler { allowedMethods := conf.API.AUTH.METHODS.OptOrEmpty(config.DEFAULT.API.AUTH.METHODS) - if isAuthMethodAllowed(method, token, conf.API.TOKENS, allowedMethods, conf.API.AUTH.TOKENS) { + if isAuthMethodAllowed(method, token, allowedMethods, conf.API.AUTH.TOKENS) { req = SetContext(req, IsAuthKey, true) req = SetContext(req, TokenKey, token) } else { @@ -311,31 +311,21 @@ type AuthToken struct { Methods []string } -func getTokenMethodMap(rawTokens []string, defaultMethods []string, tokenMethodSet []structure.Token) map[string][]string { - tokenMethodMap := map[string][]string{} - - for _, token := range rawTokens { - tokenMethodMap[token] = defaultMethods - } - - for _, set := range tokenMethodSet { - for _, token := range set.Set { - tokenMethodMap[token] = set.Methods - } - } - - return tokenMethodMap -} - -func isAuthMethodAllowed(method AuthMethod, token string, rawTokens []string, defaultMethods []string, tokenMethodSet []structure.Token) bool { - if (len(defaultMethods) == 0 || defaultMethods == nil) && (len(tokenMethodSet) == 0 || tokenMethodSet == nil) { +func isAuthMethodAllowed(method AuthMethod, token string, defaultMethods []string, tokenOverwrites []structure.Token) bool { + if len(defaultMethods) == 0 && len(tokenOverwrites) == 0 { // default: allow all return true } - tokenMethodMap := getTokenMethodMap(rawTokens, defaultMethods, tokenMethodSet) + for _, t := range tokenOverwrites { + if slices.Contains(t.Set, token) { + return slices.ContainsFunc(t.Methods, func(try string) bool { + return strings.EqualFold(try, method.Name) + }) + } + } - return slices.ContainsFunc(tokenMethodMap[token], func(try string) bool { + return slices.ContainsFunc(defaultMethods, func(try string) bool { return strings.EqualFold(try, method.Name) }) } \ No newline at end of file diff --git a/internals/proxy/middlewares/cors.go b/internals/proxy/middlewares/cors.go new file mode 100644 index 00000000..577a167c --- /dev/null +++ b/internals/proxy/middlewares/cors.go @@ -0,0 +1,69 @@ +package middlewares + +import ( + "net/http" + "net/url" + "strings" + + "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/urlutils" +) + +var CORS Middleware = Middleware{ + Name: "CORS", + Use: corsHandler, +} + +func corsHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conf := GetConfigByReq(req) + + cors := conf.SETTINGS.ACCESS.CORS.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.CORS) + + defaultMethods := cors.Methods.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.CORS.Value.Methods) + defaultHeaders := cors.Headers.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.CORS.Value.Headers) + + if len(cors.Origins) == 0 { + next.ServeHTTP(w, req) + return + } + + origin := req.Header.Get("Origin") + originURL, err := url.Parse(origin) + + var matchingOrigin *structure.Origin + + if err == nil { + for _, o := range cors.Origins { + if urlutils.NormalizeURL(originURL) == urlutils.NormalizeURL((*url.URL)(&o.URL)) { + matchingOrigin = &o + } + } + } + + if matchingOrigin == nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + w.Header().Set("Access-Control-Allow-Origin", origin) + + w.Header().Set("Access-Control-Allow-Credentials", "true") + + if req.Method == "OPTIONS" { + methods := matchingOrigin.Methods.ValueOrFallback(defaultMethods) + headers := matchingOrigin.Headers.ValueOrFallback(defaultHeaders) + + w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) + w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) + + w.WriteHeader(http.StatusNoContent) + + return + } + + next.ServeHTTP(w, req) + }) +} \ No newline at end of file diff --git a/internals/proxy/middlewares/middleware.go b/internals/proxy/middlewares/middleware.go index a9c3e319..c2401c77 100644 --- a/internals/proxy/middlewares/middleware.go +++ b/internals/proxy/middlewares/middleware.go @@ -41,43 +41,4 @@ func (chain *Chain) Then(final http.Handler) http.Handler { } return handler -} - -type ResponseMiddleware struct { - Name string - Use func(res *http.Response) error -} - -type ResponseChain struct { - middlewares []ResponseMiddleware -} - -func NewResponseChain() *ResponseChain { - return &ResponseChain{} -} - -func (chain *ResponseChain) Use(middleware ResponseMiddleware) *ResponseChain { - chain.middlewares = append(chain.middlewares, middleware) - - if strings.HasPrefix(middleware.Name, "_") { - logger.Dev("Registered ", middleware.Name, " response middleware") - } else { - logger.Debug("Registered ", middleware.Name, " response middleware") - } - - return chain -} - -func (chain *ResponseChain) Then() func(*http.Response) error { - return func(resp *http.Response) error { - for _, middleware := range chain.middlewares { - err := middleware.Use(resp) - - if err != nil { - return err - } - } - - return nil - } } \ No newline at end of file diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxies.go similarity index 96% rename from internals/proxy/middlewares/proxy.go rename to internals/proxy/middlewares/proxies.go index 0d48b07a..56b6de71 100644 --- a/internals/proxy/middlewares/proxy.go +++ b/internals/proxy/middlewares/proxies.go @@ -11,9 +11,9 @@ import ( "github.com/codeshelldev/secured-signal-api/utils/netutils" ) -var InternalProxy Middleware = Middleware{ - Name: "_Proxy", - Use: proxyHandler, +var InternalProxiesHandler Middleware = Middleware{ + Name: "_Proxies_Handler", + Use: proxiesHandler, } type ForwardedEntry struct { @@ -28,7 +28,7 @@ type OriginInfo struct { Proto string } -func proxyHandler(next http.Handler) http.Handler { +func proxiesHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { logger := GetLogger(req) diff --git a/internals/proxy/middlewares/responseheaders.go b/internals/proxy/middlewares/response/headers.go similarity index 89% rename from internals/proxy/middlewares/responseheaders.go rename to internals/proxy/middlewares/response/headers.go index 740a2618..d4bf1b84 100644 --- a/internals/proxy/middlewares/responseheaders.go +++ b/internals/proxy/middlewares/response/headers.go @@ -1,4 +1,4 @@ -package middlewares +package middleware import ( "net/http" @@ -9,10 +9,10 @@ import ( var InternalResponseHeaders = ResponseMiddleware{ Name: "_Response_Headers", - Use: responseHandler, + Use: headersHandler, } -func responseHandler(res *http.Response) error { +func headersHandler(res *http.Response) error { conf := GetConfigByReq(res.Request) resHeaders := conf.SETTINGS.HTTP.RESPONSE_HEADERS.OptOrEmpty(config.DEFAULT.SETTINGS.HTTP.RESPONSE_HEADERS) diff --git a/internals/proxy/middlewares/response/hooks.go b/internals/proxy/middlewares/response/hooks.go new file mode 100644 index 00000000..36174e97 --- /dev/null +++ b/internals/proxy/middlewares/response/hooks.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "net/http" + + . "github.com/codeshelldev/secured-signal-api/internals/proxy/common" +) + +var InternalResponseHooks = ResponseMiddleware{ + Name: "_Response_Hooks", + Use: hooksHandler, +} + +func hooksHandler(res *http.Response) error { + hooks := GetResponseHooks(res.Request) + + for _, h := range hooks { + err := h(res) + + if err != nil { + return err + } + } + + return nil +} \ No newline at end of file diff --git a/internals/proxy/middlewares/response/middleware.go b/internals/proxy/middlewares/response/middleware.go new file mode 100644 index 00000000..92ad2921 --- /dev/null +++ b/internals/proxy/middlewares/response/middleware.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/codeshelldev/gotl/pkg/logger" +) + +type ResponseMiddleware struct { + Name string + Use func(res *http.Response) error +} + +type ResponseChain struct { + middlewares []ResponseMiddleware +} + +func NewResponseChain() *ResponseChain { + return &ResponseChain{} +} + +func (chain *ResponseChain) Use(middleware ResponseMiddleware) *ResponseChain { + chain.middlewares = append(chain.middlewares, middleware) + + if strings.HasPrefix(middleware.Name, "_") { + logger.Dev("Registered ", middleware.Name, " response middleware") + } else { + logger.Debug("Registered ", middleware.Name, " response middleware") + } + + return chain +} + +func (chain *ResponseChain) Then() func(*http.Response) error { + return func(resp *http.Response) error { + for _, middleware := range chain.middlewares { + err := middleware.Use(resp) + + if err != nil { + return err + } + } + + return nil + } +} \ No newline at end of file diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index a2eae453..fa8ca92f 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -7,6 +7,7 @@ import ( "github.com/codeshelldev/gotl/pkg/logger" m "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares" + rm "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares/response" ) type Proxy struct { @@ -19,8 +20,9 @@ func Create(targetUrl *url.URL) Proxy { return Proxy{Use: func() *httputil.ReverseProxy {return nil}} } - modifyResponse := m.NewResponseChain(). - Use(m.InternalResponseHeaders). + modifyResponse := rm.NewResponseChain(). + Use(rm.InternalResponseHooks). + Use(rm.InternalResponseHeaders). Then() proxy := &httputil.ReverseProxy{ @@ -43,7 +45,8 @@ func (proxy Proxy) Init() http.Handler { Use(m.InternalInsecureAPI). Use(m.Auth). Use(m.InternalMiddlewareLogger). - Use(m.InternalProxy). + Use(m.InternalProxiesHandler). + Use(m.CORS). Use(m.InternalClientIP). Use(m.RequestLogger). Use(m.InternalAuthRequirement). diff --git a/utils/urlutils/urlutils.go b/utils/urlutils/urlutils.go new file mode 100644 index 00000000..525fc685 --- /dev/null +++ b/utils/urlutils/urlutils.go @@ -0,0 +1,16 @@ +package urlutils + +import "net/url" + +func NormalizeURL(url *url.URL) string { + host := url.Hostname() + port := url.Port() + + if (url.Scheme == "https" && port == "443") || + (url.Scheme == "http" && port == "80") || + port == "" { + return url.Scheme + "://" + host + } + + return url.Scheme + "://" + host + ":" + port +} \ No newline at end of file From 91f788eced1bb380fece7176aaabf0e6ffb8a941 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:21:53 +0100 Subject: [PATCH 19/26] improved cors handling * fixed error due to misunderstanding about behaviour when `Origin` header is not set despite cors being _enabled_ * preflight requests now only return the ACA-Header if the corresponding ACR-Header was present and matches where found / any allowed values where set on server-side --- internals/proxy/middlewares/cors.go | 54 +++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/internals/proxy/middlewares/cors.go b/internals/proxy/middlewares/cors.go index 577a167c..b6cfaf16 100644 --- a/internals/proxy/middlewares/cors.go +++ b/internals/proxy/middlewares/cors.go @@ -3,6 +3,7 @@ package middlewares import ( "net/http" "net/url" + "slices" "strings" "github.com/codeshelldev/secured-signal-api/internals/config" @@ -31,6 +32,12 @@ func corsHandler(next http.Handler) http.Handler { } origin := req.Header.Get("Origin") + + if origin == "" { + next.ServeHTTP(w, req) + return + } + originURL, err := url.Parse(origin) var matchingOrigin *structure.Origin @@ -50,14 +57,49 @@ func corsHandler(next http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Credentials", "true") - + // CORS preflight request if req.Method == "OPTIONS" { - methods := matchingOrigin.Methods.ValueOrFallback(defaultMethods) - headers := matchingOrigin.Headers.ValueOrFallback(defaultHeaders) + requestedMethod := req.Header.Get("Access-Control-Request-Method") + + if requestedMethod != "" { + allowedMethods := matchingOrigin.Methods.ValueOrFallback(defaultMethods) + + if len(allowedMethods) != 0 { + // only set if any (matching) methods + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ",")) + } + } - w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) - w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) + requestedHeaders := req.Header.Get("Access-Control-Request-Headers") + + if requestedHeaders != "" { + allowedHeaders := matchingOrigin.Headers.ValueOrFallback(defaultHeaders) + + matchingHeaders := []string{} + + // echo back allowed and requested headers + for header := range strings.SplitSeq(requestedHeaders, ",") { + header = strings.TrimSpace(header) + + var match string + + if slices.ContainsFunc(allowedHeaders, func(allowed string) bool { + if strings.EqualFold(header, allowed) { + match = allowed + return true + } + + return false + }) { + matchingHeaders = append(matchingHeaders, match) + } + } + + if len(matchingHeaders) != 0 { + // only set if any (matching) headers + w.Header().Set("Access-Control-Allow-Headers", strings.Join(matchingHeaders, ",")) + } + } w.WriteHeader(http.StatusNoContent) From cb439862d9ab8b9e7204fe3f276a4e6faa334b13 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:22:03 +0100 Subject: [PATCH 20/26] added `/not-found` to mockserver --- utils/mockserver/mockserver.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/mockserver/mockserver.go b/utils/mockserver/mockserver.go index 6d955627..8e6b66e6 100644 --- a/utils/mockserver/mockserver.go +++ b/utils/mockserver/mockserver.go @@ -48,6 +48,12 @@ func main() { fmt.Fprint(w, `{"message":"Hello from mock endpoint"}`) }) + http.HandleFunc("/not-found", func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusNotFound) + + fmt.Fprint(w, "Not Found") + }) + logger.Info("Mock server running at http://127.0.0.1:", port) err = http.ListenAndServe("127.0.0.1:" + port, nil) From fbbe975daae07ae0985b845c5534f8cfb56f7373 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:43:13 +0100 Subject: [PATCH 21/26] add nil-checks --- internals/proxy/middlewares/endpoints.go | 5 ++++ internals/proxy/middlewares/hostname.go | 35 +++++++++++++----------- internals/proxy/middlewares/ipfilter.go | 5 ++++ internals/proxy/middlewares/mapping.go | 5 ++++ internals/proxy/middlewares/policy.go | 5 ++++ 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index bd4ddf0d..eef5f739 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -21,6 +21,11 @@ func endpointsHandler(next http.Handler) http.Handler { endpoints := conf.SETTINGS.ACCESS.ENDPOINTS.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.ENDPOINTS) + if len(endpoints.Allowed) == 0 && len(endpoints.Blocked) == 0 { + next.ServeHTTP(w, req) + return + } + reqPath := req.URL.Path blocked, err := isEndpointBlocked(reqPath, endpoints.Allowed, endpoints.Blocked) diff --git a/internals/proxy/middlewares/hostname.go b/internals/proxy/middlewares/hostname.go index 63168982..b8a8e8e3 100644 --- a/internals/proxy/middlewares/hostname.go +++ b/internals/proxy/middlewares/hostname.go @@ -22,22 +22,25 @@ func hostnameHandler(next http.Handler) http.Handler { hostnames := conf.SERVICE.HOSTNAMES.OptOrEmpty(config.DEFAULT.SERVICE.HOSTNAMES) - if len(hostnames) > 0 { - URL := GetContext[*url.URL](req, OriginURLKey) - - hostname := URL.Hostname() - - if hostname == "" { - logger.Error("Encountered empty hostname") - http.Error(w, "Bad Request: invalid hostname", http.StatusBadRequest) - return - } - - if !slices.Contains(hostnames, hostname) { - logger.Warn("Client tried using Token with wrong hostname") - onUnauthorized(w) - return - } + if len(hostnames) == 0 { + next.ServeHTTP(w, req) + return + } + + URL := GetContext[*url.URL](req, OriginURLKey) + + hostname := URL.Hostname() + + if hostname == "" { + logger.Error("Encountered empty hostname") + http.Error(w, "Bad Request: invalid hostname", http.StatusBadRequest) + return + } + + if !slices.Contains(hostnames, hostname) { + logger.Warn("Client tried using Token with wrong hostname") + onUnauthorized(w) + return } next.ServeHTTP(w, req) diff --git a/internals/proxy/middlewares/ipfilter.go b/internals/proxy/middlewares/ipfilter.go index 479383c3..a6f16d59 100644 --- a/internals/proxy/middlewares/ipfilter.go +++ b/internals/proxy/middlewares/ipfilter.go @@ -23,6 +23,11 @@ func ipFilterHandler(next http.Handler) http.Handler { ipFilter := conf.SETTINGS.ACCESS.IP_FILTER.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.IP_FILTER) + if len(ipFilter.Allowed) == 0 && len(ipFilter.Blocked) == 0 { + next.ServeHTTP(w, req) + return + } + ip := GetContext[net.IP](req, ClientIPKey) if isIPBlocked(ip, ipFilter.Allowed, ipFilter.Blocked) { diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index 1206cb1d..02ea4dfd 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -25,6 +25,11 @@ func mappingHandler(next http.Handler) http.Handler { variables := conf.SETTINGS.MESSAGE.VARIABLES.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.VARIABLES) fieldMappings := conf.SETTINGS.MESSAGE.FIELD_MAPPINGS.OptOrEmpty(config.DEFAULT.SETTINGS.MESSAGE.FIELD_MAPPINGS) + if len(fieldMappings) == 0 { + next.ServeHTTP(w, req) + return + } + body, err := request.GetReqBody(req) if err != nil { diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 67a41558..1bbd4b4a 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -25,6 +25,11 @@ func policyHandler(next http.Handler) http.Handler { policies := conf.SETTINGS.ACCESS.FIELD_POLICIES.OptOrEmpty(config.DEFAULT.SETTINGS.ACCESS.FIELD_POLICIES) + if policies == nil { + next.ServeHTTP(w, req) + return + } + body, err := request.GetReqBody(req) if err != nil { From c370572428a4f57549a0ca6c431f4033dba0638f Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:44:47 +0100 Subject: [PATCH 22/26] update deps --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index bee006ad..2b2dc54d 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/knadh/koanf/parsers/yaml v1.1.0 - golang.org/x/time v0.14.0 + golang.org/x/time v0.15.0 modernc.org/sqlite v1.47.0 ) @@ -41,8 +41,8 @@ require ( github.com/knadh/koanf/providers/confmap v1.0.0 // indirect github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect github.com/knadh/koanf/providers/file v1.2.1 // indirect - github.com/knadh/koanf/v2 v2.3.3 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/knadh/koanf/v2 v2.3.4 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 8ee69ed7..4419f213 100644 --- a/go.sum +++ b/go.sum @@ -46,16 +46,16 @@ github.com/knadh/koanf/providers/env/v2 v2.0.0 h1:Ad5H3eun722u+FvchiIcEIJZsZ2M6o github.com/knadh/koanf/providers/env/v2 v2.0.0/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc= github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.3.3 h1:jLJC8XCRfLC7n4F+ZKKdBsbq1bfXTpuFhf4L7t94D94= -github.com/knadh/koanf/v2 v2.3.3/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc= +github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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= @@ -83,8 +83,8 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From c7ce31b53a19b600f3f2c82f384d9e49305cb34a Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:20:33 +0100 Subject: [PATCH 23/26] only add auth require headers after auth requirement middleware --- internals/proxy/middlewares/auth.go | 5 +---- internals/proxy/middlewares/cors.go | 30 +++++++++++++++++------------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 04b8bf95..b9541467 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -255,8 +255,6 @@ func authHandler(next http.Handler) http.Handler { method, token, _ := authChain.Eval(w, req, tokens) if token == "" { - onUnauthorized(w) - req = SetContext(req, IsAuthKey, false) } else { conf := GetConfigWithoutDefault(token) @@ -269,8 +267,6 @@ func authHandler(next http.Handler) http.Handler { } else { logger.Warn("Client tried using disabled auth method: ", method.Name) - onUnauthorized(w) - req = SetContext(req, IsAuthKey, false) } } @@ -289,6 +285,7 @@ func authRequirementHandler(next http.Handler) http.Handler { isAuthenticated := GetContext[bool](req, IsAuthKey) if !isAuthenticated { + onUnauthorized(w) return } diff --git a/internals/proxy/middlewares/cors.go b/internals/proxy/middlewares/cors.go index b6cfaf16..65fb4799 100644 --- a/internals/proxy/middlewares/cors.go +++ b/internals/proxy/middlewares/cors.go @@ -38,19 +38,9 @@ func corsHandler(next http.Handler) http.Handler { return } - originURL, err := url.Parse(origin) + matchingOrigin, allowed := isCORSOriginAllowed(origin, cors.Origins) - var matchingOrigin *structure.Origin - - if err == nil { - for _, o := range cors.Origins { - if urlutils.NormalizeURL(originURL) == urlutils.NormalizeURL((*url.URL)(&o.URL)) { - matchingOrigin = &o - } - } - } - - if matchingOrigin == nil { + if !allowed { http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -108,4 +98,20 @@ func corsHandler(next http.Handler) http.Handler { next.ServeHTTP(w, req) }) +} + +func isCORSOriginAllowed(origin string, allowed []structure.Origin) (structure.Origin, bool) { + originURL, err := url.Parse(origin) + + var matchingOrigin *structure.Origin + + if err == nil { + for _, o := range allowed { + if urlutils.NormalizeURL(originURL) == urlutils.NormalizeURL((*url.URL)(&o.URL)) { + matchingOrigin = &o + } + } + } + + return *matchingOrigin, matchingOrigin != nil } \ No newline at end of file From c65d6e67a13907be00144f649441344bfe8b24bb Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:44:11 +0100 Subject: [PATCH 24/26] . --- internals/proxy/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index fa8ca92f..0d802ace 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -46,9 +46,9 @@ func (proxy Proxy) Init() http.Handler { Use(m.Auth). Use(m.InternalMiddlewareLogger). Use(m.InternalProxiesHandler). - Use(m.CORS). Use(m.InternalClientIP). Use(m.RequestLogger). + Use(m.CORS). Use(m.InternalAuthRequirement). Use(m.Port). Use(m.Hostname). From 21f8dd126d6bc4c841f93e92eeb0e2b24a6ea4da Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:20:52 +0100 Subject: [PATCH 25/26] fixed nil panic in isCORSOriginAllowed --- internals/proxy/middlewares/cors.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internals/proxy/middlewares/cors.go b/internals/proxy/middlewares/cors.go index 65fb4799..0eee8715 100644 --- a/internals/proxy/middlewares/cors.go +++ b/internals/proxy/middlewares/cors.go @@ -100,7 +100,7 @@ func corsHandler(next http.Handler) http.Handler { }) } -func isCORSOriginAllowed(origin string, allowed []structure.Origin) (structure.Origin, bool) { +func isCORSOriginAllowed(origin string, allowed []structure.Origin) (*structure.Origin, bool) { originURL, err := url.Parse(origin) var matchingOrigin *structure.Origin @@ -113,5 +113,5 @@ func isCORSOriginAllowed(origin string, allowed []structure.Origin) (structure.O } } - return *matchingOrigin, matchingOrigin != nil + return matchingOrigin, matchingOrigin != nil } \ No newline at end of file From 217dd36b6a92fb7a0a3e30ca56bdecabf93a95c8 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 21 Mar 2026 12:29:33 +0100 Subject: [PATCH 26/26] improved CORS * added Vary header * ACA headers are sent regardless of ACR headers * ACA-Headers returns all allowed headers not just matching ones --- data/defaults.yml | 8 ++++- internals/proxy/middlewares/cors.go | 49 ++++++++--------------------- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/data/defaults.yml b/data/defaults.yml index 0e2226ca..c8889f8a 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -63,4 +63,10 @@ settings: cors: methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS] headers: - ["Content-Type", "Authorization", "Accept", "Accept-Language", "Origin"] + [ + "Content-Type", + "Content-Language", + "Authorization", + "Accept", + "Accept-Language", + ] diff --git a/internals/proxy/middlewares/cors.go b/internals/proxy/middlewares/cors.go index 0eee8715..b78a84a8 100644 --- a/internals/proxy/middlewares/cors.go +++ b/internals/proxy/middlewares/cors.go @@ -3,7 +3,6 @@ package middlewares import ( "net/http" "net/url" - "slices" "strings" "github.com/codeshelldev/secured-signal-api/internals/config" @@ -47,48 +46,26 @@ func corsHandler(next http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Origin", origin) + // add Origin header to Vary (if needed) + if w.Header().Get("Vary") != "*" { + w.Header().Add("Vary", "Origin") + } + // CORS preflight request if req.Method == "OPTIONS" { - requestedMethod := req.Header.Get("Access-Control-Request-Method") + allowedMethods := matchingOrigin.Methods.ValueOrFallback(defaultMethods) - if requestedMethod != "" { - allowedMethods := matchingOrigin.Methods.ValueOrFallback(defaultMethods) - - if len(allowedMethods) != 0 { - // only set if any (matching) methods - w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ",")) - } + if len(allowedMethods) != 0 { + // only set if any methods + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ",")) } - requestedHeaders := req.Header.Get("Access-Control-Request-Headers") - - if requestedHeaders != "" { - allowedHeaders := matchingOrigin.Headers.ValueOrFallback(defaultHeaders) - - matchingHeaders := []string{} - - // echo back allowed and requested headers - for header := range strings.SplitSeq(requestedHeaders, ",") { - header = strings.TrimSpace(header) - - var match string - - if slices.ContainsFunc(allowedHeaders, func(allowed string) bool { - if strings.EqualFold(header, allowed) { - match = allowed - return true - } - return false - }) { - matchingHeaders = append(matchingHeaders, match) - } - } + allowedHeaders := matchingOrigin.Headers.ValueOrFallback(defaultHeaders) - if len(matchingHeaders) != 0 { - // only set if any (matching) headers - w.Header().Set("Access-Control-Allow-Headers", strings.Join(matchingHeaders, ",")) - } + if len(allowedHeaders) != 0 { + // only set if any headers + w.Header().Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ",")) } w.WriteHeader(http.StatusNoContent)