Compare commits

..

No commits in common. "develop" and "authn-oidc-redirect-url" have entirely different histories.

127 changed files with 630 additions and 4595 deletions

View File

@ -1 +0,0 @@
GODEBUG="http1debug=2 http2debug=2"

2
.gitignore vendored
View File

@ -9,5 +9,3 @@
/data /data
/out /out
.dockerconfigjson .dockerconfigjson
*.prof
*.test

View File

@ -63,9 +63,6 @@ nfpms:
- src: layers - src: layers
dst: /etc/bouncer/layers dst: /etc/bouncer/layers
type: config type: config
- src: templates
dst: /etc/bouncer/templates
type: config
- dst: /etc/bouncer/bootstrap.d - dst: /etc/bouncer/bootstrap.d
type: dir type: dir
file_info: file_info:

View File

@ -1,4 +1,4 @@
FROM reg.cadoles.com/proxy_cache/library/golang:1.23 AS BUILD FROM reg.cadoles.com/proxy_cache/library/golang:1.22.0 AS BUILD
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y make && apt-get install -y make
@ -22,7 +22,6 @@ RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
# Patch config # Patch config
RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bouncer_linux_amd64_v1/config.yml \ RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.proxy.templates.dir = "/usr/share/bouncer/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.layers.queue.templateDir = "/usr/share/bouncer/layers/queue/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.layers.queue.templateDir = "/usr/share/bouncer/layers/queue/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.layers.authn.templateDir = "/usr/share/bouncer/layers/authn/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.layers.authn.templateDir = "/usr/share/bouncer/layers/authn/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.admin.auth.privateKey = "/etc/bouncer/admin-key.json"' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.admin.auth.privateKey = "/etc/bouncer/admin-key.json"' /src/dist/bouncer_linux_amd64_v1/config.yml \
@ -33,7 +32,7 @@ RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bounc
&& yq -i '.bootstrap.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.bootstrap.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.integrations.kubernetes.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml && yq -i '.integrations.kubernetes.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml
FROM reg.cadoles.com/proxy_cache/library/alpine:3.20 AS RUNTIME FROM reg.cadoles.com/proxy_cache/library/alpine:3.19.1 AS RUNTIME
RUN apk add --no-cache ca-certificates dumb-init RUN apk add --no-cache ca-certificates dumb-init
@ -43,7 +42,6 @@ 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/layers COPY --from=BUILD /src/layers /usr/share/bouncer/layers
COPY --from=BUILD /src/templates /usr/share/bouncer/templates
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
@ -52,12 +50,10 @@ EXPOSE 8080
EXPOSE 8081 EXPOSE 8081
EXPOSE 8082 EXPOSE 8082
RUN adduser -D -s /bin/sh bouncer RUN adduser -D -H bouncer
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
USER bouncer USER bouncer
WORKDIR /home/bouncer
CMD ["bouncer"] CMD ["bouncer"]

View File

@ -17,8 +17,7 @@ GOTEST_ARGS ?= -short
OPENWRT_DEVICE ?= 192.168.1.1 OPENWRT_DEVICE ?= 192.168.1.1
SIEGE_URLS_FILE ?= misc/siege/urls.txt SIEGE_URLS_FILE ?= misc/siege/urls.txt
SIEGE_CONCURRENCY ?= 200 SIEGE_CONCURRENCY ?= 100
SIEGE_DURATION ?= 5M
data/bootstrap.d/dummy.yml: data/bootstrap.d/dummy.yml:
mkdir -p data/bootstrap.d mkdir -p data/bootstrap.d
@ -60,7 +59,7 @@ deps: .env
.PHONY: goreleaser .PHONY: goreleaser
goreleaser: deps goreleaser: deps
( set -o allexport && source .env && set +o allexport && curl -sfL https://goreleaser.com/static/run | VERSION=$(GORELEASER_VERSION) GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) ) ( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
.PHONY: start-release .PHONY: start-release
start-release: start-release:
@ -82,7 +81,7 @@ finish-release:
git push --tags git push --tags
docker-build: docker-build:
docker build --pull -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) . docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) .
docker tag $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) $(DOCKER_IMAGE_NAME):latest docker tag $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) $(DOCKER_IMAGE_NAME):latest
docker-release: docker-release:
@ -115,7 +114,7 @@ grafterm: tools/grafterm/bin/grafterm
siege: siege:
$(eval TMP := $(shell mktemp)) $(eval TMP := $(shell mktemp))
cat $(SIEGE_URLS_FILE) | envsubst > $(TMP) cat $(SIEGE_URLS_FILE) | envsubst > $(TMP)
siege -R ./misc/siege/siege.conf -i -b -c $(SIEGE_CONCURRENCY) -t $(SIEGE_DURATION) -f $(TMP) siege -i -b -c $(SIEGE_CONCURRENCY) -f $(TMP)
rm -rf $(TMP) rm -rf $(TMP)
tools/gitea-release/bin/gitea-release.sh: tools/gitea-release/bin/gitea-release.sh:
@ -131,13 +130,6 @@ tools/grafterm/bin/grafterm:
mkdir -p tools/grafterm/bin mkdir -p tools/grafterm/bin
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0 GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
bench:
go test -bench=. -run '^$$' -benchtime=10s ./internal/bench
tools/benchstat/bin/benchstat:
mkdir -p tools/benchstat/bin
GOBIN=$(PWD)/tools/benchstat/bin go install golang.org/x/perf/cmd/benchstat@latest
full-version: full-version:
@echo $(FULL_VERSION) @echo $(FULL_VERSION)
@ -151,7 +143,7 @@ run-redis:
-v $(PWD)/data/redis:/data \ -v $(PWD)/data/redis:/data \
-p 6379:6379 \ -p 6379:6379 \
redis:alpine3.17 \ redis:alpine3.17 \
redis-server --save 60 1 --loglevel debug redis-server --save 60 1 --loglevel warning
redis-shell: redis-shell:
docker exec -it \ docker exec -it \

View File

@ -4,16 +4,7 @@
# Bouncer # Bouncer
Serveur mandataire inverse (_"reverse proxy"_) avec fonctionnalités avancées pilotable par API REST. Serveur mandataire inverse (_"reverse proxy"_) filtrant avec gestion de files d'attente dynamiques.
## Fonctionnalités
- Authentification unique basée sur entêtes HTTP ("Trusted headers SSO") avec:
- Fournisseur d'identité OpenID Connect ;
- Basic Auth ;
- Origine réseau ;
- Gestion de files d'attente dynamiques pour maîtriser la charge sur les services protégés ;
- Réécriture dynamique des attributs (notamment entêtes HTTP) des requêtes/réponses via un DSL.
## Documentation ## Documentation
@ -21,4 +12,4 @@ Serveur mandataire inverse (_"reverse proxy"_) avec fonctionnalités avancées p
## Licence ## Licence
AGPL-3.0 AGPL-3.0

View File

@ -1,8 +1,7 @@
# Documentation # Documentation
- [(FR) - Architecture générale](./fr/general-architecture.md)
- [(FR) - Terminologie](./fr/terminology.md)
- [(FR) - Premiers pas](./fr/getting-started.md) - [(FR) - Premiers pas](./fr/getting-started.md)
- [(FR) - Architecture générale](./fr/general-architecture.md)
## Exemples ## Exemples
@ -12,22 +11,19 @@
- [(FR) - Layers](./fr/references/layers/README.md) - [(FR) - Layers](./fr/references/layers/README.md)
- [(FR) - Métriques](./fr/references/metrics.md) - [(FR) - Métriques](./fr/references/metrics.md)
- [(FR) - Configuration](./fr/references/configuration.md) - [(FR) - Fichier de configuration](../misc/packaging/common/config.yml)
- [(FR) - API d'administration](./fr/references/admin_api.md) - [(FR) - API d'administration](./fr/references/admin_api.md)
## Tutoriels ## Tutoriels
### Utilisation ### Utilisation
- [(FR) - Le cas du "virtual hosting"](./fr/tutorials/virtual-hosting.md)
- [(FR) - Ajouter un layer 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)
- [(FR) - Ajouter une authentification OpenID Connect](./fr/tutorials/add-oidc-authn-layer.md) - [(FR) - Ajouter une authentification OpenID Connect](./fr/tutorials/add-oidc-authn-layer.md)
- [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md) - [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md)
- [(FR) - Intégration avec Kubernetes](./fr/tutorials/kubernetes-integration.md) - [(FR) - Intégration avec Kubernetes](./fr/tutorials/kubernetes-integration.md)
- [(FR) - Profilage](./fr/tutorials/profiling.md)
### Développement ### Développement
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-started-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)
- [(FR) - Étudier les performances de Bouncer](./fr/tutorials/profiling.md)

View File

@ -2,6 +2,31 @@
## Modèles de déploiement ## Modèles de déploiement
### Mode mono-noeud ### Déploiement mono-noeud
![](../resources/deployment_single_node_fr.png) ![](../resources/deployment_fr.png)
## Terminologie
Voici une liste des termes utilisés dans le lexique Bouncer.
### Proxy
Un "proxy" est une entité logique définissant le relation suivante:
- Un ou plusieurs patrons de filtrage sous la forme d'un patron d'URL avec le caractère `*` comme caractère générique. Ceux ci identifient le ou les domaines/chemins associés à l'entité;
- Une URL cible qui servira de base pour la réécriture des requêtes.
Un "proxy" peut avoir zéro ou plusieurs "layers" associés.
Un "proxy" peut être activé ou désactivé.
Un "proxy" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
### Layer
Un "layer" (calque) est une entité logique définissant un traitement à appliquer aux requêtes et/ou aux réponses transitant par un proxy.
Un "layer" peut être activé ou désactivé.
Un "layer" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire).

View File

