diff --git a/.dockerignore b/.dockerignore
index 0c6693d..c23627f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,5 +2,5 @@
.git
.github
*.yaml
-LICENSE
*.md
+!config/*.yml
diff --git a/.github/templates/README.template.md b/.github/templates/README.template.md
index f3066e4..57924fe 100644
--- a/.github/templates/README.template.md
+++ b/.github/templates/README.template.md
@@ -1,22 +1,22 @@
-
+
-## Installation
+## Getting Started
Get the latest version of the `docker-compose.yaml` file:
-And add secure Token(s) to `API_TOKEN` / `API_TOKENS`. See [API_TOKEN(s)](#api-tokens)
+```yaml
+{ { file.docker-compose.yaml } }
+```
+
+And add secure Token(s) to `api.tokens`. See [API TOKENs](#api-tokens).
> [!IMPORTANT]
> This Documentation will be using `sec-signal-api:8880` as the service host,
-> this **won't work**, instead use your containers IP + Port.
+> this **is just for simplicty**, instead use your containers or hosts IP + Port.
> Or a hostname if applicable. See [Reverse Proxy](#reverse-proxy)
-```yaml
-{ { file.docker-compose.yaml } }
-```
-
### Reverse proxy
Take a look at the [traefik](https://github.com/traefik/traefik) implementation:
@@ -76,7 +76,7 @@ Here is a simple example:
curl -X POST http://sec-signal-api:8880/v2/send?@authorization=API_TOKEN
```
-Notice the `@` infront of `authorization`. See [Formatting](#format).
+Notice the `@` infront of `authorization`. See [KeyValue Pair Injection](#keyvalue-pair-injection).
### Example
@@ -90,9 +90,7 @@ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer API_T
#### Placeholders
-If you are not comfortable / don't want to hardcode your Number and/or Recipients in you may use **Placeholders** in your Request.
-
-Built-in Placeholders: `{{ .NUMBER }}` and `{{ .RECIPIENTS }}`
+If you are not comfortable / don't want to hardcode your Number for example and/or Recipients in you, may use **Placeholders** in your Request. See [Custom Variables](#variables).
These Placeholders can be used in the Request Query or the Body of a Request like so:
@@ -121,38 +119,88 @@ http://sec-signal-api:8880/v1/receive/{{.NUMBER}}
In some cases you may not be able to access / modify the Request Body, in that case specify needed values in the Request Query:
-Supported types include **strings**, **ints** and **arrays**
-
`http://sec-signal-api:8880/?@key=value`
-| type | example |
-| :--------- | :------ |
-| string | abc |
-| int | 123 |
-| array | [1,2,3] |
-| array(int) | 1,2,3 |
-| array(str) | a,b,c |
-
-##### Format
-
In order to differentiate Injection Queries and _regular_ Queries
you have to add `@` in front of any KeyValue Pair assignment.
-## Environment Variables
+Supported types include **strings**, **ints** and **arrays**. See [Formatting](#string-to-type).
+
+## Configuration
+
+There are multiple ways to configure Secured Signal API, you can optionally use `config.yml` aswell as Environment Variables to override the config.
+
+### Config Files
+
+Config files allow **YML** formatting and also `${ENV}` to get Environment Variables.
+
+To change the internal config file location set `CONFIG_PATH` in your **Environment** to an absolute path including the filename.extension. (default: `/config/config.yml`)
+
+This example config shows all of the individual settings that can be applied:
+
+```yaml
+{ { file.examples/config.yml } }
+```
+
+#### Token Configs
+
+You can also override the `config.yml` file for each individual token by adding configs under `TOKENS_PATH` (default: `config/tokens/`)
+
+This way you can permission tokens by further restricting or adding [Endpoints](#blocked-endpoints), [Placeholders](#variables), etc.
+
+Here is an example:
+
+```yaml
+{ { file.examples/token.yml } }
+```
+
+### Environment
+
+Suppose you want to set a new [Placeholder](#placeholders) `NUMBER` in your Environment...
+
+```yaml
+environment:
+ VARIABLES__NUMBER: "000"
+```
+
+This would internally be converted into `variables.number` matching the config formatting.
+
+> [!IMPORTANT]
+> Underscores `_` are removed during Conversion, Double Underscores `__` on the other hand convert the Variable into a nested Object (`__` replaced by `.`)
+
+### String To Type
+
+> [!TIP]
+> This formatting applies to almost every situation where the only (allowed) Input Type is a string and other Output Types are needed.
+
+If you are using Environment Variables as an example you won't be able to specify an Array or a Dictionary of items, in that case you can provide a specifically formatted string which will be translated into the correct type...
+
+| type | example |
+| :--------- | :---------------- |
+| string | abc |
+| string | +123 |
+| int | 123 |
+| int | -123 |
+| json | {"a":"b","c":"d"} |
+| array(int) | [1,2,3] |
+| array(str) | [a,b,c] |
+
+> [!NOTE]
+> If you have a string that should not be turned into any other type, then you will need to escape all Type Denotations, `[]` or `{}` (also `-`) with a `\` **Backslash**.
+> **Double Backslashes** do exist but you could just leave them out completly.
+> An **Odd** number of **Backslashes** **escape** the character in front of them and an **Even** number leave the character **as-is**.
### API Token(s)
-Both `API_TOKEN` and `API_TOKENS` support multiple Tokens seperated by a `,` **Comma**.
During Authentication Secured Signal API will try to match the given Token against the list of Tokens inside of these Variables.
```yaml
-environment:
- API_TOKEN: "token1, token2, token3"
- API_TOKENS: "token1, token2, token3"
+api:
+ tokens: [token1, token2, token3]
```
> [!IMPORTANT]
-> It is highly recommended to set this Environment Variable
+> It is highly recommended use API Tokens
> _What if I just don't?_
@@ -160,7 +208,7 @@ Secured Signal API will still work, but important Security Features won't be ava
like Blocked Endpoints and any sort of Auth.
> [!NOTE]
-> Blocked Endpoints can be reactivated by manually setting them in the Environment
+> Blocked Endpoints can be reactivated by manually configuring them
### Blocked Endpoints
@@ -177,53 +225,27 @@ Because Secured Signal API is just a Proxy you can use all of the [Signal REST A
| **/v1/accounts** |
| **/v1/contacts** |
-These Endpoints are blocked by default due to Security Risks, but can be modified by setting `BLOCKED_ENDPOINTS` to a Comma seperated List:
+These Endpoints are blocked by default due to Security Risks, but can be modified by setting `blockedEndpoints` in your config:
```yaml
-environment:
- BLOCKED_ENDPOINTS: |
- /v1/register,
- /v1/unregister,
- /v1/qrcodelink,
- /v1/contacts,
+blockedEndpoints: [/v1/register, /v1/unregister, /v1/qrcodelink, /v1/contacts]
```
-#### Variables
-
-By default Secured Signal API provides the following Placeholders:
-
-- **NUMBER** = _ENV_: `NUMBER`
-- **RECIPIENTS** = _ENV_: `RECIPIENTS`
-
-### Customization
-
-Placeholders can be added by setting `VARIABLES` inside your Environment.
-
-```yaml
-environment:
- VARIABLES: |
- "NUMBER2": "002",
- "GROUP_CHAT_1": [
- "user.id", "000", "001", "group.id"
- ]
-```
+### Variables
-### Recipients
+Placeholders can be added under `variables` and can then be referenced in the Body, Query or URL.
+See [Placeholders](#placeholders).
-Set this Environment Variable to automatically provide default Recipients:
+> [!NOTE]
+> Every Placeholder Key will be converted into an Uppercase String.
+> Example: `number` becomes `NUMBER` in `{{.NUMBER}}`
```yaml
-environment:
- RECIPIENTS: |
- user.id, 000, 001, group.id,
-```
-
-example:
-
-```json
-{
- "recipients": "{{.RECIPIENTS}}"
-}
+variables:
+ number: "001",
+ recipients: [
+ "user.id", "000", "001", "group.id"
+ ]
```
### Message Aliases
@@ -244,18 +266,34 @@ To improve compatibility with other services Secured Signal API provides aliases
Secured Signal API will pick the best scoring Message Alias (if available) to extract the correct message from the Request Body.
-Message Aliases can be added by setting `MESSAGE_ALIASES` to a valid json array containing dictionaries of `alias`, the json key to be used for lookup (use `.` dots for using values from a nested dictionary and `[i]` to get values from an array):
+Message Aliases can be added by setting `messageAliases` in your config:
```yaml
-environment:
- MESSAGE_ALIASES: |
- [
- { "alias": "msg", "score": 80 },
- { "alias": "data.message", "score": 79 },
- { "alias": "array[0].message", "score": 78 },
- ]
+messageAliases:
+ [
+ { alias: "msg", score: 80 },
+ { alias: "data.message", score: 79 },
+ { alias: "array[0].message", score: 78 },
+ ]
```
+### Port
+
+To change the Port which Secured Signal API uses, you need to set `server.port` in your config. (default: `8880`)
+
+### Log Level
+
+To change the Log Level set `logLevel` to: (default: `info`)
+
+| Level |
+| ------- |
+| `info` |
+| `debug` |
+| `warn` |
+| `error` |
+| `fatal` |
+| `dev` |
+
## Contributing
Found a bug? Want to change or add something?
diff --git a/Dockerfile b/Dockerfile
index dde3acf..109e2c5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,22 @@
FROM alpine:latest
RUN apk --no-cache add ca-certificates
-ENV PORT=8880
+ENV SERVER__PORT=8880
+
+ENV DEFAULTS_PATH=/app/config/defaults.yml
+
+ENV CONFIG_PATH=/config/config.yml
+ENV TOKENS_DIR=/config/tokens
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
+COPY . .
+
COPY dist/${TARGETOS}/${TARGETARCH}/app .
RUN ls
-CMD ["./app"]
\ No newline at end of file
+CMD ["./app"]
diff --git a/config/defaults.yml b/config/defaults.yml
new file mode 100644
index 0000000..db61d52
--- /dev/null
+++ b/config/defaults.yml
@@ -0,0 +1,39 @@
+server:
+ port: 8880
+
+logLevel: INFO
+
+messageAliases:
+ [
+ { alias: msg, score: 100 },
+ { alias: content, score: 99 },
+ { alias: description, score: 98 },
+ { alias: text, score: 20 },
+ { alias: summary, score: 15 },
+ { alias: details, score: 14 },
+
+ { alias: data.message, score: 10 },
+ { alias: data.content, score: 9 },
+ { alias: data.description, score: 8 },
+ { alias: data.text, score: 7 },
+ { alias: data.summary, score: 6 },
+ { alias: data.details, score: 5 },
+
+ { alias: payload, score: 3 },
+ { alias: body, score: 2 },
+ { alias: data, score: 1 },
+ ]
+
+variables:
+ recipients: ${RECIPIENTS}
+ number: ${NUMBER}
+
+blockedEndpoints:
+ - /v1/about
+ - /v1/configuration
+ - /v1/devices
+ - /v1/register
+ - /v1/unregister
+ - /v1/qrcodelink
+ - /v1/accounts
+ - /v1/contacts
diff --git a/docker-compose.yaml b/docker-compose.yaml
index ab87c22..9979186 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -20,10 +20,10 @@ services:
aliases:
- secured-signal-api
environment:
- SIGNAL_API_URL: http://signal-api:8080
- DEFAULT_RECIPIENTS: '[ "000", "001", "002" ]'
- NUMBER: 123456789
- API_TOKEN: LOOOOOONG_STRING
+ API__URL: http://signal-api:8080
+ VARIABLES__RECIPIENTS: 000,001,002
+ VARIABLES__NUMBER: 123456789
+ API__TOKENS: LOOOOOONG_STRING
ports:
- "8880:8880"
restart: unless-stopped
diff --git a/examples/config.yml b/examples/config.yml
new file mode 100644
index 0000000..f8e24c3
--- /dev/null
+++ b/examples/config.yml
@@ -0,0 +1,17 @@
+# Example Config (all configurations shown)
+
+api:
+ port: 8880
+ url: http://signal-api:8080
+ tokens: [token1, token2]
+
+logLevel: INFO
+
+variables:
+ number: "000"
+ recipients: ["001", "group.id", "user.id"]
+
+messageAliases: [{ alias: "msg", score: 100 }]
+
+blockedEndpoints:
+ - /v1/about
diff --git a/examples/token.yml b/examples/token.yml
new file mode 100644
index 0000000..b9b384c
--- /dev/null
+++ b/examples/token.yml
@@ -0,0 +1,7 @@
+token: LOOOONG_STRING
+
+overrides:
+ variables: # Disable Placeholder
+ blockedEndpoints: # Disable Sending
+ - /v2/send
+ messageAliases: # Disable Aliases
diff --git a/examples/traefik.docker-compose.yaml b/examples/traefik.docker-compose.yaml
index 78e8d72..7ef99b8 100644
--- a/examples/traefik.docker-compose.yaml
+++ b/examples/traefik.docker-compose.yaml
@@ -8,10 +8,10 @@ services:
aliases:
- secured-signal-api
environment:
- SIGNAL_API_URL: http://signal-api:8080
- DEFAULT_RECIPIENTS: '[ "000", "001", "002" ]'
+ API__URL: http://signal-api:8080
+ DEFAULT_RECIPIENTS: 000,001,002
NUMBER: 123456789
- API_TOKEN: LOOOOOONG_STRING
+ API__TOKENS: LOOOOOONG_STRING
labels:
- traefik.enable=true
- traefik.http.routers.signal-api.rule=Host(`signal-api.mydomain.com`)
diff --git a/go.mod b/go.mod
index a647fb6..fc1c638 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,23 @@ module github.com/codeshelldev/secured-signal-api
go 1.25.1
-require go.uber.org/zap v1.27.0
+require (
+ go.uber.org/zap v1.27.0
+ gopkg.in/yaml.v3 v3.0.1
+)
-require go.uber.org/multierr v1.11.0 // indirect
+require (
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
+ github.com/knadh/koanf/maps v0.1.2 // indirect
+ github.com/knadh/koanf/parsers/yaml v1.1.0
+ github.com/knadh/koanf/providers/confmap v1.0.0
+ github.com/knadh/koanf/providers/env/v2 v2.0.0
+ github.com/knadh/koanf/providers/file v1.2.0
+ github.com/knadh/koanf/v2 v2.2.2
+ 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.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/sys v0.36.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 9fe9a49..166ba14 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,45 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
+github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+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=
+github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
+github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
+github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
+github.com/knadh/koanf/providers/env/v2 v2.0.0 h1:Ad5H3eun722u+FvchiIcEIJZsZ2M6oxCkgZfWN5B5KY=
+github.com/knadh/koanf/providers/env/v2 v2.0.0/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc=
+github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
+github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
+github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
+github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
+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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+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=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
+golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+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=
diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go
index a5676aa..7c7339c 100644
--- a/internals/proxy/middlewares/auth.go
+++ b/internals/proxy/middlewares/auth.go
@@ -1,28 +1,20 @@
package middlewares
import (
+ "context"
"encoding/base64"
"net/http"
"slices"
"strings"
+ "github.com/codeshelldev/secured-signal-api/utils/config"
log "github.com/codeshelldev/secured-signal-api/utils/logger"
)
type AuthMiddleware struct {
- Next http.Handler
- Tokens []string
+ Next http.Handler
}
-type authType string
-
-const (
- Bearer authType = "Bearer"
- Basic authType = "Basic"
- Query authType = "Query"
- None authType = "None"
-)
-
func getAuthType(str string) authType {
switch str {
case "Bearer":
@@ -40,7 +32,7 @@ func isValidToken(tokens []string, match string) (bool) {
func (data AuthMiddleware) Use() http.Handler {
next := data.Next
- tokens := data.Tokens
+ tokens := config.ENV.API_TOKENS
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if len(tokens) <= 0 {
@@ -54,13 +46,15 @@ func (data AuthMiddleware) Use() http.Handler {
var authType authType = None
+ var authToken string
+
success := false
if authHeader != "" {
authBody := strings.Split(authHeader, " ")
authType = getAuthType(authBody[0])
- authToken := authBody[1]
+ authToken = authBody[1]
switch authType {
case Bearer:
@@ -88,7 +82,7 @@ func (data AuthMiddleware) Use() http.Handler {
} else if authQuery != "" {
authType = Query
- authToken := strings.TrimSpace(authQuery)
+ authToken = strings.TrimSpace(authQuery)
if isValidToken(tokens, authToken) {
success = true
@@ -109,6 +103,9 @@ func (data AuthMiddleware) Use() http.Handler {
return
}
+ ctx := context.WithValue(req.Context(), tokenKey, authToken)
+ req = req.WithContext(ctx)
+
next.ServeHTTP(w, req)
})
}
diff --git a/internals/proxy/middlewares/body.go b/internals/proxy/middlewares/body.go
index d3888e3..67619d8 100644
--- a/internals/proxy/middlewares/body.go
+++ b/internals/proxy/middlewares/body.go
@@ -6,26 +6,26 @@ import (
"net/http"
"strconv"
+ middlewareTypes "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares/types"
"github.com/codeshelldev/secured-signal-api/utils"
log "github.com/codeshelldev/secured-signal-api/utils/logger"
request "github.com/codeshelldev/secured-signal-api/utils/request"
)
-type MessageAlias struct {
- Alias string
- Score int
-}
-
type BodyMiddleware struct {
- Next http.Handler
- MessageAliases []MessageAlias
+ Next http.Handler
}
func (data BodyMiddleware) Use() http.Handler {
next := data.Next
- messageAliases := data.MessageAliases
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ messageAliases := getSettingsByReq(req).MESSAGE_ALIASES
+
+ if messageAliases == nil {
+ messageAliases = getSettings("*").MESSAGE_ALIASES
+ }
+
body, err := request.GetReqBody(w, req)
if err != nil {
@@ -33,7 +33,7 @@ func (data BodyMiddleware) Use() http.Handler {
}
var modifiedBody bool
- var bodyData map[string]interface{}
+ var bodyData map[string]any
if !body.Empty {
bodyData = body.Data
@@ -70,7 +70,7 @@ func (data BodyMiddleware) Use() http.Handler {
})
}
-func getMessage(aliases []MessageAlias, data map[string]interface{}) (string, map[string]interface{}) {
+func getMessage(aliases []middlewareTypes.MessageAlias, data map[string]any) (string, map[string]any) {
var content string
var best int
@@ -87,10 +87,10 @@ func getMessage(aliases []MessageAlias, data map[string]interface{}) (string, ma
return content, data
}
-func processAlias(alias MessageAlias, data map[string]interface{}) (string, int, bool) {
+func processAlias(alias middlewareTypes.MessageAlias, data map[string]any) (string, int, bool) {
aliasKey := alias.Alias
- value, ok := utils.GetJsonByPath(aliasKey, data)
+ value, ok := utils.GetByPath(aliasKey, data)
aliasValue, isStr := value.(string)
diff --git a/internals/proxy/middlewares/common.go b/internals/proxy/middlewares/common.go
new file mode 100644
index 0000000..1c04a2f
--- /dev/null
+++ b/internals/proxy/middlewares/common.go
@@ -0,0 +1,44 @@
+package middlewares
+
+import (
+ "net/http"
+
+ "github.com/codeshelldev/secured-signal-api/utils/config"
+)
+
+type Context struct {
+ Next http.Handler
+}
+
+type authType string
+
+const (
+ Bearer authType = "Bearer"
+ Basic authType = "Basic"
+ Query authType = "Query"
+ None authType = "None"
+)
+
+type contextKey string
+
+const tokenKey contextKey = "token"
+
+func getSettingsByReq(req *http.Request) *config.SETTING_ {
+ token, ok := req.Context().Value(tokenKey).(string)
+
+ if !ok {
+ token = "*"
+ }
+
+ return getSettings(token)
+}
+
+func getSettings(token string) *config.SETTING_ {
+ settings, exists := config.ENV.SETTINGS[token]
+
+ if !exists || settings == nil {
+ settings = config.ENV.SETTINGS["*"]
+ }
+
+ return settings
+}
\ No newline at end of file
diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go
index 6a14c7e..173fff2 100644
--- a/internals/proxy/middlewares/endpoints.go
+++ b/internals/proxy/middlewares/endpoints.go
@@ -9,17 +9,21 @@ import (
type EndpointsMiddleware struct {
Next http.Handler
- BlockedEndpoints []string
}
func (data EndpointsMiddleware) Use() http.Handler {
next := data.Next
- BLOCKED_ENDPOINTS := data.BlockedEndpoints
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ blockedEndpoints := getSettingsByReq(req).BLOCKED_ENDPOINTS
+
+ if blockedEndpoints == nil {
+ blockedEndpoints = getSettings("*").BLOCKED_ENDPOINTS
+ }
+
reqPath := req.URL.Path
- if slices.Contains(BLOCKED_ENDPOINTS, reqPath) {
+ if slices.Contains(blockedEndpoints, reqPath) {
log.Warn("User tried to access blocked endpoint: ", reqPath)
http.Error(w, "Forbidden", http.StatusForbidden)
return
diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go
index bb7e67a..83d5a3a 100644
--- a/internals/proxy/middlewares/template.go
+++ b/internals/proxy/middlewares/template.go
@@ -16,28 +16,32 @@ import (
type TemplateMiddleware struct {
Next http.Handler
- Variables map[string]interface{}
}
func (data TemplateMiddleware) Use() http.Handler {
next := data.Next
- VARIABLES := data.Variables
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ variables := getSettingsByReq(req).VARIABLES
+
+ if variables == nil {
+ variables = getSettings("*").VARIABLES
+ }
+
body, err := request.GetReqBody(w, req)
if err != nil {
log.Error("Could not get Request Body: ", err.Error())
}
- bodyData := map[string]interface{}{}
+ bodyData := map[string]any{}
var modifiedBody bool
if !body.Empty {
var modified bool
- bodyData, modified, err = TemplateBody(body.Data, VARIABLES)
+ bodyData, modified, err = TemplateBody(body.Data, variables)
if err != nil {
log.Error("Error Templating JSON: ", err.Error())
@@ -51,7 +55,7 @@ func (data TemplateMiddleware) Use() http.Handler {
if req.URL.RawQuery != "" {
var modified bool
- req.URL.RawQuery, bodyData, modified, err = TemplateQuery(req.URL, bodyData, VARIABLES)
+ req.URL.RawQuery, bodyData, modified, err = TemplateQuery(req.URL, bodyData, variables)
if err != nil {
log.Error("Error Templating Query: ", err.Error())
@@ -85,7 +89,7 @@ func (data TemplateMiddleware) Use() http.Handler {
if req.URL.Path != "" {
var modified bool
- req.URL.Path, modified, err = TemplatePath(req.URL, VARIABLES)
+ req.URL.Path, modified, err = TemplatePath(req.URL, variables)
if err != nil {
log.Error("Error Templating Path: ", err.Error())
@@ -100,7 +104,7 @@ func (data TemplateMiddleware) Use() http.Handler {
})
}
-func TemplateBody(data map[string]interface{}, VARIABLES any) (map[string]interface{}, bool, error) {
+func TemplateBody(data map[string]any, VARIABLES any) (map[string]any, bool, error) {
var modified bool
templatedData, err := templating.RenderJSONTemplate("body", data, VARIABLES)
@@ -139,7 +143,7 @@ func TemplatePath(reqUrl *url.URL, VARIABLES any) (string, bool, error) {
return reqPath, modified, nil
}
-func TemplateQuery(reqUrl *url.URL, data map[string]interface{}, VARIABLES any) (string, map[string]interface{}, bool, error) {
+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)
diff --git a/internals/proxy/middlewares/types/types.go b/internals/proxy/middlewares/types/types.go
new file mode 100644
index 0000000..b22a473
--- /dev/null
+++ b/internals/proxy/middlewares/types/types.go
@@ -0,0 +1,6 @@
+package middlewareTypes
+
+type MessageAlias struct {
+ Alias string `koanf:"alias"`
+ Score int `koanf:"score"`
+}
\ No newline at end of file
diff --git a/main.go b/main.go
index b4b434c..9a8cdb5 100644
--- a/main.go
+++ b/main.go
@@ -7,48 +7,48 @@ import (
proxy "github.com/codeshelldev/secured-signal-api/internals/proxy"
middlewares "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares"
+ config "github.com/codeshelldev/secured-signal-api/utils/config"
docker "github.com/codeshelldev/secured-signal-api/utils/docker"
- env "github.com/codeshelldev/secured-signal-api/utils/env"
log "github.com/codeshelldev/secured-signal-api/utils/logger"
)
var initHandler *httputil.ReverseProxy
-var ENV env.ENV_
+var ENV *config.ENV_
func main() {
logLevel := os.Getenv("LOG_LEVEL")
log.Init(logLevel)
- env.Load()
+ config.Load()
- ENV = env.ENV
+ ENV = config.ENV
+
+ if ENV.LOG_LEVEL != "" {
+ log.Init(ENV.LOG_LEVEL)
+ }
initHandler = proxy.Create(ENV.API_URL)
body_m4 := middlewares.BodyMiddleware{
- Next: initHandler,
- MessageAliases: ENV.MESSAGE_ALIASES,
+ Next: initHandler,
}
temp_m3 := middlewares.TemplateMiddleware{
- Next: body_m4.Use(),
- Variables: ENV.VARIABLES,
+ Next: body_m4.Use(),
}
endp_m2 := middlewares.EndpointsMiddleware{
- Next: temp_m3.Use(),
- BlockedEndpoints: ENV.BLOCKED_ENDPOINTS,
+ Next: temp_m3.Use(),
}
auth_m1 := middlewares.AuthMiddleware{
Next: endp_m2.Use(),
- Tokens: ENV.API_TOKENS,
}
log_m0 := middlewares.LogMiddleware{
- Next: auth_m1.Use(),
+ Next: auth_m1.Use(),
}
log.Info("Initialized Proxy Handler")
diff --git a/tests/json_test.go b/tests/json_test.go
index b08b9ef..1e92182 100644
--- a/tests/json_test.go
+++ b/tests/json_test.go
@@ -8,7 +8,7 @@ import (
)
func TestJsonTemplating(t *testing.T) {
- variables := map[string]interface{}{
+ variables := map[string]any{
"array": []string{
"item0",
"item1",
@@ -28,17 +28,17 @@ func TestJsonTemplating(t *testing.T) {
"key2": "{{.int}}"
}`
- data := utils.GetJson[map[string]interface{}](json)
+ data := utils.GetJson[map[string]any](json)
- expected := map[string]interface{}{
- "dict": map[string]interface{}{
+ expected := map[string]any{
+ "dict": map[string]any{
"key": "val",
},
- "dictArray": []interface{}{
- map[string]interface{}{"key": "val"},
- map[string]interface{}{"key": []interface{}{ "item0", "item1" }},
+ "dictArray": []any{
+ map[string]any{"key": "val"},
+ map[string]any{"key": []any{ "item0", "item1" }},
},
- "key1": []interface{}{ "item0", "item1" },
+ "key1": []any{ "item0", "item1" },
"key2": 4,
}
@@ -71,7 +71,7 @@ func TestJsonPath(t *testing.T) {
"key": "val"
}`
- data := utils.GetJson[map[string]interface{}](json)
+ data := utils.GetJson[map[string]any](json)
cases := []struct{
key string
@@ -107,7 +107,7 @@ func TestJsonPath(t *testing.T) {
key := c.key
expected := c.expected
- got, ok := utils.GetJsonByPath(key, data)
+ got, ok := utils.GetByPath(key, data)
if !ok || got.(string) != expected {
t.Error("Expected: ", key, " == ", expected, "; Got: ", got)
diff --git a/tests/string_test.go b/tests/string_test.go
new file mode 100644
index 0000000..e7d79b0
--- /dev/null
+++ b/tests/string_test.go
@@ -0,0 +1,78 @@
+package tests
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/codeshelldev/secured-signal-api/utils/safestrings"
+)
+
+func TestStringEscaping(t *testing.T) {
+ str1 := `\#`
+
+ res1 := safestrings.IsEscaped(str1, "#")
+
+ if !res1 {
+ t.Error("Expected: ", str1, " == true", "; Got: ", str1, " == ", res1)
+ }
+
+ str2 := "#"
+
+ res2 := safestrings.IsEscaped(str2, "#")
+
+ if res2 {
+ t.Error("Expected: ", str2, " == false", "; Got: ", str2, " == ", res2)
+ }
+
+ str3 := `#\#`
+
+ res3 := safestrings.Contains(str3, "#")
+
+ if !res3 {
+ t.Error("Expected: ", str3, " == true", "; Got: ", str3, " == ", res3)
+ }
+}
+
+func TestStringEnclosement(t *testing.T) {
+ str1 := "[enclosed]"
+
+ res1 := safestrings.IsEnclosedBy(str1, `[`, `]`)
+
+ if !res1 {
+ t.Error("Expected: ", str1, " == true", "; Got: ", str1, " == ", res1)
+ }
+
+ str2 := `\[enclosed]`
+
+ res2 := safestrings.IsEnclosedBy(str2, `[`, `]`)
+
+ if res2 {
+ t.Error("Expected: ", str2, " == false", "; Got: ", str2, " == ", res2)
+ }
+}
+
+func TestStringToType(t *testing.T) {
+ str1 := `[item1,item2]`
+
+ res1 := safestrings.ToType(str1)
+
+ if reflect.TypeOf(res1) != reflect.TypeFor[[]string]() {
+ t.Error("Expected: ", str1, " == []string", "; Got: ", str1, " == ", reflect.TypeOf(res1))
+ }
+
+ str2 := `1`
+
+ res2 := safestrings.ToType(str2)
+
+ if reflect.TypeOf(res2) != reflect.TypeFor[int]() {
+ t.Error("Expected: ", str2, " == int", "; Got: ", str2, " == ", reflect.TypeOf(res2))
+ }
+
+ str3 := `{ "key": "value" }`
+
+ res3 := safestrings.ToType(str3)
+
+ if reflect.TypeOf(res3) != reflect.TypeFor[map[string]any]() {
+ t.Error("Expected: ", str3, " == map[string]any", "; Got: ", str3, " == ", reflect.TypeOf(res3))
+ }
+}
\ No newline at end of file
diff --git a/utils/config/config.go b/utils/config/config.go
new file mode 100644
index 0000000..8b0ed54
--- /dev/null
+++ b/utils/config/config.go
@@ -0,0 +1,220 @@
+package config
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ log "github.com/codeshelldev/secured-signal-api/utils/logger"
+ "github.com/codeshelldev/secured-signal-api/utils/safestrings"
+
+ "github.com/knadh/koanf/providers/confmap"
+ "github.com/knadh/koanf/providers/env/v2"
+ "github.com/knadh/koanf/providers/file"
+ "github.com/knadh/koanf/v2"
+)
+
+var defaultsLayer = koanf.New(".")
+var userLayer = koanf.New(".")
+var tokensLayer = koanf.New(".")
+
+var config *koanf.Koanf
+
+var configLock sync.Mutex
+
+func LoadFile(path string, config *koanf.Koanf, parser koanf.Parser) (koanf.Provider, error) {
+ f := file.Provider(path)
+
+ err := config.Load(f, parser)
+
+ if err != nil {
+ return nil, err
+ }
+
+ WatchFile(path, f)
+
+ return f, err
+}
+
+func WatchFile(path string, f *file.File) {
+ f.Watch(func(event any, err error) {
+ if err != nil {
+ return
+ }
+
+ log.Info(path, " changed, Reloading...")
+
+ configLock.Lock()
+ defer configLock.Unlock()
+
+ Load()
+ })
+}
+
+func LoadDir(path string, dir string, config *koanf.Koanf, parser koanf.Parser) error {
+ files, err := filepath.Glob(filepath.Join(dir, "*.yml"))
+
+ if err != nil {
+ return nil
+ }
+
+ var array []any
+
+ for _, f := range files {
+ tmp := koanf.New(".")
+
+ _, err := LoadFile(f, tmp, parser)
+
+ if err != nil {
+ return err
+ }
+
+ array = append(array, tmp.Raw())
+ }
+
+ wrapper := map[string]any{
+ path: array,
+ }
+
+ return config.Load(confmap.Provider(wrapper, "."), nil)
+}
+
+func LoadEnv(config *koanf.Koanf) (koanf.Provider, error) {
+ e := env.Provider(".", env.Opt{
+ TransformFunc: normalizeEnv,
+ })
+
+ err := config.Load(e, nil)
+
+ if err != nil {
+ log.Fatal("Error loading env: ", err.Error())
+ }
+
+ return e, err
+}
+
+func templateConfig(config *koanf.Koanf) {
+ data := config.All()
+
+ for key, value := range data {
+ str, isStr := value.(string)
+
+ if isStr {
+ templated := os.ExpandEnv(str)
+
+ if templated != "" {
+ data[key] = templated
+ }
+ }
+ }
+
+ config.Load(confmap.Provider(data, "."), nil)
+}
+
+func mergeLayers() *koanf.Koanf {
+ final := koanf.New(".")
+
+ final.Merge(defaultsLayer)
+ final.Merge(userLayer)
+
+ return final
+}
+
+func normalizeKeys(config *koanf.Koanf) {
+ data := map[string]any{}
+
+ for _, key := range config.Keys() {
+ lower := strings.ToLower(key)
+
+ data[lower] = config.Get(key)
+ }
+
+ config.Delete("")
+ config.Load(confmap.Provider(data, "."), nil)
+}
+
+// Transforms Children of path
+func transformChildren(config *koanf.Koanf, path string, transform func(key string, value any) (string, any)) error {
+ var sub map[string]any
+
+ if !config.Exists(path) {
+ return errors.New("invalid path")
+ }
+
+ err := config.Unmarshal(path, &sub)
+
+ if err != nil {
+ return err
+ }
+
+ transformed := make(map[string]any)
+
+ for key, val := range sub {
+ newKey, newVal := transform(key, val)
+
+ transformed[newKey] = newVal
+ }
+
+ config.Delete(path)
+
+ config.Load(confmap.Provider(map[string]any{
+ path: transformed,
+ }, "."), nil)
+
+ return nil
+}
+
+// Does the same thing as transformChildren() but does it for each Array Item inside of root and transforms subPath
+func transformChildrenUnderArray(config *koanf.Koanf, root string, subPath string, transform func(key string, value any) (string, any)) error {
+ var array []map[string]any
+
+ err := config.Unmarshal(root, &array)
+ if err != nil {
+ return err
+ }
+
+ transformed := []map[string]any{}
+
+ for _, data := range array {
+ tmp := koanf.New(".")
+
+ tmp.Load(confmap.Provider(map[string]any{
+ "item": data,
+ }, "."), nil)
+
+ err := transformChildren(tmp, "item." + subPath, transform)
+
+ if err != nil {
+ return err
+ }
+
+ item := tmp.Get("item")
+
+ if item != nil {
+ itemMap, ok := item.(map[string]any)
+
+ if ok {
+ transformed = append(transformed, itemMap)
+ }
+ }
+ }
+
+ config.Delete(root)
+
+ config.Load(confmap.Provider(map[string]any{
+ root: transformed,
+ }, "."), nil)
+
+ return nil
+}
+
+
+func normalizeEnv(key string, value string) (string, any) {
+ key = strings.ToLower(key)
+ key = strings.ReplaceAll(key, "__", ".")
+ key = strings.ReplaceAll(key, "_", "")
+
+ return key, safestrings.ToType(value)
+}
\ No newline at end of file
diff --git a/utils/config/loader.go b/utils/config/loader.go
new file mode 100644
index 0000000..be45893
--- /dev/null
+++ b/utils/config/loader.go
@@ -0,0 +1,114 @@
+package config
+
+import (
+ "errors"
+ "io/fs"
+ "os"
+ "strconv"
+ "strings"
+
+ middlewareTypes "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares/types"
+ log "github.com/codeshelldev/secured-signal-api/utils/logger"
+ "github.com/knadh/koanf/parsers/yaml"
+)
+
+type ENV_ struct {
+ CONFIG_PATH string
+ DEFAULTS_PATH string
+ TOKENS_DIR string
+ LOG_LEVEL string
+ PORT string
+ API_URL string
+ API_TOKENS []string
+ SETTINGS map[string]*SETTING_
+ INSECURE bool
+}
+
+type SETTING_ struct {
+ BLOCKED_ENDPOINTS []string `koanf:"blockedendpoints"`
+ VARIABLES map[string]any `koanf:"variables"`
+ MESSAGE_ALIASES []middlewareTypes.MessageAlias `koanf:"messagealiases"`
+}
+
+var ENV *ENV_ = &ENV_{
+ CONFIG_PATH: os.Getenv("CONFIG_PATH"),
+ DEFAULTS_PATH: os.Getenv("DEFAULTS_PATH"),
+ TOKENS_DIR: os.Getenv("TOKENS_DIR"),
+ API_TOKENS: []string{},
+ SETTINGS: map[string]*SETTING_{
+ "*": {
+ BLOCKED_ENDPOINTS: []string{},
+ MESSAGE_ALIASES: []middlewareTypes.MessageAlias{},
+ VARIABLES: map[string]any{},
+ },
+ },
+ INSECURE: false,
+}
+
+func InitEnv() {
+ ENV.PORT = strconv.Itoa(config.Int("server.port"))
+
+ ENV.LOG_LEVEL = config.String("loglevel")
+
+ ENV.API_URL = config.String("api.url")
+
+ defaultSettings := ENV.SETTINGS["*"]
+
+ config.Unmarshal("messagealiases", &defaultSettings.MESSAGE_ALIASES)
+
+ transformChildren(config, "variables", func(key string, value any) (string, any) {
+ return strings.ToUpper(key), value
+ })
+
+ config.Unmarshal("variables", &defaultSettings.VARIABLES)
+
+ defaultSettings.BLOCKED_ENDPOINTS = config.Strings("blockedendpoints")
+}
+
+func Load() {
+ LoadDefaults()
+
+ LoadConfig()
+
+ LoadTokens()
+
+ log.Debug("Loading DotEnv")
+
+ LoadEnv(userLayer)
+
+ config = mergeLayers()
+
+ normalizeKeys(config)
+
+ templateConfig(config)
+
+ InitTokens()
+
+ InitEnv()
+
+ log.Info("Finished Loading Configuration")
+}
+
+func LoadDefaults() {
+ log.Debug("Loading Config ", ENV.DEFAULTS_PATH)
+
+ _, defErr := LoadFile(ENV.DEFAULTS_PATH, defaultsLayer, yaml.Parser())
+
+ if defErr != nil {
+ log.Warn("Could not Load Defaults", ENV.DEFAULTS_PATH)
+ }
+}
+
+func LoadConfig() {
+ log.Debug("Loading Config ", ENV.CONFIG_PATH)
+
+ _, conErr := LoadFile(ENV.CONFIG_PATH, userLayer, yaml.Parser())
+
+ if conErr != nil {
+ _, err := os.Stat(ENV.CONFIG_PATH)
+
+ if !errors.Is(err, fs.ErrNotExist) {
+ log.Error("Could not Load Config ", ENV.CONFIG_PATH, ": ", conErr.Error())
+ }
+ }
+}
\ No newline at end of file
diff --git a/utils/config/tokens.go b/utils/config/tokens.go
new file mode 100644
index 0000000..ce995a0
--- /dev/null
+++ b/utils/config/tokens.go
@@ -0,0 +1,74 @@
+package config
+
+import (
+ "strconv"
+ "strings"
+
+ log "github.com/codeshelldev/secured-signal-api/utils/logger"
+ "github.com/knadh/koanf/parsers/yaml"
+)
+
+type TOKEN_CONFIG_ struct {
+ TOKENS []string `koanf:"tokens"`
+ OVERRIDES SETTING_ `koanf:"overrides"`
+}
+
+func LoadTokens() {
+ log.Debug("Loading Configs ", ENV.TOKENS_DIR)
+
+ LoadDir("tokenconfigs", ENV.TOKENS_DIR, tokensLayer, yaml.Parser())
+
+ normalizeKeys(tokensLayer)
+
+ templateConfig(tokensLayer)
+}
+
+func InitTokens() {
+ apiTokens := config.Strings("api.tokens")
+
+ var tokenConfigs []TOKEN_CONFIG_
+
+ transformChildrenUnderArray(tokensLayer, "tokenconfigs", "overrides.variables", func(key string, value any) (string, any) {
+ return strings.ToUpper(key), value
+ })
+
+ tokensLayer.Unmarshal("tokenconfigs", &tokenConfigs)
+
+ overrides := parseTokenConfigs(tokenConfigs)
+
+ for token, override := range overrides {
+ apiTokens = append(apiTokens, token)
+
+ ENV.SETTINGS[token] = &override
+ }
+
+ if len(apiTokens) <= 0 {
+ log.Warn("No API TOKEN provided this is NOT recommended")
+
+ log.Info("Disabling Security Features due to incomplete Congfiguration")
+
+ ENV.INSECURE = true
+
+ // Set Blocked Endpoints on Config to User Layer Value
+ // => effectively ignoring Default Layer
+ config.Set("blockedendpoints", userLayer.Strings("blockeendpoints"))
+ }
+
+ if len(apiTokens) > 0 {
+ log.Debug("Registered " + strconv.Itoa(len(apiTokens)) + " Tokens")
+
+ ENV.API_TOKENS = apiTokens
+ }
+}
+
+func parseTokenConfigs(configs []TOKEN_CONFIG_) (map[string]SETTING_) {
+ settings := map[string]SETTING_{}
+
+ for _, config := range configs {
+ for _, token := range config.TOKENS {
+ settings[token] = config.OVERRIDES
+ }
+ }
+
+ return settings
+}
\ No newline at end of file
diff --git a/utils/env/env.go b/utils/env/env.go
deleted file mode 100644
index 80657b8..0000000
--- a/utils/env/env.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package env
-
-import (
- "os"
- "strconv"
- "strings"
-
- middlewares "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares"
- "github.com/codeshelldev/secured-signal-api/utils"
- "github.com/codeshelldev/secured-signal-api/utils/docker"
- log "github.com/codeshelldev/secured-signal-api/utils/logger"
-)
-
-type ENV_ struct {
- PORT string
- API_URL string
- API_TOKENS []string
- BLOCKED_ENDPOINTS []string
- VARIABLES map[string]any
- MESSAGE_ALIASES []middlewares.MessageAlias
-}
-
-var ENV ENV_ = ENV_{
- BLOCKED_ENDPOINTS: []string {
- "/v1/about",
- "/v1/configuration",
- "/v1/devices",
- "/v1/register",
- "/v1/unregister",
- "/v1/qrcodelink",
- "/v1/accounts",
- "/v1/contacts",
- },
- VARIABLES: map[string]any {
- "RECIPIENTS": []string{},
- "NUMBER": os.Getenv("NUMBER"),
- },
- MESSAGE_ALIASES: []middlewares.MessageAlias{
- {
- Alias: "msg",
- Score: 100,
- },
- {
- Alias: "content",
- Score: 99,
- },
- {
- Alias: "description",
- Score: 98,
- },
- {
- Alias: "text",
- Score: 20,
- },
- {
- Alias: "body",
- Score: 15,
- },
- {
- Alias: "summary",
- Score: 10,
- },
- {
- Alias: "details",
- Score: 9,
- },
- {
- Alias: "payload",
- Score: 2,
- },
- {
- Alias: "data",
- Score: 1,
- },
- },
-}
-
-func Load() {
- ENV.PORT = os.Getenv("PORT")
- ENV.API_URL = os.Getenv("SIGNAL_API_URL")
-
- apiToken := os.Getenv("API_TOKENS")
-
- if apiToken == "" {
- apiToken = os.Getenv("API_TOKEN")
- }
-
- blockedEndpointStrArray := os.Getenv("BLOCKED_ENDPOINTS")
- recipientsStrArray := os.Getenv("RECIPIENTS")
-
- messageAliasesJSON := os.Getenv("MESSAGE_ALIASES")
- variablesJSON := os.Getenv("VARIABLES")
-
- log.Info("Loaded Environment Variables")
-
- apiTokens := utils.StringToArray(apiToken)
-
- if apiTokens == nil {
- log.Warn("No API TOKEN provided this is NOT recommended")
-
- log.Info("Disabling Security Features due to incomplete Congfiguration")
-
- ENV.BLOCKED_ENDPOINTS = []string{}
- } else {
- log.Debug("Registered " + strconv.Itoa(len(apiTokens)) + " Tokens")
-
- ENV.API_TOKENS = apiTokens
- }
-
- if blockedEndpointStrArray != "" {
- if strings.Contains(blockedEndpointStrArray, "[") || strings.Contains(blockedEndpointStrArray, "]") {
- //! Deprecated: JSON
- //TODO: Remove this in new Version
-
- log.Error("Invalid Blocked Endpoints: ", "JSON instead of Comma seperated String")
-
- docker.Exit(1)
- }
-
- ENV.BLOCKED_ENDPOINTS = utils.StringToArray(blockedEndpointStrArray)
- }
-
- if messageAliasesJSON != "" {
- ENV.MESSAGE_ALIASES = utils.GetJson[[]middlewares.MessageAlias](messageAliasesJSON)
- }
-
- if variablesJSON != "" {
- ENV.VARIABLES = utils.GetJson[map[string]any](variablesJSON)
- }
-
- if recipientsStrArray != "" {
- if strings.Contains(blockedEndpointStrArray, "[") || strings.Contains(blockedEndpointStrArray, "]") {
- //! Deprecated: JSON
- //TODO: Remove this in new Version
-
- log.Error("Invalid Blocked Endpoints: ", "JSON instead of Comma seperated String")
-
- docker.Exit(1)
- }
-
- ENV.VARIABLES["RECIPIENTS"] = utils.StringToArray(recipientsStrArray)
- }
-}
\ No newline at end of file
diff --git a/utils/logger/logger.go b/utils/logger/logger.go
index 78887b3..871ba94 100644
--- a/utils/logger/logger.go
+++ b/utils/logger/logger.go
@@ -9,9 +9,12 @@ import (
)
var _log *zap.Logger
+var _logLevel = ""
func Init(level string) {
- logLevel := getLogLevel(level)
+ _logLevel = strings.ToLower(level)
+
+ logLevel := getLogLevel(_logLevel)
cfg := zap.Config{
Level: zap.NewAtomicLevelAt(logLevel),
@@ -52,6 +55,8 @@ func getLogLevel(level string) zapcore.Level {
return zapcore.InfoLevel
case "debug":
return zapcore.DebugLevel
+ case "dev":
+ return zapcore.DebugLevel
case "warn":
return zapcore.WarnLevel
case "error":
@@ -71,6 +76,12 @@ func Debug(msg ...string) {
_log.Debug(strings.Join(msg, ""))
}
+func Dev(msg ...string) {
+ if _logLevel == "dev" {
+ _log.Debug(strings.Join(msg, ""))
+ }
+}
+
func Error(msg ...string) {
_log.Error(strings.Join(msg, ""))
}
diff --git a/utils/query/query.go b/utils/query/query.go
index 4fc3a55..98a50ce 100644
--- a/utils/query/query.go
+++ b/utils/query/query.go
@@ -1,9 +1,9 @@
package query
import (
- "regexp"
- "strconv"
"strings"
+
+ "github.com/codeshelldev/secured-signal-api/utils/safestrings"
)
func ParseRawQuery(raw string) map[string][]string {
@@ -30,59 +30,14 @@ func ParseRawQuery(raw string) map[string][]string {
return result
}
-func tryParseInt(str string) (int, bool) {
- isInt, err := regexp.MatchString(`^\d+$`, str)
-
- if isInt && err == nil {
- intValue, err := strconv.Atoi(str)
-
- if err == nil {
- return intValue, true
- }
- }
-
- return 0, false
-}
-
-func ParseTypedQueryValues(values []string) interface{} {
- var result interface{}
-
- raw := values[0]
-
- intValue, isInt := tryParseInt(raw)
+func ParseTypedQueryValues(values []string) any {
+ raw := values[len(values)-1]
- if strings.Contains(raw, ",") || (strings.Contains(raw, "[") && strings.Contains(raw, "]")) {
- if strings.Contains(raw, "[") && strings.Contains(raw, "]") {
- escapedStr := strings.ReplaceAll(raw, "[", "")
- escapedStr = strings.ReplaceAll(escapedStr, "]", "")
- raw = escapedStr
- }
-
- parts := strings.Split(raw, ",")
-
- var list []interface{}
-
- for _, part := range parts {
- _intValue, _isInt := tryParseInt(part)
-
- if _isInt {
- list = append(list, _intValue)
- } else {
- list = append(list, part)
- }
- }
- result = list
- } else if isInt {
- result = intValue
- } else {
- result = raw
- }
-
- return result
+ return safestrings.ToType(raw)
}
-func ParseTypedQuery(query string, matchPrefix string) (map[string]interface{}) {
- addedData := map[string]interface{}{}
+func ParseTypedQuery(query string, matchPrefix string) (map[string]any) {
+ addedData := map[string]any{}
queryData := ParseRawQuery(query)
diff --git a/utils/request/request.go b/utils/request/request.go
index 723e586..cf26ab5 100644
--- a/utils/request/request.go
+++ b/utils/request/request.go
@@ -19,7 +19,7 @@ const (
type BodyType string
type Body struct {
- Data map[string]interface{}
+ Data map[string]any
Raw []byte
Empty bool
}
@@ -28,7 +28,7 @@ func (body Body) ToString() string {
return string(body.Raw)
}
-func CreateBody(data map[string]interface{}) (Body, error) {
+func CreateBody(data map[string]any) (Body, error) {
if len(data) <= 0 {
err := errors.New("empty data map")
@@ -51,8 +51,8 @@ func CreateBody(data map[string]interface{}) (Body, error) {
}, nil
}
-func GetJsonData(body []byte) (map[string]interface{}, error) {
- var data map[string]interface{}
+func GetJsonData(body []byte) (map[string]any, error) {
+ var data map[string]any
err := json.Unmarshal(body, &data)
@@ -64,8 +64,8 @@ func GetJsonData(body []byte) (map[string]interface{}, error) {
return data, nil
}
-func GetFormData(body []byte) (map[string]interface{}, error) {
- data := map[string]interface{}{}
+func GetFormData(body []byte) (map[string]any, error) {
+ data := map[string]any{}
queryData := query.ParseRawQuery(string(body))
@@ -110,25 +110,25 @@ func GetReqBody(w http.ResponseWriter, req *http.Request) (Body, error) {
return Body{Empty: true}, nil
}
- var data map[string]interface{}
+ var data map[string]any
switch GetBodyType(req) {
- case Json:
- data, err = GetJsonData(bytes)
+ case Json:
+ data, err = GetJsonData(bytes)
- if err != nil {
- http.Error(w, "Bad Request: invalid JSON", http.StatusBadRequest)
+ if err != nil {
+ http.Error(w, "Bad Request: invalid JSON", http.StatusBadRequest)
- return Body{Empty: true}, err
- }
- case Form:
- data, err = GetFormData(bytes)
+ return Body{Empty: true}, err
+ }
+ case Form:
+ data, err = GetFormData(bytes)
- if err != nil {
- http.Error(w, "Bad Request: invalid Form", http.StatusBadRequest)
+ if err != nil {
+ http.Error(w, "Bad Request: invalid Form", http.StatusBadRequest)
- return Body{Empty: true}, err
- }
+ return Body{Empty: true}, err
+ }
}
isEmpty = len(data) <= 0
diff --git a/utils/safestrings/safestrings.go b/utils/safestrings/safestrings.go
new file mode 100644
index 0000000..811e7b7
--- /dev/null
+++ b/utils/safestrings/safestrings.go
@@ -0,0 +1,139 @@
+package safestrings
+
+import (
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/codeshelldev/secured-signal-api/utils"
+)
+
+func ToType(str string) any {
+ cleaned := strings.TrimSpace(str)
+
+ //* Try JSON
+ if IsEnclosedBy(cleaned, `[`, `]`) || IsEnclosedBy(cleaned, `{`, `}`) {
+ data, err := utils.GetJsonSafe[any](str)
+
+ if data != nil && err == nil {
+ return data
+ }
+ }
+
+ //* Try String Slice
+ if IsEnclosedBy(cleaned, `[`, `]`) {
+ bracketsless := strings.ReplaceAll(str, "[", "")
+ bracketsless = strings.ReplaceAll(bracketsless, "]", "")
+
+ var data []string
+
+ if Contains(str, ",") {
+ data = ToArray(bracketsless)
+ } else {
+ data = []string{bracketsless}
+ }
+
+ if data != nil {
+ if len(data) > 0 {
+ return data
+ }
+ }
+ }
+
+ //* Try Number
+ if !strings.HasPrefix(cleaned, "+") {
+ intValue, intErr := strconv.Atoi(cleaned)
+
+ if intErr == nil {
+ return intValue
+ }
+ }
+
+ return str
+}
+
+func Contains(str string, match string) bool {
+ return !IsEscaped(str, match)
+}
+
+// Checks if a string is Enclosed by `char` and are not Escaped
+func IsEnclosedBy(str string, charA, charB string) bool {
+ if NeedsEscapeForRegex(rune(charA[0])) {
+ charA = `\` + charA
+ }
+
+ if NeedsEscapeForRegex(rune(charB[0])) {
+ charB = `\` + charB
+ }
+
+ regexStr := `(^|[^\\])(\\\\)*(` + charA + `)(.*?)(^|[^\\])(\\\\)*(` + charB + ")"
+
+ re := regexp.MustCompile(regexStr)
+
+ matches := re.FindAllStringSubmatchIndex(str, -1)
+
+ filtered := [][]int{}
+
+ for _, match := range matches {
+ start := match[len(match)-2]
+ end := match[len(match)-1]
+ char := str[start:end]
+
+ if char != `\` {
+ filtered = append(filtered, match)
+ }
+ }
+
+ return len(filtered) > 0
+}
+
+// Checks if a string is completly Escaped with `\`
+func IsEscaped(str string, char string) bool {
+ if NeedsEscapeForRegex(rune(char[0])) {
+ char = `\` + char
+ }
+
+ regexStr := `(^|[^\\])(\\\\)*(` + char + ")"
+
+ re := regexp.MustCompile(regexStr)
+
+ matches := re.FindAllStringSubmatchIndex(str, -1)
+
+ filtered := [][]int{}
+
+ for _, match := range matches {
+ start := match[len(match)-2]
+ end := match[len(match)-1]
+ char := str[start:end]
+
+ if char != `\` {
+ filtered = append(filtered, match)
+ }
+ }
+
+ return len(filtered) == 0
+}
+
+func NeedsEscapeForRegex(char rune) bool {
+ special := `.+*?()|[]{}^$\\`
+
+ return strings.ContainsRune(special, char)
+}
+
+func ToArray(sliceStr string) []string {
+ if sliceStr == "" {
+ return nil
+ }
+
+ rawItems := strings.Split(sliceStr, ",")
+ items := make([]string, 0, len(rawItems))
+
+ for _, item := range rawItems {
+ trimmed := strings.TrimSpace(item)
+ if trimmed != "" {
+ items = append(items, trimmed)
+ }
+ }
+
+ return items
+}
\ No newline at end of file
diff --git a/utils/templating/templating.go b/utils/templating/templating.go
index 70f8710..37d4ec3 100644
--- a/utils/templating/templating.go
+++ b/utils/templating/templating.go
@@ -9,12 +9,12 @@ import (
"text/template"
)
-func normalize(value interface{}) string {
+func normalize(value any) string {
switch str := value.(type) {
case []string:
return "[" + strings.Join(str, ",") + "]"
- case []interface{}:
+ case []any:
items := make([]string, len(str))
for i, item := range str {
@@ -27,14 +27,20 @@ func normalize(value interface{}) string {
}
}
-func normalizeJSON(value interface{}) string {
- jsonBytes, err := json.Marshal(value)
-
- if err != nil {
- return "INVALID:JSON"
+func normalizeJSON(value any) string {
+ if value == nil {
+ return ""
}
- return "<<" + string(jsonBytes) + ">>"
+ switch value.(type) {
+ case []any, []string, map[string]any, int, float64, bool:
+ object, _ := json.Marshal(value)
+
+ return "<<" + string(object) + ">>"
+
+ default:
+ return value.(string)
+ }
}
func ParseTemplate(templt *template.Template, tmplStr string, variables any) (string, error) {
@@ -63,7 +69,7 @@ func CreateTemplateWithFunc(name string, funcMap template.FuncMap) (*template.Te
return template.New(name).Funcs(funcMap)
}
-func RenderJSONTemplate(name string, data map[string]interface{}, variables any) (map[string]interface{}, error) {
+func RenderJSONTemplate(name string, data map[string]any, variables any) (map[string]any, error) {
jsonBytes, err := json.Marshal(data)
if err != nil {
diff --git a/utils/utils.go b/utils/utils.go
index cd746b3..2159e1b 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -9,28 +9,11 @@ import (
"encoding/json"
"regexp"
"strconv"
- "strings"
-)
-
-func StringToArray(sliceStr string) []string {
- if sliceStr == "" {
- return nil
- }
-
- rawItems := strings.Split(sliceStr, ",")
- items := make([]string, 0, len(rawItems))
-
- for _, item := range rawItems {
- trimmed := strings.TrimSpace(item)
- if trimmed != "" {
- items = append(items, trimmed)
- }
- }
- return items
-}
+ "gopkg.in/yaml.v3"
+)
-func GetJsonByPath(path string, data interface{}) (interface{}, bool) {
+func GetByPath(path string, data any) (any, bool) {
// Split into parts by `.` and `[]`
re := regexp.MustCompile(`\.|\[|\]`)
@@ -49,7 +32,7 @@ func GetJsonByPath(path string, data interface{}) (interface{}, bool) {
for _, key := range cleaned {
switch currentDataType := current.(type) {
// Case: Dictionary
- case map[string]interface{}:
+ case map[string]any:
value, ok := currentDataType[key]
if !ok {
return nil, false
@@ -57,7 +40,7 @@ func GetJsonByPath(path string, data interface{}) (interface{}, bool) {
current = value
// Case: Array
- case []interface{}:
+ case []any:
index, err := strconv.Atoi(key)
if err != nil || index < 0 || index >= len(currentDataType) {
@@ -87,7 +70,27 @@ func GetJson[T any](jsonStr string) (T) {
err := json.Unmarshal([]byte(jsonStr), &result)
if err != nil {
- // JSON is empty
+ // YML is empty
+ }
+
+ return result
+}
+
+func GetYmlSafe[T any](ymlStr string) (T, error) {
+ var result T
+
+ err := yaml.Unmarshal([]byte(ymlStr), &result)
+
+ return result, err
+}
+
+func GetYml[T any](ymlStr string) (T) {
+ var result T
+
+ err := yaml.Unmarshal([]byte(ymlStr), &result)
+
+ if err != nil {
+ // YML is empty
}
return result