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 8de1c8b2..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,17 +29,12 @@ ENV TOKENS_DIR=/config/tokens ENV DB_PATH=/db/db.sqlite3 -ENV CGO_ENABLED=1 - -ARG TARGETOS -ARG TARGETARCH +ENV REDACT_TOKENS=true 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 2848aef0..c8889f8a 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -29,16 +29,44 @@ settings: { field: data, score: 1 }, ] - variables: - recipients: ${RECIPIENTS} - number: ${NUMBER} + scheduling: + enabled: true + + templating: + body: true + path: true + query: true + + injecting: + urlToBody: + query: true + path: true 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 + + cors: + methods: [GET, POST, PUT, PATCH, DELETE, OPTIONS] + headers: + [ + "Content-Type", + "Content-Language", + "Authorization", + "Accept", + "Accept-Language", + ] 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 13885370..2b2dc54d 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,52 @@ module github.com/codeshelldev/secured-signal-api -go 1.25.6 +go 1.26.1 require ( - github.com/codeshelldev/gotl/pkg/configutils v0.0.16 + github.com/codeshelldev/gotl/pkg/configutils v0.0.22 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 - github.com/codeshelldev/gotl/pkg/templating v0.0.4 + github.com/codeshelldev/gotl/pkg/templating v0.0.16 ) 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 ) require ( - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + 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 ( + 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 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-sqlite3 v1.14.34 + 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 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 980dee82..4419f213 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,41 @@ -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/codeshelldev/gotl/pkg/configutils v0.0.16 h1:iS/Yw2ruYnRGf2zZphI+JFIGkQCECh+AFbJAi0uyXLA= -github.com/codeshelldev/gotl/pkg/configutils v0.0.16/go.mod h1:Tiu27XQ7D08fcwCHp5tZsDIQwIo6q626+l77k2dse7k= +github.com/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.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/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= 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/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= @@ -42,22 +46,26 @@ 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.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-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/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.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= 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= @@ -68,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/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +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.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= 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/loader.go b/internals/config/loader.go index 98f55ce5..1991df1f 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{}, } @@ -55,8 +58,6 @@ func Load() { mainConf.MergeLayers(defaultsConf.Layer, userConf.Layer) - mainConf.TemplateConfig() - NormalizeTokens() InitConfig() @@ -140,6 +141,9 @@ func InitReload() { func InitConfig() { var config structure.CONFIG + templateConfigWithVariables(mainConf) + + // after templating reunmarshal mainConf.Unmarshal("", &config) config.TYPE = structure.MAIN @@ -165,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 } @@ -182,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/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 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 c23836fd..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 FieldPolicies 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 *FieldPolicies) 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..3362e7cf --- /dev/null +++ b/internals/config/structure/generics/generics.go @@ -0,0 +1,108 @@ +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 { + URL := url.URL(Url) + 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 5291ed48..629e7a2d 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 { @@ -14,6 +16,7 @@ type ENV struct { DB_PATH string INSECURE bool + REDACT_TOKENS bool TOKENS []string @@ -25,8 +28,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 @@ -37,23 +39,20 @@ 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"` - // 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{/}"` + URL *g.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"` - // 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"` + METHODS t.Opt[[]string] `koanf:"methods" env>aliases:".authmethods"` + TOKENS []Token `koanf:"tokens"` } type Token struct { @@ -64,40 +63,90 @@ 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 { - 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"` + VARIABLES t.Opt[map[string]any] `koanf:"variables" childtransform:"upper"` + 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"` +} + +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 { + // Enabled is needed because this isn't a data-driven setting, but rather a toggle Enabled bool `koanf:"enabled"` - MaxHorizon t.Opt[TimeDuration] `koanf:"maxhorizon"` + MaxHorizon t.Opt[g.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"` + 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"` + 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] + +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/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/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/common/template.go b/internals/proxy/common/template.go index 6f3a0ee8..c906d4a2 100644 --- a/internals/proxy/common/template.go +++ b/internals/proxy/common/template.go @@ -6,32 +6,44 @@ import ( "strings" "github.com/codeshelldev/gotl/pkg/jsonutils" - "github.com/codeshelldev/gotl/pkg/query" + 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" "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_.]+"?)`) - return "." + toPrefix + varName - }) + if err != nil { + return tmplStr + } + + 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 +} - jsonStr = toVar +func normalizeData(fromPrefix, toPrefix string, data map[string]any) (map[string]any, error) { + jsonStr := jsonutils.ToJson(data) + + 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 { @@ -63,67 +65,86 @@ 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) + bodyCopy := map[string]any{} + headersCopy := map[string][]string{} - // Normalize `keys.BodyPrefix` + "Var" and `keys.HeaderPrefix` + "Var" to ".Header_Var" and ".Body_Var" - normalizedBody, err := normalizeData(requestkeys.BodyPrefix, "Body_", body) + request.CopyHeaders(headersCopy, headers) + request.CopyMap(bodyCopy, body) + + headersCopy = normalizeHeaders(headersCopy) + + // 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 body, false, err + return bodyCopy, false, err } - normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "Header_", normalizedBody) + normalizedBody, err = normalizeData(requestkeys.HeaderPrefix, "headers", normalizedBody) if err != nil { - return body, false, err + return bodyCopy, false, err } // Prefix Body Data with Body_ - prefixedBody := prefixData("Body_", normalizedBody) + nestedBody := map[string]any{ + "body": normalizedBody, + } // Prefix Header Data with Header_ - prefixedHeaders := prefixData("Header_", request.ParseHeaders(headers)) + 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 body, false, err + return bodyCopy, false, err } - beforeStr := jsonutils.ToJson(body) + beforeStr := jsonutils.ToJson(bodyCopy) afterStr := jsonutils.ToJson(templatedData) modified = beforeStr != afterStr - return templatedData, modified, nil + return templatedData.(map[string]any), 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 +func TemplatePath(path string, VARIABLES any) (string, error) { + reqPath, err := url.PathUnescape(path) + + if err != nil { + return path, err + } - reqPath, err := url.PathUnescape(reqUrl.Path) + templt, err := templating.CreateNormalizedTemplateFromString("path", reqPath) if err != nil { - return reqUrl.Path, data, false, false, err + return path, err } - reqPath, err = templating.RenderNormalizedTemplate("path", reqPath, VARIABLES) + templated, err := templating.ExecuteTemplate(templt, VARIABLES) if err != nil { - return reqUrl.Path, data, false, false, err + return path, err } - parts := strings.Split(reqPath, "/") + return templated, nil +} + +func InjectPathIntoBody(path string, data map[string]any) (string, bool) { + var modified bool + + parts := strings.Split(path, "/") newParts := []string{} for _, part := range parts { @@ -135,7 +156,7 @@ func TemplatePath(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, continue } - keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], "@") + keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], requestkeys.BodyPrefix) if !match { continue @@ -144,33 +165,41 @@ 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, "/") + return strings.Join(newParts, "/"), modified +} - if reqUrl.Path != reqPath { - modified = true +func TemplateQuery(rawQuery string, VARIABLES any) (string, error) { + decodedQuery, _ := url.QueryUnescape(rawQuery) + + templt, err := templating.CreateNormalizedTemplateFromString("query", decodedQuery) + + if err != nil { + return rawQuery, err } - return reqPath, data, modified, modifiedBody, nil -} + templated, err := templating.ExecuteTemplate(templt, VARIABLES) -func TemplateQuery(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, error) { - var modified bool + if err != nil { + return rawQuery, err + } - decodedQuery, _ := url.QueryUnescape(reqUrl.RawQuery) + return templated, err +} - templatedQuery, _ := templating.RenderNormalizedTemplate("query", decodedQuery, VARIABLES) +func InjectQueryIntoBody(query url.Values, data map[string]any) bool { + var modified bool - originalQueryData := reqUrl.Query() + decodedQuery, _ := url.QueryUnescape(query.Encode()) - addedData, _ := query.ParseTypedQuery(templatedQuery) + parsedQuery, _ := queryutils.ParseTypedQuery(decodedQuery) - for key, val := range addedData { - keyWithoutPrefix, match := strings.CutPrefix(key, "@") + for key, val := range parsedQuery { + keyWithoutPrefix, match := strings.CutPrefix(key, requestkeys.BodyPrefix) if !match { continue @@ -178,12 +207,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/about.go b/internals/proxy/endpoints/about.go index fa42f627..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{ @@ -17,7 +16,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 +40,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) @@ -50,8 +51,8 @@ func aboutHandler(mux *http.ServeMux) *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{ @@ -75,14 +76,4 @@ func aboutHandler(mux *http.ServeMux) *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/internals/proxy/endpoints/endpoint.go b/internals/proxy/endpoints/endpoint.go new file mode 100644 index 00000000..ba367278 --- /dev/null +++ b/internals/proxy/endpoints/endpoint.go @@ -0,0 +1,14 @@ +package endpoints + +import ( + "net/http" +) + +type Endpoint struct { + Name string + Handler func(mux *http.ServeMux, next http.Handler) *http.ServeMux +} + +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/endpoints.go b/internals/proxy/endpoints/endpoints.go deleted file mode 100644 index c7077248..00000000 --- a/internals/proxy/endpoints/endpoints.go +++ /dev/null @@ -1,14 +0,0 @@ -package endpoints - -import ( - "net/http" -) - -type Endpoint struct { - Name string - Handler func(mux *http.ServeMux) *http.ServeMux -} - -func (endpoint Endpoint) Use(mux *http.ServeMux) *http.ServeMux { - return endpoint.Handler(mux) -} \ No newline at end of file diff --git a/internals/proxy/endpoints/schedule.go b/internals/proxy/endpoints/schedule.go index 4f9dd285..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" @@ -15,7 +14,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") @@ -49,8 +48,8 @@ func scheduleHandler(mux *http.ServeMux) *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) *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 } diff --git a/internals/proxy/endpoints/send.go b/internals/proxy/endpoints/send.go index af67bca3..e3d6a76b 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" @@ -21,14 +20,14 @@ 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) 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) @@ -40,32 +39,31 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { return } - bodyData := map[string]any{} + body.EnsureNotNil() var modifiedBody bool if !body.Empty { - bodyData = body.Data + if templating.MessageTemplate != "" { + headers := request.GetReqHeaders(req) - if messageTemplate != "" { - headerData := 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 { @@ -73,16 +71,12 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - - logger.Debug("Applied Message Templating: ", body.Data) } - sendAtStr, ok := bodyData[sendAtField].(string) - - if ok && bodyData[messageField] != "" && bodyData[messageField] != nil { - delete(bodyData, sendAtField) + 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) @@ -92,7 +86,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()) @@ -101,7 +95,7 @@ func sendHandler(mux *http.ServeMux) *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 @@ -119,6 +113,8 @@ func sendHandler(mux *http.ServeMux) *http.ServeMux { return } + + next.ServeHTTP(w, req) }) return mux @@ -136,13 +132,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()) { @@ -175,18 +165,20 @@ 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 - - data, _, err := TemplateBody(bodyData, headerData, variables) +func GetTemplatedMessage(template string, body map[string]any, headers map[string][]string, VARIABLES map[string]any) (string, error) { + const templatedSuffix = "_template" - if err != nil || data == nil { - return bodyData, err + bodyCopy := map[string]any{ + messageField + templatedSuffix: template, } - data[messageField] = data["message_template"] + request.CopyMap(bodyCopy, body) + + data, _, err := GetTemplatedBody(bodyCopy, headers, VARIABLES) - delete(data, "message_template") + if err != nil || data == nil { + return "", err + } - return data, 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) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 334c7ff3..b9541467 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -13,7 +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/deprecation" + "github.com/codeshelldev/secured-signal-api/utils/requestkeys" ) var Auth Middleware = Middleware{ @@ -37,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]) { @@ -66,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]) @@ -84,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 } @@ -139,21 +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) - - // 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 + "`", - }) - } + auth := req.URL.Query().Get(requestkeys.BodyPrefix + authQuery) if strings.TrimSpace(auth) == "" { return "", nil @@ -162,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() @@ -269,32 +255,18 @@ 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) 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 { - // 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) - req = SetContext(req, IsAuthKey, false) } } @@ -313,6 +285,7 @@ func authRequirementHandler(next http.Handler) http.Handler { isAuthenticated := GetContext[bool](req, IsAuthKey) if !isAuthenticated { + onUnauthorized(w) return } @@ -335,31 +308,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/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/cors.go b/internals/proxy/middlewares/cors.go new file mode 100644 index 00000000..b78a84a8 --- /dev/null +++ b/internals/proxy/middlewares/cors.go @@ -0,0 +1,94 @@ +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") + + if origin == "" { + next.ServeHTTP(w, req) + return + } + + matchingOrigin, allowed := isCORSOriginAllowed(origin, cors.Origins) + + if !allowed { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + 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" { + allowedMethods := matchingOrigin.Methods.ValueOrFallback(defaultMethods) + + if len(allowedMethods) != 0 { + // only set if any methods + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ",")) + } + + + allowedHeaders := matchingOrigin.Headers.ValueOrFallback(defaultHeaders) + + if len(allowedHeaders) != 0 { + // only set if any headers + w.Header().Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ",")) + } + + w.WriteHeader(http.StatusNoContent) + + return + } + + 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 diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index fef1a522..eef5f739 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" @@ -23,9 +21,22 @@ 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 - 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 +46,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/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 90605fd8..a6f16d59 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{ @@ -22,13 +23,14 @@ 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 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 +38,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/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/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index ec594b43..02ea4dfd 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{ @@ -24,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 { @@ -32,13 +38,12 @@ func mappingHandler(next http.Handler) http.Handler { return } + body.EnsureNotNil() + 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] @@ -46,8 +51,8 @@ func mappingHandler(next http.Handler) http.Handler { keyWithoutPrefix := key[1:] switch prefix { - case "@": - bodyData[keyWithoutPrefix] = value + case requestkeys.BodyPrefix: + body.Data[keyWithoutPrefix] = value modifiedBody = true case ".": variables[keyWithoutPrefix] = value @@ -56,8 +61,6 @@ func mappingHandler(next http.Handler) http.Handler { } if modifiedBody { - body.Data = bodyData - err := body.UpdateReq(req) if err != nil { @@ -73,7 +76,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 +90,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 +109,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..1bbd4b4a 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" ) @@ -26,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 { @@ -34,13 +38,17 @@ func policyHandler(next http.Handler) http.Handler { return } - if body.Empty { - body.Data = map[string]any{} - } + body.EnsureNotNil() - headerData := request.GetReqHeaders(req) + headers := request.GetReqHeaders(req) - shouldBlock, field := isBlockedByPolicy(body.Data, headerData, 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) @@ -64,70 +72,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.FieldPolicies) (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 } @@ -137,32 +109,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/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/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/internals/proxy/middlewares/response/headers.go b/internals/proxy/middlewares/response/headers.go new file mode 100644 index 00000000..d4bf1b84 --- /dev/null +++ b/internals/proxy/middlewares/response/headers.go @@ -0,0 +1,33 @@ +package middleware + +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: headersHandler, +} + +func headersHandler(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/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/middlewares/template.go b/internals/proxy/middlewares/template.go index 2e74f7c4..63d35b8d 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,19 @@ func templateHandler(next http.Handler) http.Handler { return } - bodyData := map[string]any{} + body.EnsureNotNil() 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 +55,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 { diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index ad40639b..0d802ace 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -7,7 +7,7 @@ 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" + rm "github.com/codeshelldev/secured-signal-api/internals/proxy/middlewares/response" ) type Proxy struct { @@ -20,26 +20,21 @@ 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") + modifyResponse := rm.NewResponseChain(). + Use(rm.InternalResponseHooks). + Use(rm.InternalResponseHeaders). + Then() - 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() + }, + ErrorLog: logger.StdError(), + ModifyResponse: modifyResponse, } return Proxy{Use: func() *httputil.ReverseProxy {return proxy}} @@ -50,9 +45,10 @@ func (proxy Proxy) Init() http.Handler { Use(m.InternalInsecureAPI). Use(m.Auth). Use(m.InternalMiddlewareLogger). - Use(m.InternalProxy). + Use(m.InternalProxiesHandler). Use(m.InternalClientIP). Use(m.RequestLogger). + Use(m.CORS). Use(m.InternalAuthRequirement). Use(m.Port). Use(m.Hostname). 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 diff --git a/main.go b/main.go index 1ad25c5e..c79e3258 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "net/url" "os" "slices" "strings" @@ -12,26 +13,29 @@ 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" + runtime "github.com/codeshelldev/secured-signal-api/utils/runtime" ) var proxy reverseProxy.Proxy func main() { - logLevel := os.Getenv("LOG_LEVEL") - - logger.Init(logLevel) + logging.Init(os.Getenv("LOG_LEVEL")) docker.Init() config.Load() + runtime.Test() + 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") @@ -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() @@ -61,8 +65,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/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/docker/docker.go b/utils/docker/docker.go index 558fc396..8551e130 100644 --- a/utils/docker/docker.go +++ b/utils/docker/docker.go @@ -2,16 +2,85 @@ 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.MinWidth = 60 + box.PaddingX = 2 + box.PaddingY = 1 + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.TextBlockSegment{ + Text: "🔬 Pre-Release 🔬", + 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\n{b,u,fg=cyan}https://codeshelldev.github.io/secured-signal-api/bug?v=" + VERSION.String() + "{/}", + }, + }, + }) + + fmt.Println(box.Render()) + } + } else { + logger.Info("Running custom ", imageTag, " Image") + } } func Run(main func()) chan os.Signal { 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/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) diff --git a/utils/runtime/tests.go b/utils/runtime/tests.go new file mode 100644 index 00000000..12ce837d --- /dev/null +++ b/utils/runtime/tests.go @@ -0,0 +1,86 @@ +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 + + if endpoints == nil { + return nil, nil + } + + if len(endpoints.Allowed) != 0 { + err := structure.StringMatchList(endpoints.Allowed).TestRules() + + if err != nil { + return err, endpoints.Allowed + } + } + + if len(endpoints.Blocked) != 0 { + 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 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 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) 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