@ -1,34 +0,0 @@
# Configuration
## Référence
Vous trouverez ici un fichier de configuration de référence, complet et commenté:
[`misc/packaging/common/config.yml`](../../../misc/packaging/common/config.yml)
## Interpolation de variables
Il est possible d'utiliser de l'interpolation de variables d'environnement dans le fichier de configuration via la syntaxe `${var}`.
Les fonctions d'interpolation suivantes sont également disponibles:
- `${var^}`
- `${var^^}`
- `${var,}`
- `${var,,}`
- `${var:position}`
- `${var:position:length}`
- `${var#substring}`
- `${var##substring}`
- `${var%substring}`
- `${var%%substring}`
- `${var/substring/replacement}`
- `${var//substring/replacement}`
- `${var/#substring/replacement}`
- `${var/%substring/replacement}`
- `${#var}`
- `${var=default}`
- `${var:=default}`
- `${var:-default}`
_Voir le package [`github.com/drone/envsubst`](https://pkg.go.dev/github.com/drone/envsubst) pour plus de détails._

View File

@ -4,4 +4,3 @@ Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entit
- [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO) - [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO)
- [Queue](./queue.md) - File d'attente dynamique - [Queue](./queue.md) - File d'attente dynamique
- [Rewriter](./rewriter.md) - Réécriture dynamiques des attributs des requêtes/réponses

View File

@ -27,8 +27,8 @@ Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus de
Le comportement des règles par défaut est le suivant: Le comportement des règles par défaut est le suivant:
1. L'ensemble des entêtes HTTP correspondant au patron `Remote-*` sont supprimés ; 1. L'ensemble des entêtes HTTP correspondant au patron `Remote-*` sont supprimés ;
2. L'identifiant de l'utilisateur identifié (`vars.user.subject`) est exporté sous la forme de l'entête HTTP `Remote-User` ; 2. L'identifiant de l'utilisateur identifié (`user.subject`) est exporté sous la forme de l'entête HTTP `Remote-User` ;
3. L'ensemble des attributs de l'utilisateur identifié (`vars.user.attrs`) sont exportés sous la forme `Remote-User-Attr-<name>``<name>` est le nom de l'attribut en minuscule, avec les `_` transformés en `-`. 3. L'ensemble des attributs de l'utilisateur identifié (`user.attrs`) sont exportés sous la forme `Remote-User-Attr-<name>``<name>` est le nom de l'attribut en minuscule, avec les `_` transformés en `-`.
### Fonctions ### Fonctions
@ -36,33 +36,21 @@ Le comportement des règles par défaut est le suivant:
Interdire l'accès à l'utilisateur. Interdire l'accès à l'utilisateur.
##### `add_header(ctx, name string, value string)` #### `set_header(name string, value string)`
Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`. Définir la valeur d'une entête HTTP via son nom `name` et sa valeur `value`.
##### `set_header(ctx, name string, value string)` #### `del_headers(pattern string)`
Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée.
##### `del_headers(ctx, pattern string)`
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`. Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires. Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
##### `set_host(ctx, host string)`
Modifier la valeur de l'entête `Host` de la requête.
##### `set_url(ctx, url string)`
Modifier l'URL du serveur cible.
### Environnement ### Environnement
Les règles ont accès aux variables suivantes pendant leur exécution. Les règles ont accès aux variables suivantes pendant leur exécution.
#### `vars.user` #### `user`
L'utilisateur identifié par le layer. L'utilisateur identifié par le layer.

View File

@ -14,12 +14,12 @@ Les options disponibles pour le layer sont décrites via un [schéma JSON](https
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json). En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
## Objet `vars.user` et attributs ## Objet `user` et attributs
L'objet `user` exposé au moteur de règles sera construit de la manière suivante: L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
- `vars.user.subject` sera initialisé avec le nom d'utilisateur identifié ; - `user.subject` sera initialisé avec le nom d'utilisateur identifié ;
- `vars.user.attrs` sera composé des attributs associés à l'utilisation (voir les options). - `user.attrs` sera composé des attributs associés à l'utilisation (voir les options).
## Métriques ## Métriques

View File

@ -14,12 +14,12 @@ Les options disponibles pour le layer sont décrites via un [schéma JSON](https
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json). En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
## Objet `vars.user` et attributs ## Objet `user` et attributs
L'objet `vars.user` exposé au moteur de règles sera construit de la manière suivante: L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
- `vars.user.subject` sera initialisé avec le couple `<remote_address>:<remote_port>` ; - `user.subject` sera initialisé avec le couple `<remote_address>:<remote_port>` ;
- `vars.user.attrs` sera vide. - `user.attrs` sera vide.
## Métriques ## Métriques

View File

@ -16,18 +16,18 @@ Les options disponibles pour le layer sont décrites via un [schéma JSON](https
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json). En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
## Objet `vars.user` et attributs ## Objet `user` et attributs
L'objet `vars.user` exposé au moteur de règles sera construit de la manière suivante: L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
- `vars.user.subject` sera initialisé avec la valeur du [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) `sub` extrait de l'`idToken` récupéré lors de l'authentification ; - `user.subject` sera initialisé avec la valeur du [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) `sub` extrait de l'`idToken` récupéré lors de l'authentification ;
- `vars.user.attrs` comportera les propriétés suivantes: - `user.attrs` comportera les propriétés suivantes:
- L'ensemble des `claims` provenant de l'`idToken` seront transposés en `claim_<name>` (ex: `idToken.iss` sera transposé en `vars.user.attrs.claim_iss`) ; - L'ensemble des `claims` provenant de l'`idToken` seront transposés en `claim_<name>` (ex: `idToken.iss` sera transposé en `user.attrs.claim_iss`) ;
- `vars.user.attrs.access_token`: le jeton d'accès associé à l'authentification ; - `user.attrs.access_token`: le jeton d'accès associé à l'authentification ;
- `vars.user.attrs.refresh_token`: le jeton de rafraîchissement associé à l'authentification (si disponible, en fonction des `scopes` demandés par le client) ; - `user.attrs.refresh_token`: le jeton de rafraîchissement associé à l'authentification (si disponible, en fonction des `scopes` demandés par le client) ;
- `vars.user.attrs.token_expiry`: Horodatage Unix (en secondes) associé à la date d'expiration du jeton d'accès ; - `user.attrs.token_expiry`: Horodatage Unix (en secondes) associé à la date d'expiration du jeton d'accès ;
- `vars.user.attrs.logout_url`: URL de déconnexion pour la suppression de la session Bouncer. - `user.attrs.logout_url`: URL de déconnexion pour la suppression de la session Bouncer.
**Attention** Cette URL ne permet dans la plupart des cas que de supprimer la session côté Bouncer. La suppression de la session côté fournisseur d'identité est conditionné à la présence ou non de l'attribut [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0-17.html#OPMetadata) dans les données du point d'entrée de découverte de service (`.wellknown/openid-configuration`). **Attention** Cette URL ne permet dans la plupart des cas que de supprimer la session côté Bouncer. La suppression de la session côté fournisseur d'identité est conditionné à la présence ou non de l'attribut [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0-17.html#OPMetadata) dans les données du point d'entrée de découverte de service (`.wellknown/openid-configuration`).

View File

@ -1,188 +0,0 @@
# Layer "Rewriter"
## Description
Ce layer permet de modifier dynamiquement certains attributs de requêtes/réponses transitant par le proxy.
## Type
`rewriter`
## Schéma des options
Les options disponibles pour le layer sont décrites via un [schéma JSON](https://json-schema.org/specification). Elles sont documentées dans le [schéma visible ici](../../../../internal/proxy/director/layer/rewriter/layer-options.json).
## Moteur de règles
Les options `rules.request` et `rules.response` permettent de définir des listes de règles utilisant un DSL modifiant de manière dynamique les attributs des requêtes/réponses transitant par le proxy.
Les listes d'instructions sont exécutées séquentiellement.
Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus des fonctionnalités natives du langage, Bouncer ajoute un certain nombre de fonctions spécifiques au contexte d'utilisation.
### Fonctions
#### Communes
##### `add_header(ctx, name string, value string)`
Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`.
##### `set_header(ctx, name string, value string)`
Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée.
##### `del_headers(ctx, pattern string)`
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
##### `get_cookie(ctx, name string) Cookie`
Récupère un cookie depuis la requête/réponse (en fonction du contexte d'utilisation).
Retourne `nil` si le cookie n'existe pas.
**Cookie**
```js
// Plus d'informations sur https://pkg.go.dev/net/http#Cookie
{
name: "string", // Nom du cookie
value: "string", // Valeur associée au cookie
path: "string", // Chemin associé au cookie (présent uniquement dans un contexte de réponse)
domain: "string", // Domaine associé au cookie (présent uniquement dans un contexte de réponse)
expires: "string", // Date d'expiration du cookie (présent uniquement dans un contexte de réponse)
max_age: "string", // Age maximum du cookie (présent uniquement dans un contexte de réponse)
secure: "boolean", // Le cookie doit-il être présent uniquement en HTTPS ? (présent uniquement dans un contexte de réponse)
http_only: "boolean", // Le cookie est il accessible en Javascript ? (présent uniquement dans un contexte de réponse)
same_site: "int" // Voir https://pkg.go.dev/net/http#SameSite (présent uniquement dans un contexte de réponse)
}
```
##### `add_cookie(ctx, cookie Cookie)`
Définit un cookie sur la requête/réponse (en fonction du contexte d'utilisation).
Voir la méthode `get_cookie()` pour voir les attributs potentiels.
#### Requête
##### `set_host(ctx, host string)`
Modifier la valeur de l'entête `Host` de la requête.
##### `set_url(ctx, url string)`
Modifier l'URL du serveur cible.
##### `redirect(ctx, statusCode int, url string)`
Interrompt la requête et retourne une redirection HTTP au client.
Le code HTTP utilisé doit être supérieur ou égale à `300` et inférieur à `400` (non inclus).
#### Réponse
_Pas de fonctions spécifiques._
### Environnement
Les règles ont accès aux variables suivantes pendant leur exécution. **Ces données sont en lecture seule.**
#### Requête
##### `vars.original_url`
L'URL originale, avant réécriture du `Host` par Bouncer.
```js
{
scheme: "string", // Schéma HTTP de l'URL
opaque: "string", // Données opaque de l'URL
user: { // Identifiants d'URL (Basic Auth)
username: "",
password: ""
},
host: "string", // Nom d'hôte (<domaine>:<port>) de l'URL
path: "string", // Chemin de l'URL (format assaini)
raw_path: "string", // Chemin de l'URL (format brut)
raw_query: "string", // Variables d'URL (format brut)
fragment : "string", // Fragment d'URL (format assaini)
raw_fragment : "string" // Fragment d'URL (format brut)
}
```
##### `vars.request`
La requête en cours de traitement.
```js
{
method: "string", // Méthode HTTP
host: "string", // Nom d'hôte (`Host`) associé à la requête
url: { // URL associée à la requête sous sa forme structurée
scheme: "string", // Schéma HTTP de l'URL
opaque: "string", // Données opaque de l'URL
user: { // Identifiants d'URL (Basic Auth)
username: "",
password: ""
},
host: "string", // Nom d'hôte (<domaine>:<port>) de l'URL
path: "string", // Chemin de l'URL (format assaini)
raw_path: "string", // Chemin de l'URL (format brut)
raw_query: "string", // Variables d'URL (format brut)
fragment : "string", // Fragment d'URL (format assaini)
raw_fragment : "string" // Fragment d'URL (format brut)
},
raw_url: "string", // URL associée à la requête (format assaini)
proto: "string", // Numéro de version du protocole utilisé
proto_major: "int", // Numéro de version majeure du protocole utilisé
proto_minor: "int", // Numéro de version mineur du protocole utilisé
header: { // Table associative des entêtes HTTP associés à la requête
"string": ["string"]
},
content_length: "int", // Taille du corps de la requête
transfer_encoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête
trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête
"string": ["string"]
},
remote_addr: "string", // Adresse du client HTTP à l'origine de la requête
request_uri: "string" // URL "brute" associée à la requêtes (avant opérations d'assainissement, utiliser "url" plutôt)
}
```
#### Réponse
##### `vars.response`
La réponse en cours de traitement.
```js
{
status_code: "int", // Code de statut de la réponse
status: "string", // Message associé au code de statut
proto: "string", // Numéro de version du protocole utilisé
proto_major: "int", // Numéro de version majeure du protocole utilisé
proto_minor: "int", // Numéro de version mineur du protocole utilisé
header: { // Table associative des entêtes HTTP associés à la requête
"string": ["string"]
},
content_length: "int", // Taille du corps de la réponse
transfer_encoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête
trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête
"string": ["string"]
},
}
```
##### `vars.request`
_Voir section précédente._
##### `vars.original_url`
_Voir section précédente._
## Métriques
_Pas de métriques spécifiques._

View File

@ -1,29 +0,0 @@
# Terminologie
Voici une liste des termes utilisés dans le lexique Bouncer.
## Proxy
Un proxy est une entité logique définie par les propriétés suivantes:
- Il possède **un ou plusieurs filtres d'origine** sous la forme de motifs d'URL avec le caractère `*` comme joker. Ces filtres identifient le ou les URLs associées au proxy.
- Il peut avoir **zéro ou une URL cible**, qui servira de base pour la réécriture des requêtes. Si l'URL est absente, on parle alors de "passthrough" (voir note).
- Il peut avoir **zéro ou plusieurs "layers" associés**.
- Il peut être **activé ou désactivé**.
- Il a **un poids qui définit son niveau de priorité** dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
Pour résumer un proxy répond à la question "_Quelle URL orienter vers quel serveur cible ?_".
> **Passthrough**
>
> Un proxy "passthrough" est un proxy n'ayant pas d'URL cible (champ vide). Dans ce cas si les motifs d'URLs correspondent à l'URL de la requête Bouncer appliquera les layers associés puis passera la main aux proxies suivants.
## Layer
Un layer est une entité logique définie par les propriétés suivantes:
- Il a **un type auquel est associé un schéma d'options** permettant de configurer son comportement.
- Il peut être **activé ou désactivé**.
- Il a **un poids qui définit son niveau de priorité** dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
Pour résumer un layer répond à la question "_Quel traitement appliquer à la requête et/ou réponse ?_".

View File

@ -55,7 +55,7 @@ Par défaut ce serveur écoute sur le port 8082. Il est possible de modifier l'a
bouncer admin proxy update --proxy-name my-proxy --proxy-enabled bouncer admin proxy update --proxy-name my-proxy --proxy-enabled
``` ```
3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous travaillez avec une instance Bouncer locale avec la configuration par défaut 3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut
4. Créer un layer de type `authn-oidc` pour notre nouveau proxy 4. Créer un layer de type `authn-oidc` pour notre nouveau proxy

View File

@ -10,13 +10,13 @@ Avoir un environnement de développement local fonctionnel. Voir tutoriel ["Dém
### Préparer la structure de base du nouveau layer ### Préparer la structure de base du nouveau layer
Une implémentation d'un layer se compose majoritairement de 3 éléments: Une implémetation d'un layer se compose majoritairement de 3 éléments:
- Une structure qui implémente une ou plusieurs interfaces (`director.MiddlewareLayer`, `director.RequestTransformerLayer` et/ou `director.ResponseTransformerLayer`); - Une structure qui implémente une ou plusieurs interfaces (`director.MiddlewareLayer`, `director.RequestTransformerLayer` et/ou `director.ResponseTransformerLayer`);
- Un schéma au format [JSON Schema](http://json-schema.org/) qui permettra de valider les "options" de notre layer; - Un schéma au format [JSON Schema](http://json-schema.org/) qui permettra de valider les "options" de notre layer;
- Un fichier d'amorçage qui permettra à Bouncer de référencer notre nouveau layer. - Un fichier d'amorçage qui permettra à Bouncer de référencer notre nouveau layer.
1. Créer le répertoire du `package` Go qui contiendra le code de votre layer. Celui ci sappellera `basicauth`: 1. Créer le répertoire du `package` Go qui contiendra le code de votre layer. Celui ci s'appelera `basicauth`:
``` ```
mkdir -p internal/proxy/director/layer/basicauth mkdir -p internal/proxy/director/layer/basicauth
@ -133,22 +133,26 @@ Une implémentation d'un layer se compose majoritairement de 3 éléments:
## Tester l'intégration de notre nouveau layer ## Tester l'intégration de notre nouveau layer
À ce stade, notre nouveau layer est normalement référencé et donc "utilisable" dans Bouncer (si on omet le fait qu'il déclenchera un `panic()`). À ce stade, notre nouveau layer est normalement référencé et donc "utilisable" dans Bouncer (si on omet le fait qu'il déclenchera une `panic()`).
1. Vérifier que notre layer est bien référencé en exécutant la commande: 1. Vérifier que notre layer est bien référencé en exécutant la commande:
``` ```
./bin/bouncer admin definition layer query --with-type basicauth ./bin/bouncer admin layer create --help
``` ```
La sortie devrait ressembler à: La sortie devrait ressembler à:
``` ```
+-----------+-----------------------------------+ NAME:
| TYPE | OPTIONS | bouncer admin layer create - Create layer
+-----------+-----------------------------------+
| basicauth | {"type":"object","properties":... | USAGE:
+-----------+-----------------------------------+ bouncer admin layer create [command options] [arguments...]
OPTIONS:
--layer-type LAYER_TYPE Set LAYER_TYPE as layer's type (available: [basicauth queue])
[...]
``` ```
Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`. Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`.

View File

@ -1,68 +0,0 @@
# Étudier les performances de Bouncer
## In situ
Il est possible d'activer via la configuration de Bouncer de endpoints capable de générer des fichiers de profil au format [`pprof`](https://github.com/google/pprof). Par défaut, le point d'entrée est `.bouncer/profiling` (l'activation et la personnalisation de ce point d'entrée sont modifiables via la [configuration](../../../misc/packaging/common/config.yml)).
**Exemple:** Visualiser l'utilisation mémoire de Bouncer
```bash
go tool pprof -web http://<bouncer_proxy>/.bouncer/profiling/heap
```
L'ensemble des profils disponibles sont visibles à l'adresse `http://<bouncer_proxy>/.bouncer/profiling`.
## En développement
Le package `./internal` est dédié à l'étude des performances de Bouncer. Il contient une suite de benchmarks simulant de proxies avec différentes configurations de layers afin d'évaluer les points d'engorgement sur le traitement des requêtes.
Voir le répertoire `./internal/bench/testdata/proxies` pour voir les différentes configurations de cas.
### Lancer les benchmarks
Le plus simple est d'utiliser la commande `make bench` qui exécutera séquentiellement tous les benchmarks. Il est également possible de lancer un benchmark spécifique via la commande suivante:
```bash
go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
```
Par exemple:
```bash
# Pour exécuter ./internal/bench/testdata/proxies/basic-auth.yml
go test -bench='BenchmarkProxies/basic-auth' -run='^$' ./internal/bench
```
### Visualiser les profils d'exécution
Vous pouvez visualiser les profils d'exécution via la commande suivante:
```shell
go tool pprof -web path/to/file.prof
```
Par défaut l'exécution des benchmarks créera automatiquement des fichiers de profil dans le répertoire `./internal/bench/testdata/proxies`.
Par exemple:
```shell
go tool pprof -web ./internal/bench/testdata/proxies/basic-auth.prof
```
### Comparer les évolutions
```bash
# Lancer un premier benchmark
go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
# Faire une sauvegarde du fichier de profil
cp ./internal/bench/testdata/proxies/$BENCH_CASE.prof ./internal/bench/testdata/proxies/$BENCH_CASE-prev.prof
# Faire des modifications sur les sources
# Lancer un second benchmark
go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
# Visualiser la différence entre les deux profils
go tool pprof -web -base=./internal/bench/testdata/proxies/$BENCH_CASE-prev.prof ./internal/bench/testdata/proxies/$BENCH_CASE.prof
```

View File

@ -1,129 +0,0 @@
# Le cas du "virtual hosting"
De nombreux serveurs HTTP utilisent le mécanisme du ["virtual hosting"](https://en.wikipedia.org/wiki/Virtual_hosting) afin d'héberger plusieurs sites/applications différentes sur un même serveur, se basant alors sur l'entête HTTP `Host` pour effectuer le routage.
## Exemple
Pour exemple, avec le site [example.net](https://example.net) il est facile de tester ce type de comportement. Ainsi, en exécutant une requête HTTP avec `curl`:
```shell
curl -I https://example.net
```
On obtient le résultat suivant:
```
HTTP/2 200
accept-ranges: bytes
age: 568237
cache-control: max-age=604800
content-type: text/html; charset=UTF-8
date: Thu, 27 Jun 2024 08:32:46 GMT
etag: "3147526947"
expires: Thu, 04 Jul 2024 08:32:46 GMT
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
server: ECAcc (bsb/2789)
x-cache: HIT
content-length: 1256
```
Ce résultat indique que le serveur a correctement orienté notre requête (code HTTP `200`) et qu'il nous a renvoyé la réponse attendue.
Si maintenant on modifie l'entête `Host` de notre requête pour la remplacer par une valeur arbitraire:
```shell
curl -I -H 'Host: localhost:8080' https://example.net
```
On obtient alors le résultat:
```
HTTP/2 404
content-type: text/html
date: Thu, 27 Jun 2024 08:38:04 GMT
server: ECAcc (bsb/2789)
content-length: 345
```
Le serveur nous répond avec un code HTTP `404`, indiquant qu'il n'a pas trouvé la page demandée.
> **Note**
> Le code HTTP retourné par le serveur peut varier en fonction des implémentations. Parfois la requête sera orientée vers la page par défaut, parfois vous recevrez un code d'erreur HTTP comme `404`, `421`, etc.
## Avec Bouncer
Ce mécanisme peut parfois poser problème avec Bouncer car par défaut celui ci n'effectue pas de réécriture de l'entête `Host`. Pour exemple:
1. Créez puis activez un nouveau proxy pointant vers https://example.net
```shell
bouncer admin proxy create --proxy-name example --proxy-to https://example.net
bouncer admin proxy update --proxy-name example --proxy-enabled=true
```
2. Avec `curl`, faites une requête sur votre nouveau proxy:
```shell
curl -I http://localhost:8080
```
La réponse devrait ressembler à:
```
HTTP/1.1 404 Not Found
Content-Length: 345
Content-Type: text/html
Date: Thu, 27 Jun 2024 08:49:05 GMT
Server: ECAcc (bsb/2789)
```
On retrouve bien notre code HTTP `404` tel que vu plus haut. En effet, vu que l'on accède au proxy Bouncer avec `http://localhost:8080` alors le serveur distant recevra l'entête `Host: localhost:8080`.
### Comment corriger la situation ?
Le layer [`rewriter`](../references/layers/rewriter.md) a été implémenté notamment pour répondre à ce type de cas. Voyons comment l'utiliser:
1. Créez puis activez un nouveau layer pour votre proxy `example`:
```bash
# Création du layer
bouncer admin layer create --proxy-name example --layer-name host-rewrite --layer-type rewriter
# Mise à jour et activation du layer
bouncer admin layer update \
--proxy-name example \
--layer-name host-rewrite \
--layer-options '{ "rules": { "request": ["set_host(\"example.net\")"] } }' \
--layer-enabled=true
```
> **Les règles**
>
> Le layer `rewriter` permet la modification des requêtes/réponses via un moteur de règles.
>
> [Voir la page du layer pour plus d'informations](../references/layers/rewriter.md) sur la syntaxe ainsi que sur l'API à disposition des règles.
2. Testez maintenant à nouveau un appel vers votre proxy:
```shell
curl -I http://localhost:8080
```
La réponse devrait ressembler à:
```
HTTP/1.1 200 OK
Accept-Ranges: bytes
Age: 569980
Cache-Control: max-age=604800
Content-Length: 1256
Content-Type: text/html; charset=UTF-8
Date: Thu, 27 Jun 2024 09:01:49 GMT
Etag: "3147526947"
Expires: Thu, 04 Jul 2024 09:01:49 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECAcc (bsb/2789)
X-Cache: HIT
```
Cette fois ci, le serveur distant a bien identifié la cible de notre requête.

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

29
go.mod
View File

@ -1,11 +1,11 @@
module forge.cadoles.com/cadoles/bouncer module forge.cadoles.com/cadoles/bouncer
go 1.23 go 1.21
toolchain go1.23.0 toolchain go1.22.0
require ( require (
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926 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/bsm/redislock v0.9.4 github.com/bsm/redislock v0.9.4
github.com/btcsuite/btcd/btcutil v1.1.3 github.com/btcsuite/btcd/btcutil v1.1.3
@ -92,9 +92,8 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/net v0.26.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
@ -111,18 +110,18 @@ require (
require ( require (
cdr.dev/slog v1.6.1 // indirect cdr.dev/slog v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // 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.5 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.0 github.com/lestrrat-go/jwx/v2 v2.0.19
github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lib/pq v1.10.0 // indirect github.com/lib/pq v1.10.0 // indirect
github.com/lithammer/shortuuid/v4 v4.0.0 github.com/lithammer/shortuuid/v4 v4.0.0
@ -132,11 +131,11 @@ require (
github.com/urfave/cli/v2 v2.25.3 github.com/urfave/cli/v2 v2.25.3
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88 gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88
golang.org/x/crypto v0.24.0 golang.org/x/crypto v0.19.0
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.21.0 // indirect golang.org/x/term v0.17.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.16.1 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1

56
go.sum
View File

@ -8,8 +8,8 @@ cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926 h1:gSTTuW2lqH66cGVrhplrVrqos62BY1/GxR3KYh2TElk= forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 h1:FTk0ZoaV5N8Tkps5Da5RrDMZZXSHZIuD67Hy1Y4fsos=
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw= forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -83,8 +83,8 @@ github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@ -133,8 +133,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@ -208,12 +208,12 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/blackmagic v1.0.2/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=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk= github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.0 h1:0zs7Ya6+39qoit7gwAf+cYm1zzgS3fceIdo7RmQ5lkw= github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY=
github.com/lestrrat-go/jwx/v2 v2.1.0/go.mod h1:Xpw9QIaUGiIUD1Wx0NcY1sIHwFf8lDuZn/cmxtXYRys= github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
@ -339,8 +339,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@ -375,13 +375,13 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -395,8 +395,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -405,8 +405,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -433,13 +433,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -447,8 +447,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -457,8 +457,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -70,7 +70,7 @@ func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool)
ctx := r.Context() ctx := r.Context()
user, err := auth.CtxUser(ctx) user, err := auth.CtxUser(ctx)
if err != nil { if err != nil {
logger.Error(ctx, "could not retrieve user", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not retrieve user", logger.E(errors.WithStack(err)))
forbidden(w, r) forbidden(w, r)

View File

@ -6,6 +6,7 @@ import (
"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" "gitlab.com/wpetit/goweb/logger"
) )
@ -28,8 +29,11 @@ func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schem
}{ }{
Message: message, Message: message,
}) })
return
} }
func logAndCaptureError(ctx context.Context, message string, err error) { func logAndCaptureError(ctx context.Context, message string, err error) {
logger.Error(ctx, message, logger.CapturedE(err)) sentry.CaptureException(err)
logger.Error(ctx, message, logger.E(err))
} }

View File

@ -27,7 +27,7 @@ func (s *Server) initRepositories(ctx context.Context) error {
} }
func (s *Server) initRedisClient(ctx context.Context) error { func (s *Server) initRedisClient(ctx context.Context) error {
client := setup.NewSharedClient(s.redisConfig) client := setup.NewRedisClient(ctx, s.redisConfig)
s.redisClient = client s.redisClient = client

View File

@ -121,7 +121,7 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
type CreateProxyRequest struct { type CreateProxyRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
To string `json:"to"` To string `json:"to" validate:"required"`
From []string `json:"from" validate:"required"` From []string `json:"from" validate:"required"`
} }

View File

@ -2,12 +2,10 @@ package admin
import ( import (
"context" "context"
"expvar"
"fmt" "fmt"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/pprof"
"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"
@ -116,9 +114,7 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router.Use(middleware.RealIP) router.Use(middleware.RealIP)
} }
router.Use(middleware.RequestID)
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter())) router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
router.Use(middleware.Recoverer)
if s.serverConfig.Sentry.DSN != "" { if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware") logger.Info(ctx, "enabling sentry http middleware")
@ -159,35 +155,6 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
}) })
} }
if s.serverConfig.Profiling.Enabled {
profiling := s.serverConfig.Profiling
logger.Info(ctx, "enabling profiling", logger.F("endpoint", profiling.Endpoint))
router.Group(func(r chi.Router) {
if profiling.BasicAuth != nil {
logger.Info(ctx, "enabling authentication on profiling endpoint")
r.Use(middleware.BasicAuth(
"profiling",
profiling.BasicAuth.CredentialsMap(),
))
}
r.Route(string(profiling.Endpoint), func(r chi.Router) {
r.HandleFunc("/", pprof.Index)
r.HandleFunc("/cmdline", pprof.Cmdline)
r.HandleFunc("/profile", pprof.Profile)
r.HandleFunc("/symbol", pprof.Symbol)
r.HandleFunc("/trace", pprof.Trace)
r.Handle("/vars", expvar.Handler())
r.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
pprof.Handler(name).ServeHTTP(w, r)
})
})
})
}
router.Route("/api/v1", func(r chi.Router) { router.Route("/api/v1", func(r chi.Router) {
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(auth.Middleware( r.Use(auth.Middleware(

View File

@ -52,7 +52,7 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
for _, auth := range authenticators { for _, auth := range authenticators {
user, err = auth.Authenticate(ctx, r) user, err = auth.Authenticate(ctx, r)
if err != nil { if err != nil {
logger.Debug(ctx, "could not authenticate request", logger.CapturedE(errors.WithStack(err))) logger.Debug(ctx, "could not authenticate request", logger.E(errors.WithStack(err)))
continue continue
} }

View File

@ -1,318 +0,0 @@
package proxy_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"runtime/pprof"
"strings"
"testing"
"time"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gitlab.com/wpetit/goweb/logger"
"gopkg.in/yaml.v3"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
)
func BenchmarkProxies(b *testing.B) {
proxyFiles, err := filepath.Glob("testdata/proxies/*.yml")
if err != nil {
b.Fatalf("%+v", errors.WithStack(err))
}
for _, f := range proxyFiles {
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
b.Run(name, func(b *testing.B) {
heap, err := os.Create(filepath.Join("testdata", "proxies", name+"_heap.prof"))
if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could not create heap profile"))
}
defer func() {
defer heap.Close()
if err := pprof.WriteHeapProfile(heap); err != nil {
b.Fatalf("%+v", errors.WithStack(err))
}
}()
conf, err := loadProxyBenchConfig(f)
if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could notre load bench config"))
}
proxy, backend, err := createProxy(name, conf, b.Logf)
if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could not create proxy"))
}
defer proxy.Close()
if backend != nil {
defer backend.Close()
}
client := proxy.Client()
proxyURL, err := url.Parse(proxy.URL)
if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could not parse proxy url"))
}
if conf.Fetch.URL.Path != "" {
proxyURL.Path = conf.Fetch.URL.Path
}
if conf.Fetch.URL.RawQuery != "" {
proxyURL.RawQuery = conf.Fetch.URL.RawQuery
}
if conf.Fetch.URL.User.Username != "" || conf.Fetch.URL.User.Password != "" {
proxyURL.User = url.UserPassword(conf.Fetch.URL.User.Username, conf.Fetch.URL.User.Password)
}
rawProxyURL := proxyURL.String()
b.Logf("fetching url '%s'", rawProxyURL)
profile, err := os.Create(filepath.Join("testdata", "proxies", name+"_cpu.prof"))
if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could not create cpu profile"))
}
defer profile.Close()
if err := pprof.StartCPUProfile(profile); err != nil {
b.Fatalf("%+v", errors.WithStack(err))
}
defer pprof.StopCPUProfile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := client.Get(rawProxyURL)
if err != nil {
b.Errorf("could not fetch proxy url: %+v", errors.WithStack(err))
}
body, err := io.ReadAll(res.Body)
if err != nil {
b.Errorf("could not read response body: %+v", errors.WithStack(err))
}
b.Logf("%s \n %v", res.Status, string(body))
if err := res.Body.Close(); err != nil {
b.Errorf("could not close response body: %+v", errors.WithStack(err))
}
}
})
}
}
type proxyBenchConfig struct {
Proxy config.BootstrapProxyConfig `yaml:"proxy"`
Fetch fetchBenchConfig `yaml:"fetch"`
}
type fetchBenchConfig struct {
URL fetchURLBenchConfig `yaml:"url"`
}
type fetchURLBenchConfig struct {
Path string `yaml:"path"`
RawQuery string `yaml:"rawQuery"`
User fetchURLUserBenchConfig `yaml:"user"`
}
type fetchURLUserBenchConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}
func loadProxyBenchConfig(filename string) (*proxyBenchConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, errors.Wrapf(err, "could not read file '%s'", filename)
}
conf := proxyBenchConfig{}
if err := yaml.Unmarshal(data, &conf); err != nil {
return nil, errors.Wrapf(err, "could not unmarshal config")
}
return &conf, nil
}
func createProxy(name string, conf *proxyBenchConfig, logf func(format string, a ...any)) (*httptest.Server, *httptest.Server, error) {
redisEndpoint := os.Getenv("BOUNCER_BENCH_REDIS_ADDR")
if redisEndpoint == "" {
redisEndpoint = "127.0.0.1:6379"
}
client := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{redisEndpoint},
})
proxyRepository := redisStore.NewProxyRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay)
layerRepository := redisStore.NewLayerRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay)
var backend *httptest.Server
if conf.Proxy.To == "" {
backend = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("Hello, world.")); err != nil {
logf("[ERROR] %+v", errors.WithStack(err))
}
}))
if err := waitFor(backend.URL, 5*time.Second); err != nil {
return nil, nil, errors.WithStack(err)
}
logf("started backend '%s'", backend.URL)
}
ctx := context.Background()
proxyName := store.ProxyName("bench-" + name)
proxies, err := proxyRepository.QueryProxy(ctx)
if err != nil {
return nil, nil, errors.WithStack(err)
}
// Cleanup existing proxies
for _, p := range proxies {
if err := proxyRepository.DeleteProxy(ctx, p.Name); err != nil {
return nil, nil, errors.WithStack(err)
}
}
logf("creating proxy '%s'", proxyName)
to := string(conf.Proxy.To)
if to == "" {
to = backend.URL
}
if _, err := proxyRepository.CreateProxy(ctx, proxyName, to, conf.Proxy.From...); err != nil {
return nil, nil, errors.WithStack(err)
}
if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil {
return nil, nil, errors.WithStack(err)
}
for layerName, layerConf := range conf.Proxy.Layers {
if err := layerRepository.DeleteLayer(ctx, proxyName, store.LayerName(layerName)); err != nil {
return nil, nil, errors.WithStack(err)
}
_, err := layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), store.LayerType(layerConf.Type), layerConf.Options.Data)
if err != nil {
return nil, nil, errors.WithStack(err)
}
_, err = layerRepository.UpdateLayer(ctx, proxyName, store.LayerName(layerName), store.WithLayerUpdateEnabled(bool(layerConf.Enabled)))
if err != nil {
return nil, nil, errors.WithStack(err)
}
}
appConf := config.NewDefault()
appConf.Logger.Level = config.InterpolatedInt(logger.LevelError)
appConf.Layers.Authn.TemplateDir = "../../layers/authn/templates"
appConf.Layers.Queue.TemplateDir = "../../layers/queue/templates"
layers, err := setup.GetLayers(context.Background(), appConf)
if err != nil {
return nil, nil, errors.WithStack(err)
}
director := director.New(
proxyRepository, layerRepository,
director.WithLayerCache(
ttl.NewCache(
memory.NewCache[string, []*store.Layer](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
),
director.WithProxyCache(
ttl.NewCache(
memory.NewCache[string, []*store.Proxy](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
),
director.WithLayers(layers...),
)
directorMiddleware := director.Middleware()
handler := proxy.New(
proxy.WithRequestTransformers(
director.RequestTransformer(),
),
proxy.WithResponseTransformers(
director.ResponseTransformer(),
),
proxy.WithReverseProxyFactory(func(ctx context.Context, target *url.URL) *httputil.ReverseProxy {
reverse := httputil.NewSingleHostReverseProxy(target)
reverse.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
logf("[ERROR] %s", errors.WithStack(err))
}
return reverse
}),
)
server := httptest.NewServer(directorMiddleware(handler))
return server, backend, nil
}
func waitFor(url string, ttl time.Duration) error {
var lastErr error
timeout := time.After(ttl)
for {
select {
case <-timeout:
if lastErr != nil {
return lastErr
}
return errors.New("wait timed out")
default:
res, err := http.Get(url)
if err != nil {
lastErr = errors.WithStack(err)
continue
}
if res.StatusCode >= 200 && res.StatusCode < 400 {
return nil
}
}
}
}

View File

@ -1,20 +0,0 @@
proxy:
from: ["*"]
to: ""
layers:
basic-auth:
type: authn-basic
enabled: true
options:
users:
- username: foo
passwordHash: "$2y$10$ShTc856wMB8PCxyr46qJRO8z06MpV4UejAVRDJ/bixhu0XTGn7Giy"
attributes:
email: foo@bar.com
rules:
- set_header(ctx, "Remote-User-Attr-Email", vars.user.attrs.email)
fetch:
url:
user:
username: foo
password: bar

View File

@ -1,3 +0,0 @@
proxy:
from: ["*"]
to: ""

View File

@ -1,10 +0,0 @@
proxy:
from: ["*"]
to: ""
layers:
queue:
type: queue
enabled: true
options:
capacity: 100
keepAlive: 10s

View File

@ -1,12 +0,0 @@
proxy:
from: ["*"]
to: ""
layers:
host-rewriter:
type: rewriter
enabled: true
options:
rules:
request:
- set_host(ctx, vars.request.url.host)
- set_header(ctx, "X-Proxied-With", "bouncer")

View File

@ -1,6 +0,0 @@
package cache
type Cache[K comparable, V any] interface {
Get(key K) (V, bool)
Set(key K, value V)
}

View File

@ -1,34 +0,0 @@
package memory
import (
"sync"
cache "forge.cadoles.com/cadoles/bouncer/internal/cache"
)
type Cache[K comparable, V any] struct {
store *sync.Map
}
// Get implements cache.Cache.
func (c *Cache[K, V]) Get(key K) (V, bool) {
raw, exists := c.store.Load(key)
if !exists {
return *new(V), false
}
return raw.(V), exists
}
// Set implements cache.Cache.
func (c *Cache[K, V]) Set(key K, value V) {
c.store.Store(key, value)
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
store: new(sync.Map),
}
}
var _ cache.Cache[string, bool] = &Cache[string, bool]{}

View File

@ -1,39 +0,0 @@
package ttl
import (
"time"
cache "forge.cadoles.com/cadoles/bouncer/internal/cache"
)
type Cache[K comparable, V any] struct {
timestamps cache.Cache[K, time.Time]
values cache.Cache[K, V]
ttl time.Duration
}
// Get implements cache.Cache.
func (c *Cache[K, V]) Get(key K) (V, bool) {
timestamp, exists := c.timestamps.Get(key)
if !exists || timestamp.Add(c.ttl).Before(time.Now()) {
return *new(V), false
}
return c.values.Get(key)
}
// Set implements cache.Cache.
func (c *Cache[K, V]) Set(key K, value V) {
c.timestamps.Set(key, time.Now())
c.values.Set(key, value)
}
func NewCache[K comparable, V any](values cache.Cache[K, V], timestamps cache.Cache[K, time.Time], ttl time.Duration) *Cache[K, V] {
return &Cache[K, V]{
values: values,
timestamps: timestamps,
ttl: ttl,
}
}
var _ cache.Cache[string, bool] = &Cache[string, bool]{}

View File

@ -1,39 +0,0 @@
package ttl
import (
"testing"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
)
func TestCache(t *testing.T) {
cache := NewCache(
memory.NewCache[string, int](),
memory.NewCache[string, time.Time](),
time.Second,
)
key := "foo"
if _, exists := cache.Get(key); exists {
t.Errorf("cache.Get(\"%s\"): should not exists", key)
}
cache.Set(key, 1)
value, exists := cache.Get(key)
if !exists {
t.Errorf("cache.Get(\"%s\"): should exists", key)
}
if e, g := 1, value; e != g {
t.Errorf("cache.Get(\"%s\"): expected '%v', got '%v'", key, e, g)
}
time.Sleep(time.Second)
if _, exists := cache.Get("foo"); exists {
t.Errorf("cache.Get(\"%s\"): should not exists", key)
}
}

View File

@ -2,6 +2,7 @@ package chi
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"time" "time"
@ -36,19 +37,12 @@ type LogEntry struct {
// Panic implements middleware.LogEntry // Panic implements middleware.LogEntry
func (e *LogEntry) Panic(v interface{}, stack []byte) { func (e *LogEntry) Panic(v interface{}, stack []byte) {
logger.Error( logger.Error(e.ctx, fmt.Sprintf("%s %s", e.method, e.path), logger.F("stack", string(stack)))
e.ctx, "http panic",
logger.F("stack", string(stack)),
logger.F("host", e.host),
logger.F("method", e.method),
logger.F("path", e.path),
)
} }
// Write implements middleware.LogEntry // Write implements middleware.LogEntry
func (e *LogEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { func (e *LogEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
logger.Info( logger.Info(e.ctx, fmt.Sprintf("%s %s - %d", e.method, e.path, status),
e.ctx, "http request",
logger.F("host", e.host), logger.F("host", e.host),
logger.F("status", status), logger.F("status", status),
logger.F("bytes", bytes), logger.F("bytes", bytes),

View File

@ -2,7 +2,6 @@ package layer
import ( import (
"os" "os"
"slices"
"forge.cadoles.com/cadoles/bouncer/internal/client" "forge.cadoles.com/cadoles/bouncer/internal/client"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
@ -53,16 +52,14 @@ func QueryCommand() *cli.Command {
client := client.New(baseFlags.ServerURL, client.WithToken(token)) client := client.New(baseFlags.ServerURL, client.WithToken(token))
layers, err := client.QueryLayer(ctx.Context, proxyName, options...) proxies, err := client.QueryLayer(ctx.Context, proxyName, options...)
if err != nil { if err != nil {
return errors.WithStack(apierr.Wrap(err)) return errors.WithStack(apierr.Wrap(err))
} }
slices.SortFunc(layers, sortLayerssByWeight)
hints := layerHeaderHints(baseFlags.OutputMode) hints := layerHeaderHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(layers)...); err != nil { if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(proxies)...); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -70,13 +67,3 @@ func QueryCommand() *cli.Command {
}, },
} }
} }
func sortLayerssByWeight(a *store.LayerHeader, b *store.LayerHeader) int {
if a.Weight < b.Weight {
return 1
}
if a.Weight > b.Weight {
return -1
}
return 0
}

View File

@ -13,7 +13,6 @@ func layerHeaderHints(outputMode format.OutputMode) format.Hints {
format.NewProp("Type", "Type"), format.NewProp("Type", "Type"),
format.NewProp("Enabled", "Enabled"), format.NewProp("Enabled", "Enabled"),
format.NewProp("Weight", "Weight"), format.NewProp("Weight", "Weight"),
format.NewProp("Revision", "Revision"),
}, },
} }
} }
@ -26,7 +25,6 @@ func layerHints(outputMode format.OutputMode) format.Hints {
format.NewProp("Type", "Type"), format.NewProp("Type", "Type"),
format.NewProp("Enabled", "Enabled"), format.NewProp("Enabled", "Enabled"),
format.NewProp("Weight", "Weight"), format.NewProp("Weight", "Weight"),
format.NewProp("Revision", "Revision"),
format.NewProp("Options", "Options"), format.NewProp("Options", "Options"),
format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)), format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)),
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)), format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),

