Compare commits

...

15 Commits

Author SHA1 Message Date
5a7062d53e fix: remove log message
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-07-01 11:33:59 -06:00
74409f18e8 feat: update packaged configuration
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-30 17:51:01 -06:00
ab7f64a684 Merge pull request 'Métriques de base Prometheus' (#4) from metrics into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #4
2023-07-01 01:32:10 +02:00
d5cc15de3b chore: run service in debug mode by default
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Cadoles/bouncer/pipeline/pr-develop This commit looks good
2023-06-30 10:26:52 -06:00
56609ec316 feat: add basic prometheus metrics integration
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
2023-06-30 10:26:27 -06:00
5bf391b6bf fix(docker): add layers templates in image
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-29 20:36:11 -06:00
74928fe413 chore: add log message for workdir and configuration loading
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-29 20:16:25 -06:00
ff1d01828d fix: docker image build
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-29 20:14:20 -06:00
851f5d64cc doc: add getting started with sources tutorial 2023-06-29 20:13:21 -06:00
e0d81c061b chore: add png logo
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-29 14:45:47 -06:00
440d467938 fix(doc): bad link
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-26 16:37:55 +02:00
f8d33299b9 fix(doc): typo
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-26 14:25:42 +02:00
6fed6358b2 fix(doc): typo
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
2023-06-26 14:24:34 +02:00
ef869a02ea doc: add 'how to create a custom layer' tutorial
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-24 12:28:16 -06:00
6559d1f594 fix(command,proxy): allow from flag to be optional
Some checks reported errors
Cadoles/bouncer/pipeline/head Something is wrong with the build of this commit
2023-06-24 12:27:29 -06:00
31 changed files with 991 additions and 80 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
/admin-key.json
/config.yml
/tools
/out
/dist
/data
/bin
/.bouncer-token
/.env

View File

@ -1,4 +1,4 @@
FROM golang:1.19 AS BUILD
FROM golang:1.20 AS BUILD
RUN apt-get update \
&& apt-get install -y make
@ -19,12 +19,18 @@ RUN mkdir -p /usr/local/bin \
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1 /app
COPY --from=BUILD /src/config.yml /etc/bouncer/config.yml
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/
RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer \
&& /usr/share/bouncer/bin/bouncer -c '' config dump > /etc/bouncer/config.yml
EXPOSE 8080
EXPOSE 8081
ENTRYPOINT ["/app/bouncer"]
ENV BOUNCER_WORKDIR=/usr/share/bouncer
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"]
CMD ["bouncer"]

View File

@ -5,11 +5,11 @@ GITCHLOG_ARGS ?=
SHELL := /bin/bash
BOUNCER_VERSION ?=
GIT_VERSION := $(shell git describe --always)
GIT_COMMIT := $(shell git rev-parse --short HEAD)
DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_COMMIT)$(if $(shell git diff --stat),-dirty,)
DOCKER_IMAGE_NAME ?= cadoles/bouncer
DOCKER_IMAGE_NAME ?= reg.cadoles.com/cadoles/bouncer
DOCKER_IMAGE_TAG ?= $(FULL_VERSION)
GOTEST_ARGS ?= -short
@ -25,16 +25,6 @@ test: test-go ## Executing tests
test-go: deps
( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... )
test-install-script: tools/bin/bash_unit
tools/bin/bash_unit ./misc/script/test_install.sh
tools/bin/bash_unit:
mkdir -p tools/bin
cd tools/bin && bash <(curl -s https://raw.githubusercontent.com/pgrange/bash_unit/master/install.sh)
lint: ## Lint sources code
golangci-lint run --enable-all $(LINT_ARGS)
build: build-bouncer ## Build artefacts
build-bouncer: deps ## Build executable
@ -83,9 +73,6 @@ finish-release:
git push --all
git push --tags
install-git-hooks:
git config core.hooksPath .githooks
docker-build:
docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) .
@ -106,7 +93,7 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_COMMIT)" \
GITEA_RELEASE_IS_DRAFT="false" \
GITEA_RELEASE_BODY="" \
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \

View File

@ -10,4 +10,11 @@
## Tutoriels
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
### Utilisation
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
### Développement
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-start-with-sources.md)
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)

View File

@ -2,4 +2,4 @@
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
- [Queue](./queue) - File d'attente dynamique
- [Queue](./queue.md) - File d'attente dynamique

View File

@ -24,4 +24,4 @@ Ce layer permet d'ajouter un mécanisme de file d'attente dynamique au proxy ass
### Schéma
Voir le [schéma JSON](../../../../internal/queue/schema/layer-options.json).
Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json).

View File

