Compare commits

...

17 Commits

Author SHA1 Message Date
bf15732935 feat: disable sentry integration when no dsn is defined
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-09-23 10:13:04 +02:00
8317ac5b9a feat: add configurable profiling endpoints (#38) 2024-09-23 10:12:42 +02:00
f35384c0f3 feat: create profiling package + rewrite profiling tutorial
Some checks reported warnings
Cadoles/bouncer/pipeline/head This commit was not built
2024-06-28 17:44:51 +02:00
c73fe8cca5 feat(rewriter): pass structured url to ease request rewriting
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-28 10:46:38 +02:00
3c1939f418 feat: add revision number to proxy and layers to identify changes
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-27 17:03:50 +02:00
3565618335 doc: update link title
Some checks failed
Cadoles/bouncer/pipeline/head There was a failure building this commit
2024-06-27 15:53:01 +02:00
64ca8fe1e4 fix: wrong bit size 2024-06-27 15:27:14 +02:00
d5669a4eb5 fix: multiple environment variables interpolation in configuration file
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-27 15:00:25 +02:00
f3aa8b9be6 doc: add profiling tutorial link
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-27 14:13:08 +02:00
2de5e285a3 doc: add virtual hosting example
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-27 11:12:58 +02:00
87e1c65607 fix: security vulnerabilities
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
ref #31
2024-06-27 10:04:29 +02:00
f092520ee4 chore: tidy deps
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-26 16:31:14 +02:00
9084b6e05f Merge pull request 'Implémentation des proxies "passthrough"' (#30) from passthrough-proxy into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #30
2024-06-26 16:23:28 +02:00
5494abded4 feat: passthrough proxies
Some checks are pending
Cadoles/bouncer/pipeline/head Build started...
Cadoles/bouncer/pipeline/pr-develop Build started...
2024-06-26 16:22:30 +02:00
059af1b6ee fix: add missing templates in docker image
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-26 15:00:23 +02:00
532f30c155 chore: remove artefact Dockerfile
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
2024-06-26 15:00:07 +02:00
49f2ccbc7a feat: templatized proxy error page
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-26 14:36:28 +02:00
59 changed files with 1490 additions and 497 deletions

2
.gitignore vendored
View File

@ -10,4 +10,4 @@
/out /out
.dockerconfigjson .dockerconfigjson
*.prof *.prof
proxy.test *.test

View File

@ -63,6 +63,9 @@ 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.22.0 AS BUILD FROM reg.cadoles.com/proxy_cache/library/golang:1.22 AS BUILD
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y make && apt-get install -y make
@ -22,6 +22,7 @@ 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 \
@ -32,7 +33,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.19.1 AS RUNTIME FROM reg.cadoles.com/proxy_cache/library/alpine:3.20 AS RUNTIME
RUN apk add --no-cache ca-certificates dumb-init RUN apk add --no-cache ca-certificates dumb-init
@ -42,6 +43,7 @@ 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

View File

@ -81,7 +81,7 @@ finish-release:
git push --tags git push --tags
docker-build: docker-build:
docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) . docker build --pull -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:
@ -131,7 +131,7 @@ tools/grafterm/bin/grafterm:
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: bench:
go test -bench=. -run '^$$' -count=10 ./... go test -bench=. -run '^$$' ./internal/bench
tools/benchstat/bin/benchstat: tools/benchstat/bin/benchstat:
mkdir -p tools/benchstat/bin mkdir -p tools/benchstat/bin

View File

@ -4,7 +4,16 @@
# Bouncer # Bouncer
Serveur mandataire inverse (_"reverse proxy"_) filtrant avec gestion de files d'attente dynamiques. Serveur mandataire inverse (_"reverse proxy"_) avec fonctionnalités avancées pilotable par API REST.
## 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

View File

@ -1,7 +1,8 @@
# Documentation # Documentation
- [(FR) - Premiers pas](./fr/getting-started.md)
- [(FR) - Architecture générale](./fr/general-architecture.md) - [(FR) - Architecture générale](./fr/general-architecture.md)
- [(FR) - Terminologie](./fr/terminology.md)
- [(FR) - Premiers pas](./fr/getting-started.md)
## Exemples ## Exemples
@ -11,19 +12,22 @@
- [(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) - Fichier de configuration](../misc/packaging/common/config.yml) - [(FR) - Configuration](./fr/references/configuration.md)
- [(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,31 +2,6 @@
## Modèles de déploiement ## Modèles de déploiement
### Déploiement mono-noeud ### Mode mono-noeud
![](../resources/deployment_fr.png) ![](../resources/deployment_single_node_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

@ -0,0 +1,34 @@
# 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

@ -66,7 +66,21 @@ La requête en cours de traitement.
{ {
method: "string", // Méthode HTTP method: "string", // Méthode HTTP
host: "string", // Nom d'hôte (`Host`) associé à la requête host: "string", // Nom d'hôte (`Host`) associé à la requête
url: "string", // URL associée à 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)
"rawPath": "string", // Chemin de l'URL (format brut)
"rawQuery": "string", // Variables d'URL (format brut)
"fragment" : "string", // Fragment d'URL (format assaini)
"rawFragment" : "string" // Fragment d'URL (format brut)
},
rawUrl: "string", // URL associée à la requête (format assaini)
proto: "string", // Numéro de version du protocole utilisé proto: "string", // Numéro de version du protocole utilisé
protoMajor: "int", // Numéro de version majeure du protocole utilisé protoMajor: "int", // Numéro de version majeure du protocole utilisé
protoMinor: "int", // Numéro de version mineur du protocole utilisé protoMinor: "int", // Numéro de version mineur du protocole utilisé

29
doc/fr/terminology.md Normal file
View File

@ -0,0 +1,29 @@
# 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

@ -1,31 +1,68 @@
# Analyser les performances de Bouncer # Étudier les performances de Bouncer
1. Lancer un benchmark du proxy ## In situ
```shell 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)).
go test -bench=. -run '^$' -count=5 -cpuprofile bench_proxy.prof ./internal/proxy
**Exemple:** Visualiser l'utilisation mémoire de Bouncer
```bash
go tool pprof -web http://<bouncer_proxy>/.bouncer/profiling/heap
``` ```
2. Visualiser les temps d'exécution L'ensemble des profils disponibles sont visibles à l'adresse `http://<bouncer_proxy>/.bouncer/profiling`.
```shell ## En développement
go tool pprof -web bench_proxy.prof
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
``` ```
3. Comparer les performances d'une exécution à l'autre 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 ```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 # Lancer un premier benchmark
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_before.txt 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 # Faire des modifications sur les sources
# Lancer un second benchmark # Lancer un second benchmark
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_after.txt go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
# Installer l'outil benchstat # Visualiser la différence entre les deux profils
make tools/benchstat/bin/benchstat go tool pprof -web -base=./internal/bench/testdata/proxies/$BENCH_CASE-prev.prof ./internal/bench/testdata/proxies/$BENCH_CASE.prof
# Comparer les rapports
tools/benchstat/bin/benchstat bench_before.txt bench_after.txt
``` ```

View File

@ -0,0 +1,129 @@
# 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

26
go.mod
View File

@ -1,11 +1,11 @@
module forge.cadoles.com/cadoles/bouncer module forge.cadoles.com/cadoles/bouncer
go 1.21 go 1.22
toolchain go1.22.0 toolchain go1.22.0
require ( require (
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926
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,9 @@ 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.19.0 // indirect golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.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 +111,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.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.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.2 // indirect github.com/goccy/go-json v0.10.3 // 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.4 // indirect github.com/lestrrat-go/httprc v1.0.5 // 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.0.19 github.com/lestrrat-go/jwx/v2 v2.1.0
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 +132,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.19.0 golang.org/x/crypto v0.24.0
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.17.0 // indirect golang.org/x/term v0.21.0 // indirect
golang.org/x/tools v0.16.1 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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

52
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-20230701194111-c6b3d482cca6 h1:FTk0ZoaV5N8Tkps5Da5RrDMZZXSHZIuD67Hy1Y4fsos= 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/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw= forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926/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.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/httprc v1.0.5/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.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= github.com/lestrrat-go/jwx/v2 v2.1.0 h1:0zs7Ya6+39qoit7gwAf+cYm1zzgS3fceIdo7RmQ5lkw=
github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= github.com/lestrrat-go/jwx/v2 v2.1.0/go.mod h1:Xpw9QIaUGiIUD1Wx0NcY1sIHwFf8lDuZn/cmxtXYRys=
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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
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=
@ -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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.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.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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

@ -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" validate:"required"` To string `json:"to"`
From []string `json:"from" validate:"required"` From []string `json:"from" validate:"required"`
} }

View File

@ -6,6 +6,7 @@ import (
"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"
@ -155,6 +156,34 @@ 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 metrics 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.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

@ -0,0 +1,300 @@
package proxy_test
import (
"context"
"io"
"log"
"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"
"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) {
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+".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 {
log.Fatal(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)
}
}
layers, err := setup.GetLayers(context.Background(), config.NewDefault())
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

@ -0,0 +1,20 @@
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("Remote-User-Attr-Email", user.attrs.email)
fetch:
url:
user:
username: foo
password: bar

View File

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

View File

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

View File

@ -2,6 +2,7 @@ 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"
@ -52,14 +53,16 @@ func QueryCommand() *cli.Command {
client := client.New(baseFlags.ServerURL, client.WithToken(token)) client := client.New(baseFlags.ServerURL, client.WithToken(token))
proxies, err := client.QueryLayer(ctx.Context, proxyName, options...) layers, 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(proxies)...); err != nil { if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(layers)...); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -67,3 +70,13 @@ 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,6 +13,7 @@ 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"),
}, },
} }
} }
@ -25,6 +26,7 @@ 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(true), flag.ProxyTo(),
flag.ProxyFrom(), flag.ProxyFrom(),
), ),
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {

View File

@ -30,12 +30,11 @@ func ProxyName() cli.Flag {
const KeyProxyTo = "proxy-to" const KeyProxyTo = "proxy-to"
func ProxyTo(required bool) cli.Flag { func ProxyTo() 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,6 +2,7 @@ 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"
@ -51,6 +52,8 @@ 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 {
@ -61,3 +64,13 @@ 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(false), flag.ProxyTo(),
flag.ProxyFrom(), flag.ProxyFrom(),
flag.ProxyEnabled(), flag.ProxyEnabled(),
flag.ProxyWeight(), flag.ProxyWeight(),

View File

@ -12,6 +12,7 @@ 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"),
}, },
} }
} }
@ -25,6 +26,7 @@ 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,12 +35,15 @@ 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 != "" {
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
if err != nil { if err != nil {
return errors.Wrap(err, "could not initialize sentry client") 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 {

View File

@ -30,12 +30,15 @@ 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")
if conf.Proxy.Sentry.DSN != "" {
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion) flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
if err != nil { if err != nil {
return errors.Wrap(err, "could not initialize sentry client") 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 {

View File

@ -5,6 +5,7 @@ type AdminServerConfig struct {
CORS CORSConfig `yaml:"cors"` CORS CORSConfig `yaml:"cors"`
Auth AuthConfig `yaml:"auth"` Auth AuthConfig `yaml:"auth"`
Metrics MetricsConfig `yaml:"metrics"` Metrics MetricsConfig `yaml:"metrics"`
Profiling ProfilingConfig `yaml:"profiling"`
Sentry SentryConfig `yaml:"sentry"` Sentry SentryConfig `yaml:"sentry"`
} }
@ -15,6 +16,7 @@ func NewDefaultAdminServerConfig() AdminServerConfig {
Auth: NewDefaultAuthConfig(), Auth: NewDefaultAuthConfig(),
Metrics: NewDefaultMetricsConfig(), Metrics: NewDefaultMetricsConfig(),
Sentry: NewDefaultSentryConfig(), Sentry: NewDefaultSentryConfig(),
Profiling: NewDefaultProfilingConfig(),
} }
} }

View File

@ -80,9 +80,22 @@ 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 {
data, err := os.ReadFile(f) proxy, err := loadBootstrappedProxyConfig(f)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not read file '%s'", f) return nil, errors.Wrapf(err, "could not load proxy bootstrap file '%s'", f)
}
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
proxies[name] = *proxy
}
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{} proxy := BootstrapProxyConfig{}
@ -91,11 +104,7 @@ func loadBootstrapDir(dir string) (map[store.ProxyName]BootstrapProxyConfig, err
return nil, errors.Wrapf(err, "could not unmarshal proxy") return nil, errors.Wrapf(err, "could not unmarshal proxy")
} }
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))) return &proxy, nil
proxies[name] = proxy
}
return proxies, 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 {

View File

@ -2,7 +2,6 @@ package config
import ( import (
"os" "os"
"regexp"
"strconv" "strconv"
"time" "time"
@ -11,9 +10,6 @@ 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 {
@ -23,12 +19,13 @@ func (is *InterpolatedString) UnmarshalYAML(value *yaml.Node) error {
return errors.WithStack(err) return errors.WithStack(err)
} }
if match := reVar.FindStringSubmatch(str); len(match) > 0 { str, err := envsubst.EvalEnv(str)
*is = InterpolatedString(os.Getenv(match[1])) if err != nil {
} else { return errors.WithStack(err)
*is = InterpolatedString(str)
} }
*is = InterpolatedString(str)
return nil return nil
} }
@ -41,8 +38,9 @@ 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)
} }
if match := reVar.FindStringSubmatch(str); len(match) > 0 { str, err := envsubst.EvalEnv(str)
str = os.Getenv(match[1]) if err != nil {
return errors.WithStack(err)
} }
intVal, err := strconv.ParseInt(str, 10, 32) intVal, err := strconv.ParseInt(str, 10, 32)
@ -64,11 +62,12 @@ 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)
} }
if match := reVar.FindStringSubmatch(str); len(match) > 0 { str, err := envsubst.EvalEnv(str)
str = os.Getenv(match[1]) if err != nil {
return errors.WithStack(err)
} }
floatVal, err := strconv.ParseFloat(str, 10) floatVal, err := strconv.ParseFloat(str, 32)
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)
} }
@ -87,8 +86,9 @@ 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)
} }
if match := reVar.FindStringSubmatch(str); len(match) > 0 { str, err := envsubst.EvalEnv(str)
str = os.Getenv(match[1]) if err != nil {
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,22 +165,15 @@ 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 {
//match := reVar.FindStringSubmatch(value) value, err := envsubst.EvalEnv(value)
re := regexp.MustCompile(`\${(.*?)}`) if err != nil {
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
@ -200,8 +193,9 @@ 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)
} }
if match := reVar.FindStringSubmatch(str); len(match) > 0 { str, err := envsubst.EvalEnv(str)
str = os.Getenv(match[1]) if err != nil {
return errors.WithStack(err)
} }
duration, err := time.ParseDuration(str) duration, err := time.ParseDuration(str)

View File

@ -0,0 +1,15 @@
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,22 +7,28 @@ 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"`
Profiling ProfilingConfig `yaml:"profiling"`
Transport TransportConfig `yaml:"transport"` Transport TransportConfig `yaml:"transport"`
Dial DialConfig `yaml:"dial"` Dial DialConfig `yaml:"dial"`
Sentry SentryConfig `yaml:"sentry"` Sentry SentryConfig `yaml:"sentry"`
Cache CacheConfig `yaml:"cache"` Cache CacheConfig `yaml:"cache"`
Templates TemplatesConfig `yaml:"templates"`
} }
func NewDefaultProxyServerConfig() ProxyServerConfig { func NewDefaultProxyServerConfig() ProxyServerConfig {
return ProxyServerConfig{ return ProxyServerConfig{
Debug: false,
HTTP: NewHTTPConfig("0.0.0.0", 8080), HTTP: NewHTTPConfig("0.0.0.0", 8080),
Metrics: NewDefaultMetricsConfig(), Metrics: NewDefaultMetricsConfig(),
Transport: NewDefaultTransportConfig(), Transport: NewDefaultTransportConfig(),
Dial: NewDefaultDialConfig(), Dial: NewDefaultDialConfig(),
Sentry: NewDefaultSentryConfig(), Sentry: NewDefaultSentryConfig(),
Cache: NewDefaultCacheConfig(), Cache: NewDefaultCacheConfig(),
Templates: NewDefaultTemplatesConfig(),
Profiling: NewDefaultProfilingConfig(),
} }
} }
@ -115,3 +121,13 @@ func NewDefaultCacheConfig() CacheConfig {
TTL: *NewInterpolatedDuration(time.Second * 30), TTL: *NewInterpolatedDuration(time.Second * 30),
} }
} }
type TemplatesConfig struct {
Dir InterpolatedString `yaml:"dir"`
}
func NewDefaultTemplatesConfig() TemplatesConfig {
return TemplatesConfig{
Dir: "./templates",
}
}

