Compare commits

...

13 Commits

Author SHA1 Message Date
553513d647 Merge pull request 'Implémentation du layer "circuitbreaker"' (#6) from circuitbreaker into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #6
2023-07-06 16:46:17 +02:00
60487c11d6 feat: optional real-ip middleware
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2023-07-06 08:16:17 -06:00
e6f18e7cd8 fix(doc): typo
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2023-07-06 07:59:20 -06:00
a207291c04 feat: implements circuitbreaker layer 2023-07-06 07:59:20 -06:00
64b5182f8b fix(doc): bad link
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-06 07:41:53 -06:00
ce2c19f9b3 feat(layer,queue): implement matchURLs option
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-05 13:54:01 -06:00
1ffec1f173 feat(layer,queue): prevent browser caching for queue page
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-05 13:35:21 -06:00
aab5452fa2 feat: sentry integration
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
ref #3
2023-07-05 12:05:30 -06:00
a176b754cd feat: add queue adapter tests
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-05 08:55:15 -06:00
7b04eb2418 fix(doc): bad link
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-05 15:14:27 +02:00
f8d9ff15b5 doc: add link to misc/docker-compose
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
2023-07-05 15:13:31 +02:00
5bd7cbc132 fix(docker): move templates to expected path
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-03 19:42:44 -06:00
1b06f07ce8 feat: update packaged configuration
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-01 13:55:26 -06:00
40 changed files with 932 additions and 66 deletions

View File

@ -36,7 +36,7 @@ ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
COPY --from=BUILD /src/layers /usr/share/bouncer/ COPY --from=BUILD /src/layers /usr/share/bouncer/layers
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/config.yml /etc/bouncer/config.yml COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/config.yml /etc/bouncer/config.yml
RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer

View File

@ -9,15 +9,15 @@
## Référence ## Référence
- [(FR) - Layers](./fr/references/layers/README.md) - [(FR) - Layers](./fr/references/layers/README.md)
- [Fichier de configuration](../misc/packaging/common/config.yml) - [(FR) - Fichier de configuration](../misc/packaging/common/config.yml)
## Tutoriels ## Tutoriels
### Utilisation ### Utilisation
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md) - [(FR) - Ajouter un layer de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
### Développement ### Développement
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-start-with-sources.md) - [(FR) - Démarrer avec les sources](./fr/tutorials/getting-started-with-sources.md)
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md) - [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)

View File

@ -3,3 +3,4 @@
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy": Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
- [Queue](./queue.md) - File d'attente dynamique - [Queue](./queue.md) - File d'attente dynamique
- [Circuit Breaker](./circuitbreaker.md) - Coupure d'accès à un site ou une sous section de celui ci

View File

@ -0,0 +1,37 @@
# Layer "Circuit Breaker"
## Description
Ce layer permet de bloquer l'accès à un site (ou une section de celui ci) ciblé par un proxy.
## Type
`circuitbreaker`
## Options
### `authorizedCIDRs`
- **Type:** `[]string`
- **Valeur par défaut:** `[]`
- **Description:** Autoriser les adresses distantes contenues dans un des masques réseau (en notation ["CIDR"](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation) définis à contourner la restriction d'accès.
### `matchURLs`
- **Type:** `[]string`
- **Valeur par défaut:** `["*"]`
- **Description:** Limiter l'action du layer à cette liste de patrons d'URLs.
Par exemple, si vous souhaitez limiter votre restriction d'accès à l'ensemble d'une section "`/blog`" d'un site, vous pouvez déclarer la valeur `["*/blog*"]`. Les autres URLs du site ne seront pas affectées par la restriction.
### `templateBlock`
- **Type:** `string`
- **Valeur par défaut:** `"default"`
- **Description:** Bloc du template HTML pour effectuer le rendu de la page indiquant la restriction d'accès.
Voir le [fichier de configuration de référence](../../../../misc/packaging/common/config.yml), section `layers.circuitbreaker` pour voir les options permettant de personnaliser le chemin du répertoire contenant les templates.
### Schéma
Voir le [schéma JSON](../../../../internal/proxy/director/layer/circuitbreaker/layer-options.json).

View File

@ -22,6 +22,14 @@ Ce layer permet d'ajouter un mécanisme de file d'attente dynamique au proxy ass
- **Valeur par défaut:** `1m` - **Valeur par défaut:** `1m`
- **Description:** Durée de vie d'une session dans la file d'attente sans activité avant expiration. - **Description:** Durée de vie d'une session dans la file d'attente sans activité avant expiration.
### `matchURLs`
- **Type:** `[]string`
- **Valeur par défaut:** `["*"]`
- **Description:** Limiter l'action de la file d'attente à cette liste de patrons d'URLs.
Par exemple, si vous souhaitez limiter votre file à l'ensemble d'une section "`/blog`" d'un site, vous pouvez déclarer la valeur `["*/blog*"]`. Les autres URLs du site ne seront pas affectées par cette file d'attente.
### Schéma ### Schéma
Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json). Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json).

10
go.mod
View File