@ -1,8 +1,8 @@
# Ajouter un calque de type "file d'attente"
# Ajouter un layer de type "file d'attente"
## Étapes
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau calque ("layer") pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau layer pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
```bash
# Création d'un calque nommé 'my-queue' pour le proxy 'cadoles' de type 'queue'
@ -19,7 +19,7 @@
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
```
2. À ce stade, le calque est encore inactif. Définir la capacité de la file d'attente à 1 et activer le calque en utilisant le CLI
2. À ce stade, le layer est encore inactif. Définir la capacité de la file d'attente à 1 et activer le layer en utilisant le CLI:
```bash
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --layer-enabled=true --layer-options '{"capacity": 1}'
@ -43,6 +43,6 @@
3. Le proxy `cadoles` a désormais une file d'attente avec une capacité d'un seul utilisateur. Vous pouvez effectuer le test en ouvrant votre navigateur sur l'adresse `http://<ip_serveur>:8080/` puis en ouvrant une fenêtre de navigation privée sur la même adresse:
- La première fenêtre devrait afficher le site Cadoles;
- La seconde fenêtre devrait afficher le message suivant: `queued (rank: 2, status: 2/1)`.
- La seconde fenêtre devrait afficher une page indiquant qu'on est en file d'attente.
Si vous laissez expirer la "session" de la première fenêtre (environ 1 minute par défaut) et que vous rafraîchissez la seconde, vous devriez avoir une inversion des états.

View File

