Compare commits

...

19 Commits

Author SHA1 Message Date
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
8a751afc97 chore: add http debug env in default environment file
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-26 13:53:52 +02:00
4907c0b51f feat: return status 502 on proxy error 2024-06-26 13:53:22 +02:00
1881f27928 refactor: use new rule engine package
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
2024-06-26 13:52:49 +02:00
114608931b Merge pull request 'Layer rewriter: réécriture dynamiques des attributs de requêtes/réponses' (#29) from rewriter-layer into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #29
2024-06-25 17:02:49 +02:00
05b547da48 feat: rewriter layer
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2024-06-25 16:50:29 +02:00
9d902a7494 doc: update create custom layer tutorial
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-25 11:32:54 +02:00
ea68724635 fix: update dummy bootstrap example
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-25 11:25:34 +02:00
1009eb19aa feat: use destination path as prefix when rewritting url
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-24 17:18:31 +02:00
19fda6aa64 feat(authn-oidc): allow overwriting of cookie name
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-05 16:13:45 +02:00
65238f1ff3 feat(authn-oidc): include proxy in cookie name
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-05 16:00:23 +02:00
d4da9cba8d chore: fix goreleaser version
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-06-05 15:54:00 +02:00
d5fed4c2ac feat(authn): add templatized error page
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
ref CNOUS/mse#3907
2024-06-05 15:53:17 +02:00
c7ac331b10 chore: add interface description
Some checks failed
Cadoles/bouncer/pipeline/head There was a failure building this commit
2024-06-05 12:52:01 +02:00
55 changed files with 1361 additions and 279 deletions

View File

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

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

@ -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 \
@ -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

@ -59,7 +59,7 @@ deps: .env
.PHONY: goreleaser .PHONY: goreleaser
goreleaser: deps goreleaser: deps
( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) ) ( set -o allexport && source .env && set +o allexport && curl -sfL https://goreleaser.com/static/run | VERSION=$(GORELEASER_VERSION) GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
.PHONY: start-release .PHONY: start-release
start-release: start-release:

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

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

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

View File

@ -36,16 +36,28 @@ Le comportement des règles par défaut est le suivant:
Interdire l'accès à l'utilisateur. Interdire l'accès à l'utilisateur.
#### `set_header(name string, value string)` ##### `add_header(name string, value string)`
Définir la valeur d'une entête HTTP via son nom `name` et sa valeur `value`. Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`.
#### `del_headers(pattern string)` ##### `set_header(name string, value string)`
Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée.
##### `del_headers(pattern string)`
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`. Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires. Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
##### `set_host(host string)`
Modifier la valeur de l'entête `Host` de la requête.
##### `set_url(url string)`
Modifier l'URL du serveur cible.
### Environnement ### Environnement
Les règles ont accès aux variables suivantes pendant leur exécution. Les règles ont accès aux variables suivantes pendant leur exécution.

View File

@ -0,0 +1,116 @@
# Layer "Rewriter"
## Description
Ce layer permet de modifier dynamiquement certains attributs de requêtes/réponses transitant par le proxy.
## Type
`rewriter`
## Schéma des options
Les options disponibles pour le layer sont décrites via un [schéma JSON](https://json-schema.org/specification). Elles sont documentées dans le [schéma visible ici](../../../../internal/proxy/director/layer/rewriter/layer-options.json).
## Moteur de règles
Les options `rules.request` et `rules.response` permettent de définir des listes de règles utilisant un DSL modifiant de manière dynamique les attributs des requêtes/réponses transitant par le proxy.
Les listes d'instructions sont exécutées séquentiellement.
Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus des fonctionnalités natives du langage, Bouncer ajoute un certain nombre de fonctions spécifiques au contexte d'utilisation.
### Fonctions
#### Communes
##### `add_header(name string, value string)`
Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`.
##### `set_header(name string, value string)`
Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée.
##### `del_headers(pattern string)`
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
#### Requête
##### `set_host(host string)`
Modifier la valeur de l'entête `Host` de la requête.
##### `set_url(url string)`
Modifier l'URL du serveur cible.
#### Réponse
_Pas de fonctions spécifiques._
### Environnement
Les règles ont accès aux variables suivantes pendant leur exécution. **Ces données sont en lecture seule.**
#### Requête
##### `request`
La requête en cours de traitement.
```js
{
method: "string", // Méthode HTTP
host: "string", // Nom d'hôte (`Host`) associé à la requête
url: "string", // URL associée à la requête
proto: "string", // Numéro de version du protocole utilisé
protoMajor: "int", // Numéro de version majeure du protocole utilisé
protoMinor: "int", // Numéro de version mineur du protocole utilisé
header: { // Table associative des entêtes HTTP associés à la requête
"string": ["string"]
},
contentLength: "int", // Taille du corps de la requête
transferEncoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête
trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête
"string": ["string"]
},
remoteAddr: "string", // Adresse du client HTTP à l'origine de la requête
requestUri: "string" // URL "brute" associée à la requêtes (avant opérations d'assainissement, utiliser "url" plutôt)
}
```
#### Réponse
##### `response`
La réponse en cours de traitement.
```js
{
statusCode: "int", // Code de statut de la réponse
status: "string", // Message associé au code de statut
proto: "string", // Numéro de version du protocole utilisé
protoMajor: "int", // Numéro de version majeure du protocole utilisé
protoMinor: "int", // Numéro de version mineur du protocole utilisé
header: { // Table associative des entêtes HTTP associés à la requête
"string": ["string"]
},
contentLength: "int", // Taille du corps de la réponse
transferEncoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête
trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête
"string": ["string"]
},
}
```
##### `request`
_Voir section précédente._
## Métriques
_Pas de métriques spécifiques._

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

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

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.21
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

4
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=

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

@ -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

@ -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

@ -31,6 +31,7 @@ type QueueLayerConfig struct {
} }
type AuthnLayerConfig struct { type AuthnLayerConfig struct {
Debug InterpolatedBool `yaml:"debug"`
TemplateDir InterpolatedString `yaml:"templateDir"` TemplateDir InterpolatedString `yaml:"templateDir"`
OIDC AuthnOIDCLayerConfig `yaml:"oidc"` OIDC AuthnOIDCLayerConfig `yaml:"oidc"`
} }