View File

@ -19,7 +19,7 @@ func CreateCommand() *cli.Command {
Name: "create", Name: "create",
Usage: "Create proxy", Usage: "Create proxy",
Flags: proxyFlag.WithProxyFlags( Flags: proxyFlag.WithProxyFlags(
flag.ProxyTo(), flag.ProxyTo(true),
flag.ProxyFrom(), flag.ProxyFrom(),
), ),
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {

View File

@ -30,11 +30,12 @@ func ProxyName() cli.Flag {
const KeyProxyTo = "proxy-to" const KeyProxyTo = "proxy-to"
func ProxyTo() cli.Flag { func ProxyTo(required bool) cli.Flag {
return &cli.StringFlag{ return &cli.StringFlag{
Name: KeyProxyTo, Name: KeyProxyTo,
Usage: "Set `PROXY_TO` as proxy's destination url", Usage: "Set `PROXY_TO` as proxy's destination url",
Value: "", Value: "",
Required: required,
} }
} }

View File

@ -2,7 +2,6 @@ package proxy
import ( import (
"os" "os"
"slices"
"forge.cadoles.com/cadoles/bouncer/internal/client" "forge.cadoles.com/cadoles/bouncer/internal/client"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
@ -52,8 +51,6 @@ func QueryCommand() *cli.Command {
return errors.WithStack(apierr.Wrap(err)) return errors.WithStack(apierr.Wrap(err))
} }
slices.SortFunc(proxies, sortProxiesByWeight)
hints := proxyHeaderHints(baseFlags.OutputMode) hints := proxyHeaderHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(proxies)...); err != nil { if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(proxies)...); err != nil {
@ -64,13 +61,3 @@ func QueryCommand() *cli.Command {
}, },
} }
} }
func sortProxiesByWeight(a *store.ProxyHeader, b *store.ProxyHeader) int {
if a.Weight < b.Weight {
return 1
}
if a.Weight > b.Weight {
return -1
}
return 0
}

View File

@ -19,7 +19,7 @@ func UpdateCommand() *cli.Command {
Name: "update", Name: "update",
Usage: "Update proxy", Usage: "Update proxy",
Flags: proxyFlag.WithProxyFlags( Flags: proxyFlag.WithProxyFlags(
flag.ProxyTo(), flag.ProxyTo(false),
flag.ProxyFrom(), flag.ProxyFrom(),
flag.ProxyEnabled(), flag.ProxyEnabled(),
flag.ProxyWeight(), flag.ProxyWeight(),

View File

@ -12,7 +12,6 @@ func proxyHeaderHints(outputMode format.OutputMode) format.Hints {
format.NewProp("Name", "Name"), format.NewProp("Name", "Name"),
format.NewProp("Enabled", "Enabled"), format.NewProp("Enabled", "Enabled"),
format.NewProp("Weight", "Weight"), format.NewProp("Weight", "Weight"),
format.NewProp("Revision", "Revision"),
}, },
} }
} }
@ -26,7 +25,6 @@ func proxyHints(outputMode format.OutputMode) format.Hints {
format.NewProp("To", "To"), format.NewProp("To", "To"),
format.NewProp("Enabled", "Enabled"), format.NewProp("Enabled", "Enabled"),
format.NewProp("Weight", "Weight"), format.NewProp("Weight", "Weight"),
format.NewProp("Revision", "Revision"),
format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)), format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)),
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)), format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
}, },