@ -6,6 +6,7 @@ require (
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/btcsuite/btcd/btcutil v1.1.3 github.com/btcsuite/btcd/btcutil v1.1.3
github.com/getsentry/sentry-go v0.22.0
github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/chi/v5 v5.0.8
github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/jedib0t/go-pretty/v6 v6.4.6
github.com/mitchellh/mapstructure v1.4.1 github.com/mitchellh/mapstructure v1.4.1
@ -52,11 +53,12 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
@ -70,12 +72,12 @@ require (
github.com/dlclark/regexp2 v1.9.0 // indirect github.com/dlclark/regexp2 v1.9.0 // indirect
github.com/fatih/color v1.15.0 // indirect github.com/fatih/color v1.15.0 // indirect
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-playground/locales v0.12.1 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/leodido/go-urn v1.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect

18
go.sum
View File

@ -178,19 +178,24 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM=
github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@ -314,8 +319,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@ -382,6 +388,7 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -419,8 +426,9 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@ -667,6 +675,7 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -695,6 +704,7 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -1,11 +1,14 @@
package admin package admin
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"forge.cadoles.com/cadoles/bouncer/internal/schema" "forge.cadoles.com/cadoles/bouncer/internal/schema"
"github.com/getsentry/sentry-go"
"gitlab.com/wpetit/goweb/api" "gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
) )
const ErrCodeAlreadyExist api.ErrorCode = "already-exist" const ErrCodeAlreadyExist api.ErrorCode = "already-exist"
@ -29,3 +32,8 @@ func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schem
return return
} }
func logAndCaptureError(ctx context.Context, message string, err error) {
sentry.CaptureException(err)
logger.Error(ctx, message, logger.E(err))
}

View File