View File

@ -7,22 +7,26 @@ import (
) )
type ProxyServerConfig struct { type ProxyServerConfig struct {
Debug InterpolatedBool `yaml:"debug"`
HTTP HTTPConfig `yaml:"http"` HTTP HTTPConfig `yaml:"http"`
Metrics MetricsConfig `yaml:"metrics"` Metrics MetricsConfig `yaml:"metrics"`
Transport TransportConfig `yaml:"transport"` 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(),
} }
} }
@ -115,3 +119,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

@ -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,36 +54,39 @@ 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)
} }
r.URL.Host = toURL.Host r.URL.Host = toURL.Host
r.URL.Scheme = toURL.Scheme r.URL.Scheme = toURL.Scheme
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),
)
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(match.Name)}).Add(1) return r, nil
}
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

@ -17,6 +17,7 @@ import (
type Layer struct { type Layer struct {
layerType store.LayerType layerType store.LayerType
auth Authenticator auth Authenticator
debug bool
templateDir string templateDir string
} }
@ -40,8 +41,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not execute pre-auth hook", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not execute pre-auth hook", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -65,8 +67,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not authenticate user", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -77,8 +80,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not apply rules", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not apply rules", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -94,8 +98,9 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err))) err = errors.WithStack(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) logger.Error(ctx, "could not execute post-auth hook", logger.E(err))
l.renderErrorPage(w, r, layer, options, err)
return return
} }
@ -108,12 +113,47 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
} }
} }
func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) { type baseTemplateData struct {
w.WriteHeader(http.StatusForbidden) Layer *store.Layer
l.renderPage(w, r, layer, "forbidden", options.Templates.Forbidden.Block, user) Debug bool
Request *http.Request
} }
func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, page string, block string, user *User) { func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) {
templateData := struct {
baseTemplateData
User *User
}{
baseTemplateData: baseTemplateData{
Layer: layer,
Debug: l.debug,
Request: r,
},
User: user,
}
w.WriteHeader(http.StatusForbidden)
l.renderPage(w, r, "forbidden", options.Templates.Forbidden.Block, templateData)
}
func (l *Layer) renderErrorPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, err error) {
templateData := struct {
baseTemplateData
Err error
}{
baseTemplateData: baseTemplateData{
Layer: layer,
Debug: l.debug,
Request: r,
},
Err: err,
}
w.WriteHeader(http.StatusInternalServerError)
l.renderPage(w, r, "error", options.Templates.Error.Block, templateData)
}
func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, page string, block string, templateData any) {
ctx := r.Context() ctx := r.Context()
pattern := filepath.Join(l.templateDir, page+".gohtml") pattern := filepath.Join(l.templateDir, page+".gohtml")
@ -128,18 +168,8 @@ func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, layer *store.
return return
} }
templateData := struct {
Layer *store.Layer
User *User
}{
Layer: layer,
User: user,
}
w.Header().Add("Cache-Control", "no-cache") w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil { if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil {
logger.Error(ctx, "could not render authn page", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not render authn page", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -160,6 +190,7 @@ func NewLayer(layerType store.LayerType, auth Authenticator, funcs ...OptionFunc
layerType: layerType, layerType: layerType,
auth: auth, auth: auth,
templateDir: opts.TemplateDir, templateDir: opts.TemplateDir,
debug: opts.Debug,
} }
} }