View File

@ -28,10 +28,10 @@ func NewDefaultSentryConfig() SentryConfig {
Debug: false, Debug: false,
FlushTimeout: NewInterpolatedDuration(2 * time.Second), FlushTimeout: NewInterpolatedDuration(2 * time.Second),
AttachStacktrace: true, AttachStacktrace: true,
SampleRate: 1, SampleRate: 0.2,
EnableTracing: true, EnableTracing: true,
TracesSampleRate: 0.2, TracesSampleRate: 0.2,
ProfilesSampleRate: 1, ProfilesSampleRate: 0.2,
IgnoreErrors: []string{}, IgnoreErrors: []string{},
SendDefaultPII: false, SendDefaultPII: false,
ServerName: "", ServerName: "",

View File

@ -34,19 +34,6 @@ 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)
} }

View File

@ -36,15 +36,15 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
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()))
var match *store.Proxy layers := make([]*store.Layer, 0)
MAIN:
for _, p := range proxies { for _, p := range proxies {
for _, from := range p.From { for _, from := range p.From {
logger.Debug( logger.Debug(
ctx, "matching request with proxy's from", ctx, "matching request with proxy's from",
logger.F("from", from), logger.F("from", from),
) )
if matches := wildcard.Match(url.String(), from); !matches { if matches := wildcard.Match(url.String(), from); !matches {
continue continue
} }
@ -54,16 +54,26 @@ MAIN:
logger.F("from", from), logger.F("from", from),
) )
match = p ctx = logger.With(ctx,
break MAIN logger.F("proxy", p.Name),
} logger.F("host", r.Host),
logger.F("remoteAddr", r.RemoteAddr),
)
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(p.Name)}).Add(1)
proxyLayers, err := d.getLayers(ctx, p.Name)
if err != nil {
return r, errors.WithStack(err)
} }
if match == nil { layers = append(layers, proxyLayers...)
return r, nil
if p.To == "" {
continue
} }
toURL, err := url.Parse(match.To) toURL, err := url.Parse(p.To)
if err != nil { if err != nil {
return r, errors.WithStack(err) return r, errors.WithStack(err)
} }
@ -72,24 +82,11 @@ MAIN:
r.URL.Scheme = toURL.Scheme r.URL.Scheme = toURL.Scheme
r.URL.Path = toURL.JoinPath(r.URL.Path).Path r.URL.Path = toURL.JoinPath(r.URL.Path).Path
ctx = logger.With(ctx, ctx = withLayers(ctx, layers)
logger.F("proxy", match.Name), r = r.WithContext(ctx)
logger.F("host", r.Host),
logger.F("remoteAddr", r.RemoteAddr),
)
logger.Debug( return r, nil
ctx, "rewritten url", }
logger.F("rewrittenURL", r.URL.String()),
)
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)