@ -1,6 +1,7 @@
package admin package admin
import ( import (
"fmt"
"net/http" "net/http"
"sort" "sort"
@ -10,7 +11,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api" "gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
) )
type QueryLayerResponse struct { type QueryLayerResponse struct {
@ -38,7 +38,7 @@ func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
options..., options...,
) )
if err != nil { if err != nil {
logger.Error(ctx, "could not list layers", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not list layers", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -85,7 +85,7 @@ func (s *Server) getLayer(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -120,7 +120,7 @@ func (s *Server) deleteLayer(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not delete layer", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not delete layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -156,7 +156,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
layerName, err := store.ValidateName(createLayerReq.Name) layerName, err := store.ValidateName(createLayerReq.Name)
if err != nil { if err != nil {
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "invalid 'name' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return return
@ -165,7 +165,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
layerType := store.LayerType(createLayerReq.Type) layerType := store.LayerType(createLayerReq.Type)
if !setup.LayerTypeExists(layerType) { if !setup.LayerTypeExists(layerType) {
logger.Error(r.Context(), "unknown layer type", logger.E(errors.WithStack(err)), logger.F("layerType", layerType)) logAndCaptureError(ctx, fmt.Sprintf("unknown layer type '%s'", layerType), errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return return
@ -179,7 +179,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not create layer", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not create layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -223,7 +223,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -247,7 +247,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
if updateLayerReq.Options != nil { if updateLayerReq.Options != nil {
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type) layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not retrieve layer options schema", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -258,7 +258,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
}(updateLayerReq.Options) }(updateLayerReq.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil { if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not validate layer options", errors.WithStack(err))
var invalidDataErr *schema.InvalidDataError var invalidDataErr *schema.InvalidDataError
if errors.As(err, &invalidDataErr) { if errors.As(err, &invalidDataErr) {
@ -286,7 +286,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not update layer", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not update layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -300,21 +300,7 @@ func getLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool
name, err := store.ValidateName(rawLayerName) name, err := store.ValidateName(rawLayerName)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err))) logAndCaptureError(r.Context(), "could not parse layer name", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return "", false
}
return store.LayerName(name), true
}
func geLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) {
rawLayerName := chi.URLParam(r, "layerName")
name, err := store.ValidateName(rawLayerName)
if err != nil {
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return "", false return "", false

View File

@ -11,7 +11,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api" "gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
) )
type QueryProxyResponse struct { type QueryProxyResponse struct {
@ -37,7 +36,7 @@ func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) {
options..., options...,
) )
if err != nil { if err != nil {
logger.Error(ctx, "could not list proxies", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not list proxies", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -79,7 +78,7 @@ func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not get proxy", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not get proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -109,7 +108,7 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not delete proxy", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not delete proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -140,14 +139,14 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
name, err := store.ValidateName(createProxyReq.Name) name, err := store.ValidateName(createProxyReq.Name)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not parse 'name' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return return
} }
if _, err := url.Parse(createProxyReq.To); err != nil { if _, err := url.Parse(createProxyReq.To); err != nil {
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return return
@ -161,7 +160,7 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not create proxy", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not create proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -207,7 +206,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
if updateProxyReq.To != nil { if updateProxyReq.To != nil {
_, err := url.Parse(*updateProxyReq.To) _, err := url.Parse(*updateProxyReq.To)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return return
@ -235,7 +234,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
return return
} }
logger.Error(ctx, "could not update proxy", logger.E(errors.WithStack(err))) logAndCaptureError(ctx, "could not update proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
@ -249,7 +248,7 @@ func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool
name, err := store.ValidateName(rawProxyName) name, err := store.ValidateName(rawProxyName)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not parse proxy name", logger.E(errors.WithStack(err))) logAndCaptureError(r.Context(), "could not parse proxy name", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return "", false return "", false
@ -263,7 +262,7 @@ func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defa
if rawValue != "" { if rawValue != "" {
value, err := strconv.ParseInt(rawValue, 10, 64) value, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.E(errors.WithStack(err))) logAndCaptureError(r.Context(), "could not parse int param", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return 0, false return 0, false
@ -296,7 +295,7 @@ func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request,
for _, rv := range rawValues { for _, rv := range rawValues {
v, err := validate(rv) v, err := validate(rv)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not parse ids slice param", logger.F("param", param), logger.E(errors.WithStack(err))) logAndCaptureError(r.Context(), "could not parse ids slice param", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return nil, false return nil, false

View File

@ -9,9 +9,11 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/auth" "forge.cadoles.com/cadoles/bouncer/internal/auth"
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt" "forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
"forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/jwk" "forge.cadoles.com/cadoles/bouncer/internal/jwk"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
@ -90,7 +92,21 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router := chi.NewRouter() router := chi.NewRouter()
router.Use(middleware.Logger) if s.serverConfig.HTTP.UseRealIP {
router.Use(middleware.RealIP)
}
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware")
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
router.Use(sentryMiddleware.Handle)
}
corsMiddleware := cors.New(cors.Options{ corsMiddleware := cors.New(cors.Options{
AllowedOrigins: s.serverConfig.CORS.AllowedOrigins, AllowedOrigins: s.serverConfig.CORS.AllowedOrigins,

View File

@ -7,6 +7,7 @@ import (
"sort" "sort"
"time" "time"
"github.com/getsentry/sentry-go"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -90,6 +91,8 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
return return
} }
sentry.CaptureException(err)
debug := ctx.Bool("debug") debug := ctx.Bool("debug")
if !debug { if !debug {

View File

@ -6,6 +6,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/admin" "forge.cadoles.com/cadoles/bouncer/internal/admin"
"forge.cadoles.com/cadoles/bouncer/internal/command/common" "forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
@ -27,6 +28,14 @@ func RunCommand() *cli.Command {
logger.SetFormat(logger.Format(conf.Logger.Format)) logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level)) logger.SetLevel(logger.Level(conf.Logger.Level))
projectVersion := ctx.String("projectVersion")
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Admin.Sentry, projectVersion)
if err != nil {
return errors.Wrap(err, "could not initialize sentry client")
}
defer flushSentry()
srv := admin.NewServer( srv := admin.NewServer(
admin.WithServerConfig(conf.Admin), admin.WithServerConfig(conf.Admin),
admin.WithRedisConfig(conf.Redis), admin.WithRedisConfig(conf.Redis),

View File

@ -28,6 +28,14 @@ func RunCommand() *cli.Command {
logger.SetFormat(logger.Format(conf.Logger.Format)) logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level)) logger.SetLevel(logger.Level(conf.Logger.Level))
projectVersion := ctx.String("projectVersion")
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
if err != nil {
return errors.Wrap(err, "could not initialize sentry client")
}
defer flushSentry()
layers, err := setup.GetLayers(ctx.Context, conf) layers, err := setup.GetLayers(ctx.Context, conf)
if err != nil { if err != nil {
return errors.Wrap(err, "could not initialize director layers") return errors.Wrap(err, "could not initialize director layers")

View File

@ -5,6 +5,7 @@ type AdminServerConfig struct {
CORS CORSConfig `yaml:"cors"` CORS CORSConfig `yaml:"cors"`
Auth AuthConfig `yaml:"auth"` Auth AuthConfig `yaml:"auth"`
Metrics MetricsConfig `yaml:"metrics"` Metrics MetricsConfig `yaml:"metrics"`
Sentry SentryConfig `yaml:"sentry"`
} }
func NewDefaultAdminServerConfig() AdminServerConfig { func NewDefaultAdminServerConfig() AdminServerConfig {
@ -13,6 +14,7 @@ func NewDefaultAdminServerConfig() AdminServerConfig {
CORS: NewDefaultCORSConfig(), CORS: NewDefaultCORSConfig(),
Auth: NewDefaultAuthConfig(), Auth: NewDefaultAuthConfig(),
Metrics: NewDefaultMetricsConfig(), Metrics: NewDefaultMetricsConfig(),
Sentry: NewDefaultSentryConfig(),
} }
} }

View File

@ -53,6 +53,29 @@ func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
type InterpolatedFloat float64
func (ifl *InterpolatedFloat) UnmarshalYAML(value *yaml.Node) error {
var str string
if err := value.Decode(&str); err != nil {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
}
if match := reVar.FindStringSubmatch(str); len(match) > 0 {
str = os.Getenv(match[1])
}
floatVal, err := strconv.ParseFloat(str, 10)
if err != nil {
return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line)
}
*ifl = InterpolatedFloat(floatVal)
return nil
}
type InterpolatedBool bool type InterpolatedBool bool
func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error { func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {

View File

@ -3,11 +3,13 @@ package config
type HTTPConfig struct { type HTTPConfig struct {
Host InterpolatedString `yaml:"host"` Host InterpolatedString `yaml:"host"`
Port InterpolatedInt `yaml:"port"` Port InterpolatedInt `yaml:"port"`
UseRealIP InterpolatedBool `yaml:"useRealIP"`
} }
func NewHTTPConfig(host string, port int) HTTPConfig { func NewHTTPConfig(host string, port int) HTTPConfig {
return HTTPConfig{ return HTTPConfig{
Host: InterpolatedString(host), Host: InterpolatedString(host),
Port: InterpolatedInt(port), Port: InterpolatedInt(port),
UseRealIP: true,
} }
} }

View File

@ -4,6 +4,7 @@ import "time"
type LayersConfig struct { type LayersConfig struct {
Queue QueueLayerConfig `yaml:"queue"` Queue QueueLayerConfig `yaml:"queue"`
CircuitBreaker CircuitBreakerLayerConfig `yaml:"circuitbreaker"`
} }
func NewDefaultLayersConfig() LayersConfig { func NewDefaultLayersConfig() LayersConfig {
@ -12,6 +13,9 @@ func NewDefaultLayersConfig() LayersConfig {
TemplateDir: "./layers/queue/templates", TemplateDir: "./layers/queue/templates",
DefaultKeepAlive: NewInterpolatedDuration(time.Minute), DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
}, },
CircuitBreaker: CircuitBreakerLayerConfig{
TemplateDir: "./layers/circuitbreaker/templates",
},
} }
} }
@ -19,3 +23,7 @@ type QueueLayerConfig struct {
TemplateDir InterpolatedString `yaml:"templateDir"` TemplateDir InterpolatedString `yaml:"templateDir"`
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"` DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
} }
type CircuitBreakerLayerConfig struct {
TemplateDir InterpolatedString `yaml:"templateDir"`
}

View File

@ -7,6 +7,7 @@ type ProxyServerConfig struct {
Metrics MetricsConfig `yaml:"metrics"` Metrics MetricsConfig `yaml:"metrics"`
Transport TransportConfig `yaml:"transport"` Transport TransportConfig `yaml:"transport"`
Dial DialConfig `yaml:"dial"` Dial DialConfig `yaml:"dial"`
Sentry SentryConfig `yaml:"sentry"`
} }
// See https://pkg.go.dev/net/http#Transport // See https://pkg.go.dev/net/http#Transport
@ -32,6 +33,7 @@ func NewDefaultProxyServerConfig() ProxyServerConfig {
Metrics: NewDefaultMetricsConfig(), Metrics: NewDefaultMetricsConfig(),
Transport: NewDefaultTransportConfig(), Transport: NewDefaultTransportConfig(),
Dial: NewDefaultDialConfig(), Dial: NewDefaultDialConfig(),
Sentry: NewDefaultSentryConfig(),
} }
} }

43
internal/config/sentry.go Normal file
View File

@ -0,0 +1,43 @@
package config
import "time"
// Sentry configuration
// See https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
type SentryConfig struct {
DSN InterpolatedString `yaml:"dsn"`
Debug InterpolatedBool `yaml:"debug"`
FlushTimeout *InterpolatedDuration `yaml:"flushTimeout"`
AttachStacktrace InterpolatedBool `yaml:"attachStacktrace"`
SampleRate InterpolatedFloat `yaml:"sampleRate"`
EnableTracing InterpolatedBool `yaml:"enableTracing"`
TracesSampleRate InterpolatedFloat `yaml:"tracesSampleRate"`
ProfilesSampleRate InterpolatedFloat `yaml:"profilesSampleRate"`
IgnoreErrors InterpolatedStringSlice `yaml:"ignoreErrors"`
SendDefaultPII InterpolatedBool `yaml:"sendDefaultPII"`
ServerName InterpolatedString `yaml:"serverName"`
Environment InterpolatedString `yaml:"environment"`
MaxBreadcrumbs InterpolatedInt `yaml:"maxBreadcrumbs"`
MaxSpans InterpolatedInt `yaml:"maxSpans"`
MaxErrorDepth InterpolatedInt `yaml:"maxErrorDepth"`
}
func NewDefaultSentryConfig() SentryConfig {
return SentryConfig{
DSN: "",
Debug: false,
FlushTimeout: NewInterpolatedDuration(2 * time.Second),
AttachStacktrace: true,
SampleRate: 1,
EnableTracing: true,
TracesSampleRate: 0.2,
ProfilesSampleRate: 1,
IgnoreErrors: []string{},
SendDefaultPII: false,
ServerName: "",
Environment: "",
MaxBreadcrumbs: 0,
MaxSpans: 1000,
MaxErrorDepth: 10,
}
}

43
internal/logger/writer.go Normal file
View File

@ -0,0 +1,43 @@
package logger
import (
"context"
"io"
"gitlab.com/wpetit/goweb/logger"
)
type Writer struct {
ctx context.Context
level logger.Level
}
// Write implements io.Writer.
func (w *Writer) Write(p []byte) (n int, err error) {
w.log(string(p))
return len(p), nil
}
func (w *Writer) log(message string) {
switch w.level {
case logger.LevelDebug:
logger.Debug(w.ctx, message)
case logger.LevelInfo:
logger.Info(w.ctx, message)
case logger.LevelWarn:
logger.Warn(w.ctx, message)
case logger.LevelError:
logger.Error(w.ctx, message)
case logger.LevelCritical:
logger.Critical(w.ctx, message)
default:
logger.Debug(w.ctx, message)
}
}
func NewWriter(ctx context.Context, level logger.Level) *Writer {
return &Writer{ctx, level}
}
var _ io.Writer = &Writer{}

View File

@ -0,0 +1,23 @@
{
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/circuitbreaker-layer-options",
"title": "Circuit breaker layer options",
"type": "object",
"properties": {
"matchURLs": {
"type": "array",
"items": {
"type": "string"
}
},
"authorizedCIDRs": {
"type": "array",
"items": {
"type": "string"
}
},
"templateBlock": {
"type": "string"
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,151 @@
package circuitbreaker
import (
"context"
"html/template"
"net"
"net/http"
"path/filepath"
"sync"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const LayerType store.LayerType = "circuitbreaker"
type Layer struct {
templateDir string
loadOnce sync.Once
tmpl *template.Template
}
// LayerType implements director.MiddlewareLayer
func (l *Layer) LayerType() store.LayerType {
return LayerType
}
// Middleware implements director.MiddlewareLayer
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
options, err := fromStoreOptions(layer.Options)
if err != nil {
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
matches, err := l.matchAnyAuthorizedCIDRs(ctx, r.RemoteAddr, options.AuthorizedCIDRs)
if err != nil {
logger.Error(ctx, "could not match authorized cidrs", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if matches {
h.ServeHTTP(w, r)
return
}
matches = wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
if !matches {
h.ServeHTTP(w, r)
return
}
l.renderCircuitBreakerPage(w, r, layer, options)
}
return http.HandlerFunc(fn)
}
}
func (l *Layer) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
remoteHost, _, err := net.SplitHostPort(remoteHostPort)
if err != nil {
return false, errors.WithStack(err)
}
remoteAddr := net.ParseIP(remoteHost)
if remoteAddr == nil {
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
}
for _, rawCIDR := range CIDRs {
_, net, err := net.ParseCIDR(rawCIDR)
if err != nil {
return false, errors.WithStack(err)
}
match := net.Contains(remoteAddr)
if !match {
continue
}
return true, nil
}
logger.Debug(ctx, "comparing remote host with authorized cidrs", logger.F("remoteAddr", remoteAddr))
return false, nil
}
func (l *Layer) renderCircuitBreakerPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions) {
ctx := r.Context()
pattern := filepath.Join(l.templateDir, "*.gohtml")
logger.Info(ctx, "loading circuit breaker page templates", logger.F("pattern", pattern))
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
if err != nil {
logger.Error(ctx, "could not load circuit breaker templates", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
templateData := struct {
Layer *store.Layer
LayerOptions *LayerOptions
}{
Layer: layer,
LayerOptions: options,
}
w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
if err := tmpl.ExecuteTemplate(w, options.TemplateBlock, templateData); err != nil {
logger.Error(ctx, "could not render circuit breaker page", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func New(funcs ...OptionFunc) *Layer {
opts := defaultOptions()
for _, fn := range funcs {
fn(opts)
}
return &Layer{
templateDir: opts.TemplateDir,
}
}
var _ director.MiddlewareLayer = &Layer{}

View File

@ -0,0 +1,36 @@
package circuitbreaker
import (
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
type LayerOptions struct {
MatchURLs []string `mapstructure:"matchURLs"`
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
TemplateBlock string `mapstructure:"templateBlock"`
}
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
layerOptions := LayerOptions{
MatchURLs: []string{"*"},
AuthorizedCIDRs: []string{},
TemplateBlock: "default",
}
config := mapstructure.DecoderConfig{
Result: &layerOptions,
}
decoder, err := mapstructure.NewDecoder(&config)
if err != nil {
return nil, err
}
if err := decoder.Decode(storeOptions); err != nil {
return nil, errors.WithStack(err)
}
return &layerOptions, nil
}

View File

@ -0,0 +1,19 @@
package circuitbreaker
type Options struct {
TemplateDir string
}
type OptionFunc func(*Options)
func defaultOptions() *Options {
return &Options{
TemplateDir: "./templates",
}
}
func WithTemplateDir(templateDir string) OptionFunc {
return func(o *Options) {
o.TemplateDir = templateDir
}
}

View File

@ -0,0 +1,8 @@
package circuitbreaker
import (
_ "embed"
)
//go:embed layer-options.json
var RawLayerOptionsSchema []byte

View File

@ -11,15 +11,15 @@ import (
type LayerOptions struct { type LayerOptions struct {
Capacity int64 `mapstructure:"capacity"` Capacity int64 `mapstructure:"capacity"`
Matchers []string `mapstructure:"matchers"`
KeepAlive time.Duration `mapstructure:"keepAlive"` KeepAlive time.Duration `mapstructure:"keepAlive"`
MatchURLs []string `mapstructure:"matchURLs"`
} }
func fromStoreOptions(storeOptions store.LayerOptions, defaultKeepAlive time.Duration) (*LayerOptions, error) { func fromStoreOptions(storeOptions store.LayerOptions, defaultKeepAlive time.Duration) (*LayerOptions, error) {
layerOptions := LayerOptions{ layerOptions := LayerOptions{
Capacity: 1000, Capacity: 1000,
Matchers: []string{"*"},
KeepAlive: defaultKeepAlive, KeepAlive: defaultKeepAlive,
MatchURLs: []string{"*"},
} }
config := mapstructure.DecoderConfig{ config := mapstructure.DecoderConfig{

View File

@ -1,6 +1,8 @@
package queue package queue
import "time" import (
"time"
)
type Options struct { type Options struct {
TemplateDir string TemplateDir string

View File

@ -7,11 +7,13 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
@ -56,6 +58,13 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
if !matches {
h.ServeHTTP(w, r)
return
}
defer q.updateMetrics(ctx, layer.Proxy, layer.Name, options) defer q.updateMetrics(ctx, layer.Proxy, layer.Name, options)
cookieName := q.getCookieName(layer.Name) cookieName := q.getCookieName(layer.Name)
@ -181,6 +190,8 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
RefreshRate: refreshRate, RefreshRate: refreshRate,
} }
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Retry-After", strconv.FormatInt(int64(refreshRate.Seconds()), 10))
w.WriteHeader(http.StatusServiceUnavailable) w.WriteHeader(http.StatusServiceUnavailable)
if err := q.tmpl.ExecuteTemplate(w, "queue", templateData); err != nil { if err := q.tmpl.ExecuteTemplate(w, "queue", templateData); err != nil {

View File

@ -30,7 +30,7 @@ func (a *Adapter) Refresh(ctx context.Context, queueName string, keepAlive time.
cmd := tx.ZRangeByScore(ctx, lastSeenKey, &redis.ZRangeBy{ cmd := tx.ZRangeByScore(ctx, lastSeenKey, &redis.ZRangeBy{
Min: "0", Min: "0",
Max: strconv.FormatInt(expires.Unix(), 10), Max: strconv.FormatInt(expires.UnixNano(), 10),
}) })
members, err := cmd.Result() members, err := cmd.Result()
@ -75,7 +75,7 @@ func (a *Adapter) Touch(ctx context.Context, queueName string, sessionId string)
for retry > 0 { for retry > 0 {
err := withTx(ctx, a.client, func(ctx context.Context, tx *redis.Tx) error { err := withTx(ctx, a.client, func(ctx context.Context, tx *redis.Tx) error {
now := time.Now().UTC().Unix() now := time.Now().UTC().UnixNano()
err := tx.ZAddNX(ctx, rankKey, redis.Z{Score: float64(now), Member: sessionId}).Err() err := tx.ZAddNX(ctx, rankKey, redis.Z{Score: float64(now), Member: sessionId}).Err()
if err != nil { if err != nil {

View File

@ -0,0 +1,12 @@
package redis
import (
"testing"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue/testsuite"
)
func TestAdapter(t *testing.T) {
adapter := NewAdapter(client, 3)
testsuite.TestAdapter(t, adapter)
}

View File

@ -0,0 +1,58 @@
package redis
import (
"context"
"log"
"os"
"testing"
"github.com/ory/dockertest/v3"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
var client redis.UniversalClient
func TestMain(m *testing.M) {
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
// uses pool to try to connect to Docker
err = pool.Client.Ping()
if err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
// pulls an image, creates a container based on it and runs it
resource, err := pool.Run("redis", "alpine3.17", []string{})
if err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
if err := pool.Retry(func() error {
client = redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{resource.GetHostPort("6379/tcp")},
})
ctx := context.Background()
if cmd := client.Ping(ctx); cmd.Err() != nil {
return errors.WithStack(err)
}
return nil
}); err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
code := m.Run()
if err := pool.Purge(resource); err != nil {
log.Fatalf("%+v", errors.WithStack(err))
}
os.Exit(code)
}

View File

@ -9,6 +9,12 @@
}, },
"keepAlive": { "keepAlive": {
"type": "string" "type": "string"
},
"matchURLs": {
"type": "array",
"items": {
"type": "string"
}
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@ -0,0 +1,99 @@
package testsuite
import (
"context"
"fmt"
"testing"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
"github.com/pkg/errors"
)
type adapterTestCase struct {
Name string
Do func(adapter queue.Adapter) error
}
var adapterTestCases = []adapterTestCase{
{
Name: "Test queue ranking",
Do: func(adapter queue.Adapter) error {
ctx := context.Background()
queueName := "test_queue_ranking"
sessionIdPattern := "session-%d"
totalSessions := int64(100)
for idx := int64(0); idx < totalSessions; idx++ {
sessionId := fmt.Sprintf(sessionIdPattern, idx)
rank, err := adapter.Touch(ctx, queueName, sessionId)
if err != nil {
return errors.Wrapf(err, "could not touch session '%s' (index: %d, rank: %d)", sessionId, idx, rank)
}
if e, g := int64(idx), rank; e != g {
return errors.Errorf("rank('%s'): expected '%v', got '%v'", sessionId, e, g)
}
}
status, err := adapter.Status(ctx, queueName)
if err != nil {
return errors.Wrap(err, "could not retrieve queue status")
}
if e, g := totalSessions, status.Sessions; e != g {
return errors.Errorf("status.Sessions: expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Test session expiration",
Do: func(adapter queue.Adapter) error {
ctx := context.Background()
queueName := "test_session_expiration"
sessionId := "session-1"
rank, err := adapter.Touch(ctx, queueName, sessionId)
if err != nil {
return errors.Wrapf(err, "could not touch session '%s'", sessionId)
}
if e, g := int64(0), rank; e != g {
return errors.Errorf("rank('%s'): expected '%v', got '%v'", sessionId, e, g)
}
<-time.After(time.Second)
if err := adapter.Refresh(ctx, queueName, time.Second); err != nil {
return errors.Wrap(err, "could not refresh queue")
}
status, err := adapter.Status(ctx, queueName)
if err != nil {
return errors.Wrap(err, "could not retrieve queue status")
}
if e, g := int64(0), status.Sessions; e != g {
return errors.Errorf("status.Sessions: expected '%v', got '%v'", e, g)
}
return nil
},
},
}
func TestAdapter(t *testing.T, adapter queue.Adapter) {
for _, tc := range adapterTestCases {
func(tc adapterTestCase) {
t.Run(tc.Name, func(t *testing.T) {
if err := tc.Do(adapter); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
})
}(tc)
}
}

View File

@ -15,6 +15,8 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -87,8 +89,22 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
s.directorLayers..., s.directorLayers...,
) )
if s.serverConfig.HTTP.UseRealIP {
router.Use(middleware.RealIP)
}
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter())) router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware")
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
router.Use(sentryMiddleware.Handle)
}
if s.serverConfig.Metrics.Enabled { if s.serverConfig.Metrics.Enabled {
metrics := s.serverConfig.Metrics metrics := s.serverConfig.Metrics
@ -169,7 +185,10 @@ func (s *Server) createReverseProxy(ctx context.Context, target *url.URL) *httpu
} }
func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) { func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
logger.Error(r.Context(), "proxy error", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
logger.Error(r.Context(), "proxy error", logger.E(err))
sentry.CaptureException(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }

View File

@ -0,0 +1,21 @@
package setup
import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/circuitbreaker"
)
func init() {
RegisterLayer(circuitbreaker.LayerType, setupCircuitBreakerLayer, circuitbreaker.RawLayerOptionsSchema)
}
func setupCircuitBreakerLayer(conf *config.Config) (director.Layer, error) {
options := []circuitbreaker.OptionFunc{
circuitbreaker.WithTemplateDir(string(conf.Layers.CircuitBreaker.TemplateDir)),
}
return circuitbreaker.New(
options...,
), nil
}

42
internal/setup/sentry.go Normal file
View File

@ -0,0 +1,42 @@
package setup
import (
"context"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config"
loggerWriter "forge.cadoles.com/cadoles/bouncer/internal/logger"
"github.com/getsentry/sentry-go"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func SetupSentry(ctx context.Context, conf config.SentryConfig, release string) (func(), error) {
err := sentry.Init(sentry.ClientOptions{
Dsn: string(conf.DSN),
Debug: bool(conf.Debug),
AttachStacktrace: bool(conf.AttachStacktrace),
SampleRate: float64(conf.SampleRate),
EnableTracing: bool(conf.EnableTracing),
TracesSampleRate: float64(conf.TracesSampleRate),
ProfilesSampleRate: float64(conf.ProfilesSampleRate),
IgnoreErrors: conf.IgnoreErrors,
SendDefaultPII: bool(conf.SendDefaultPII),
ServerName: string(conf.ServerName),
Release: release,
Environment: string(conf.Environment),
MaxBreadcrumbs: int(conf.MaxBreadcrumbs),
MaxSpans: int(conf.MaxSpans),
MaxErrorDepth: int(conf.MaxErrorDepth),
DebugWriter: loggerWriter.NewWriter(ctx, logger.LevelDebug),
})
if err != nil {
return nil, errors.WithStack(err)
}
flush := func() {
sentry.Flush(time.Duration(*conf.FlushTimeout))
}
return flush, nil
}

View File

@ -0,0 +1,73 @@
{{ define "default" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Accès bloqué - {{ .Layer.Name }}</title>
<style>
html {
box-sizing: border-box;
font-size: 16px;
}
*, *:before, *:after {
box-sizing: inherit;
}
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
margin: 0;
padding: 0;
font-weight: normal;
}
html, body {
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
background-color: #f7f7f7;
}
#container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
}
#card {
padding: 1.5em 1em;
border: 1px solid #e0e0e0;
background-color: white;
border-radius: 5px;
box-shadow: 2px 2px #cccccc1c;
color: #333333 !important;
}
.title {
margin-bottom: 1.2em;
}
p {
margin-bottom: 0.5em;
}
.footer {
font-size: 0.7em;
margin-top: 2em;
text-align: right;
}
</style>
</head>
<body>
<div id="container">
<div id="card">
<h2 class="title">Page indisponible</h2>
<p>La page à laquelle vous souhaitez accéder est actuellement indisponible.</p>
<p class="footer">Propulsé par <a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.</p>
</div>
</div>
</body>
</html>
{{ end }}

View File

@ -1,6 +1,6 @@
# Exemple de déploiement multi-noeuds avec Docker-Compose # Exemple de déploiement multi-noeuds avec Docker-Compose
Le répertoire `misc/docker-compose` contient un exemple de déploiement de Bouncer multi-noeuds avec: Le répertoire [`misc/docker-compose`](./) contient un exemple de déploiement de Bouncer multi-noeuds avec:
- 3 instances du service `bouncer-proxy`; - 3 instances du service `bouncer-proxy`;
- 1 instance du service `haproxy` en frontal en charge du load-balancing; - 1 instance du service `haproxy` en frontal en charge du load-balancing;

View File

@ -6,6 +6,9 @@ admin:
host: 127.0.0.1 host: 127.0.0.1
# Port d'écoute du service # Port d'écoute du service
port: 8081 port: 8081
# Utiliser les entêtes HTTP True-Client-IP, X-Real-IP ou X-Forwarded-For
# pour le calcul de l'adresse distante à l'origine des requêtes
useRealIP: true
# Configuration CORS du service # Configuration CORS du service
# Uniquement nécessaire si un frontend web # Uniquement nécessaire si un frontend web
@ -46,6 +49,25 @@ admin:
# Mettre à null pour désactiver l'authentification # Mettre à null pour désactiver l'authentification
basicAuth: null basicAuth: null
# Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry:
dsn: ""
debug: false
flushTimeout: 2s
attachStacktrace: true
sampleRate: 1
enableTracing: true
tracesSampleRate: 0.2
profilesSampleRate: 1
ignoreErrors: []
sendDefaultPII: false
serverName: ""
environment: ""
maxBreadcrumbs: 0
maxSpans: 1000
maxErrorDepth: 10
# Configuration du service "proxy" # Configuration du service "proxy"
proxy: proxy:
http: http:
@ -54,6 +76,9 @@ proxy:
host: 0.0.0.0 host: 0.0.0.0
# Port d'écoute du service # Port d'écoute du service
port: 8080 port: 8080
# Utiliser les entêtes HTTP True-Client-IP, X-Real-IP ou X-Forwarded-For
# pour le calcul de l'adresse distante à l'origine des requêtes
useRealIP: true
# Métriques Prometheus # Métriques Prometheus
metrics: metrics:
@ -65,8 +90,53 @@ proxy:
# de publication # de publication
# Mettre à null pour désactiver l'authentification # Mettre à null pour désactiver l'authentification
basicAuth: basicAuth:
credentials:
prom: etheus prom: etheus
# Configuration du transport HTTP(S)
# Voir https://pkg.go.dev/net/http#Transport
transport:
forceAttemptHTTP2: true
maxIdleConns: 100
maxIdleConnsPerHost: 100
maxConnsPerHost: 100
idleConnTimeout: 1m30s
tlsHandshakeTimeout: 10s
expectContinueTimeout: 1s
disableKeepAlives: false
disableCompression: false
responseHeaderTimeout: 10s
writeBufferSize: 4096
readBufferSize: 4096
maxResponseHeaderBytes: 0
# Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry:
dsn: ""
debug: false
flushTimeout: 2s
attachStacktrace: true
sampleRate: 1
enableTracing: true
tracesSampleRate: 0.2
profilesSampleRate: 1
ignoreErrors: []
sendDefaultPII: false
serverName: ""
environment: ""
maxBreadcrumbs: 0
maxSpans: 1000
maxErrorDepth: 10
# Configuration des connexions TCP
# Voir https://pkg.go.dev/net#Dialer
dial:
timeout: 30s
keepAlive: 30s
fallbackDelay: 300ms
dualStack: true
# Configuration du client Redis # Configuration du client Redis
# #
# Les modes "standalone", "sentinel" et "cluster" de Redis sont supportés: # Les modes "standalone", "sentinel" et "cluster" de Redis sont supportés:
@ -98,3 +168,9 @@ layers:
templateDir: "/etc/bouncer/layers/queue/templates" templateDir: "/etc/bouncer/layers/queue/templates"
# Temps de vie par défaut d'une session # Temps de vie par défaut d'une session
defaultKeepAlive: 1m defaultKeepAlive: 1m
# Configuration du layer "circuitbreaker"
circuitbreaker:
# Répertoire contenant les templates
templateDir: "/etc/bouncer/layers/circuitbreaker/templates"