View File

@ -35,16 +35,13 @@ func RunCommand() *cli.Command {
logger.SetLevel(logger.Level(conf.Logger.Level)) logger.SetLevel(logger.Level(conf.Logger.Level))
projectVersion := ctx.String("projectVersion") projectVersion := ctx.String("projectVersion")
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Admin.Sentry, projectVersion)
if conf.Proxy.Sentry.DSN != "" { if err != nil {
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion) return errors.Wrap(err, "could not initialize sentry client")
if err != nil {
return errors.Wrap(err, "could not initialize sentry client")
}
defer flushSentry()
} }
defer flushSentry()
integrations, err := setup.SetupIntegrations(ctx.Context, conf) integrations, err := setup.SetupIntegrations(ctx.Context, conf)
if err != nil { if err != nil {
return errors.Wrap(err, "could not setup integrations") return errors.Wrap(err, "could not setup integrations")

View File

@ -4,14 +4,14 @@
<h2>Incoming headers</h2> <h2>Incoming headers</h2>
<table style="width: 100%"> <table style="width: 100%">
<thead> <thead>
<tr style="text-align: left"> <tr>
<th>Key</th> <th>Key</th>
<th>Value</th> <th>Value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range $key, $val := .Request.Header }} {{ range $key, $val := .Request.Header }}
<tr style="text-align: left"> <tr>
<td> <td>
<b>{{ $key }}</b> <b>{{ $key }}</b>
</td> </td>
@ -27,7 +27,7 @@
<h2>Incoming cookies</h2> <h2>Incoming cookies</h2>
<table style="width: 100%"> <table style="width: 100%">
<thead> <thead>
<tr style="text-align: left"> <tr>
<th>Name</th> <th>Name</th>
<th>Domain</th> <th>Domain</th>
<th>Path</th> <th>Path</th>
@ -41,7 +41,7 @@
</thead> </thead>
<tbody> <tbody>
{{ range $cookie := .Request.Cookies }} {{ range $cookie := .Request.Cookies }}
<tr style="text-align: left"> <tr>
<td> <td>
<b>{{ $cookie.Name }}</b> <b>{{ $cookie.Name }}</b>
</td> </td>

View File

@ -53,7 +53,7 @@ func RunCommand() *cli.Command {
} }
if err := tmpl.Execute(w, data); err != nil { if err := tmpl.Execute(w, data); err != nil {
logger.Error(ctx.Context, "could not execute template", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx.Context, "could not execute template", logger.E(errors.WithStack(err)))
} }
}) })

View File

@ -3,7 +3,6 @@ package proxy
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/command/common" "forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/proxy" "forge.cadoles.com/cadoles/bouncer/internal/proxy"
@ -30,16 +29,13 @@ func RunCommand() *cli.Command {
logger.SetLevel(logger.Level(conf.Logger.Level)) logger.SetLevel(logger.Level(conf.Logger.Level))
projectVersion := ctx.String("projectVersion") projectVersion := ctx.String("projectVersion")
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
if conf.Proxy.Sentry.DSN != "" { if err != nil {
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion) return errors.Wrap(err, "could not initialize sentry client")
if err != nil {
return errors.Wrap(err, "could not initialize sentry client")
}
defer flushSentry()
} }
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")
@ -49,7 +45,6 @@ func RunCommand() *cli.Command {
proxy.WithServerConfig(conf.Proxy), proxy.WithServerConfig(conf.Proxy),
proxy.WithRedisConfig(conf.Redis), proxy.WithRedisConfig(conf.Redis),
proxy.WithDirectorLayers(layers...), proxy.WithDirectorLayers(layers...),
proxy.WithDirectorCacheTTL(time.Duration(conf.Proxy.Cache.TTL)),
) )
addrs, srvErrs := srv.Start(ctx.Context) addrs, srvErrs := srv.Start(ctx.Context)

View File

@ -1,22 +1,20 @@
package config package config
type AdminServerConfig struct { type AdminServerConfig struct {
HTTP HTTPConfig `yaml:"http"` HTTP HTTPConfig `yaml:"http"`
CORS CORSConfig `yaml:"cors"` CORS CORSConfig `yaml:"cors"`
Auth AuthConfig `yaml:"auth"` Auth AuthConfig `yaml:"auth"`
Metrics MetricsConfig `yaml:"metrics"` Metrics MetricsConfig `yaml:"metrics"`
Profiling ProfilingConfig `yaml:"profiling"` Sentry SentryConfig `yaml:"sentry"`
Sentry SentryConfig `yaml:"sentry"`
} }
func NewDefaultAdminServerConfig() AdminServerConfig { func NewDefaultAdminServerConfig() AdminServerConfig {
return AdminServerConfig{ return AdminServerConfig{
HTTP: NewHTTPConfig("127.0.0.1", 8081), HTTP: NewHTTPConfig("127.0.0.1", 8081),
CORS: NewDefaultCORSConfig(), CORS: NewDefaultCORSConfig(),
Auth: NewDefaultAuthConfig(), Auth: NewDefaultAuthConfig(),
Metrics: NewDefaultMetricsConfig(), Metrics: NewDefaultMetricsConfig(),
Sentry: NewDefaultSentryConfig(), Sentry: NewDefaultSentryConfig(),
Profiling: NewDefaultProfilingConfig(),
} }
} }

View File

@ -80,33 +80,24 @@ func loadBootstrapDir(dir string) (map[store.ProxyName]BootstrapProxyConfig, err
proxies := make(map[store.ProxyName]BootstrapProxyConfig) proxies := make(map[store.ProxyName]BootstrapProxyConfig)
for _, f := range files { for _, f := range files {
proxy, err := loadBootstrappedProxyConfig(f) data, err := os.ReadFile(f)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not load proxy bootstrap file '%s'", f) return nil, errors.Wrapf(err, "could not read file '%s'", f)
}
proxy := BootstrapProxyConfig{}
if err := yaml.Unmarshal(data, &proxy); err != nil {
return nil, errors.Wrapf(err, "could not unmarshal proxy")
} }
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))) name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
proxies[name] = *proxy proxies[name] = proxy
} }
return proxies, nil return proxies, nil
} }
func loadBootstrappedProxyConfig(filename string) (*BootstrapProxyConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, errors.Wrapf(err, "could not read file '%s'", filename)
}
proxy := BootstrapProxyConfig{}
if err := yaml.Unmarshal(data, &proxy); err != nil {
return nil, errors.Wrapf(err, "could not unmarshal proxy")
}
return &proxy, nil
}
func overrideProxies(base map[store.ProxyName]BootstrapProxyConfig, proxies map[store.ProxyName]BootstrapProxyConfig) map[store.ProxyName]BootstrapProxyConfig { func overrideProxies(base map[store.ProxyName]BootstrapProxyConfig, proxies map[store.ProxyName]BootstrapProxyConfig) map[store.ProxyName]BootstrapProxyConfig {
for name, proxy := range proxies { for name, proxy := range proxies {
base[name] = proxy base[name] = proxy

View File

@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"regexp"
"strconv" "strconv"
"time" "time"
@ -10,6 +11,9 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// var reVar = regexp.MustCompile(`^\${(\w+)}$`)
var reVar = regexp.MustCompile(`\${(.*?)}`)
type InterpolatedString string type InterpolatedString string
func (is *InterpolatedString) UnmarshalYAML(value *yaml.Node) error { func (is *InterpolatedString) UnmarshalYAML(value *yaml.Node) error {
@ -19,13 +23,12 @@ func (is *InterpolatedString) UnmarshalYAML(value *yaml.Node) error {
return errors.WithStack(err) return errors.WithStack(err)
} }
str, err := envsubst.EvalEnv(str) if match := reVar.FindStringSubmatch(str); len(match) > 0 {
if err != nil { *is = InterpolatedString(os.Getenv(match[1]))
return errors.WithStack(err) } else {
*is = InterpolatedString(str)
} }
*is = InterpolatedString(str)
return nil return nil
} }
@ -38,9 +41,8 @@ func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
} }
str, err := envsubst.EvalEnv(str) if match := reVar.FindStringSubmatch(str); len(match) > 0 {
if err != nil { str = os.Getenv(match[1])
return errors.WithStack(err)
} }
intVal, err := strconv.ParseInt(str, 10, 32) intVal, err := strconv.ParseInt(str, 10, 32)
@ -62,12 +64,11 @@ func (ifl *InterpolatedFloat) UnmarshalYAML(value *yaml.Node) error {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
} }
str, err := envsubst.EvalEnv(str) if match := reVar.FindStringSubmatch(str); len(match) > 0 {
if err != nil { str = os.Getenv(match[1])
return errors.WithStack(err)
} }
floatVal, err := strconv.ParseFloat(str, 32) floatVal, err := strconv.ParseFloat(str, 10)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line) return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line)
} }
@ -86,9 +87,8 @@ func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
} }
str, err := envsubst.EvalEnv(str) if match := reVar.FindStringSubmatch(str); len(match) > 0 {
if err != nil { str = os.Getenv(match[1])
return errors.WithStack(err)
} }
boolVal, err := strconv.ParseBool(str) boolVal, err := strconv.ParseBool(str)
@ -127,7 +127,7 @@ func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
func (im InterpolatedMap) interpolateRecursive(data any) (any, error) { func (im *InterpolatedMap) interpolateRecursive(data any) (any, error) {
switch typ := data.(type) { switch typ := data.(type) {
case map[string]any: case map[string]any:
for key, value := range typ { for key, value := range typ {
@ -165,15 +165,22 @@ type InterpolatedStringSlice []string
func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error { func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error {
var data []string var data []string
var evErr error
if err := value.Decode(&data); err != nil { if err := value.Decode(&data); err != nil {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line)
} }
for index, value := range data { for index, value := range data {
value, err := envsubst.EvalEnv(value) //match := reVar.FindStringSubmatch(value)
if err != nil { re := regexp.MustCompile(`\${(.*?)}`)
return errors.WithStack(err)
res := re.FindAllStringSubmatch(value, 10)
if len(res) > 0 {
value, evErr = envsubst.EvalEnv(value)
if evErr != nil {
return evErr
}
} }
data[index] = value data[index] = value
@ -193,19 +200,13 @@ func (id *InterpolatedDuration) UnmarshalYAML(value *yaml.Node) error {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
} }
str, err := envsubst.EvalEnv(str) if match := reVar.FindStringSubmatch(str); len(match) > 0 {
if err != nil { str = os.Getenv(match[1])
return errors.WithStack(err)
} }
duration, err := time.ParseDuration(str) duration, err := time.ParseDuration(str)
if err != nil { if err != nil {
nanoseconds, err := strconv.ParseInt(str, 10, 64) return errors.Wrapf(err, "could not parse duration '%v', line '%d'", str, value.Line)
if err != nil {
return errors.Wrapf(err, "could not parse duration '%v', line '%d'", str, value.Line)
}
duration = time.Duration(nanoseconds)
} }
*id = InterpolatedDuration(duration) *id = InterpolatedDuration(duration)

View File

@ -21,9 +21,6 @@ func NewDefaultLayersConfig() LayersConfig {
Timeout: NewInterpolatedDuration(10 * time.Second), Timeout: NewInterpolatedDuration(10 * time.Second),
}, },
}, },
Sessions: AuthnLayerSessionConfig{
TTL: NewInterpolatedDuration(time.Hour),
},
}, },
} }
} }
@ -34,14 +31,8 @@ type QueueLayerConfig struct {
} }
type AuthnLayerConfig struct { type AuthnLayerConfig struct {
Debug InterpolatedBool `yaml:"debug"` TemplateDir InterpolatedString `yaml:"templateDir"`
TemplateDir InterpolatedString `yaml:"templateDir"` OIDC AuthnOIDCLayerConfig `yaml:"oidc"`
OIDC AuthnOIDCLayerConfig `yaml:"oidc"`
Sessions AuthnLayerSessionConfig `yaml:"sessions"`
}
type AuthnLayerSessionConfig struct {
TTL *InterpolatedDuration `yaml:"ttl"`
} }
type AuthnOIDCLayerConfig struct { type AuthnOIDCLayerConfig struct {

View File

@ -1,15 +0,0 @@
package config
type ProfilingConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
Endpoint InterpolatedString `yaml:"endpoint"`
BasicAuth *BasicAuthConfig `yaml:"basicAuth"`
}
func NewDefaultProfilingConfig() ProfilingConfig {
return ProfilingConfig{
Enabled: true,
Endpoint: "/.bouncer/profiling",
BasicAuth: nil,
}
}

View File

@ -7,29 +7,11 @@ import (
) )
type ProxyServerConfig struct { type ProxyServerConfig struct {
Debug InterpolatedBool `yaml:"debug"` HTTP HTTPConfig `yaml:"http"`
HTTP HTTPConfig `yaml:"http"` Metrics MetricsConfig `yaml:"metrics"`
Metrics MetricsConfig `yaml:"metrics"` Transport TransportConfig `yaml:"transport"`
Profiling ProfilingConfig `yaml:"profiling"` Dial DialConfig `yaml:"dial"`
Transport TransportConfig `yaml:"transport"` Sentry SentryConfig `yaml:"sentry"`
Dial DialConfig `yaml:"dial"`
Sentry SentryConfig `yaml:"sentry"`
Cache CacheConfig `yaml:"cache"`
Templates TemplatesConfig `yaml:"templates"`
}
func NewDefaultProxyServerConfig() ProxyServerConfig {
return ProxyServerConfig{
Debug: false,
HTTP: NewHTTPConfig("0.0.0.0", 8080),
Metrics: NewDefaultMetricsConfig(),
Transport: NewDefaultTransportConfig(),
Dial: NewDefaultDialConfig(),
Sentry: NewDefaultSentryConfig(),
Cache: NewDefaultCacheConfig(),
Templates: NewDefaultTemplatesConfig(),
Profiling: NewDefaultProfilingConfig(),
}
} }
// See https://pkg.go.dev/net/http#Transport // See https://pkg.go.dev/net/http#Transport
@ -76,22 +58,13 @@ func (c TransportConfig) AsTransport() *http.Transport {
return httpTransport return httpTransport
} }
func NewDefaultTransportConfig() TransportConfig { func NewDefaultProxyServerConfig() ProxyServerConfig {
return TransportConfig{ return ProxyServerConfig{
ForceAttemptHTTP2: true, HTTP: NewHTTPConfig("0.0.0.0", 8080),
MaxIdleConns: 100, Metrics: NewDefaultMetricsConfig(),
MaxIdleConnsPerHost: 100, Transport: NewDefaultTransportConfig(),
MaxConnsPerHost: 100, Dial: NewDefaultDialConfig(),
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second), Sentry: NewDefaultSentryConfig(),
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
DisableCompression: false,
DisableKeepAlives: false,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
MaxResponseHeaderBytes: 0,
InsecureSkipVerify: false,
} }
} }
@ -112,22 +85,21 @@ func NewDefaultDialConfig() DialConfig {
} }
} }
type CacheConfig struct { func NewDefaultTransportConfig() TransportConfig {
TTL InterpolatedDuration `yaml:"ttl"` return TransportConfig{
} ForceAttemptHTTP2: true,
MaxIdleConns: 100,
func NewDefaultCacheConfig() CacheConfig { MaxIdleConnsPerHost: 100,
return CacheConfig{ MaxConnsPerHost: 100,
TTL: *NewInterpolatedDuration(time.Second * 30), IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
} TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
} ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
type TemplatesConfig struct { DisableCompression: false,
Dir InterpolatedString `yaml:"dir"` DisableKeepAlives: false,
} ReadBufferSize: 4096,
WriteBufferSize: 4096,
func NewDefaultTemplatesConfig() TemplatesConfig { MaxResponseHeaderBytes: 0,
return TemplatesConfig{ InsecureSkipVerify: false,
Dir: "./templates",
} }
} }

View File

@ -9,35 +9,21 @@ const (
) )
type RedisConfig struct { type RedisConfig struct {
Adresses InterpolatedStringSlice `yaml:"addresses"` Adresses InterpolatedStringSlice `yaml:"addresses"`
Master InterpolatedString `yaml:"master"` Master InterpolatedString `yaml:"master"`
ReadTimeout InterpolatedDuration `yaml:"readTimeout"` ReadTimeout InterpolatedDuration `yaml:"readTimeout"`
WriteTimeout InterpolatedDuration `yaml:"writeTimeout"` WriteTimeout InterpolatedDuration `yaml:"writeTimeout"`
DialTimeout InterpolatedDuration `yaml:"dialTimeout"` DialTimeout InterpolatedDuration `yaml:"dialTimeout"`
LockMaxRetries InterpolatedInt `yaml:"lockMaxRetries"` LockMaxRetries InterpolatedInt `yaml:"lockMaxRetries"`
RouteByLatency InterpolatedBool `yaml:"routeByLatency"`
ContextTimeoutEnabled InterpolatedBool `yaml:"contextTimeoutEnabled"`
MaxRetries InterpolatedInt `yaml:"maxRetries"`
PingInterval InterpolatedDuration `yaml:"pingInterval"`
PoolSize InterpolatedInt `yaml:"poolSize"`
PoolTimeout InterpolatedDuration `yaml:"poolTimeout"`
MinIdleConns InterpolatedInt `yaml:"minIdleConns"`
MaxIdleConns InterpolatedInt `yaml:"maxIdleConns"`
ConnMaxIdleTime InterpolatedDuration `yaml:"connMaxIdleTime"`
ConnMaxLifetime InterpolatedDuration `yaml:"connMaxLifeTime"`
} }
func NewDefaultRedisConfig() RedisConfig { func NewDefaultRedisConfig() RedisConfig {
return RedisConfig{ return RedisConfig{
Adresses: InterpolatedStringSlice{"localhost:6379"}, Adresses: InterpolatedStringSlice{"localhost:6379"},
Master: "", Master: "",
ReadTimeout: InterpolatedDuration(30 * time.Second), ReadTimeout: InterpolatedDuration(30 * time.Second),
WriteTimeout: InterpolatedDuration(30 * time.Second), WriteTimeout: InterpolatedDuration(30 * time.Second),
DialTimeout: InterpolatedDuration(30 * time.Second), DialTimeout: InterpolatedDuration(30 * time.Second),
LockMaxRetries: 10, LockMaxRetries: 10,
MaxRetries: 3,
PingInterval: InterpolatedDuration(30 * time.Second),
ContextTimeoutEnabled: true,
RouteByLatency: true,
} }
} }

View File