View File

@ -74,7 +74,7 @@ func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransforme
} }
} }
func New() *Layer { func New(funcs ...OptionFunc) *Layer {
return &Layer{} return &Layer{}
} }

View File

@ -0,0 +1,16 @@
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

@ -12,9 +12,27 @@ type RequestEnv struct {
Request RequestInfo `expr:"request"` Request RequestInfo `expr:"request"`
} }
type URLEnv struct {
Scheme string `expr:"scheme"`
Opaque string `expr:"opaque"`
User UserInfoEnv `expr:"user"`
Host string `expr:"host"`
Path string `expr:"path"`
RawPath string `expr:"rawPath"`
RawQuery string `expr:"rawQuery"`
Fragment string `expr:"fragment"`
RawFragment string `expr:"rawFragment"`
}
type UserInfoEnv struct {
Username string `expr:"username"`
Password string `expr:"password"`
}
type RequestInfo struct { type RequestInfo struct {
Method string `expr:"method"` Method string `expr:"method"`
URL string `expr:"url"` URL URLEnv `expr:"url"`
RawURL string `expr:"rawUrl"`
Proto string `expr:"proto"` Proto string `expr:"proto"`
ProtoMajor int `expr:"protoMajor"` ProtoMajor int `expr:"protoMajor"`
ProtoMinor int `expr:"protoMinor"` ProtoMinor int `expr:"protoMinor"`
@ -33,10 +51,7 @@ func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error
return nil return nil
} }
engine, err := rule.NewEngine[*RequestEnv]( engine, err := l.getRequestRuleEngine(r, options)
ruleHTTP.WithRequestFuncs(r),
rule.WithRules(options.Rules.Request...),
)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -44,7 +59,24 @@ func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error
env := &RequestEnv{ env := &RequestEnv{
Request: RequestInfo{ Request: RequestInfo{
Method: r.Method, Method: r.Method,
URL: r.URL.String(), URL: URLEnv{
Scheme: r.URL.Scheme,
Opaque: r.URL.Opaque,
User: UserInfoEnv{
Username: r.URL.User.Username(),
Password: func() string {
passwd, _ := r.URL.User.Password()
return passwd
}(),
},
Host: r.URL.Host,
Path: r.URL.Path,
RawPath: r.URL.RawPath,
RawQuery: r.URL.RawQuery,
Fragment: r.URL.Fragment,
RawFragment: r.URL.RawFragment,
},
RawURL: r.URL.String(),
Proto: r.Proto, Proto: r.Proto,
ProtoMajor: r.ProtoMajor, ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor, ProtoMinor: r.ProtoMinor,
@ -65,6 +97,18 @@ func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error
return nil return nil
} }
func (l *Layer) getRequestRuleEngine(r *http.Request, options *LayerOptions) (*rule.Engine[*RequestEnv], error) {
engine, err := rule.NewEngine[*RequestEnv](
rule.WithRules(options.Rules.Request...),
ruleHTTP.WithRequestFuncs(r),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}
type ResponseEnv struct { type ResponseEnv struct {
Request RequestInfo `expr:"request"` Request RequestInfo `expr:"request"`
Response ResponseInfo `expr:"response"` Response ResponseInfo `expr:"response"`
@ -84,15 +128,12 @@ type ResponseInfo struct {
} }
func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) error { func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) error {
rules := options.Rules.Request rules := options.Rules.Response
if len(rules) == 0 { if len(rules) == 0 {
return nil return nil
} }
engine, err := rule.NewEngine[*ResponseEnv]( engine, err := l.getResponseRuleEngine(r, options)
rule.WithRules(options.Rules.Response...),
ruleHTTP.WithResponseFuncs(r),
)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -100,7 +141,24 @@ func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) erro
env := &ResponseEnv{ env := &ResponseEnv{
Request: RequestInfo{ Request: RequestInfo{
Method: r.Request.Method, Method: r.Request.Method,
URL: r.Request.URL.String(), URL: URLEnv{
Scheme: r.Request.URL.Scheme,
Opaque: r.Request.URL.Opaque,
User: UserInfoEnv{
Username: r.Request.URL.User.Username(),
Password: func() string {
passwd, _ := r.Request.URL.User.Password()
return passwd
}(),
},
Host: r.Request.URL.Host,
Path: r.Request.URL.Path,
RawPath: r.Request.URL.RawPath,
RawQuery: r.Request.URL.RawQuery,
Fragment: r.Request.URL.Fragment,
RawFragment: r.Request.URL.RawFragment,
},
RawURL: r.Request.URL.String(),
Proto: r.Request.Proto, Proto: r.Request.Proto,
ProtoMajor: r.Request.ProtoMajor, ProtoMajor: r.Request.ProtoMajor,
ProtoMinor: r.Request.ProtoMinor, ProtoMinor: r.Request.ProtoMinor,
@ -131,3 +189,15 @@ func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) erro
return nil return nil
} }
func (l *Layer) getResponseRuleEngine(r *http.Response, options *LayerOptions) (*rule.Engine[*ResponseEnv], error) {
engine, err := rule.NewEngine[*ResponseEnv](
rule.WithRules(options.Rules.Response...),
ruleHTTP.WithResponseFuncs(r),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}

View File

@ -1,156 +0,0 @@
package proxy_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"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/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"
)
func BenchmarkProxy(b *testing.B) {
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)
layerRepository := redisStore.NewLayerRepository(client)
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 {
b.Logf("[ERROR] %+v", errors.WithStack(err))
}
}))
defer backend.Close()
if err := waitFor(backend.URL, 5*time.Second); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
b.Logf("started backend '%s'", backend.URL)
ctx := context.Background()
proxyName := store.ProxyName(b.Name())
b.Logf("creating proxy '%s'", proxyName)
if err := proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
if _, err := proxyRepository.CreateProxy(ctx, proxyName, backend.URL, "*"); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil {
b.Fatalf("[FATAL] %+v", 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,
),
),
)
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) {
b.Logf("[ERROR] %s", errors.WithStack(err))
}
return reverse
}),
)
server := httptest.NewServer(directorMiddleware(handler))
defer server.Close()
b.Logf("started proxy '%s'", server.URL)
httpClient := server.Client()
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := httpClient.Get(server.URL)
if err != nil {
b.Errorf("could not fetch server 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 - %v", res.Status, string(body))
if err := res.Body.Close(); err != nil {
b.Errorf("could not close response body: %+v", errors.WithStack(err))
}
}
}
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