View File

@ -17,6 +17,7 @@ type LayerOptions struct {
type TemplatesOptions struct { type TemplatesOptions struct {
Forbidden TemplateOptions `mapstructure:"forbidden"` Forbidden TemplateOptions `mapstructure:"forbidden"`
Error TemplateOptions `mapstructure:"error"`
} }
type TemplateOptions struct { type TemplateOptions struct {
@ -43,6 +44,9 @@ func DefaultLayerOptions() LayerOptions {
Forbidden: TemplateOptions{ Forbidden: TemplateOptions{
Block: "default", Block: "default",
}, },
Error: TemplateOptions{
Block: "default",
},
}, },
} }

View File

@ -42,7 +42,7 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
return errors.WithStack(err) return errors.WithStack(err)
} }
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name)) sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Proxy, layer.Name))
if err != nil { if err != nil {
logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err)))
} }
@ -121,7 +121,7 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name)) sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Proxy, layer.Name))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -401,8 +401,14 @@ func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*C
return client, nil return client, nil
} }
func (a *Authenticator) getCookieName(cookieName string, layerName store.LayerName) string { const defaultCookieNamePrefix = "_bouncer_authn_oidc"
return fmt.Sprintf("%s_%s", cookieName, layerName)
func (a *Authenticator) getCookieName(cookieName string, proxyName store.ProxyName, layerName store.LayerName) string {
if cookieName != "" {
return cookieName
}
return strings.ToLower(fmt.Sprintf("%s_%s_%s", defaultCookieNamePrefix, proxyName, layerName))
} }
var ( var (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
{
"type": "object",
"properties": {
"rules": {
"title": "Règles appliquées aux requêtes/réponses transitant par le proxy",
"type": "object",
"properties": {
"request": {
"title": "Règles appliquées aux requêtes transitant par le proxy",
"type": "array",
"items": {
"type": "string"
}
},
"response": {
"title": "Règles appliquées aux réponses transitant par le proxy",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"matchURLs": {
"title": "Liste de filtrage des URLs sur lesquelles le layer est actif",
"description": "Par exemple, si vous souhaitez limiter votre layer à l'ensemble d'une section '`/blog`' d'un site, vous pouvez déclarer la valeur `['*/blog*']`. Les autres URLs du site ne seront pas affectées par ce layer.",
"default": [
"*"
],
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,84 @@
package rewriter
import (
"net/http"
proxy "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const LayerType store.LayerType = "rewriter"
type Layer struct{}
func (l *Layer) LayerType() store.LayerType {
return LayerType
}
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
options, err := fromStoreOptions(layer.Options)
if err != nil {
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
if !matches {
next.ServeHTTP(w, r)
return
}
if err := l.applyRequestRules(r, options); err != nil {
logger.Error(ctx, "could not apply request rules", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
// ResponseTransformer implements director.ResponseTransformerLayer.
func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransformer {
return func(r *http.Response) error {
options, err := fromStoreOptions(layer.Options)
if err != nil {
return errors.WithStack(err)
}
matches := wildcard.MatchAny(r.Request.URL.String(), options.MatchURLs...)
if !matches {
return nil
}
if err := l.applyResponseRules(r, options); err != nil {
return errors.WithStack(err)
}
return nil
}
}
func New() *Layer {
return &Layer{}
}
var (
_ director.MiddlewareLayer = &Layer{}
_ director.ResponseTransformerLayer = &Layer{}
)

View File

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

View File

@ -0,0 +1,133 @@
package rewriter
import (
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
"github.com/pkg/errors"
)
type RequestEnv struct {
Request RequestInfo `expr:"request"`
}
type RequestInfo struct {
Method string `expr:"method"`
URL string `expr:"url"`
Proto string `expr:"proto"`
ProtoMajor int `expr:"protoMajor"`
ProtoMinor int `expr:"protoMinor"`
Header map[string][]string `expr:"header"`
ContentLength int64 `expr:"contentLength"`
TransferEncoding []string `expr:"transferEncoding"`
Host string `expr:"host"`
Trailer map[string][]string `expr:"trailer"`
RemoteAddr string `expr:"remoteAddr"`
RequestURI string `expr:"requestUri"`
}
func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error {
rules := options.Rules.Request
if len(rules) == 0 {
return nil
}
engine, err := rule.NewEngine[*RequestEnv](
ruleHTTP.WithRequestFuncs(r),
rule.WithRules(options.Rules.Request...),
)
if err != nil {
return errors.WithStack(err)
}
env := &RequestEnv{
Request: RequestInfo{
Method: r.Method,
URL: r.URL.String(),
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Header: r.Header,
ContentLength: r.ContentLength,
TransferEncoding: r.TransferEncoding,
Host: r.Host,
Trailer: r.Trailer,
RemoteAddr: r.RemoteAddr,
RequestURI: r.RequestURI,
},
}
if _, err := engine.Apply(env); err != nil {
return errors.WithStack(err)
}
return nil
}
type ResponseEnv struct {
Request RequestInfo `expr:"request"`
Response ResponseInfo `expr:"response"`
}
type ResponseInfo struct {
Status string `expr:"status"`
StatusCode int `expr:"statusCode"`
Proto string `expr:"proto"`
ProtoMajor int `expr:"protoMajor"`
ProtoMinor int `expr:"protoMinor"`
Header map[string][]string `expr:"header"`
ContentLength int64 `expr:"contentLength"`
TransferEncoding []string `expr:"transferEncoding"`
Uncompressed bool `expr:"uncompressed"`
Trailer map[string][]string `expr:"trailer"`
}
func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) error {
rules := options.Rules.Request
if len(rules) == 0 {
return nil
}
engine, err := rule.NewEngine[*ResponseEnv](
rule.WithRules(options.Rules.Response...),
ruleHTTP.WithResponseFuncs(r),
)
if err != nil {
return errors.WithStack(err)
}
env := &ResponseEnv{
Request: RequestInfo{
Method: r.Request.Method,
URL: r.Request.URL.String(),
Proto: r.Request.Proto,
ProtoMajor: r.Request.ProtoMajor,
ProtoMinor: r.Request.ProtoMinor,
Header: r.Request.Header,
ContentLength: r.Request.ContentLength,
TransferEncoding: r.Request.TransferEncoding,
Host: r.Request.Host,
Trailer: r.Request.Trailer,
RemoteAddr: r.Request.RemoteAddr,
RequestURI: r.Request.RequestURI,
},
Response: ResponseInfo{
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Header: r.Header,
ContentLength: r.ContentLength,
TransferEncoding: r.TransferEncoding,
Trailer: r.Trailer,
Status: r.Status,
StatusCode: r.StatusCode,
},
}
if _, err := engine.Apply(env); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

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

View File

@ -3,11 +3,14 @@ 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/url" "net/url"
"path/filepath"
"strconv"
"time" "time"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
@ -17,6 +20,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"
@ -152,6 +157,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 +186,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)
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) 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 {

44
internal/rule/engine.go Normal file
View File

@ -0,0 +1,44 @@
package rule
import (
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/pkg/errors"
)
type Engine[E any] struct {
rules []*vm.Program
}
func (e *Engine[E]) Apply(env E) ([]any, error) {
results := make([]any, 0, len(e.rules))
for i, r := range e.rules {
result, err := expr.Run(r, env)
if err != nil {
return nil, errors.Wrapf(err, "could not run rule #%d", i)
}
results = append(results, result)
}
return results, nil
}
func NewEngine[E any](funcs ...OptionFunc) (*Engine[E], error) {
opts := NewOptions(funcs...)
engine := &Engine[E]{
rules: make([]*vm.Program, 0, len(opts.Rules)),
}
for i, r := range opts.Rules {
program, err := expr.Compile(r, opts.Expr...)
if err != nil {
return nil, errors.Wrapf(err, "could not compile rule #%d", i)
}
engine.rules = append(engine.rules, program)
}
return engine, nil
}

View File

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

View File

@ -0,0 +1,122 @@
package http
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"github.com/expr-lang/expr"
"github.com/pkg/errors"
)
func setRequestHostFunc(r *http.Request) expr.Option {
return expr.Function(
"set_host",
func(params ...any) (any, error) {
host := params[0].(string)
r.Host = host
return true, nil
},
new(func(string) bool),
)
}
func setRequestURL(r *http.Request) expr.Option {
return expr.Function(
"set_url",
func(params ...any) (any, error) {
rawURL := params[0].(string)
url, err := url.Parse(rawURL)
if err != nil {
return false, errors.WithStack(err)
}
r.URL = url
return true, nil
},
new(func(string) bool),
)
}
func addRequestHeaderFunc(r *http.Request) expr.Option {
return expr.Function(
"add_header",
func(params ...any) (any, error) {
name := params[0].(string)
rawValue := params[1]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r.Header.Add(name, value)
return true, nil
},
new(func(string, string) bool),
)
}
func setRequestHeaderFunc(r *http.Request) expr.Option {
return expr.Function(
"set_header",
func(params ...any) (any, error) {
name := params[0].(string)
rawValue := params[1]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r.Header.Set(name, value)
return true, nil
},
new(func(string, string) bool),
)
}
func delRequestHeadersFunc(r *http.Request) expr.Option {
return expr.Function(
"del_headers",
func(params ...any) (any, error) {
pattern := params[0].(string)
deleted := false
for key := range r.Header {
if !wildcard.Match(key, pattern) {
continue
}
r.Header.Del(key)
deleted = true
}
return deleted, nil
},
new(func(string) bool),
)
}

View File

@ -0,0 +1,88 @@
package http
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"github.com/expr-lang/expr"
)
func addResponseHeaderFunc(r *http.Response) expr.Option {
return expr.Function(
"add_header",
func(params ...any) (any, error) {
name := params[0].(string)
rawValue := params[1]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r.Header.Add(name, value)
return true, nil
},
new(func(string, string) bool),
)
}
func setResponseHeaderFunc(r *http.Response) expr.Option {
return expr.Function(
"set_header",
func(params ...any) (any, error) {
name := params[0].(string)
rawValue := params[1]
var value string
switch v := rawValue.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", rawValue)
}
r.Header.Set(name, value)
return true, nil
},
new(func(string, string) bool),
)
}
func delResponseHeadersFunc(r *http.Response) expr.Option {
return expr.Function(
"del_headers",
func(params ...any) (any, error) {
pattern := params[0].(string)
deleted := false
for key := range r.Header {
if !wildcard.Match(key, pattern) {
continue
}
r.Header.Del(key)
deleted = true
}
return deleted, nil
},
new(func(string) bool),
)
}

35
internal/rule/options.go Normal file
View File

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

View File

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

View File

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

View File

@ -33,5 +33,9 @@ func setupAuthnOIDCLayer(conf *config.Config) (director.Layer, error) {
store, store,
oidc.WithHTTPTransport(transport), oidc.WithHTTPTransport(transport),
oidc.WithHTTPClientTimeout(time.Duration(*conf.Layers.Authn.OIDC.HTTPClient.Timeout)), oidc.WithHTTPClientTimeout(time.Duration(*conf.Layers.Authn.OIDC.HTTPClient.Timeout)),
oidc.WithAuthnOptions(
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
authn.WithDebug(bool(conf.Layers.Authn.Debug)),
),
), nil ), nil
} }

View File

@ -0,0 +1,15 @@
package setup
import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/rewriter"
)
func init() {
RegisterLayer(rewriter.LayerType, setupRewriterLayer, rewriter.RawLayerOptionsSchema)
}
func setupRewriterLayer(conf *config.Config) (director.Layer, error) {
return rewriter.New(), nil
}

View File

@ -0,0 +1,104 @@
{{ define "default" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Erreur - {{ .Layer.Name }}</title>
<style>
html {
box-sizing: border-box;
font-size: 16px;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}
html,
body {
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
background-color: #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;
}
.footer {
font-size: 0.7em;
margin-top: 2em;
text-align: right;
}
.back-link {
text-align: center;
}
</style>
</head>
<body>
<div id="container">
<div id="card">
<h2 class="title">Une erreur est survenue !</h2>
{{ if .Debug }}
<pre>{{ .Err }}</pre>
{{ end }}
{{/* if a public base url is provided, show navigation link */}}
{{ $oidc := ( index .Layer.Options "oidc" ) }}
{{ $publicBaseURL := ( index $oidc "publicBaseURL" ) }}
{{ if $publicBaseURL }}
<div class="back-link">
<a href="{{ $publicBaseURL }}" title="Retourner sur l'application"
>Retourner sur l'application</a
>
</div>
{{ end }}
<p class="footer">
Propulsé par
<a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.
</p>
</div>
</div>
</body>
</html>
{{ end }}

View File

@ -19,7 +19,8 @@ layers:
# clientId: my-client-id # clientId: my-client-id
# clientSecret: my-client-id # clientSecret: my-client-id
# issuerURL: https://forge.cadoles.com/ # issuerURL: https://forge.cadoles.com/
# postLogoutRedirectURL: http://localhost:8080 # postLogoutRedirectURLs:
# - http://localhost:8080
# scopes: ["profile", "openid", "email"] # scopes: ["profile", "openid", "email"]
# authParams: # authParams:
# acr_values: "eidas2" # acr_values: "eidas2"

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

@ -70,6 +70,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

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 }}