@ -29,10 +29,10 @@ func NewDefaultSentryConfig() SentryConfig {
FlushTimeout: NewInterpolatedDuration(2 * time.Second), FlushTimeout: NewInterpolatedDuration(2 * time.Second),
AttachStacktrace: true, AttachStacktrace: true,
SampleRate: 1, SampleRate: 1,
EnableTracing: false, EnableTracing: true,
TracesSampleRate: 0.1, TracesSampleRate: 0.2,
ProfilesSampleRate: 0.1, ProfilesSampleRate: 1,
IgnoreErrors: []string{"context canceled", "net/http: abort"}, IgnoreErrors: []string{},
SendDefaultPII: false, SendDefaultPII: false,
ServerName: "", ServerName: "",
Environment: "", Environment: "",

View File

@ -38,7 +38,7 @@ func (l *Locker) WithLock(ctx context.Context, key string, timeout time.Duration
defer func() { defer func() {
if err := lock.Release(ctx); err != nil { if err := lock.Release(ctx); err != nil {
logger.Error(ctx, "could not release lock", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not release lock", logger.E(errors.WithStack(err)))
} }
logger.Debug(ctx, "lock released") logger.Debug(ctx, "lock released")

View File

@ -30,7 +30,7 @@ func retryWithBackoff(ctx context.Context, attempts int, fn func(ctx context.Con
return errors.Wrapf(err, "execution failed after %d attempts", attempts) return errors.Wrapf(err, "execution failed after %d attempts", attempts)
} }
logger.Error(ctx, "error while executing func, retrying with backoff", logger.CapturedE(err), logger.F("backoffDelay", backoffDelay), logger.F("remainingAttempts", attempts-count)) logger.Error(ctx, "error while executing func, retrying with backoff", logger.E(err), logger.F("backoffDelay", backoffDelay), logger.F("remainingAttempts", attempts-count))
time.Sleep(backoffDelay) time.Sleep(backoffDelay)

View File

@ -2,12 +2,10 @@ package director
import ( import (
"context" "context"
"net/http"
"net/url" "net/url"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
type contextKey string type contextKey string
@ -16,7 +14,6 @@ const (
contextKeyProxy contextKey = "proxy" contextKeyProxy contextKey = "proxy"
contextKeyLayers contextKey = "layers" contextKeyLayers contextKey = "layers"
contextKeyOriginalURL contextKey = "originalURL" contextKeyOriginalURL contextKey = "originalURL"
contextKeyHandleError contextKey = "handleError"
) )
var ( var (
@ -37,6 +34,19 @@ func OriginalURL(ctx context.Context) (*url.URL, error) {
return url, nil return url, nil
} }
func withProxy(ctx context.Context, proxy *store.Proxy) context.Context {
return context.WithValue(ctx, contextKeyProxy, proxy)
}
func ctxProxy(ctx context.Context) (*store.Proxy, error) {
proxy, err := ctxValue[*store.Proxy](ctx, contextKeyProxy)
if err != nil {
return nil, errors.WithStack(err)
}
return proxy, nil
}
func withLayers(ctx context.Context, layers []*store.Layer) context.Context { func withLayers(ctx context.Context, layers []*store.Layer) context.Context {
return context.WithValue(ctx, contextKeyLayers, layers) return context.WithValue(ctx, contextKeyLayers, layers)
} }
@ -63,22 +73,3 @@ func ctxValue[T any](ctx context.Context, key contextKey) (T, error) {
return value, nil return value, nil
} }
type HandleErrorFunc func(w http.ResponseWriter, r *http.Request, status int, err error)
func withHandleError(ctx context.Context, fn HandleErrorFunc) context.Context {
return context.WithValue(ctx, contextKeyHandleError, fn)
}
func HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, status int, err error) {
err = errors.WithStack(err)
fn, ok := ctx.Value(contextKeyHandleError).(HandleErrorFunc)
if !ok {
logger.Error(ctx, err.Error(), logger.CapturedE(err))
http.Error(w, http.StatusText(status), status)
return
}
fn(w, r, status, err)
}

View File

@ -7,7 +7,6 @@ import (
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard" "forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/cache"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -18,11 +17,6 @@ type Director struct {
proxyRepository store.ProxyRepository proxyRepository store.ProxyRepository
layerRepository store.LayerRepository layerRepository store.LayerRepository
layerRegistry *LayerRegistry layerRegistry *LayerRegistry
proxyCache cache.Cache[string, []*store.Proxy]
layerCache cache.Cache[string, []*store.Layer]
handleError HandleErrorFunc
} }
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) { func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
@ -34,66 +28,67 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
} }
url := getRequestURL(r) url := getRequestURL(r)
ctx = withOriginalURL(ctx, url) ctx = withOriginalURL(ctx, url)
ctx = logger.With(ctx, logger.F("url", url.String())) ctx = logger.With(ctx, logger.F("url", url.String()))
layers := make([]*store.Layer, 0) var match *store.Proxy
MAIN:
for _, p := range proxies { for _, p := range proxies {
for _, from := range p.From { for _, from := range p.From {
logger.Debug(
ctx, "matching request with proxy's from",
logger.F("from", from),
)
if matches := wildcard.Match(url.String(), from); !matches { if matches := wildcard.Match(url.String(), from); !matches {
continue continue
} }
proxyCtx := logger.With(ctx, logger.Debug(
logger.F("proxy", p.Name), ctx, "proxy's from matched",
logger.F("host", r.Host), logger.F("from", from),
logger.F("remoteAddr", r.RemoteAddr),
) )
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(p.Name)}).Add(1) match = p
break MAIN
proxyLayers, err := d.getLayers(proxyCtx, p.Name)
if err != nil {
return r, errors.WithStack(err)
}
layers = append(layers, proxyLayers...)
if p.To == "" {
continue
}
toURL, err := url.Parse(p.To)
if err != nil {
return r, errors.WithStack(err)
}
r.URL.Host = toURL.Host
r.URL.Scheme = toURL.Scheme
r.URL.Path = toURL.JoinPath(r.URL.Path).Path
proxyCtx = withLayers(proxyCtx, layers)
r = r.WithContext(proxyCtx)
return r, nil
} }
} }
if match == nil {
return r, nil
}
toURL, err := url.Parse(match.To)
if err != nil {
return r, errors.WithStack(err)
}
r.URL.Host = toURL.Host
r.URL.Scheme = toURL.Scheme
ctx = logger.With(ctx,
logger.F("proxy", match.Name),
logger.F("host", r.Host),
logger.F("remoteAddr", r.RemoteAddr),
)
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(match.Name)}).Add(1)
ctx = withProxy(ctx, match)
layers, err := d.getLayers(ctx, match.Name)
if err != nil {
return r, errors.WithStack(err)
}
ctx = withLayers(ctx, layers) ctx = withLayers(ctx, layers)
r = r.WithContext(ctx) r = r.WithContext(ctx)
return r, nil return r, nil
} }
const proxiesCacheKey = "proxies"
func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
proxies, exists := d.proxyCache.Get(proxiesCacheKey)
if exists {
return proxies, nil
}
headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true)) headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
@ -101,7 +96,7 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
sort.Sort(store.ByProxyWeight(headers)) sort.Sort(store.ByProxyWeight(headers))
proxies = make([]*store.Proxy, 0, len(headers)) proxies := make([]*store.Proxy, 0, len(headers))
for _, h := range headers { for _, h := range headers {
if !h.Enabled { if !h.Enabled {
@ -116,19 +111,10 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
proxies = append(proxies, proxy) proxies = append(proxies, proxy)
} }
d.proxyCache.Set(proxiesCacheKey, proxies)
return proxies, nil return proxies, nil
} }
func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) { func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) {
cacheKey := "layers-" + string(proxyName)
layers, exists := d.layerCache.Get(cacheKey)
if exists {
return layers, nil
}
headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true)) headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
@ -136,7 +122,7 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
sort.Sort(store.ByLayerWeight(headers)) sort.Sort(store.ByLayerWeight(headers))
layers = make([]*store.Layer, 0, len(headers)) layers := make([]*store.Layer, 0, len(headers))
for _, h := range headers { for _, h := range headers {
if !h.Enabled { if !h.Enabled {
@ -151,22 +137,19 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
layers = append(layers, layer) layers = append(layers, layer)
} }
d.layerCache.Set(cacheKey, layers)
return layers, nil return layers, nil
} }
func (d *Director) RequestTransformer() proxy.RequestTransformer { func (d *Director) RequestTransformer() proxy.RequestTransformer {
return func(r *http.Request) { return func(r *http.Request) {
ctx := r.Context() ctx := r.Context()
layers, err := ctxLayers(ctx) layers, err := ctxLayers(ctx)
if err != nil { if err != nil {
if errors.Is(err, errContextKeyNotFound) { if errors.Is(err, errContextKeyNotFound) {
return return
} }
logger.Error(ctx, "could not retrieve layers from context", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not retrieve layers from context", logger.E(errors.WithStack(err)))
return return
} }
@ -216,16 +199,15 @@ func (d *Director) ResponseTransformer() proxy.ResponseTransformer {
func (d *Director) Middleware() proxy.Middleware { func (d *Director) Middleware() proxy.Middleware {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
ctx := withHandleError(r.Context(), d.handleError)
r = r.WithContext(ctx)
r, err := d.rewriteRequest(r) r, err := d.rewriteRequest(r)
if err != nil { if err != nil {
HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not rewrite request")) logger.Error(r.Context(), "could not rewrite request", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
ctx = r.Context() ctx := r.Context()
layers, err := ctxLayers(ctx) layers, err := ctxLayers(ctx)
if err != nil { if err != nil {
@ -233,7 +215,9 @@ func (d *Director) Middleware() proxy.Middleware {
return return
} }
HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not retrieve proxy and layers from context")) logger.Error(ctx, "could not retrieve proxy and layers from context", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -256,17 +240,8 @@ func (d *Director) Middleware() proxy.Middleware {
} }
} }
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, funcs ...OptionFunc) *Director { func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director {
opts := NewOptions(funcs...) registry := NewLayerRegistry(layers...)
registry := NewLayerRegistry(opts.Layers...) return &Director{proxyRepository, layerRepository, registry}
return &Director{
proxyRepository: proxyRepository,
layerRepository: layerRepository,
layerRegistry: registry,
proxyCache: opts.ProxyCache,
layerCache: opts.LayerCache,
handleError: opts.HandleError,
}
} }

View File

@ -1,18 +1,13 @@
package authn package authn
import ( import (
"bytes"
"html/template" "html/template"
"io"
"net/http" "net/http"
"path/filepath" "path/filepath"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard" "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/proxy/director/layer/util"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -22,9 +17,6 @@ import (
type Layer struct { type Layer struct {
layerType store.LayerType layerType store.LayerType
auth Authenticator auth Authenticator
debug bool
ruleEngineCache *util.RuleEngineCache[*Vars, *LayerOptions]
templateDir string templateDir string
} }
@ -36,7 +28,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
options, err := fromStoreOptions(layer.Options) options, err := fromStoreOptions(layer.Options)
if err != nil { if err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not parse layer options")) logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -46,9 +40,8 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
err = errors.WithStack(err) logger.Error(ctx, "could not execute pre-auth hook", logger.E(errors.WithStack(err)))
logger.Error(ctx, "could not execute pre-auth hook", logger.CapturedE(err)) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -72,22 +65,20 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
err = errors.WithStack(err) logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err)))
logger.Error(ctx, "could not authenticate user", logger.CapturedE(err)) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
l.renderErrorPage(w, r, layer, options, err)
return return
} }
if err := l.applyRules(ctx, r, layer, options, user); err != nil { if err := l.applyRules(r, options, user); err != nil {
if errors.Is(err, ErrForbidden) { if errors.Is(err, ErrForbidden) {
l.renderForbiddenPage(w, r, layer, options, user) l.renderForbiddenPage(w, r, layer, options, user)
return return
} }
err = errors.WithStack(err) logger.Error(ctx, "could not apply rules", logger.E(errors.WithStack(err)))
logger.Error(ctx, "could not apply rules", logger.CapturedE(err)) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -103,9 +94,8 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
err = errors.WithStack(err) logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err)))
logger.Error(ctx, "could not execute post-auth hook", logger.CapturedE(err)) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -118,47 +108,12 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
} }
} }
type baseTemplateData struct {
Layer *store.Layer
Debug bool
Request *http.Request
}
func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) { func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) {
templateData := struct {
baseTemplateData
User *User
}{
baseTemplateData: baseTemplateData{
Layer: layer,
Debug: l.debug,
Request: r,
},
User: user,
}
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
l.renderPage(w, r, "forbidden", options.Templates.Forbidden.Block, templateData) l.renderPage(w, r, layer, "forbidden", options.Templates.Forbidden.Block, user)
} }
func (l *Layer) renderErrorPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, err error) { func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, page string, block string, user *User) {
templateData := struct {
baseTemplateData
Err error
}{
baseTemplateData: baseTemplateData{
Layer: layer,
Debug: l.debug,
Request: r,
},
Err: err,
}
w.WriteHeader(http.StatusInternalServerError)
l.renderPage(w, r, "error", options.Templates.Error.Block, templateData)
}
func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, page string, block string, templateData any) {
ctx := r.Context() ctx := r.Context()
pattern := filepath.Join(l.templateDir, page+".gohtml") pattern := filepath.Join(l.templateDir, page+".gohtml")
@ -167,21 +122,29 @@ func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, page string,
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern) tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
if err != nil { if err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not load authn templates")) logger.Error(ctx, "could not load authn templates", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
templateData := struct {
Layer *store.Layer
User *User
}{
Layer: layer,
User: user,
}
w.Header().Add("Cache-Control", "no-cache") w.Header().Add("Cache-Control", "no-cache")
var buf bytes.Buffer w.WriteHeader(http.StatusOK)
if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil { if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not render authn page")) logger.Error(ctx, "could not render authn page", logger.E(errors.WithStack(err)))
return http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
if _, err := io.Copy(w, &buf); err != nil { return
logger.Error(ctx, "could not write authn page", logger.CapturedE(errors.WithStack(err)))
} }
} }
@ -194,22 +157,9 @@ func NewLayer(layerType store.LayerType, auth Authenticator, funcs ...OptionFunc
opts := NewOptions(funcs...) opts := NewOptions(funcs...)
return &Layer{ return &Layer{
ruleEngineCache: util.NewInMemoryRuleEngineCache[*Vars, *LayerOptions](func(options *LayerOptions) (*rule.Engine[*Vars], error) {
engine, err := rule.NewEngine[*Vars](
rule.WithRules(options.Rules...),
rule.WithExpr(getAuthnAPI()...),
ruleHTTP.WithRequestFuncs(),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}),
layerType: layerType, layerType: layerType,
auth: auth, auth: auth,
templateDir: opts.TemplateDir, templateDir: opts.TemplateDir,
debug: opts.Debug,
} }
} }

View File

@ -17,7 +17,6 @@ type LayerOptions struct {
type TemplatesOptions struct { type TemplatesOptions struct {
Forbidden TemplateOptions `mapstructure:"forbidden"` Forbidden TemplateOptions `mapstructure:"forbidden"`
Error TemplateOptions `mapstructure:"error"`
} }
type TemplateOptions struct { type TemplateOptions struct {
@ -28,13 +27,12 @@ func DefaultLayerOptions() LayerOptions {
return LayerOptions{ return LayerOptions{
MatchURLs: []string{"*"}, MatchURLs: []string{"*"},
Rules: []string{ Rules: []string{
"del_headers(ctx, 'Remote-*')", "del_headers('Remote-*')",
"set_header(ctx,'Remote-User', vars.user.subject)", "set_header('Remote-User', user.subject)",
`map( `map(
toPairs(vars.user.attrs), { toPairs(user.attrs), {
let name = replace(lower(string(get(#, 0))), '_', '-'); let name = replace(lower(string(get(#, 0))), '_', '-');
set_header( set_header(
ctx,
'Remote-User-Attr-' + name, 'Remote-User-Attr-' + name,
get(#, 1) get(#, 1)
) )
@ -45,9 +43,6 @@ func DefaultLayerOptions() LayerOptions {
Forbidden: TemplateOptions{ Forbidden: TemplateOptions{
Block: "default", Block: "default",
}, },
Error: TemplateOptions{
Block: "default",
},
}, },
} }

View File

@ -39,23 +39,6 @@ func TestMatchAuthorizedCIDRs(t *testing.T) {
}, },
ExpectedResult: false, ExpectedResult: false,
}, },
{
RemoteHostPort: "192.168.1.15:43349",
AuthorizedCIDRs: []string{
"192.168.1.5/32",
"192.168.1.0/24",
},
ExpectedResult: true,
},
{
RemoteHostPort: "192.168.1.15:43349",
AuthorizedCIDRs: []string{
"192.168.1.5/32",
"192.168.1.6/32",
"192.168.1.7/32",
},
ExpectedResult: false,
},
} }
auth := Authenticator{} auth := Authenticator{}

View File

@ -42,9 +42,9 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
return errors.WithStack(err) return errors.WithStack(err)
} }
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Proxy, layer.Name)) sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name))
if err != nil { if err != nil {
logger.Error(ctx, "could not retrieve session", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err)))
} }
loginCallbackURL, err := a.getLoginCallbackURL(originalURL, layer.Proxy, layer.Name, options) loginCallbackURL, err := a.getLoginCallbackURL(originalURL, layer.Proxy, layer.Name, options)
@ -86,7 +86,7 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
if postLogoutRedirectURL != "" { if postLogoutRedirectURL != "" {
isAuthorized := slices.Contains(options.OIDC.PostLogoutRedirectURLs, postLogoutRedirectURL) isAuthorized := slices.Contains(options.OIDC.PostLogoutRedirectURLs, postLogoutRedirectURL)
if !isAuthorized { if !isAuthorized {
director.HandleError(ctx, w, r, http.StatusBadRequest, errors.New("unauthorized post-logout redirect")) http.Error(w, "unauthorized post-logout redirect", http.StatusBadRequest)
return errors.WithStack(authn.ErrSkipRequest) return errors.WithStack(authn.ErrSkipRequest)
} }
} }
@ -121,14 +121,14 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Proxy, layer.Name)) sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
defer func() { defer func() {
if err := sess.Save(r, w); err != nil { if err := sess.Save(r, w); err != nil {
logger.Error(ctx, "could not save session", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
} }
}() }()
@ -401,14 +401,8 @@ func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*C
return client, nil return client, nil
} }
const defaultCookieNamePrefix = "_bouncer_authn_oidc" func (a *Authenticator) getCookieName(cookieName string, layerName store.LayerName) string {
return fmt.Sprintf("%s_%s", cookieName, layerName)
func (a *Authenticator) getCookieName(cookieName string, proxyName store.ProxyName, layerName store.LayerName) string {
if cookieName != "" {
return cookieName
}
return strings.ToLower(fmt.Sprintf("%s_%s_%s", defaultCookieNamePrefix, proxyName, layerName))
} }
var ( var (

View File

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@ -69,7 +68,8 @@ func (c *Client) login(w http.ResponseWriter, r *http.Request, sess *sessions.Se
sess.Values[sessionKeyPostLoginRedirectURL] = postLoginRedirectURL sess.Values[sessionKeyPostLoginRedirectURL] = postLoginRedirectURL
if err := sess.Save(r, w); err != nil { if err := sess.Save(r, w); err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not save session")) logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -127,7 +127,7 @@ func (c *Client) HandleLogout(w http.ResponseWriter, r *http.Request, sess *sess
rawIDToken, err := c.getRawIDToken(sess) rawIDToken, err := c.getRawIDToken(sess)
if err != nil { if err != nil {
logger.Error(ctx, "could not retrieve raw id token", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not retrieve raw id token", logger.E(errors.WithStack(err)))
} }
sess.Values[sessionKeyIDToken] = nil sess.Values[sessionKeyIDToken] = nil

View File

@ -14,5 +14,5 @@ func NewLayer(store sessions.Store, funcs ...OptionFunc) *authn.Layer {
httpTransport: opts.HTTPTransport, httpTransport: opts.HTTPTransport,
httpClientTimeout: opts.HTTPClientTimeout, httpClientTimeout: opts.HTTPClientTimeout,
store: store, store: store,
}, opts.AuthnOptions...) })
} }