@ -3,11 +3,15 @@ package proxy
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"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"
@ -17,6 +21,8 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/Masterminds/sprig/v3"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
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"
@ -141,6 +147,34 @@ 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 metrics 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.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())
@ -152,6 +186,7 @@ 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)
@ -180,18 +215,83 @@ 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.errorHandler reverseProxy.ErrorHandler = s.handleError
return reverseProxy return reverseProxy
} }
func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) { func (s *Server) handleDefault(w http.ResponseWriter, r *http.Request) {
err := errors.Errorf("no proxy target found")
logger.Error(r.Context(), "proxy error", logger.E(err))
sentry.CaptureException(err)
s.renderErrorPage(w, r, err, http.StatusBadGateway, http.StatusText(http.StatusBadGateway))
}
func (s *Server) handleError(w http.ResponseWriter, r *http.Request, err error) {
err = errors.WithStack(err) err = errors.WithStack(err)
logger.Error(r.Context(), "proxy error", logger.E(err)) logger.Error(r.Context(), "proxy error", logger.E(err))
sentry.CaptureException(err) sentry.CaptureException(err)
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) s.renderErrorPage(w, r, err, http.StatusBadGateway, http.StatusText(http.StatusBadGateway))
}
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.E(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
}
if err := blockTmpl.Execute(w, templateData); err != nil {
logger.Error(ctx, "could not render proxy page", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
} }
func NewServer(funcs ...OptionFunc) *Server { func NewServer(funcs ...OptionFunc) *Server {

View File

@ -17,9 +17,9 @@ func NewRedisClient(ctx context.Context, conf config.RedisConfig) redis.Universa
} }
func NewProxyRepository(ctx context.Context, client redis.UniversalClient) (store.ProxyRepository, error) { func NewProxyRepository(ctx context.Context, client redis.UniversalClient) (store.ProxyRepository, error) {
return redisStore.NewProxyRepository(client), nil return redisStore.NewProxyRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay), nil
} }
func NewLayerRepository(ctx context.Context, client redis.UniversalClient) (store.LayerRepository, error) { func NewLayerRepository(ctx context.Context, client redis.UniversalClient) (store.LayerRepository, error) {
return redisStore.NewLayerRepository(client), nil return redisStore.NewLayerRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay), nil
} }