@ -0,0 +1,446 @@
# Créer son propre layer
Dans ce tutoriel, nous verrons comment implémenter un layer personnalisé qui permettra d'ajouter une authentification de type [`Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) à un proxy.
## Prérequis
Avoir un environnement de développement local fonctionnel. Voir tutoriel ["Démarrer avec les sources"](./getting-started-with-sources.md).
## Étapes
### Préparer la structure de base du nouveau layer
Une implémetation d'un layer se compose majoritairement de 3 éléments:
- Une structure qui implémente une ou plusieurs interfaces (`director.MiddlewareLayer`, `director.RequestTransformerLayer` et/ou `director.ResponseTransformerLayer`);
- 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.
1. Créer le répertoire du `package` Go qui contiendra le code de votre layer. Celui ci s'appelera `basicauth`:
```
mkdir -p internal/proxy/director/layer/basicauth
```
2. Créer la structure de base du layer:
```go
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
package basicauth
import (
proxy "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
)
// On définit le "type" (la chaîne de caractères) qui
// sera associé à notre layer
const LayerType store.LayerType = "basicauth"
// On déclare la structure qui
// servira de socle pour notre layer
type BasicAuth struct{}
// Les deux méthodes suivantes, attachées à
// notre structure BasicAuth, permettent de remplir
// le contrat définit par l'interface
// director.MiddlewareLayer
// LayerType implements director.MiddlewareLayer.
func (*BasicAuth) LayerType() store.LayerType {
return LayerType
}
// C'est dans la méthode "Middleware" qu'on pourra implémenter la
// logique appliquée par notre layer.
// En l'état actuel l'exécution de la méthode provoquera un panic().
// Middleware implements director.MiddlewareLayer.
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
panic("unimplemented")
}
func New() *BasicAuth {
return &BasicAuth{}
}
// Cette déclaration permet de profiter
// des capacités du compilateur pour s'assurer
// que la structure BasicAuth remplie toujours le
// contrat imposé par l'interface director.MiddlewareLayer
var _ director.MiddlewareLayer = &BasicAuth{}
```
3. Créer le schéma JSON qui sera associé aux options possibles pour notre layer:
```json
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
{
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
"title": "BasicAuth layer options",
"type": "object",
"properties": {},
"additionalProperties": false
}
```
4. Puis créer le fichier Go qui embarquera ces données dans notre binaire via le package [`embed`](https://pkg.go.dev/embed):
```go
// Fichier internal/proxy/director/layer/basicauth/layer_options.go
package basicauth
import (
_ "embed"
)
//go:embed layer-options.json
var RawLayerOptionsSchema []byte
```
5. Enfin, créer le fichier d'amorçage pour référencer notre nouveau layer avec Bouncer:
```go
// Fichier internal/setup/basicauth_layer.go
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/basicauth"
)
// On créait une function d'initialisation qui enregistra les éléments suivants auprès de Bouncer:
// - Notre nouveau type de layer
// - Une fonction capable de créer une instance de notre layer
// - Le schéma associé aux options de notre layer
func init() {
RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema)
}
// La fonction de création de notre layer
// reçoit automatiquement la configuration actuelle de Bouncer.
// Une layer plus avancé pourrait être configurable de cette manière
// en créant une nouvelle section de configuration dédiée.
func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) {
return &basicauth.BasicAuth{}, nil
}
```
## 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()`).
1. Vérifier que notre layer est bien référencé en exécutant la commande:
```
./bin/bouncer admin layer create --help
```
La sortie devrait ressembler à:
```
NAME:
bouncer admin layer create - Create layer
USAGE:
bouncer admin layer create [command options] [arguments...]
OPTIONS:
--layer-type LAYER_TYPE Set LAYER_TYPE as layer's type (available: [basicauth queue])
[...]
```
Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`.
2. Créer un proxy puis une instance de notre nouveau layer associée à celui ci:
```bash
# Créer un proxy
./bin/bouncer admin proxy create --proxy-name cadoles --proxy-to https://www.cadoles.com
# Activer celui-ci
./bin/bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
# Ajouter un layer de notre nouveau type à notre proxy
./bin/bouncer admin layer create --proxy-name cadoles --layer-name mybasicauth --layer-type basicauth
# Activer notre nouveau layer
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-enabled=true
```
**Notre layer est actif** ! Cependant il est loin d'être fonctionnel. En effet, si vous faites un:
```
curl -v http://localhost:8080
```
Vous n'aurez guère en retour qu'un:
```
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.1.2
> Accept: */*
>
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server
```
Et également dans la console où s'exécute le service `bouncer-proxy`, vous aurez le message:
```
2023/06/23 18:39:59 http: panic serving 127.0.0.1:59868: unimplemented
```
**Il est temps d'implémenter réellement la logique associée à notre layer !**
> **Note** Vous pouvez désactiver votre layer via le drapeau `--layer-enabled=false` et voir le site Cadoles s'afficher à nouveau !
## Implémenter l'authentification sur notre nouveau layer
Nous allons modifier la méthode `Middleware(layer *store.Layer) proxy.Middleware` attachée à notre structure `BasicAuth`.
1. Modifier le fichier contenant la structure de notre layer de la manière suivante:
```go
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
// [...]
// Middleware implements director.MiddlewareLayer.
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
// La méthode doit retourner un "Middleware" qui est un alias
// pour les fonctions généralement utilisées
// dans les librairies http en Go pour créer
// une fonction d'interception/transformation de requête.
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// On récupère les identifiants "basic auth" transmis (ou non)
// avec la requête
username, password, ok := r.BasicAuth()
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
unauthorized := func() {
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
// de la popup d'authentification dans le navigateur web de l'utilisateur.
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
// On retoure un code d'erreur HTTP 401 (Unauthorized)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
if !ok {
// L'entête Authorization est absente ou ne correspondant
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
// on interrompt le traitement de la requête ici
unauthorized()
return
}
// On vérifie les identifiants associés à la requête
isAuthenticated := authenticate(username, password)
// Si les identifiants sont non reconnus alors
// on interrompt le traitement de la requête
if !isAuthenticated {
unauthorized()
return
}
// L'authentification a réussie ! On passe la main au handler HTTP suivant
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
// La méthode authenticate() prend un couple d'identifiants
// est vérifie en temps constant si ceux ci correspondent à un couple
// d'identifiants attendus.
func authenticate(username, password string) bool {
// On génère une empreinte au format sha256 pour nos identifiants
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))
// On effectue de même avec les identifiants attendus.
// Pour l'instant, on utilise un couple d'identifiants en "dur".
expectedUsernameHash := sha256.Sum256([]byte("foo"))
expectedPasswordHash := sha256.Sum256([]byte("baz"))
// On utilise la méthode subtle.ConstantTimeCompare()
// pour faire la comparaison des identifiants en temps constant
// et ainsi éviter les attaques par timing.
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
// L'utilisateur est authentifié si son nom et son mot de passe
// correspondent avec ceux attendus.
return usernameMatch && passwordMatch
}
```
2. Dans votre navigateur, essayez d'ouvrir l'URL http://127.0.0.1:8080. La popup d'authentification devrait s'afficher et vous devriez pouvoir utiliser les identifiants définis dans la fonction `authenticate()` pour vous authentifier et accéder au site de Cadoles !
> **Note** Essayez de désactiver le layer. L'authentification est automatiquement désactivée également !
## Déclarer des options pour pouvoir utiliser des identifiants dynamiques
En l'état actuel notre layer est fonctionnel. Cependant il souffre d'un problème notable: les identifiants attendus sont statiques et embarqués en dur dans le code. Nous allons utiliser le schéma associé à nos options, jusqu'alors vide, pour pouvoir créer une paire d'identifiants attendus dynamique.
1. Modifier le schéma JSON des options de notre layer:
```json
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
{
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
"title": "BasicAuth layer options",
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
},
"additionalProperties": false
}
```
2. On modifie notre méthode `Middleware()` et la fonction `authenticate()` pour utiliser ces nouvelles options:
```go
package basicauth
import (
"crypto/sha256"
"crypto/subtle"
"net/http"
proxy "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"gitlab.com/wpetit/goweb/logger"
)
const LayerType store.LayerType = "basicauth"
type BasicAuth struct{}
// LayerType implements director.MiddlewareLayer.
func (*BasicAuth) LayerType() store.LayerType {
return LayerType
}
// Middleware implements director.MiddlewareLayer.
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
// La méthode doit retourner un "Middleware" qui est un alias
// pour les fonctions généralement utilisées
// dans les librairies http en Go pour créer
// une fonction d'interception/transformation de requête.
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// On récupère les identifiants "basic auth" transmis (ou non)
// avec la requête
username, password, ok := r.BasicAuth()
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
unauthorized := func() {
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
// de la popup d'authentification dans le navigateur web de l'utilisateur.
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
// On retoure un code d'erreur HTTP 401 (Unauthorized)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
if !ok {
// L'entête Authorization est absente ou ne correspondant
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
// on interrompt le traitement de la requête ici
unauthorized()
return
}
// On extrait les identifiants des options associées à notre layer
expectedUsername, usernameExists := layer.Options["username"].(string)
expectedPassword, passwordExists := layer.Options["password"].(string)
// Si le nom d'utilisateur ou le mot de passe attendu n'existe pas
// alors on retourne une erreur HTTP 500 à l'utilisateur.
if !usernameExists || !passwordExists {
logger.Error(r.Context(), "basicauth layer missing password or username option")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// On vérifie les identifiants associés à la requête
isAuthenticated := authenticate(username, password, expectedUsername, expectedPassword)
// Si les identifiants sont non reconnus alors
// on interrompt le traitement de la requête
if !isAuthenticated {
unauthorized()
return
}
// L'authentification a réussie ! On passe la main au handler HTTP suivant
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func authenticate(username, password string, expectedUsername, expectedPassword string) bool {
// On génère une empreinte au format sha256 pour nos identifiants
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))
// On effectue de même avec les identifiants attendus.
expectedUsernameHash := sha256.Sum256([]byte(expectedUsername))
expectedPasswordHash := sha256.Sum256([]byte(expectedPassword))
// On utilise la méthode subtle.ConstantTimeCompare()
// pour faire la comparaison des identifiants en temps constant
// et ainsi éviter les attaques par timing.
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
// L'utilisateur est authentifié si son nom et son mot de passe
// correspondent avec ceux attendus.
return usernameMatch && passwordMatch
}
func New() *BasicAuth {
return &BasicAuth{}
}
var _ director.MiddlewareLayer = &BasicAuth{}
```
3. Modifiez votre layer via la commande d'administration pour déclarer une paire d'identifiants:
```bash
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-options='{"username":"jdoe","password":"notsosecret"}'
```
4. Essayer d'accéder à l'adresse http://127.0.0.1:8080 avec votre navigateur. La popup d'authentification devrait s'afficher et vous devriez pouvoir vous authentifier avec le nouveau couple d'identifiants définis dans les options de votre layer !
> **Note** Vous pouvez modifier les identifiants plusieurs fois via la commande et vérifier que la fenêtre s'affiche toujours à nouveau, demandant les nouveaux identifiants.

View File

@ -0,0 +1,125 @@
# Démarrer avec les sources
Dans ce tutoriel, nous verrons comment lancer un environnement de développement en local sur notre machine afin de travailler sur les sources de Bouncer.
## Prérequis
Les éléments suivants doivent être installés sur votre machine:
- [Golang > 1.20](https://go.dev/)
- [Docker](https://www.docker.com/)
- [Git](https://git-scm.com/)
- [GNU Make](https://www.gnu.org/software/make/)
Les ports suivants doivent être disponibles sur votre machine:
- `8080`
- `8081`
## Étapes
1. Cloner le dépôt des sources du projet Bouncer
```
git clone https://forge.cadoles.com/Cadoles/bouncer
```
2. Se positionner dans le répertoire du projet
```
cd bouncer
```
3. Lancer le projet en mode "développement"
```
make watch
```
Si toutes les dépendances sont correctement installées et configurées sur votre machine, la console devrait afficher une série de messages pour ensuite s'arrêter sur quelque chose ressemblant à:
```
14:47:06: daemon: make run BOUNCER_CMD="--config config.yml server admin run"
2023-06-23 20:47:06.095 [INFO] <./internal/command/server/admin/run.go:42> RunCommand.func1 listening {"url": "http://127.0.0.1:8081"}
2023-06-23 20:47:06.095 [INFO] <./internal/admin/server.go:126> (*Server).run http server listening
14:47:06: daemon: make run-redis
bouncer-redis
docker run --rm -t \
--name bouncer-redis \
-v /home/wpetit/workspace/bouncer/data/redis:/data \
-p 6379:6379 \
redis:alpine3.17 \
redis-server --save 60 1 --loglevel warning
1:C 23 Jun 2023 20:47:06.754 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 23 Jun 2023 20:47:06.754 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 23 Jun 2023 20:47:06.754 # Configuration loaded
1:M 23 Jun 2023 20:47:06.759 # Warning: Could not create server TCP listening socket ::*:6379: unable to bind socket, errno: 97
1:M 23 Jun 2023 20:47:06.760 # Server initialized
1:M 23 Jun 2023 20:47:06.760 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect
```
À ce stade, le serveur `bouncer-admin` écoute sur http://127.0.0.1:8081 et le serveur `bouncer-proxy` sur http://127.0.0.1:8080.
> **Note**
>
> L'outil [`modd`](https://github.com/cortesi/modd) est utilisé pour surveiller les modifications sur les sources et relancer automatiquement la compilation et les services en cas de changement.
## Commandes `make` utiles
### `make watch`
Surveiller les sources, compiler celles ci en cas de modifications et lancer les services `bouncer-proxy` et `bouncer-admin`.
#### `make test`
Exécuter les tests unitaires/d'intégration du projet.
#### `make build`
Compiler une version de développement du binaire `bouncer`.
#### `make docker-build`
Construire une image Docker pour Bouncer.
Vous pouvez ensuite lancer l'image localement avec la commande:
```
docker run \
-it --rm \
reg.cadoles.com/cadoles/bouncer:<tag> \
-p 8080:8080 \
bouncer server proxy run
```
## Arborescence du projet
```bash
.
├── bin # Répertoire de destination des binaires Go de développement
├── cmd # Package principal (main) du binaire Bouncer
├── data # Répertoire des données de développement (Redis)
├── dist # Répertoire de destination des archives/paquets pour la publication
├── doc # Répertoire de documentation du projet
├── internal # Source Go du projet
│   ├── admin
│   ├── auth
│   ├── chi
│   ├── client
│   ├── command
│   ├── config
│   ├── format
│   ├── imports
│   ├── jwk
│   ├── proxy
│   ├── schema
│   ├── setup
│   └── store
├── layers # Fichiers annexes liés aux layers (templates HTML)
│   └── queue
├── misc # Fichiers annexes
│   ├── jenkins # Fichiers liés au pipeline d'intégration continue Jenkins
│   ├── logo # Logo du projet
│   └── packaging # Fichiers liés à l'empaquetage des binaires
└── tools # Outils utilisés en développement
```

12
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/continuity v0.3.0 // indirect
@ -30,18 +31,24 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
@ -52,6 +59,7 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
@ -88,7 +96,7 @@ require (
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect

21
go.sum
View File

@ -80,6 +80,8 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
@ -230,6 +232,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -309,6 +313,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -347,6 +352,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -379,6 +386,7 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -386,7 +394,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
@ -398,6 +414,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -665,6 +682,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -891,6 +910,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@ -16,6 +16,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gitlab.com/wpetit/goweb/logger"
)
@ -101,6 +102,25 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router.Use(corsMiddleware.Handler)
if s.serverConfig.Metrics.Enabled {
metrics := s.serverConfig.Metrics
logger.Info(ctx, "enabling metrics", logger.F("endpoint", metrics.Endpoint))
router.Group(func(r chi.Router) {
if metrics.BasicAuth != nil {
logger.Info(ctx, "enabling authentication on metrics endpoint")
r.Use(middleware.BasicAuth(
"metrics",
metrics.BasicAuth.CredentialsMap(),
))
}
r.Handle(string(metrics.Endpoint), promhttp.Handler())
})
}
router.Route("/api/v1", func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(auth.Middleware(

View File

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

View File

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

View File

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

View File

@ -18,6 +18,8 @@ func Dump() *cli.Command {
Usage: "Dump the current configuration",
Flags: flags,
Action: func(ctx *cli.Context) error {
logger.SetLevel(logger.LevelError)
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")

View File

@ -50,9 +50,10 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "workdir",
Value: "",
Usage: "The working directory",
Name: "workdir",
Value: "",
EnvVars: []string{"BOUNCER_WORKDIR"},
Usage: "The working directory",
},
&cli.StringFlag{
Name: "projectVersion",

View File

@ -1,16 +1,18 @@
package config
type AdminServerConfig struct {
HTTP HTTPConfig `yaml:"http"`
CORS CORSConfig `yaml:"cors"`
Auth AuthConfig `yaml:"auth"`
HTTP HTTPConfig `yaml:"http"`
CORS CORSConfig `yaml:"cors"`
Auth AuthConfig `yaml:"auth"`
Metrics MetricsConfig `yaml:"metrics"`
}
func NewDefaultAdminServerConfig() AdminServerConfig {
return AdminServerConfig{
HTTP: NewHTTPConfig("127.0.0.1", 8081),
CORS: NewDefaultCORSConfig(),
Auth: NewDefaultAuthConfig(),
HTTP: NewHTTPConfig("127.0.0.1", 8081),
CORS: NewDefaultCORSConfig(),
Auth: NewDefaultAuthConfig(),
Metrics: NewDefaultMetricsConfig(),
}
}

View File

@ -9,7 +9,7 @@ type LoggerConfig struct {
func NewDefaultLoggerConfig() LoggerConfig {
return LoggerConfig{
Level: InterpolatedInt(logger.LevelInfo),
Level: InterpolatedInt(logger.LevelError),
Format: InterpolatedString(logger.FormatHuman),
}
}

View File

@ -0,0 +1,35 @@
package config
import "fmt"
type MetricsConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
Endpoint InterpolatedString `yaml:"endpoint"`
BasicAuth *BasicAuthConfig `yaml:"basicAuth"`
}
type BasicAuthConfig struct {
Credentials *InterpolatedMap `yaml:"credentials"`
}
func (c *BasicAuthConfig) CredentialsMap() map[string]string {
if c.Credentials == nil {
return map[string]string{}
}
credentials := make(map[string]string, len(*c.Credentials))
for k, v := range *c.Credentials {
credentials[k] = fmt.Sprintf("%v", v)
}
return credentials
}
func NewDefaultMetricsConfig() MetricsConfig {
return MetricsConfig{
Enabled: true,
Endpoint: "/.bouncer/metrics",
BasicAuth: nil,
}
}

View File

@ -1,11 +1,13 @@
package config
type ProxyServerConfig struct {
HTTP HTTPConfig `yaml:"http"`
HTTP HTTPConfig `yaml:"http"`
Metrics MetricsConfig `yaml:"metrics"`
}
func NewDefaultProxyServerConfig() ProxyServerConfig {
return ProxyServerConfig{
HTTP: NewHTTPConfig("0.0.0.0", 8080),
HTTP: NewHTTPConfig("0.0.0.0", 8080),
Metrics: NewDefaultMetricsConfig(),
}
}

View File

@ -10,6 +10,7 @@ import (
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"gitlab.com/wpetit/goweb/logger"
)
@ -59,6 +60,8 @@ MAIN:
logger.F("remoteAddr", r.RemoteAddr),
)
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(match.Name)}).Add(1)
ctx = withProxy(ctx, match)
layers, err := d.getLayers(ctx, match.Name)

View File

@ -0,0 +1,73 @@
package queue
import (
"sync"
"time"
"github.com/pkg/errors"
)
type DebouncerMap struct {
debouncers sync.Map
}
func NewDebouncerMap() *DebouncerMap {
return &DebouncerMap{
debouncers: sync.Map{},
}
}
func (m *DebouncerMap) Do(key string, after time.Duration, fn func()) {
newDebouncer := NewDebouncer(after)
rawDebouncer, loaded := m.debouncers.LoadOrStore(key, newDebouncer)
debouncer, ok := rawDebouncer.(*Debouncer)
if !ok {
panic(errors.Errorf("unexpected debouncer value, expected '%T', got '%T'", newDebouncer, rawDebouncer))
}
if loaded {
debouncer.Update(after)
}
debouncer.Do(fn)
}
func NewDebouncer(after time.Duration) *Debouncer {
return &Debouncer{after: after}
}
type Debouncer struct {
mu sync.Mutex
after time.Duration
timer *time.Timer
fn func()
}
func (d *Debouncer) Do(fn func()) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.fn = fn
d.timer = time.AfterFunc(d.after, d.fn)
}
func (d *Debouncer) Update(after time.Duration) {
d.mu.Lock()
defer d.mu.Unlock()
if after == d.after {
return
}
if d.timer != nil {
d.timer.Stop()
}
d.after = after
d.timer = time.AfterFunc(d.after, d.fn)
}

View File

@ -0,0 +1,31 @@
package queue
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
metricNamespace = "bouncer_layer_queue"
metricLabelProxy = "proxy"
metricLabelLayer = "layer"
)
var (
metricQueueSessions = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "sessions",
Help: "Bouncer's queue layer current sessions",
Namespace: metricNamespace,
},
[]string{metricLabelProxy, metricLabelLayer},
)
metricQueueCapacity = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "capacity",
Help: "Bouncer's queue layer capacity",
Namespace: metricNamespace,
},
[]string{metricLabelProxy, metricLabelLayer},
)
)

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"html/template"
"math/rand"
"net/http"
"path/filepath"
"sync"
@ -16,6 +17,7 @@ import (
"github.com/Masterminds/sprig/v3"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"gitlab.com/wpetit/goweb/logger"
)
@ -30,7 +32,9 @@ type Queue struct {
loadOnce sync.Once
tmpl *template.Template
refreshJobRunning uint32
refreshJobRunning uint32
updateMetricsJobRunning uint32
postKeepAliveDebouncer *DebouncerMap
}
// LayerType implements director.MiddlewareLayer
@ -52,6 +56,8 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
return
}
defer q.updateMetrics(ctx, layer.Proxy, layer.Name, options)
cookieName := q.getCookieName(layer.Name)
cookie, err := r.Cookie(cookieName)
@ -72,8 +78,6 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
sessionID := cookie.Value
queueName := string(layer.Name)
q.refreshQueue(queueName, options.KeepAlive)
rank, err := q.adapter.Touch(ctx, queueName, sessionID)
if err != nil {
logger.Error(ctx, "could not retrieve session rank", logger.E(errors.WithStack(err)))
@ -102,6 +106,30 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
}
}
func (q *Queue) updateSessionsMetric(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) {
if !atomic.CompareAndSwapUint32(&q.updateMetricsJobRunning, 0, 1) {
return
}
defer atomic.StoreUint32(&q.updateMetricsJobRunning, 0)
queueName := string(layerName)
status, err := q.adapter.Status(ctx, queueName)
if err != nil {
logger.Error(ctx, "could not retrieve queue status", logger.E(errors.WithStack(err)))
return
}
metricQueueSessions.With(
prometheus.Labels{
metricLabelLayer: string(layerName),
metricLabelProxy: string(proxyName),
},
).Set(float64(status.Sessions))
}
func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueName string, options *LayerOptions, rank int64) {
ctx := r.Context()
@ -135,20 +163,22 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
return
}
refreshRate := time.Duration(int64(options.KeepAlive.Seconds()/2)) * time.Second
templateData := struct {
QueueName string
LayerOptions *LayerOptions
Rank int64
CurrentSessions int64
MaxSessions int64
RefreshRate int64
RefreshRate time.Duration
}{
QueueName: queueName,
LayerOptions: options,
Rank: rank + 1,
CurrentSessions: status.Sessions,
MaxSessions: options.Capacity,
RefreshRate: 5,
RefreshRate: refreshRate,
}
w.WriteHeader(http.StatusServiceUnavailable)
@ -161,24 +191,55 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
}
}
func (q *Queue) refreshQueue(queueName string, keepAlive time.Duration) {
func (q *Queue) refreshQueue(ctx context.Context, layerName store.LayerName, keepAlive time.Duration) {
if !atomic.CompareAndSwapUint32(&q.refreshJobRunning, 0, 1) {
return
}
go func() {
defer atomic.StoreUint32(&q.refreshJobRunning, 0)
defer atomic.StoreUint32(&q.refreshJobRunning, 0)
ctx, cancel := context.WithTimeout(context.Background(), keepAlive*2)
defer cancel()
if err := q.adapter.Refresh(ctx, string(layerName), keepAlive); err != nil {
logger.Error(ctx, "could not refresh queue",
logger.E(errors.WithStack(err)),
logger.F("queue", layerName),
)
}
}
if err := q.adapter.Refresh(ctx, queueName, keepAlive); err != nil {
logger.Error(ctx, "could not refresh queue",
logger.E(errors.WithStack(err)),
logger.F("queue", queueName),
)
}
}()
func (q *Queue) updateMetrics(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) {
// Update queue capacity metric
metricQueueCapacity.With(
prometheus.Labels{
metricLabelLayer: string(layerName),
metricLabelProxy: string(proxyName),
},
).Set(float64(options.Capacity))
// Refresh queue data and metrics
q.refreshQueue(ctx, layerName, options.KeepAlive)
q.updateSessionsMetric(ctx, proxyName, layerName)
// (Re)schedule an update job after session ttl + semi-random time padding
// to update metrics after last session expiration
randDuration := rand.Int63n(int64(options.KeepAlive))
timePadding := options.KeepAlive/2 + time.Duration(randDuration)
after := options.KeepAlive + timePadding
debouncingKey := fmt.Sprintf("%s/%s", proxyName, layerName)
q.postKeepAliveDebouncer.Do(debouncingKey, after, func() {
ctx := logger.With(
context.Background(),
logger.F("proxy", proxyName),
logger.F("layer", layerName),
logger.F("after", after),
)
logger.Info(ctx, "running post keep alive refresh job")
q.refreshQueue(ctx, layerName, options.KeepAlive)
q.updateSessionsMetric(ctx, proxyName, layerName)
})
}
func (q *Queue) getCookieName(layerName store.LayerName) string {
@ -192,9 +253,10 @@ func New(adapter Adapter, funcs ...OptionFunc) *Queue {
}
return &Queue{
adapter: adapter,
templateDir: opts.TemplateDir,
defaultKeepAlive: opts.DefaultKeepAlive,
adapter: adapter,
templateDir: opts.TemplateDir,
defaultKeepAlive: opts.DefaultKeepAlive,
postKeepAliveDebouncer: NewDebouncerMap(),
}
}

View File

@ -0,0 +1,20 @@
package director
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
metricNamespace = "bouncer_proxy_director"
metricLabelProxy = "proxy"
)
var metricProxyRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "proxy_requests_total",
Help: "Bouncer proxy total requests",
Namespace: metricNamespace,
},
[]string{metricLabelProxy},
)

View File

@ -15,6 +15,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gitlab.com/wpetit/goweb/logger"
)
@ -84,18 +85,40 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
)
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
router.Use(director.Middleware())
handler := proxy.New(
proxy.WithRequestTransformers(
director.RequestTransformer(),
),
proxy.WithResponseTransformers(
director.ResponseTransformer(),
),
)
if s.serverConfig.Metrics.Enabled {
metrics := s.serverConfig.Metrics
router.Handle("/*", handler)
logger.Info(ctx, "enabling metrics", logger.F("endpoint", metrics.Endpoint))
router.Group(func(r chi.Router) {
if metrics.BasicAuth != nil {
logger.Info(ctx, "enabling authentication on metrics endpoint")
r.Use(middleware.BasicAuth(
"metrics",
metrics.BasicAuth.CredentialsMap(),
))
}
r.Handle(string(metrics.Endpoint), promhttp.Handler())
})
}
router.Group(func(r chi.Router) {
r.Use(director.Middleware())
handler := proxy.New(
proxy.WithRequestTransformers(
director.RequestTransformer(),
),
proxy.WithResponseTransformers(
director.ResponseTransformer(),
),
)
r.Handle("/*", handler)
})
if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) {
errs <- errors.WithStack(err)

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Veuillez patienter - {{ .QueueName }}</title>
<meta http-equiv="refresh" content="{{ .RefreshRate }}">
<meta http-equiv="refresh" content="{{ .RefreshRate.Seconds }}">
<style>
html {
box-sizing: border-box;

BIN
misc/logo/bouncer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -26,10 +26,26 @@ admin:
- Authorization
- Sentry-Trace
debug: false
# Authentification JWT
auth:
# Origine du jeton JWT
issuer: http://127.0.0.1:8081
# Clé privée permettant de signer les jetons
# JWT générés pour l'usage de l'API d'administration.
privateKey: /etc/bouncer/admin-key.json
# Métriques Prometheus
metrics:
# Activer ou désactiver la publication des métriques
enabled: true
# Route de publication des métriques
endpoint: /.bouncer/metrics
# Authentification "basic auth" sur la page
# de publication
# Mettre à null pour désactiver l'authentification
basicAuth: null
# Configuration du service "proxy"
proxy:
http:
@ -38,6 +54,18 @@ proxy:
host: 0.0.0.0
# Port d'écoute du service
port: 8080
# Métriques Prometheus
metrics:
# Activer ou désactiver la publication des métriques
enabled: true
# Route de publication des métriques
endpoint: /.bouncer/metrics
# Authentification "basic auth" sur la page
# de publication
# Mettre à null pour désactiver l'authentification
basicAuth:
prom: etheus
# Configuration du client Redis
#

View File

@ -8,8 +8,8 @@ layers/**
prep: make build-bouncer
prep: make config.yml
prep: make .bouncer-token
daemon: make run BOUNCER_CMD="--config config.yml server admin run"
daemon: make run BOUNCER_CMD="--config config.yml server proxy run"
daemon: make run BOUNCER_CMD="--debug --config config.yml server admin run"
daemon: make run BOUNCER_CMD="--debug --config config.yml server proxy run"
}
{