View File

@ -8,6 +8,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const defaultCookieName = "_bouncer_authn_oidc"
type LayerOptions struct { type LayerOptions struct {
authn.LayerOptions authn.LayerOptions
OIDC OIDCOptions `mapstructure:"oidc"` OIDC OIDCOptions `mapstructure:"oidc"`
@ -55,7 +57,7 @@ func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
Scopes: []string{"openid"}, Scopes: []string{"openid"},
}, },
Cookie: CookieOptions{ Cookie: CookieOptions{
Name: "", Name: defaultCookieName,
Path: "/", Path: "/",
HTTPOnly: true, HTTPOnly: true,
MaxAge: time.Hour, MaxAge: time.Hour,

View File

@ -3,14 +3,11 @@ package oidc
import ( import (
"net/http" "net/http"
"time" "time"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
) )
type Options struct { type Options struct {
HTTPTransport *http.Transport HTTPTransport *http.Transport
HTTPClientTimeout time.Duration HTTPClientTimeout time.Duration
AuthnOptions []authn.OptionFunc
} }
type OptionFunc func(opts *Options) type OptionFunc func(opts *Options)
@ -27,17 +24,10 @@ func WithHTTPClientTimeout(timeout time.Duration) OptionFunc {
} }
} }
func WithAuthnOptions(funcs ...authn.OptionFunc) OptionFunc {
return func(opts *Options) {
opts.AuthnOptions = funcs
}
}
func NewOptions(funcs ...OptionFunc) *Options { func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{ opts := &Options{
HTTPTransport: http.DefaultTransport.(*http.Transport), HTTPTransport: http.DefaultTransport.(*http.Transport),
HTTPClientTimeout: 30 * time.Second, HTTPClientTimeout: 30 * time.Second,
AuthnOptions: make([]authn.OptionFunc, 0),
} }
for _, fn := range funcs { for _, fn := range funcs {

View File

@ -2,7 +2,6 @@ package authn
type Options struct { type Options struct {
TemplateDir string TemplateDir string
Debug bool
} }
type OptionFunc func(*Options) type OptionFunc func(*Options)
@ -10,7 +9,6 @@ type OptionFunc func(*Options)
func NewOptions(funcs ...OptionFunc) *Options { func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{ opts := &Options{
TemplateDir: "./templates", TemplateDir: "./templates",
Debug: false,
} }
for _, fn := range funcs { for _, fn := range funcs {
@ -25,9 +23,3 @@ func WithTemplateDir(templateDir string) OptionFunc {
o.TemplateDir = templateDir o.TemplateDir = templateDir
} }
} }
func WithDebug(debug bool) OptionFunc {
return func(o *Options) {
o.Debug = debug
}
}

View File

@ -1,54 +1,110 @@
package authn package authn
import ( import (
"context" "fmt"
"net/http" "net/http"
"strconv"
"strings"
"time"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http" "forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type Vars struct { func (l *Layer) getHeaderRuleOptions(r *http.Request) []expr.Option {
User *User `expr:"user"`
}
func (l *Layer) applyRules(ctx context.Context, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) error {
key := string(layer.Proxy) + "-" + string(layer.Name)
revisionedEngine := l.ruleEngineCache.Get(key)
engine, err := revisionedEngine.Get(ctx, layer.Revision, options)
if err != nil {
return errors.WithStack(err)
}
vars := &Vars{
User: user,
}
ctx = ruleHTTP.WithRequest(ctx, r)
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err)
}
return nil
}
func getAuthnAPI() []expr.Option {
options := make([]expr.Option, 0) options := make([]expr.Option, 0)
// forbidden() allows the layer to hijack the current request and return a 403 Forbidden HTTP status setHeader := expr.Function(
"set_header",
func(params ...any) (any, error) {
name := params[0].(string)
rawValue := params[1]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r.Header.Set(name, value)
return true, nil
},
new(func(string, string) bool),
)
options = append(options, setHeader)
delHeaders := expr.Function(
"del_headers",
func(params ...any) (any, error) {
pattern := params[0].(string)
deleted := false
for key := range r.Header {
if !wildcard.Match(key, pattern) {
continue
}
r.Header.Del(key)
deleted = true
}
return deleted, nil
},
new(func(string) bool),
)
options = append(options, delHeaders)
return options
}
func (l *Layer) applyRules(r *http.Request, options *LayerOptions, user *User) error {
rules := options.Rules
if len(rules) == 0 {
return nil
}
env := map[string]any{
"user": user,
}
rulesOptions := l.getHeaderRuleOptions(r)
var ruleErr error
forbidden := expr.Function( forbidden := expr.Function(
"forbidden", "forbidden",
func(params ...any) (any, error) { func(params ...any) (any, error) {
return true, errors.WithStack(ErrForbidden) ruleErr = errors.WithStack(ErrForbidden)
return true, nil
}, },
new(func() bool), new(func() bool),
) )
options = append(options, forbidden) rulesOptions = append(rulesOptions, forbidden)
return options for i, r := range rules {
program, err := expr.Compile(r, rulesOptions...)
if err != nil {
return errors.Wrapf(err, "could not compile rule #%d", i)
}
if _, err := expr.Run(program, env); err != nil {
return errors.Wrapf(err, "could not execute rule #%d", i)
}
if ruleErr != nil {
return errors.WithStack(ruleErr)
}
}
return nil
} }

View File

@ -10,10 +10,7 @@ type Status struct {
} }
type Adapter interface { type Adapter interface {
// Touch updates the session TTL and returns its current rank
Touch(ctx context.Context, queueName string, sessionId string) (int64, error) Touch(ctx context.Context, queueName string, sessionId string) (int64, error)
// Status returns the queue current status
Status(ctx context.Context, queueName string) (*Status, error) Status(ctx context.Context, queueName string) (*Status, error)
// Refresh forces a refresh of the queue, taking into account the given TTL for sessions
Refresh(ctx context.Context, queueName string, keepAlive time.Duration) error Refresh(ctx context.Context, queueName string, keepAlive time.Duration) error
} }

View File

@ -1,11 +1,9 @@
package queue package queue
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"html/template" "html/template"
"io"
"math/rand" "math/rand"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -54,7 +52,9 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
options, err := fromStoreOptions(layer.Options, q.defaultKeepAlive) options, err := fromStoreOptions(layer.Options, q.defaultKeepAlive)
if err != nil { if err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not parse layer options")) logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -65,13 +65,13 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
defer q.updateMetrics(layer.Proxy, layer.Name, options) defer q.updateMetrics(ctx, layer.Proxy, layer.Name, options)
cookieName := q.getCookieName(layer.Name) cookieName := q.getCookieName(layer.Name)
cookie, err := r.Cookie(cookieName) cookie, err := r.Cookie(cookieName)
if err != nil && !errors.Is(err, http.ErrNoCookie) { if err != nil && !errors.Is(err, http.ErrNoCookie) {
logger.Error(ctx, "could not retrieve cookie", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not retrieve cookie", logger.E(errors.WithStack(err)))
} }
if cookie == nil { if cookie == nil {
@ -89,7 +89,9 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
rank, err := q.adapter.Touch(ctx, queueName, sessionID) rank, err := q.adapter.Touch(ctx, queueName, sessionID)
if err != nil { if err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not update queue session rank")) logger.Error(ctx, "could not retrieve session rank", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -124,7 +126,7 @@ func (q *Queue) updateSessionsMetric(ctx context.Context, proxyName store.ProxyN
status, err := q.adapter.Status(ctx, queueName) status, err := q.adapter.Status(ctx, queueName)
if err != nil { if err != nil {
logger.Error(ctx, "could not retrieve queue status", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not retrieve queue status", logger.E(errors.WithStack(err)))
return return
} }
@ -142,7 +144,9 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
status, err := q.adapter.Status(ctx, queueName) status, err := q.adapter.Status(ctx, queueName)
if err != nil { if err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not retrieve queue status")) logger.Error(ctx, "could not retrieve queue status", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -153,7 +157,7 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern) tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
if err != nil { if err != nil {
logger.Error(ctx, "could not load queue templates", logger.CapturedE(errors.WithStack(err))) logger.Error(ctx, "could not load queue templates", logger.E(errors.WithStack(err)))
return return
} }
@ -162,7 +166,9 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
}) })
if q.tmpl == nil { if q.tmpl == nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.New("queue page templates not loaded")) logger.Error(ctx, "queue page templates not loaded", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -188,16 +194,12 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
w.Header().Add("Retry-After", strconv.FormatInt(int64(refreshRate.Seconds()), 10)) w.Header().Add("Retry-After", strconv.FormatInt(int64(refreshRate.Seconds()), 10))
w.WriteHeader(http.StatusServiceUnavailable) w.WriteHeader(http.StatusServiceUnavailable)
var buf bytes.Buffer if err := q.tmpl.ExecuteTemplate(w, "queue", templateData); err != nil {
logger.Error(ctx, "could not render queue page", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
if err := q.tmpl.ExecuteTemplate(&buf, "queue", templateData); err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not render queue page"))
return return
} }
if _, err := io.Copy(w, &buf); err != nil {
logger.Error(ctx, "could not write queue page", logger.CapturedE(errors.WithStack(err)))
}
} }
func (q *Queue) refreshQueue(ctx context.Context, layerName store.LayerName, keepAlive time.Duration) { func (q *Queue) refreshQueue(ctx context.Context, layerName store.LayerName, keepAlive time.Duration) {
@ -209,15 +211,13 @@ func (q *Queue) refreshQueue(ctx context.Context, layerName store.LayerName, kee
if err := q.adapter.Refresh(ctx, string(layerName), keepAlive); err != nil { if err := q.adapter.Refresh(ctx, string(layerName), keepAlive); err != nil {
logger.Error(ctx, "could not refresh queue", logger.Error(ctx, "could not refresh queue",
logger.CapturedE(errors.WithStack(err)), logger.E(errors.WithStack(err)),
logger.F("queue", layerName), logger.F("queue", layerName),
) )
} }
} }
func (q *Queue) updateMetrics(proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) { func (q *Queue) updateMetrics(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) {
ctx := context.Background()
// Update queue capacity metric // Update queue capacity metric
metricQueueCapacity.With( metricQueueCapacity.With(
prometheus.Labels{ prometheus.Labels{

View File

@ -1,79 +0,0 @@
package rewriter
import (
"context"
"fmt"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr"
"github.com/pkg/errors"
)
type errRedirect struct {
statusCode int
url string
}
func (e *errRedirect) StatusCode() int {
return e.statusCode
}
func (e *errRedirect) URL() string {
return e.url
}
func (e *errRedirect) Error() string {
return fmt.Sprintf("redirect %d %s", e.statusCode, e.url)
}
func newErrRedirect(statusCode int, url string) *errRedirect {
return &errRedirect{
url: url,
statusCode: statusCode,
}
}
var _ error = &errRedirect{}
func redirectFunc() expr.Option {
return expr.Function(
"redirect",
func(params ...any) (any, error) {
_, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
statusCode, err := rule.Assert[int](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
if statusCode < 300 || statusCode >= 400 {
return nil, errors.Errorf("unexpected redirect status code '%d'", statusCode)
}
url, err := rule.Assert[string](params[2])
if err != nil {
return nil, errors.WithStack(err)
}
return nil, newErrRedirect(statusCode, url)
},
new(func(context.Context, int, string) bool),
)
}
func WithRewriterFuncs() rule.OptionFunc {
return func(opts *rule.Options) {
funcs := []expr.Option{
redirectFunc(),
}
if len(opts.Expr) == 0 {
opts.Expr = make([]expr.Option, 0)
}
opts.Expr = append(opts.Expr, funcs...)
}
}

View File

@ -1,38 +0,0 @@
{
"type": "object",
"properties": {
"rules": {
"title": "Règles appliquées aux requêtes/réponses transitant par le proxy",
"type": "object",
"properties": {
"request": {
"title": "Règles appliquées aux requêtes transitant par le proxy",
"type": "array",
"items": {
"type": "string"
}
},
"response": {
"title": "Règles appliquées aux réponses transitant par le proxy",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"matchURLs": {
"title": "Liste de filtrage des URLs sur lesquelles le layer est actif",
"description": "Par exemple, si vous souhaitez limiter votre layer à 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 ce layer.",
"default": [
"*"
],
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}

View File

@ -1,118 +0,0 @@
package rewriter
import (
"net/http"
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/layer/util"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
)
const LayerType store.LayerType = "rewriter"
type Layer struct {
requestRuleEngineCache *util.RuleEngineCache[*RequestVars, *LayerOptions]
responseRuleEngineCache *util.RuleEngineCache[*ResponseVars, *LayerOptions]
}
func (l *Layer) LayerType() store.LayerType {
return LayerType
}
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
options, err := fromStoreOptions(layer.Options)
if err != nil {
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not parse layer options"))
return
}
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
if !matches {
next.ServeHTTP(w, r)
return
}
if err := l.applyRequestRules(ctx, r, layer, options); err != nil {
var redirect *errRedirect
if errors.As(err, &redirect) {
http.Redirect(w, r, redirect.URL(), redirect.StatusCode())
return
}
director.HandleError(ctx, w, r, http.StatusInternalServerError, errors.Wrap(err, "could not apply request rules"))
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
// ResponseTransformer implements director.ResponseTransformerLayer.
func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransformer {
return func(r *http.Response) error {
options, err := fromStoreOptions(layer.Options)
if err != nil {
return errors.WithStack(err)
}
matches := wildcard.MatchAny(r.Request.URL.String(), options.MatchURLs...)
if !matches {
return nil
}
ctx := r.Request.Context()
if err := l.applyResponseRules(ctx, r, layer, options); err != nil {
return errors.WithStack(err)
}
return nil
}
}
func New(funcs ...OptionFunc) *Layer {
return &Layer{
requestRuleEngineCache: util.NewInMemoryRuleEngineCache(func(options *LayerOptions) (*rule.Engine[*RequestVars], error) {
engine, err := rule.NewEngine[*RequestVars](
rule.WithRules(options.Rules.Request...),
ruleHTTP.WithRequestFuncs(),
WithRewriterFuncs(),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}),
responseRuleEngineCache: util.NewInMemoryRuleEngineCache(func(options *LayerOptions) (*rule.Engine[*ResponseVars], error) {
engine, err := rule.NewEngine[*ResponseVars](
rule.WithRules(options.Rules.Response...),
ruleHTTP.WithResponseFuncs(),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}),
}
}
var (
_ director.MiddlewareLayer = &Layer{}
_ director.ResponseTransformerLayer = &Layer{}
)

View File

@ -1,56 +0,0 @@
package rewriter
import (
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
type LayerOptions struct {
MatchURLs []string `mapstructure:"matchURLs"`
Rules Rules `mapstructure:"rules"`
}
type Rules struct {
Request []string `mapstructure:"request"`
Response []string `mapstructure:"response"`
}
func DefaultLayerOptions() LayerOptions {
return LayerOptions{
MatchURLs: []string{"*"},
Rules: Rules{
Request: []string{},
Response: []string{},
},
}
}
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
layerOptions := DefaultLayerOptions()
if err := FromStoreOptions(storeOptions, &layerOptions); err != nil {
return nil, errors.WithStack(err)
}
return &layerOptions, nil
}
func FromStoreOptions(storeOptions store.LayerOptions, dest any) error {
config := mapstructure.DecoderConfig{
Result: dest,
ZeroFields: true,
}
decoder, err := mapstructure.NewDecoder(&config)
if err != nil {
return errors.WithStack(err)
}
if err := decoder.Decode(storeOptions); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -1,16 +0,0 @@
package rewriter
type Options struct {
}
type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{}
for _, fn := range funcs {
fn(opts)
}
return opts
}

View File

@ -1,204 +0,0 @@
package rewriter
import (
"context"
"net/http"
"net/url"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
)
type RequestVars struct {
Request RequestVar `expr:"request"`
OriginalURL URLVar `expr:"original_url"`
}
type URLVar struct {
Scheme string `expr:"scheme"`
Opaque string `expr:"opaque"`
User UserVar `expr:"user"`
Host string `expr:"host"`
Path string `expr:"path"`
RawPath string `expr:"raw_path"`
RawQuery string `expr:"raw_query"`
Fragment string `expr:"fragment"`
RawFragment string `expr:"raw_fragment"`
}
func fromURL(url *url.URL) URLVar {
return URLVar{
Scheme: url.Scheme,
Opaque: url.Opaque,
User: UserVar{
Username: url.User.Username(),
Password: func() string {
passwd, _ := url.User.Password()
return passwd
}(),
},
Host: url.Host,
Path: url.Path,
RawPath: url.RawPath,
RawQuery: url.RawQuery,
Fragment: url.Fragment,
RawFragment: url.RawFragment,
}
}
type UserVar struct {
Username string `expr:"username"`
Password string `expr:"password"`
}
type RequestVar struct {
Method string `expr:"method"`
URL URLVar `expr:"url"`
RawURL string `expr:"raw_url"`
Proto string `expr:"proto"`
ProtoMajor int `expr:"proto_major"`
ProtoMinor int `expr:"proto_minor"`
Header map[string][]string `expr:"header"`
ContentLength int64 `expr:"content_length"`
TransferEncoding []string `expr:"transfer_encoding"`
Host string `expr:"host"`
Trailer map[string][]string `expr:"trailer"`
RemoteAddr string `expr:"remote_addr"`
RequestURI string `expr:"request_uri"`
}
func fromRequest(r *http.Request) RequestVar {
return RequestVar{
Method: r.Method,
URL: fromURL(r.URL),
RawURL: r.URL.String(),
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Header: r.Header,
ContentLength: r.ContentLength,
TransferEncoding: r.TransferEncoding,
Host: r.Host,
Trailer: r.Trailer,
RemoteAddr: r.RemoteAddr,
RequestURI: r.RequestURI,
}
}
func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layer *store.Layer, options *LayerOptions) error {
rules := options.Rules.Request
if len(rules) == 0 {
return nil
}
engine, err := l.getRequestRuleEngine(ctx, layer, options)
if err != nil {
return errors.WithStack(err)
}
originalURL, err := director.OriginalURL(ctx)
if err != nil {
return errors.WithStack(err)
}
vars := &RequestVars{
OriginalURL: fromURL(originalURL),
Request: fromRequest(r),
}
ctx = ruleHTTP.WithRequest(ctx, r)
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err)
}
return nil
}
func (l *Layer) getRequestRuleEngine(ctx context.Context, layer *store.Layer, options *LayerOptions) (*rule.Engine[*RequestVars], error) {
key := string(layer.Proxy) + "-" + string(layer.Name)
revisionedEngine := l.requestRuleEngineCache.Get(key)
engine, err := revisionedEngine.Get(ctx, layer.Revision, options)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}
type ResponseVars struct {
OriginalURL URLVar `expr:"original_url"`
Request RequestVar `expr:"request"`
Response ResponseVar `expr:"response"`
}
type ResponseVar struct {
Status string `expr:"status"`
StatusCode int `expr:"status_code"`
Proto string `expr:"proto"`
ProtoMajor int `expr:"proto_major"`
ProtoMinor int `expr:"proto_minor"`
Header map[string][]string `expr:"header"`
ContentLength int64 `expr:"content_length"`
TransferEncoding []string `expr:"transfer_encoding"`
Uncompressed bool `expr:"uncompressed"`
Trailer map[string][]string `expr:"trailer"`
}
func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layer *store.Layer, options *LayerOptions) error {
rules := options.Rules.Response
if len(rules) == 0 {
return nil
}
engine, err := l.getResponseRuleEngine(ctx, layer, options)
if err != nil {
return errors.WithStack(err)
}
originalURL, err := director.OriginalURL(ctx)
if err != nil {
return errors.WithStack(err)
}
vars := &ResponseVars{
OriginalURL: fromURL(originalURL),
Request: fromRequest(r.Request),
Response: ResponseVar{
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Header: r.Header,
ContentLength: r.ContentLength,
TransferEncoding: r.TransferEncoding,
Trailer: r.Trailer,
Status: r.Status,
StatusCode: r.StatusCode,
},
}
ctx = ruleHTTP.WithResponse(ctx, r)
ctx = ruleHTTP.WithRequest(ctx, r.Request)
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err)
}
return nil
}
func (l *Layer) getResponseRuleEngine(ctx context.Context, layer *store.Layer, options *LayerOptions) (*rule.Engine[*ResponseVars], error) {
key := string(layer.Proxy) + "-" + string(layer.Name)
revisionedEngine := l.responseRuleEngineCache.Get(key)
engine, err := revisionedEngine.Get(ctx, layer.Revision, options)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}

View File

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

View File

@ -1,51 +0,0 @@
package util
import (
"context"
"sync"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type RuleEngineFactoryFunc[V any, O any] func(ops O) (*rule.Engine[V], error)
type RevisionedRuleEngine[V any, O any] struct {
mutex sync.RWMutex
revision int
engine *rule.Engine[V]
factory RuleEngineFactoryFunc[V, O]
}
func (e *RevisionedRuleEngine[V, O]) Get(ctx context.Context, revision int, opts O) (*rule.Engine[V], error) {
e.mutex.RLock()
if revision == e.revision {
logger.Debug(ctx, "using cached rule engine", logger.F("layerRevision", revision))
defer e.mutex.RUnlock()
return e.engine, nil
}
e.mutex.RUnlock()
e.mutex.Lock()
defer e.mutex.Unlock()
logger.Debug(ctx, "creating rule engine", logger.F("layerRevision", revision))
engine, err := e.factory(opts)
if err != nil {
return nil, errors.WithStack(err)
}
e.engine = engine
e.revision = revision
return engine, nil
}
func NewRevisionedRuleEngine[V any, O any](factory RuleEngineFactoryFunc[V, O]) *RevisionedRuleEngine[V, O] {
return &RevisionedRuleEngine[V, O]{
factory: factory,
}
}

View File

@ -1,28 +0,0 @@
package util
import (
"forge.cadoles.com/cadoles/bouncer/internal/cache"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
)
type RuleEngineCache[V any, O any] struct {
cache cache.Cache[string, *RevisionedRuleEngine[V, O]]
factory RuleEngineFactoryFunc[V, O]
}
func (c *RuleEngineCache[V, O]) Get(key string) *RevisionedRuleEngine[V, O] {
revisionedRuleEngine, exists := c.cache.Get(key)
if !exists {
revisionedRuleEngine = NewRevisionedRuleEngine(c.factory)
c.cache.Set(key, revisionedRuleEngine)
}
return revisionedRuleEngine
}
func NewInMemoryRuleEngineCache[V any, O any](factory RuleEngineFactoryFunc[V, O]) *RuleEngineCache[V, O] {
return &RuleEngineCache[V, O]{
factory: factory,
cache: memory.NewCache[string, *RevisionedRuleEngine[V, O]](),
}
}

View File

@ -1,71 +0,0 @@
package director
import (
"net/http"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/cache"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"gitlab.com/wpetit/goweb/logger"
)
type Options struct {
Layers []Layer
ProxyCache cache.Cache[string, []*store.Proxy]
LayerCache cache.Cache[string, []*store.Layer]
HandleError HandleErrorFunc
}
type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{
Layers: make([]Layer, 0),
ProxyCache: ttl.NewCache(
memory.NewCache[string, []*store.Proxy](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
LayerCache: ttl.NewCache(
memory.NewCache[string, []*store.Layer](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
HandleError: func(w http.ResponseWriter, r *http.Request, status int, err error) {
logger.Error(r.Context(), err.Error(), logger.CapturedE(err))
http.Error(w, http.StatusText(status), status)
},
}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func WithLayers(layers ...Layer) OptionFunc {
return func(opts *Options) {
opts.Layers = layers
}
}
func WithProxyCache(cache cache.Cache[string, []*store.Proxy]) OptionFunc {
return func(opts *Options) {
opts.ProxyCache = cache
}
}
func WithLayerCache(cache cache.Cache[string, []*store.Layer]) OptionFunc {
return func(opts *Options) {
opts.LayerCache = cache
}
}
func WithHandleErrorFunc(fn HandleErrorFunc) OptionFunc {
return func(opts *Options) {
opts.HandleError = fn
}
}

View File

@ -9,7 +9,7 @@ import (
) )
func (s *Server) initRepositories(ctx context.Context) error { func (s *Server) initRepositories(ctx context.Context) error {
client := setup.NewSharedClient(s.redisConfig) client := setup.NewRedisClient(ctx, s.redisConfig)
if err := s.initProxyRepository(ctx, client); err != nil { if err := s.initProxyRepository(ctx, client); err != nil {
return errors.WithStack(err) return errors.WithStack(err)

View File

@ -1,27 +1,23 @@
package proxy package proxy
import ( import (
"time"
"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"
) )
type Option struct { type Option struct {
ServerConfig config.ProxyServerConfig ServerConfig config.ProxyServerConfig
RedisConfig config.RedisConfig RedisConfig config.RedisConfig
DirectorLayers []director.Layer DirectorLayers []director.Layer
DirectorCacheTTL time.Duration
} }
type OptionFunc func(*Option) type OptionFunc func(*Option)
func defaultOption() *Option { func defaultOption() *Option {
return &Option{ return &Option{
ServerConfig: config.NewDefaultProxyServerConfig(), ServerConfig: config.NewDefaultProxyServerConfig(),
RedisConfig: config.NewDefaultRedisConfig(), RedisConfig: config.NewDefaultRedisConfig(),
DirectorLayers: make([]director.Layer, 0), DirectorLayers: make([]director.Layer, 0),
DirectorCacheTTL: 30 * time.Second,
} }
} }
@ -42,9 +38,3 @@ func WithDirectorLayers(layers ...director.Layer) OptionFunc {
opt.DirectorLayers = layers opt.DirectorLayers = layers
} }
} }
func WithDirectorCacheTTL(ttl time.Duration) OptionFunc {
return func(opt *Option) {
opt.DirectorCacheTTL = ttl
}
}

View File

@ -1,31 +1,21 @@
package proxy package proxy
import ( import (
"bytes"
"context" "context"
"expvar"
"fmt" "fmt"
"html/template"
"io"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/http/pprof"
"net/url" "net/url"
"path/filepath"
"strconv"
"time" "time"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi" 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/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"
"github.com/Masterminds/sprig/v3"
sentryhttp "github.com/getsentry/sentry-go/http" 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"
@ -35,12 +25,11 @@ import (
) )
type Server struct { type Server struct {
serverConfig config.ProxyServerConfig serverConfig config.ProxyServerConfig
redisConfig config.RedisConfig redisConfig config.RedisConfig
directorLayers []director.Layer directorLayers []director.Layer
directorCacheTTL time.Duration proxyRepository store.ProxyRepository
proxyRepository store.ProxyRepository layerRepository store.LayerRepository
layerRepository store.LayerRepository
} }
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) { func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
@ -97,31 +86,14 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
director := director.New( director := director.New(
s.proxyRepository, s.proxyRepository,
s.layerRepository, s.layerRepository,
director.WithLayers(s.directorLayers...), s.directorLayers...,
director.WithLayerCache(
ttl.NewCache(
memory.NewCache[string, []*store.Layer](),
memory.NewCache[string, time.Time](),
s.directorCacheTTL,
),
),
director.WithProxyCache(
ttl.NewCache(
memory.NewCache[string, []*store.Proxy](),
memory.NewCache[string, time.Time](),
s.directorCacheTTL,
),
),
director.WithHandleErrorFunc(s.handleError),
) )
if s.serverConfig.HTTP.UseRealIP { if s.serverConfig.HTTP.UseRealIP {
router.Use(middleware.RealIP) router.Use(middleware.RealIP)
} }
router.Use(middleware.RequestID)
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter())) router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
router.Use(middleware.Recoverer)
if s.serverConfig.Sentry.DSN != "" { if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware") logger.Info(ctx, "enabling sentry http middleware")
@ -152,35 +124,6 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
}) })
} }
if s.serverConfig.Profiling.Enabled {
profiling := s.serverConfig.Profiling
logger.Info(ctx, "enabling profiling", logger.F("endpoint", profiling.Endpoint))
router.Group(func(r chi.Router) {
if profiling.BasicAuth != nil {
logger.Info(ctx, "enabling authentication on profiling endpoint")
r.Use(middleware.BasicAuth(
"profiling",
profiling.BasicAuth.CredentialsMap(),
))
}
r.Route(string(profiling.Endpoint), func(r chi.Router) {
r.HandleFunc("/", pprof.Index)
r.HandleFunc("/cmdline", pprof.Cmdline)
r.HandleFunc("/profile", pprof.Profile)
r.HandleFunc("/symbol", pprof.Symbol)
r.HandleFunc("/trace", pprof.Trace)
r.Handle("/vars", expvar.Handler())
r.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
pprof.Handler(name).ServeHTTP(w, r)
})
})
})
}
router.Group(func(r chi.Router) { router.Group(func(r chi.Router) {
r.Use(director.Middleware()) r.Use(director.Middleware())
@ -192,7 +135,6 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
director.ResponseTransformer(), director.ResponseTransformer(),
), ),
proxy.WithReverseProxyFactory(s.createReverseProxy), proxy.WithReverseProxyFactory(s.createReverseProxy),
proxy.WithDefaultHandler(http.HandlerFunc(s.handleDefault)),
) )
r.Handle("/*", handler) r.Handle("/*", handler)
@ -221,91 +163,18 @@ func (s *Server) createReverseProxy(ctx context.Context, target *url.URL) *httpu
httpTransport.DialContext = dialer.DialContext httpTransport.DialContext = dialer.DialContext
reverseProxy.Transport = httpTransport reverseProxy.Transport = httpTransport
reverseProxy.ErrorHandler = s.handleProxyError reverseProxy.ErrorHandler = s.errorHandler
return reverseProxy return reverseProxy
} }
func (s *Server) handleDefault(w http.ResponseWriter, r *http.Request) { func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
s.handleError(w, r, http.StatusBadGateway, errors.Errorf("no proxy target found"))
}
func (s *Server) handleError(w http.ResponseWriter, r *http.Request, status int, err error) {
err = errors.WithStack(err) err = errors.WithStack(err)
if errors.Is(err, context.Canceled) { logger.Error(r.Context(), "proxy error", logger.E(err))
logger.Warn(r.Context(), err.Error(), logger.E(err)) sentry.CaptureException(err)
} else {
logger.Error(r.Context(), err.Error(), logger.CapturedE(err))
}
s.renderErrorPage(w, r, err, status, http.StatusText(status)) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (s *Server) handleProxyError(w http.ResponseWriter, r *http.Request, err error) {
s.handleError(w, r, http.StatusBadGateway, err)
}
func (s *Server) renderErrorPage(w http.ResponseWriter, r *http.Request, err error, statusCode int, status string) {
templateData := struct {
StatusCode int
Status string
Err error
Debug bool
}{
Debug: bool(s.serverConfig.Debug),
StatusCode: statusCode,
Status: status,
Err: err,
}
w.WriteHeader(statusCode)
s.renderPage(w, r, "error", strconv.FormatInt(int64(statusCode), 10), templateData)
}
func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page string, block string, templateData any) {
ctx := r.Context()
templatesConf := s.serverConfig.Templates
pattern := filepath.Join(string(templatesConf.Dir), page+".gohtml")
logger.Info(ctx, "loading proxy templates", logger.F("pattern", pattern))
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
if err != nil {
logger.Error(ctx, "could not load proxy templates", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Cache-Control", "no-cache")
blockTmpl := tmpl.Lookup(block)
if blockTmpl == nil {
blockTmpl = tmpl.Lookup("default")
}
if blockTmpl == nil {
logger.Error(ctx, "could not find template block nor default one", logger.F("block", block))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
var buf bytes.Buffer
if err := blockTmpl.Execute(&buf, templateData); err != nil {
logger.Error(ctx, "could not render proxy page", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if _, err := io.Copy(w, &buf); err != nil {
logger.Error(ctx, "could not write page", logger.CapturedE(errors.WithStack(err)))
}
} }
func NewServer(funcs ...OptionFunc) *Server { func NewServer(funcs ...OptionFunc) *Server {
@ -315,9 +184,8 @@ func NewServer(funcs ...OptionFunc) *Server {
} }
return &Server{ return &Server{
serverConfig: opt.ServerConfig, serverConfig: opt.ServerConfig,
redisConfig: opt.RedisConfig, redisConfig: opt.RedisConfig,
directorLayers: opt.DirectorLayers, directorLayers: opt.DirectorLayers,
directorCacheTTL: opt.DirectorCacheTTL,
} }
} }

View File

@ -1,79 +0,0 @@
package rule
import (
"context"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/pkg/errors"
)
type Engine[V any] struct {
rules []*vm.Program
}
func (e *Engine[V]) Apply(ctx context.Context, vars V) ([]any, error) {
type Env[V any] struct {
Context context.Context `expr:"ctx"`
Vars V `expr:"vars"`
}
env := Env[V]{
Context: ctx,
Vars: vars,
}
results := make([]any, 0, len(e.rules))
for i, r := range e.rules {
result, err := expr.Run(r, env)
if err != nil {
return nil, errors.Wrapf(err, "could not run rule #%d", i)
}
results = append(results, result)
}
return results, nil
}
func NewEngine[E any](funcs ...OptionFunc) (*Engine[E], error) {
opts := NewOptions(funcs...)
engine := &Engine[E]{
rules: make([]*vm.Program, 0, len(opts.Rules)),
}
for i, r := range opts.Rules {
program, err := expr.Compile(r, opts.Expr...)
if err != nil {
return nil, errors.Wrapf(err, "could not compile rule #%d", i)
}
engine.rules = append(engine.rules, program)
}
return engine, nil
}
func Context[T any](ctx context.Context, key any) (T, bool) {
raw := ctx.Value(key)
if raw == nil {
return *new(T), false
}
value, err := Assert[T](raw)
if err != nil {
return *new(T), false
}
return value, true
}
func Assert[T any](raw any) (T, error) {
value, ok := raw.(T)
if !ok {
return *new(T), errors.Errorf("unexpected value '%T'", value)
}
return value, nil
}

View File

@ -1,31 +0,0 @@
package http
import (
"context"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
)
type contextKey string
const (
contextKeyRequest contextKey = "request"
contextKeyResponse contextKey = "response"
)
func WithRequest(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, contextKeyRequest, r)
}
func WithResponse(ctx context.Context, r *http.Response) context.Context {
return context.WithValue(ctx, contextKeyResponse, r)
}
func CtxRequest(ctx context.Context) (*http.Request, bool) {
return rule.Context[*http.Request](ctx, contextKeyRequest)
}
func CtxResponse(ctx context.Context) (*http.Response, bool) {
return rule.Context[*http.Response](ctx, contextKeyResponse)
}

View File

@ -1,44 +0,0 @@
package http
import (
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr"
)
func WithRequestFuncs() rule.OptionFunc {
return func(opts *rule.Options) {
funcs := []expr.Option{
setRequestURLFunc(),
setRequestHeaderFunc(),
addRequestHeaderFunc(),
delRequestHeadersFunc(),
setRequestHostFunc(),
getRequestCookieFunc(),
addRequestCookieFunc(),
}
if len(opts.Expr) == 0 {
opts.Expr = make([]expr.Option, 0)
}
opts.Expr = append(opts.Expr, funcs...)
}
}
func WithResponseFuncs() rule.OptionFunc {
return func(opts *rule.Options) {
funcs := []expr.Option{
setResponseHeaderFunc(),
addResponseHeaderFunc(),
delResponseHeadersFunc(),
addResponseCookieFunc(),
getResponseCookieFunc(),
}
if len(opts.Expr) == 0 {
opts.Expr = make([]expr.Option, 0)
}
opts.Expr = append(opts.Expr, funcs...)
}
}

View File

@ -1,323 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr"
"github.com/pkg/errors"
)
func setRequestHostFunc() expr.Option {
return expr.Function(
"set_host",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
host, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.Host = host
return true, nil
},
new(func(context.Context, string) bool),
)
}
func setRequestURLFunc() expr.Option {
return expr.Function(
"set_url",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
rawURL, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
url, err := url.Parse(rawURL)
if err != nil {
return false, errors.WithStack(err)
}
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.URL = url
return true, nil
},
new(func(context.Context, string) bool),
)
}
func addRequestHeaderFunc() expr.Option {
return expr.Function(
"add_header",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
value := formatValue(params[2])
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.Header.Add(name, value)
return true, nil
},
new(func(context.Context, string, string) bool),
)
}
func setRequestHeaderFunc() expr.Option {
return expr.Function(
"set_header",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
value := formatValue(params[2])
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.Header.Set(name, value)
return true, nil
},
new(func(context.Context, string, string) bool),
)
}
func delRequestHeadersFunc() expr.Option {
return expr.Function(
"del_headers",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
pattern, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
deleted := false
for key := range r.Header {
if !wildcard.Match(key, pattern) {
continue
}
r.Header.Del(key)
deleted = true
}
return deleted, nil
},
new(func(context.Context, string) bool),
)
}
type CookieVar struct {
Name string `expr:"name"`
Value string `expr:"value"`
Path string `expr:"path"`
Domain string `expr:"domain"`
Expires time.Time `expr:"expires"`
MaxAge int `expr:"max_age"`
Secure bool `expr:"secure"`
HttpOnly bool `expr:"http_only"`
SameSite http.SameSite `expr:"same_site"`
}
func getRequestCookieFunc() expr.Option {
return expr.Function(
"get_cookie",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
cookie, err := r.Cookie(name)
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return nil, errors.WithStack(err)
}
if cookie == nil {
return nil, nil
}
return CookieVar{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
Expires: cookie.Expires,
MaxAge: cookie.MaxAge,
Secure: cookie.Secure,
HttpOnly: cookie.HttpOnly,
SameSite: cookie.SameSite,
}, nil
},
new(func(context.Context, string) CookieVar),
)
}
func addRequestCookieFunc() expr.Option {
return expr.Function(
"add_cookie",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
values, err := rule.Assert[map[string]any](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
cookie, err := cookieFrom(values)
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := CtxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.AddCookie(cookie)
return true, nil
},
new(func(context.Context, map[string]any) bool),
)
}
func cookieFrom(values map[string]any) (*http.Cookie, error) {
cookie := &http.Cookie{}
if name, ok := values["name"].(string); ok {
cookie.Name = name
}
if value, ok := values["value"].(string); ok {
cookie.Value = value
}
if domain, ok := values["domain"].(string); ok {
cookie.Domain = domain
}
if path, ok := values["path"].(string); ok {
cookie.Path = path
}
if httpOnly, ok := values["http_only"].(bool); ok {
cookie.HttpOnly = httpOnly
}
if maxAge, ok := values["max_age"].(int); ok {
cookie.MaxAge = maxAge
}
if secure, ok := values["secure"].(bool); ok {
cookie.Secure = secure
}
if sameSite, ok := values["same_site"].(http.SameSite); ok {
cookie.SameSite = sameSite
} else if sameSite, ok := values["same_site"].(int); ok {
cookie.SameSite = http.SameSite(sameSite)
}
if expires, ok := values["expires"].(time.Time); ok {
cookie.Expires = expires
} else if rawExpires, ok := values["expires"].(string); ok {
expires, err := time.Parse(http.TimeFormat, rawExpires)
if err != nil {
return nil, errors.WithStack(err)
}
cookie.Expires = expires
}
return cookie, nil
}
func formatValue(v any) string {
var value string
switch v := v.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", v)
}
return value
}

View File

@ -1,324 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"testing"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/pkg/errors"
)
func TestSetRequestHost(t *testing.T) {
type Vars struct {
NewHost string `expr:"newHost"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setRequestHostFunc()),
rule.WithRules(
"set_host(ctx, vars.newHost)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
vars := Vars{
NewHost: "foobar",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewHost, req.Host; e != g {
t.Errorf("req.Host: expected '%v', got '%v'", e, g)
}
}
func TestSetRequestURL(t *testing.T) {
type Vars struct {
NewURL string `expr:"newURL"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setRequestURLFunc()),
rule.WithRules(
"set_url(ctx, vars.newURL)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
vars := Vars{
NewURL: "http://localhost",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewURL, req.URL.String(); e != g {
t.Errorf("req.URL.String(): expected '%v', got '%v'", e, g)
}
}
func TestAddRequestHeader(t *testing.T) {
type Vars struct {
NewHeaderKey string `expr:"newHeaderKey"`
NewHeaderValue string `expr:"newHeaderValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(addRequestHeaderFunc()),
rule.WithRules(
"add_header(ctx, vars.newHeaderKey, vars.newHeaderValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
vars := Vars{
NewHeaderKey: "X-My-Header",
NewHeaderValue: "foobar",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewHeaderValue, req.Header.Get(vars.NewHeaderKey); e != g {
t.Errorf("req.Header.Get(vars.NewHeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestSetRequestHeader(t *testing.T) {
type Vars struct {
HeaderKey string `expr:"headerKey"`
HeaderValue string `expr:"headerValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setRequestHeaderFunc()),
rule.WithRules(
"set_header(ctx, vars.headerKey, vars.headerValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
vars := Vars{
HeaderKey: "X-My-Header",
HeaderValue: "foobar",
}
req.Header.Set(vars.HeaderKey, "test")
ctx := context.Background()
ctx = WithRequest(ctx, req)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.HeaderValue, req.Header.Get(vars.HeaderKey); e != g {
t.Errorf("req.Header.Get(vars.HeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestDelRequestHeaders(t *testing.T) {
type Vars struct {
HeaderPattern string `expr:"headerPattern"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(delRequestHeadersFunc()),
rule.WithRules(
"del_headers(ctx, vars.headerPattern)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
vars := Vars{
HeaderPattern: "X-My-*",
}
req.Header.Set("X-My-Header", "test")
ctx := context.Background()
ctx = WithRequest(ctx, req)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if val := req.Header.Get("X-My-Header"); val != "" {
t.Errorf("req.Header.Get(\"X-My-Header\") should be empty, got '%v'", val)
}
}
func TestAddRequestCookie(t *testing.T) {
type TestCase struct {
Cookie map[string]any
Check func(t *testing.T, tc TestCase, req *http.Request)
ShouldFail bool
}
testCases := []TestCase{
{
Cookie: map[string]any{
"name": "test",
},
Check: func(t *testing.T, tc TestCase, req *http.Request) {
cookie, err := req.Cookie(tc.Cookie["name"].(string))
if err != nil {
t.Errorf("%+v", errors.WithStack(err))
return
}
if e, g := tc.Cookie["name"], cookie.Name; e != g {
t.Errorf("cookie.Name: expected '%v', got '%v'", e, g)
}
},
},
{
Cookie: map[string]any{
"name": "foo",
"value": "test",
},
Check: func(t *testing.T, tc TestCase, req *http.Request) {
cookie, err := req.Cookie(tc.Cookie["name"].(string))
if err != nil {
t.Errorf("%+v", errors.WithStack(err))
return
}
if e, g := tc.Cookie["name"], cookie.Name; e != g {
t.Errorf("cookie.Name: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["value"], cookie.Value; e != g {
t.Errorf("cookie.Value: expected '%v', got '%v'", e, g)
}
},
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("Case_%d", idx), func(t *testing.T) {
type Vars struct {
NewCookie map[string]any `expr:"new_cookie"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(addRequestCookieFunc()),
rule.WithRules(
`add_cookie(ctx, vars.new_cookie)`,
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
vars := Vars{
NewCookie: tc.Cookie,
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if tc.ShouldFail {
t.Error("engine.Apply() should have failed")
}
if tc.Check != nil {
tc.Check(t, tc, req)
}
})
}
}
func TestGetRequestCookie(t *testing.T) {
type Vars struct {
CookieName string `expr:"cookieName"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(getRequestCookieFunc()),
rule.WithRules(
"let cookie = get_cookie(ctx, vars.cookieName); cookie.value",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
vars := Vars{
CookieName: "foo",
}
cookie := &http.Cookie{
Name: vars.CookieName,
Value: "bar",
}
req.AddCookie(cookie)
ctx := context.Background()
ctx = WithRequest(ctx, req)
results, err := engine.Apply(ctx, vars)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := cookie.Value, results[0]; e != g {
t.Errorf("result[0]: expected '%v', got '%v'", e, g)
}
}
func createRuleEngine[V any](t *testing.T, funcs ...rule.OptionFunc) *rule.Engine[V] {
engine, err := rule.NewEngine[V](funcs...)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
return engine
}

View File

@ -1,214 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr"
"github.com/pkg/errors"
)
func addResponseHeaderFunc() expr.Option {
return expr.Function(
"add_header",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
rawValue := params[2]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r, ok := CtxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
r.Header.Add(name, value)
return true, nil
},
new(func(context.Context, string, string) bool),
)
}
func setResponseHeaderFunc() expr.Option {
return expr.Function(
"set_header",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
rawValue := params[2]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r, ok := CtxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
r.Header.Set(name, value)
return true, nil
},
new(func(context.Context, string, string) bool),
)
}
func delResponseHeadersFunc() expr.Option {
return expr.Function(
"del_headers",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
pattern, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := CtxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
deleted := false
for key := range r.Header {
if !wildcard.Match(key, pattern) {
continue
}
r.Header.Del(key)
deleted = true
}
return deleted, nil
},
new(func(context.Context, string) bool),
)
}
func addResponseCookieFunc() expr.Option {
return expr.Function(
"add_cookie",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
values, err := rule.Assert[map[string]any](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
cookie, err := cookieFrom(values)
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := CtxResponse(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.Header.Add("Set-Cookie", cookie.String())
return true, nil
},
new(func(context.Context, map[string]any) bool),
)
}
func getResponseCookieFunc() expr.Option {
return expr.Function(
"get_cookie",
func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
res, ok := CtxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
var cookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name != name {
continue
}
cookie = c
break
}
if cookie == nil {
return nil, nil
}
return CookieVar{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
Expires: cookie.Expires,
MaxAge: cookie.MaxAge,
Secure: cookie.Secure,
HttpOnly: cookie.HttpOnly,
SameSite: cookie.SameSite,
}, nil
},
new(func(context.Context, string) CookieVar),
)
}

View File

@ -1,317 +0,0 @@
package http
import (
"context"
"fmt"
"io"
"net/http"
"testing"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/pkg/errors"
)
func TestAddResponseHeader(t *testing.T) {
type Vars struct {
NewHeaderKey string `expr:"newHeaderKey"`
NewHeaderValue string `expr:"newHeaderValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(addResponseHeaderFunc()),
rule.WithRules(
"add_header(ctx, vars.newHeaderKey, vars.newHeaderValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
ctx := context.Background()
ctx = WithResponse(ctx, resp)
vars := Vars{
NewHeaderKey: "X-My-Header",
NewHeaderValue: "foobar",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewHeaderValue, resp.Header.Get(vars.NewHeaderKey); e != g {
t.Errorf("resp.Header.Get(vars.NewHeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestResponseSetHeader(t *testing.T) {
type Vars struct {
HeaderKey string `expr:"headerKey"`
HeaderValue string `expr:"headerValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setResponseHeaderFunc()),
rule.WithRules(
"set_header(ctx, vars.headerKey, vars.headerValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
vars := Vars{
HeaderKey: "X-My-Header",
HeaderValue: "foobar",
}
resp.Header.Set(vars.HeaderKey, "test")
ctx := context.Background()
ctx = WithResponse(ctx, resp)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.HeaderValue, resp.Header.Get(vars.HeaderKey); e != g {
t.Errorf("resp.Header.Get(vars.HeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestResponseDelHeaders(t *testing.T) {
type Vars struct {
HeaderPattern string `expr:"headerPattern"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(delResponseHeadersFunc()),
rule.WithRules(
"del_headers(ctx, vars.headerPattern)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
vars := Vars{
HeaderPattern: "X-My-*",
}
resp.Header.Set("X-My-Header", "test")
ctx := context.Background()
ctx = WithResponse(ctx, resp)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if val := resp.Header.Get("X-My-Header"); val != "" {
t.Errorf("resp.Header.Get(\"X-My-Header\") should be empty, got '%v'", val)
}
}
func TestAddResponseCookie(t *testing.T) {
type TestCase struct {
Cookie map[string]any
Check func(t *testing.T, tc TestCase, res *http.Response)
ShouldFail bool
}
testCases := []TestCase{
{
Cookie: map[string]any{
"name": "foo",
"value": "test",
"domain": "example.net",
"path": "/custom",
"same_site": http.SameSiteStrictMode,
"http_only": true,
"secure": false,
"expires": time.Now().UTC().Truncate(time.Second),
},
Check: func(t *testing.T, tc TestCase, res *http.Response) {
var cookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name == tc.Cookie["name"] {
cookie = c
break
}
}
if cookie == nil {
t.Errorf("could not find cookie '%s'", tc.Cookie["name"])
return
}
if e, g := tc.Cookie["name"], cookie.Name; e != g {
t.Errorf("cookie.Name: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["value"], cookie.Value; e != g {
t.Errorf("cookie.Value: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["domain"], cookie.Domain; e != g {
t.Errorf("cookie.Domain: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["path"], cookie.Path; e != g {
t.Errorf("cookie.Path: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["secure"], cookie.Secure; e != g {
t.Errorf("cookie.Secure: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["http_only"], cookie.HttpOnly; e != g {
t.Errorf("cookie.HttpOnly: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["same_site"], cookie.SameSite; e != g {
t.Errorf("cookie.SameSite: expected '%v', got '%v'", e, g)
}
if e, g := tc.Cookie["expires"], cookie.Expires; e != g {
t.Errorf("cookie.Expires: expected '%v', got '%v'", e, g)
}
},
},
{
Cookie: map[string]any{
"name": "foo",
"expires": time.Now().UTC().Format(http.TimeFormat),
},
Check: func(t *testing.T, tc TestCase, res *http.Response) {
var cookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name == tc.Cookie["name"] {
cookie = c
break
}
}
if cookie == nil {
t.Errorf("could not find cookie '%s'", tc.Cookie["name"])
return
}
if e, g := tc.Cookie["expires"], cookie.Expires.Format(http.TimeFormat); e != g {
t.Errorf("cookie.Expires: expected '%v', got '%v'", e, g)
}
},
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("Case_%d", idx), func(t *testing.T) {
type Vars struct {
NewCookie map[string]any `expr:"new_cookie"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(addResponseCookieFunc()),
rule.WithRules(
`add_cookie(ctx, vars.new_cookie)`,
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
vars := Vars{
NewCookie: tc.Cookie,
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
ctx = WithResponse(ctx, resp)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if tc.ShouldFail {
t.Error("engine.Apply() should have failed")
}
if tc.Check != nil {
tc.Check(t, tc, resp)
}
})
}
}
func TestGetResponseCookie(t *testing.T) {
type Vars struct {
CookieName string `expr:"cookieName"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(getResponseCookieFunc()),
rule.WithRules(
"let cookie = get_cookie(ctx, vars.cookieName); cookie.value",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
vars := Vars{
CookieName: "foo",
}
cookie := &http.Cookie{
Name: vars.CookieName,
Value: "bar",
}
resp.Header.Add("Set-Cookie", cookie.String())
ctx := context.Background()
ctx = WithResponse(ctx, resp)
results, err := engine.Apply(ctx, vars)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := cookie.Value, results[0]; e != g {
t.Errorf("result[0]: expected '%v', got '%v'", e, g)
}
}
func createResponse(req *http.Request, statusCode int, body io.Reader) *http.Response {
return &http.Response{
Status: http.StatusText(statusCode),
StatusCode: statusCode,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Body: io.NopCloser(body),
ContentLength: -1,
Request: req,
Header: make(http.Header, 0),
}
}

View File

@ -1,35 +0,0 @@
package rule
import "github.com/expr-lang/expr"
type Options struct {
Rules []string
Expr []expr.Option
}
type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{
Expr: make([]expr.Option, 0),
Rules: make([]string, 0),
}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func WithRules(rules ...string) OptionFunc {
return func(opts *Options) {
opts.Rules = rules
}
}
func WithExpr(options ...expr.Option) OptionFunc {
return func(opts *Options) {
opts.Expr = options
}
}

View File

@ -10,7 +10,6 @@ import (
type Options struct { type Options struct {
Session sessions.Options Session sessions.Options
KeyPrefix string KeyPrefix string
TTL time.Duration
} }
type OptionFunc func(opts *Options) type OptionFunc func(opts *Options)
@ -26,7 +25,6 @@ func NewOptions(funcs ...OptionFunc) *Options {
SameSite: http.SameSiteDefaultMode, SameSite: http.SameSiteDefaultMode,
}, },
KeyPrefix: "session:", KeyPrefix: "session:",
TTL: time.Hour,
} }
for _, fn := range funcs { for _, fn := range funcs {
@ -47,9 +45,3 @@ func WithKeyPrefix(prefix string) OptionFunc {
opts.KeyPrefix = prefix opts.KeyPrefix = prefix
} }
} }
func WithTTL(ttl time.Duration) OptionFunc {
return func(opts *Options) {
opts.TTL = ttl
}
}

View File

@ -31,7 +31,6 @@ type Store struct {
keyPrefix string keyPrefix string
keyGen KeyGenFunc keyGen KeyGenFunc
serializer SessionSerializer serializer SessionSerializer
ttl time.Duration
} }
type KeyGenFunc func() (string, error) type KeyGenFunc func() (string, error)
@ -44,7 +43,6 @@ func NewStore(adapter StoreAdapter, funcs ...OptionFunc) *Store {
keyPrefix: opts.KeyPrefix, keyPrefix: opts.KeyPrefix,
keyGen: generateRandomKey, keyGen: generateRandomKey,
serializer: GobSerializer{}, serializer: GobSerializer{},
ttl: opts.TTL,
} }
return rs return rs
@ -64,21 +62,20 @@ func (s *Store) New(r *http.Request, name string) (*sessions.Session, error) {
if err != nil { if err != nil {
return session, nil return session, nil
} }
session.ID = c.Value session.ID = c.Value
err = s.load(r.Context(), session) err = s.load(r.Context(), session)
if err == nil { if err == nil {
session.IsNew = false session.IsNew = false
} else if !errors.Is(err, ErrNotFound) { } else if !errors.Is(err, ErrNotFound) {
return session, errors.WithStack(err) return nil, errors.WithStack(err)
} }
return session, nil return session, nil
} }
func (s *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { func (s *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
if session.Options.MaxAge < 0 { if session.Options.MaxAge <= 0 {
if err := s.delete(r.Context(), session); err != nil { if err := s.delete(r.Context(), session); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -123,12 +120,7 @@ func (s *Store) save(ctx context.Context, session *sessions.Session) error {
return errors.WithStack(err) return errors.WithStack(err)
} }
ttl := time.Duration(session.Options.MaxAge) * time.Second if err := s.adapter.Set(ctx, s.keyPrefix+session.ID, b, time.Duration(session.Options.MaxAge)*time.Second); err != nil {
if s.ttl < ttl || ttl == 0 {
ttl = s.ttl
}
if err := s.adapter.Set(ctx, s.keyPrefix+session.ID, b, ttl); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }

View File

@ -21,7 +21,6 @@ func init() {
func setupAuthnBasicLayer(conf *config.Config) (director.Layer, error) { func setupAuthnBasicLayer(conf *config.Config) (director.Layer, error) {
options := []authn.OptionFunc{ options := []authn.OptionFunc{
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)), authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
authn.WithDebug(bool(conf.Layers.Authn.Debug)),
} }
return basic.NewLayer(options...), nil return basic.NewLayer(options...), nil

View File

@ -21,7 +21,6 @@ func init() {
func setupAuthnNetworkLayer(conf *config.Config) (director.Layer, error) { func setupAuthnNetworkLayer(conf *config.Config) (director.Layer, error) {
options := []authn.OptionFunc{ options := []authn.OptionFunc{
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)), authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
authn.WithDebug(bool(conf.Layers.Authn.Debug)),
} }
return network.NewLayer(options...), nil return network.NewLayer(options...), nil

Some files were not shown because too many files have changed in this diff Show More