View File

@ -13,6 +13,7 @@ type (
type LayerHeader struct { type LayerHeader struct {
Proxy ProxyName `json:"proxy"` Proxy ProxyName `json:"proxy"`
Name LayerName `json:"name"` Name LayerName `json:"name"`
Revision int `json:"revision"`
Type LayerType `json:"type"` Type LayerType `json:"type"`
Weight int `json:"weight"` Weight int `json:"weight"`

View File

@ -8,7 +8,7 @@ type ProxyName Name
type ProxyHeader struct { type ProxyHeader struct {
Name ProxyName `json:"name"` Name ProxyName `json:"name"`
Revision int `json:"revision"`
Weight int `json:"weight"` Weight int `json:"weight"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
} }

View File

@ -3,10 +3,18 @@ package redis
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"math/rand"
"strings" "strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gitlab.com/wpetit/goweb/logger"
)
var (
DefaultTxMaxAttempts = 20
DefaultTxBaseDelay = 100 * time.Millisecond
) )
type jsonWrapper[T any] struct { type jsonWrapper[T any] struct {
@ -65,6 +73,33 @@ func key(parts ...string) string {
return strings.Join(parts, ":") return strings.Join(parts, ":")
} }
func WithRetry(ctx context.Context, client redis.UniversalClient, key string, fn func(ctx context.Context, tx *redis.Tx) error, maxAttempts int, baseDelay time.Duration) error {
var err error
delay := baseDelay
for attempt := 0; attempt < maxAttempts; attempt++ {
if err = WithTx(ctx, client, key, fn); err != nil {
err = errors.WithStack(err)
logger.Debug(ctx, "redis transaction failed", logger.E(err))
if errors.Is(err, redis.TxFailedErr) {
logger.Debug(ctx, "retrying redis transaction", logger.F("attempts", attempt), logger.F("delay", delay))
time.Sleep(delay)
delay = delay*2 + time.Duration(rand.Int63n(int64(baseDelay)))
continue
}
return err
}
return nil
}
return errors.WithStack(redis.TxFailedErr)
}
func WithTx(ctx context.Context, client redis.UniversalClient, key string, fn func(ctx context.Context, tx *redis.Tx) error) error { func WithTx(ctx context.Context, client redis.UniversalClient, key string, fn func(ctx context.Context, tx *redis.Tx) error) error {
txf := func(tx *redis.Tx) error { txf := func(tx *redis.Tx) error {
if err := fn(ctx, tx); err != nil { if err := fn(ctx, tx); err != nil {

View File

@ -10,6 +10,7 @@ import (
type layerHeaderItem struct { type layerHeaderItem struct {
Proxy string `redis:"proxy"` Proxy string `redis:"proxy"`
Name string `redis:"name"` Name string `redis:"name"`
Revision int `redis:"revision"`
Type string `redis:"type"` Type string `redis:"type"`
Weight int `redis:"weight"` Weight int `redis:"weight"`
@ -20,6 +21,7 @@ func (i *layerHeaderItem) ToLayerHeader() (*store.LayerHeader, error) {
layerHeader := &store.LayerHeader{ layerHeader := &store.LayerHeader{
Proxy: store.ProxyName(i.Proxy), Proxy: store.ProxyName(i.Proxy),
Name: store.LayerName(i.Name), Name: store.LayerName(i.Name),
Revision: i.Revision,
Type: store.LayerType(i.Type), Type: store.LayerType(i.Type),
Weight: i.Weight, Weight: i.Weight,
Enabled: i.Enabled, Enabled: i.Enabled,

View File

@ -15,6 +15,8 @@ const (
type LayerRepository struct { type LayerRepository struct {
client redis.UniversalClient client redis.UniversalClient
txMaxAttempts int
txRetryBaseDelay time.Duration
} }
// CreateLayer implements store.LayerRepository // CreateLayer implements store.LayerRepository
@ -28,12 +30,13 @@ func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.Proxy
Name: string(layerName), Name: string(layerName),
Type: string(layerType), Type: string(layerType),
Weight: 0, Weight: 0,
Revision: 0,
Enabled: false, Enabled: false,
}, },
CreatedAt: wrap(now), CreatedAt: wrap(now),
UpdatedAt: wrap(now), UpdatedAt: wrap(now),
Options: wrap(store.LayerOptions{}), Options: wrap(options),
} }
txf := func(tx *redis.Tx) error { txf := func(tx *redis.Tx) error {
@ -57,6 +60,11 @@ func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.Proxy
return errors.WithStack(err) return errors.WithStack(err)
} }
layerItem, err = r.txGetLayerItem(ctx, tx, proxyName, layerName)
if err != nil {
return errors.WithStack(err)
}
return nil return nil
} }
@ -67,16 +75,16 @@ func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.Proxy
return &store.Layer{ return &store.Layer{
LayerHeader: store.LayerHeader{ LayerHeader: store.LayerHeader{
Name: layerName, Name: store.LayerName(layerItem.Name),
Proxy: proxyName, Proxy: store.ProxyName(layerItem.Proxy),
Type: layerType, Type: store.LayerType(layerItem.Type),
Weight: 0, Weight: layerItem.Weight,
Enabled: false, Enabled: layerItem.Enabled,
}, },
CreatedAt: now, CreatedAt: layerItem.CreatedAt.Value(),
UpdatedAt: now, UpdatedAt: layerItem.UpdatedAt.Value(),
Options: store.LayerOptions{}, Options: layerItem.Options.Value(),
}, nil }, nil
} }
@ -96,7 +104,7 @@ func (r *LayerRepository) GetLayer(ctx context.Context, proxyName store.ProxyNam
key := layerKey(proxyName, layerName) key := layerKey(proxyName, layerName)
var layerItem *layerItem var layerItem *layerItem
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { err := WithRetry(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
pItem, err := r.txGetLayerItem(ctx, tx, proxyName, layerName) pItem, err := r.txGetLayerItem(ctx, tx, proxyName, layerName)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -105,7 +113,7 @@ func (r *LayerRepository) GetLayer(ctx context.Context, proxyName store.ProxyNam
layerItem = pItem layerItem = pItem
return nil return nil
}) }, r.txMaxAttempts, r.txRetryBaseDelay)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -197,7 +205,7 @@ func (r *LayerRepository) UpdateLayer(ctx context.Context, proxyName store.Proxy
key := layerKey(proxyName, layerName) key := layerKey(proxyName, layerName)
var layerItem layerItem var layerItem layerItem
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { err := WithRetry(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
item, err := r.txGetLayerItem(ctx, tx, proxyName, layerName) item, err := r.txGetLayerItem(ctx, tx, proxyName, layerName)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -216,6 +224,7 @@ func (r *LayerRepository) UpdateLayer(ctx context.Context, proxyName store.Proxy
} }
item.UpdatedAt = wrap(time.Now().UTC()) item.UpdatedAt = wrap(time.Now().UTC())
item.Revision = item.Revision + 1
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
p.HMSet(ctx, key, item.layerHeaderItem) p.HMSet(ctx, key, item.layerHeaderItem)
@ -230,7 +239,7 @@ func (r *LayerRepository) UpdateLayer(ctx context.Context, proxyName store.Proxy
layerItem = *item layerItem = *item
return nil return nil
}) }, r.txMaxAttempts, r.txRetryBaseDelay)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -243,9 +252,11 @@ func (r *LayerRepository) UpdateLayer(ctx context.Context, proxyName store.Proxy
return layer, nil return layer, nil
} }
func NewLayerRepository(client redis.UniversalClient) *LayerRepository { func NewLayerRepository(client redis.UniversalClient, txMaxAttempts int, txRetryBaseDelay time.Duration) *LayerRepository {
return &LayerRepository{ return &LayerRepository{
client: client, client: client,
txMaxAttempts: txMaxAttempts,
txRetryBaseDelay: txRetryBaseDelay,
} }
} }

View File

@ -7,6 +7,6 @@ import (
) )
func TestLayerRepository(t *testing.T) { func TestLayerRepository(t *testing.T) {
repository := NewLayerRepository(client) repository := NewLayerRepository(client, DefaultTxMaxAttempts, DefaultTxBaseDelay)
testsuite.TestLayerRepository(t, repository) testsuite.TestLayerRepository(t, repository)
} }

View File

@ -9,6 +9,7 @@ import (
type proxyHeaderItem struct { type proxyHeaderItem struct {
Name string `redis:"name"` Name string `redis:"name"`
Revision int `redis:"revision"`
Weight int `redis:"weight"` Weight int `redis:"weight"`
Enabled bool `redis:"enabled"` Enabled bool `redis:"enabled"`
@ -20,6 +21,7 @@ type proxyHeaderItem struct {
func (i *proxyHeaderItem) ToProxyHeader() (*store.ProxyHeader, error) { func (i *proxyHeaderItem) ToProxyHeader() (*store.ProxyHeader, error) {
proxyHeader := &store.ProxyHeader{ proxyHeader := &store.ProxyHeader{
Name: store.ProxyName(i.Name), Name: store.ProxyName(i.Name),
Revision: i.Revision,
Weight: i.Weight, Weight: i.Weight,
Enabled: i.Enabled, Enabled: i.Enabled,
} }

View File

@ -15,6 +15,8 @@ const (
type ProxyRepository struct { type ProxyRepository struct {
client redis.UniversalClient client redis.UniversalClient
txMaxAttempts int
txRetryBaseDelay time.Duration
} }
// GetProxy implements store.ProxyRepository // GetProxy implements store.ProxyRepository
@ -22,7 +24,7 @@ func (r *ProxyRepository) GetProxy(ctx context.Context, name store.ProxyName) (*
key := proxyKey(name) key := proxyKey(name)
var proxyItem *proxyItem var proxyItem *proxyItem
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { err := WithRetry(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
pItem, err := r.txGetProxyItem(ctx, tx, name) pItem, err := r.txGetProxyItem(ctx, tx, name)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -31,7 +33,7 @@ func (r *ProxyRepository) GetProxy(ctx context.Context, name store.ProxyName) (*
proxyItem = pItem proxyItem = pItem
return nil return nil
}) }, r.txMaxAttempts, r.txRetryBaseDelay)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -89,6 +91,7 @@ func (r *ProxyRepository) CreateProxy(ctx context.Context, name store.ProxyName,
CreatedAt: wrap(now), CreatedAt: wrap(now),
UpdatedAt: wrap(now), UpdatedAt: wrap(now),
Weight: 0, Weight: 0,
Revision: 0,
Enabled: false, Enabled: false,
}, },
To: to, To: to,
@ -191,7 +194,7 @@ func (r *ProxyRepository) UpdateProxy(ctx context.Context, name store.ProxyName,
key := proxyKey(name) key := proxyKey(name)
var proxyItem proxyItem var proxyItem proxyItem
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error { err := WithRetry(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
item, err := r.txGetProxyItem(ctx, tx, name) item, err := r.txGetProxyItem(ctx, tx, name)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -214,6 +217,7 @@ func (r *ProxyRepository) UpdateProxy(ctx context.Context, name store.ProxyName,
} }
item.UpdatedAt = wrap(time.Now().UTC()) item.UpdatedAt = wrap(time.Now().UTC())
item.Revision = item.Revision + 1
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error { _, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
p.HMSet(ctx, key, item.proxyHeaderItem) p.HMSet(ctx, key, item.proxyHeaderItem)
@ -228,7 +232,7 @@ func (r *ProxyRepository) UpdateProxy(ctx context.Context, name store.ProxyName,
proxyItem = *item proxyItem = *item
return nil return nil
}) }, r.txMaxAttempts, r.txRetryBaseDelay)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -241,9 +245,11 @@ func (r *ProxyRepository) UpdateProxy(ctx context.Context, name store.ProxyName,
return proxy, nil return proxy, nil
} }
func NewProxyRepository(client redis.UniversalClient) *ProxyRepository { func NewProxyRepository(client redis.UniversalClient, txMaxAttempts int, txRetryBaseDelay time.Duration) *ProxyRepository {
return &ProxyRepository{ return &ProxyRepository{
client: client, client: client,
txMaxAttempts: 20,
txRetryBaseDelay: txRetryBaseDelay,
} }
} }

View File

@ -7,6 +7,6 @@ import (
) )
func TestProxyRepository(t *testing.T) { func TestProxyRepository(t *testing.T) {
repository := NewProxyRepository(client) repository := NewProxyRepository(client, DefaultTxMaxAttempts, DefaultTxBaseDelay)
testsuite.TestProxyRepository(t, repository) testsuite.TestProxyRepository(t, repository)
} }

View File

@ -3,6 +3,7 @@ package testsuite
import ( import (
"context" "context"
"reflect" "reflect"
"sync"
"testing" "testing"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
@ -49,6 +50,10 @@ var layerRepositoryTestCases = []layerRepositoryTestCase{
return errors.Errorf("layer.UpdatedAt should not be zero value") return errors.Errorf("layer.UpdatedAt should not be zero value")
} }
if layer.Revision != 0 {
return errors.Errorf("layer.Revision should be zero")
}
return nil return nil
}, },
}, },
@ -230,6 +235,86 @@ var layerRepositoryTestCases = []layerRepositoryTestCase{
return errors.New("could not find created layer in query results") return errors.New("could not find created layer in query results")
} }
return nil
},
},
{
Name: "Create then update layer",
Do: func(repo store.LayerRepository) error {
ctx := context.Background()
var layerName store.LayerName = "create_then_update_layer"
var proxyName store.ProxyName = store.ProxyName(string(layerName) + "_proxy")
var layerType store.LayerType = "dummy"
var layerOptions store.LayerOptions = store.LayerOptions{}
createdLayer, err := repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err != nil {
return errors.WithStack(err)
}
if e, g := 0, createdLayer.Revision; e != g {
return errors.Errorf("createdLayer.Revision: expected '%v', got '%v'", e, g)
}
updatedLayer, err := repo.UpdateLayer(ctx, proxyName, layerName)
if err != nil {
return errors.Wrap(err, "err should be nil")
}
if e, g := 1, updatedLayer.Revision; e != g {
return errors.Errorf("updatedLayer.Revision: expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Update layer concurrently",
Do: func(repo store.LayerRepository) error {
ctx := context.Background()
var layerName store.LayerName = "update_layer_concurrently"
var proxyName store.ProxyName = store.ProxyName(string(layerName) + "_proxy")
var layerType store.LayerType = "dummy"
var layerOptions store.LayerOptions = store.LayerOptions{}
createdLayer, err := repo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions)
if err != nil {
return errors.WithStack(err)
}
if createdLayer.Revision != 0 {
return errors.Errorf("createdLayer.Revision should be zero")
}
var wg sync.WaitGroup
total := 100
wg.Add(total)
for i := 0; i < total; i++ {
go func(i int) {
defer wg.Done()
if _, err := repo.UpdateLayer(ctx, createdLayer.Proxy, createdLayer.Name); err != nil {
panic(errors.Wrap(err, "err should be nil"))
}
}(i)
}
wg.Wait()
layer, err := repo.GetLayer(ctx, createdLayer.Proxy, createdLayer.Name)
if err != nil {
return errors.Wrap(err, "err should be nil")
}
if e, g := total, layer.Revision; e != g {
return errors.Errorf("layer.Revision: expected '%v', got '%v'", e, g)
}
return nil return nil
}, },
}, },

View File

@ -3,6 +3,7 @@ package testsuite
import ( import (
"context" "context"
"reflect" "reflect"
"sync"
"testing" "testing"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
@ -51,6 +52,10 @@ var proxyRepositoryTestCases = []proxyRepositoryTestCase{
return errors.Errorf("proxy.UpdatedAt should not be zero value") return errors.Errorf("proxy.UpdatedAt should not be zero value")
} }
if proxy.Revision != 0 {
return errors.Errorf("proxy.Revision should be zero")
}
return nil return nil
}, },
}, },
@ -99,6 +104,10 @@ var proxyRepositoryTestCases = []proxyRepositoryTestCase{
return errors.Errorf("foundProxy.UpdatedAt: expected '%v', got '%v'", createdProxy.UpdatedAt, foundProxy.UpdatedAt) return errors.Errorf("foundProxy.UpdatedAt: expected '%v', got '%v'", createdProxy.UpdatedAt, foundProxy.UpdatedAt)
} }
if foundProxy.Revision != 0 {
return errors.Errorf("foundProxy.Revision should be zero")
}
return nil return nil
}, },
}, },
@ -194,6 +203,84 @@ var proxyRepositoryTestCases = []proxyRepositoryTestCase{
return errors.Errorf("err: expected store.ErrAlreadyExists, got '%+v'", err) return errors.Errorf("err: expected store.ErrAlreadyExists, got '%+v'", err)
} }
return nil
},
},
{
Name: "Create then update proxy",
Do: func(repo store.ProxyRepository) error {
ctx := context.Background()
to := "http://example.com"
var name store.ProxyName = "create_then_update_proxy"
createdProxy, err := repo.CreateProxy(ctx, name, to, "127.0.0.1:*", "localhost:*")
if err != nil {
return errors.Wrap(err, "err should be nil")
}
if createdProxy.Revision != 0 {
return errors.Errorf("createdProxy.Revision should be zero")
}
updatedProxy, err := repo.UpdateProxy(ctx, name)
if err != nil {
return errors.Wrap(err, "err should be nil")
}
if e, g := 1, updatedProxy.Revision; e != g {
return errors.Errorf("updatedProxy.Revision: expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Update proxy concurrently",
Do: func(repo store.ProxyRepository) error {
ctx := context.Background()
to := "http://example.com"
var name store.ProxyName = "update_proxy_concurrently"
createdProxy, err := repo.CreateProxy(ctx, name, to, "127.0.0.1:*", "localhost:*")
if err != nil {
return errors.Wrap(err, "err should be nil")
}
if createdProxy.Revision != 0 {
return errors.Errorf("createdProxy.Revision should be zero")
}
var wg sync.WaitGroup
total := 100
wg.Add(total)
for i := 0; i < total; i++ {
go func(i int) {
defer wg.Done()
if _, err := repo.UpdateProxy(ctx, name); err != nil {
panic(errors.Wrap(err, "err should be nil"))
}
}(i)
}
wg.Wait()
proxy, err := repo.GetProxy(ctx, name)
if err != nil {
return errors.Wrap(err, "err should be nil")
}
if e, g := total, proxy.Revision; e != g {
return errors.Errorf("proxy.Revision: expected '%v', got '%v'", e, g)
}
return nil return nil
}, },
}, },

View File

@ -1,49 +0,0 @@
FROM golang:1.20 AS BUILD
RUN apt-get update \
&& apt-get install -y make
ARG YQ_VERSION=4.34.1
RUN mkdir -p /usr/local/bin \
&& wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 \
&& chmod +x /usr/local/bin/yq
COPY . /src
WORKDIR /src
RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
# Patch config
RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /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 '.admin.auth.privateKey = "/etc/bouncer/admin-key.json"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml
FROM alpine:3.18 AS RUNTIME
ARG DUMB_INIT_VERSION=1.2.5
RUN apk add --no-cache ca-certificates
RUN mkdir -p /usr/local/bin \
&& wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 \
&& chmod +x /usr/local/bin/dumb-init
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
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/layers /usr/share/bouncer/layers
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
EXPOSE 8080
EXPOSE 8081
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
CMD ["bouncer"]

View File

@ -49,6 +49,19 @@ admin:
# Mettre à null pour désactiver l'authentification # Mettre à null pour désactiver l'authentification
basicAuth: null basicAuth: null
# Profiling
profiling:
# Activer ou désactiver les endpoints de profiling
enabled: true
# Route de publication des endpoints de profiling
endpoint: /.bouncer/profiling
# Authentification "basic auth" sur les endpoints
# de profiling
# Mettre à null pour désactiver l'authentification
basicAuth:
credentials:
prof: iling
# Configuration de l'intégration Sentry # Configuration de l'intégration Sentry
# Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions # Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
sentry: sentry:
@ -59,7 +72,7 @@ admin:
sampleRate: 1 sampleRate: 1
enableTracing: true enableTracing: true
tracesSampleRate: 0.2 tracesSampleRate: 0.2
profilesSampleRate: 1 profilesSampleRate: 0.2
ignoreErrors: [] ignoreErrors: []
sendDefaultPII: false sendDefaultPII: false
serverName: "" serverName: ""
@ -70,6 +83,12 @@ admin:
# Configuration du service "proxy" # Configuration du service "proxy"
proxy: proxy:
# Activer/désactiver le mode debug (affichage ou non des messages d'erreur)
debug: false
# Configuration des templates utilisés par le proxy
templates:
# Répertoire contenant les templates
dir: /etc/bouncer/templates
http: http:
# Hôte d'écoute du service, # Hôte d'écoute du service,
# 0.0.0.0 pour écouter sur toutes les interfaces # 0.0.0.0 pour écouter sur toutes les interfaces
@ -93,6 +112,19 @@ proxy:
credentials: credentials:
prom: etheus prom: etheus
# Profiling
profiling:
# Activer ou désactiver les endpoints de profiling
enabled: true
# Route de publication des endpoints de profiling
endpoint: /.bouncer/profiling
# Authentification "basic auth" sur les endpoints
# de profiling
# Mettre à null pour désactiver l'authentification
basicAuth:
credentials:
prof: iling
# Configuration de la mise en cache # Configuration de la mise en cache
# locale des données proxy/layers # locale des données proxy/layers
cache: cache:

96
templates/error.gohtml Normal file
View File

@ -0,0 +1,96 @@
{{ define "default" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{{ $title := print .StatusCode " - " .Status }}
<title>{{ $title }}</title>
<style>
html {
box-sizing: border-box;
font-size: 16px;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}
html,
body {
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
background-color: #ffe4e4;
}
#container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
}
#card {
padding: 1.5em 1em;
border: 1px solid #d90202;
background-color: #feb0b0;
border-radius: 5px;
box-shadow: 2px 2px #cccccc1c;
color: #810000 !important;
}
.title {
margin-bottom: 1.2em;
}
p {
margin-bottom: 0.5em;
}
code,
pre {
font-family: monospace;
}
.footer {
font-size: 0.7em;
margin-top: 2em;
text-align: right;
}
</style>
</head>
<body>
<div id="container">
<div id="card">
<h2 class="title">{{ $title }}</h2>
{{ if .Debug }}
<pre>{{ .Err }}</pre>
{{ end }}
<p class="footer">
Propulsé par
<a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.
</p>
</div>
</div>
</body>
</html>
{{ end }}