Compare commits

...

5 Commits

Author SHA1 Message Date
62c48d2388 feat(dockerfile): updating docker image with newest versions
Some checks failed
Cadoles/bouncer/pipeline/pr-develop There was a failure building this commit
Cadoles/bouncer/pipeline/head This commit looks good
2024-02-05 10:56:34 +01: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
8d91f646c2 feat: refactor layers registration
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-23 17:54:34 -06:00
e32c72e030 feat: command flags cleanup
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2023-06-23 11:08:35 -06:00
28 changed files with 894 additions and 242 deletions

View File

@ -1,4 +1,4 @@
FROM golang:1.19 AS BUILD
FROM reg.cadoles.com/proxy_cache/library/golang:1.21.6 AS BUILD
RUN apt-get update \
&& apt-get install -y make
@ -9,7 +9,7 @@ WORKDIR /src
RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
FROM busybox:latest AS RUNTIME
FROM reg.cadoles.com/proxy_cache/library/busybox:latest AS RUNTIME
ARG DUMB_INIT_VERSION=1.2.5
@ -27,4 +27,4 @@ EXPOSE 8081
ENTRYPOINT ["/app/bouncer"]
CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"]
CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"]

View File

@ -10,4 +10,10 @@
## 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) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)

View File

@ -59,7 +59,7 @@
```bash
# Création du proxy nommé 'cadoles' vers https://www.cadoles.com
bouncer admin proxy create --to https://www.cadoles.com --proxy-name cadoles
bouncer admin proxy create --proxy-to https://www.cadoles.com --proxy-name cadoles
```
Un message équivalent à celui ci devrait s'afficher:
@ -77,7 +77,7 @@
```bash
# Activation du proxy
bouncer admin proxy update --proxy-name cadoles --enabled=true
bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
```
Un message équivalent à celui ci devrait s'afficher:

View File

@ -1,4 +1,4 @@
# Ajouter un calque de type "file d'attente"
# Ajouter un layer de type "file d'attente"
## Étapes
@ -22,7 +22,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
```bash
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --enabled=true --options '{"capacity": 1}'
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --layer-enabled=true --layer-options '{"capacity": 1}'
```
Un message équivalent à celui ci devrait s'afficher:
@ -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,499 @@
# Créer son propre layer
Dans ce tutoriel, nous allons voir comme 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
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/)
## Étapes
### Préparer son environnement de développement
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.
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.
### 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

@ -5,6 +5,7 @@ import (
"sort"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
@ -155,13 +156,22 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
layerName, err := store.ValidateName(createLayerReq.Name)
if err != nil {
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return
}
layer, err := s.layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), store.LayerType(createLayerReq.Type), createLayerReq.Options)
layerType := store.LayerType(createLayerReq.Type)
if !setup.LayerTypeExists(layerType) {
logger.Error(r.Context(), "unknown layer type", logger.E(errors.WithStack(err)), logger.F("layerType", layerType))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return
}
layer, err := s.layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), layerType, createLayerReq.Options)
if err != nil {
if errors.Is(err, store.ErrAlreadyExist) {
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil)
@ -235,7 +245,19 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
}
if updateLayerReq.Options != nil {
if err := schema.ValidateLayerOptions(ctx, layer.Type, updateLayerReq.Options); err != nil {
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
if err != nil {
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
}
rawOptions := func(opts *store.LayerOptions) map[string]any {
return *opts
}(updateLayerReq.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
var invalidDataErr *schema.InvalidDataError

View File

@ -2,27 +2,22 @@ package flag
import (
"encoding/json"
"fmt"
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
const (
FlagLayerName = "layer-name"
FlagLayerType = "layer-type"
FlagLayerOptions = "layer-options"
FlagKeyLayerType = "layer-type"
)
func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
baseFlags := proxyFlag.WithProxyFlags(
&cli.StringFlag{
Name: FlagLayerName,
Usage: "use `LAYER_NAME` as targeted layer",
Value: "",
Required: true,
},
LayerName(),
)
flags = append(flags, baseFlags...)
@ -32,22 +27,63 @@ func WithLayerFlags(flags ...cli.Flag) []cli.Flag {
func WithLayerCreateFlags(flags ...cli.Flag) []cli.Flag {
return WithLayerFlags(
&cli.StringFlag{
Name: FlagLayerType,
Usage: "Set `LAYER_TYPE` as layer's type",
Value: "",
Required: true,
},
&cli.StringFlag{
Name: FlagLayerOptions,
Usage: "Set `LAYER_OPTIONS` as layer's options",
Value: "{}",
},
LayerType(),
LayerOptions(),
)
}
const KeyLayerName = "layer-name"
func LayerName() cli.Flag {
return &cli.StringFlag{
Name: KeyLayerName,
Usage: "use `LAYER_NAME` as targeted layer",
Value: "",
Required: true,
}
}
const KeyLayerType = "layer-type"
func LayerType() cli.Flag {
return &cli.StringFlag{
Name: KeyLayerType,
Usage: fmt.Sprintf("Set `LAYER_TYPE` as layer's type (available: %v)", setup.GetLayerTypes()),
Value: "",
Required: true,
}
}
const KeyLayerOptions = "layer-options"
func LayerOptions() cli.Flag {
return &cli.StringFlag{
Name: KeyLayerOptions,
Usage: "Set `LAYER_OPTIONS` as layer's options",
Value: "{}",
}
}
const KeyLayerWeight = "layer-weight"
func LayerWeight() cli.Flag {
return &cli.IntFlag{
Name: KeyLayerWeight,
Usage: "Set `LAYER_WEIGHT` as layer's weight",
}
}
const KeyLayerEnabled = "layer-enabled"
func LayerEnabled() cli.Flag {
return &cli.BoolFlag{
Name: KeyLayerEnabled,
Usage: "Enable or disable layer",
}
}
func AssertLayerName(ctx *cli.Context) (store.LayerName, error) {
rawLayerName := ctx.String(FlagLayerName)
rawLayerName := ctx.String(KeyLayerName)
name, err := store.ValidateName(rawLayerName)
if err != nil {
@ -58,13 +94,18 @@ func AssertLayerName(ctx *cli.Context) (store.LayerName, error) {
}
func AssertLayerType(ctx *cli.Context) (store.LayerType, error) {
rawLayerType := ctx.String(FlagLayerType)
rawLayerType := ctx.String(FlagKeyLayerType)
return store.LayerType(rawLayerType), nil
layerType := store.LayerType(rawLayerType)
if !setup.LayerTypeExists(layerType) {
return "", errors.Errorf("unknown layer type '%s'", layerType)
}
return layerType, nil
}
func AssertLayerOptions(ctx *cli.Context) (store.LayerOptions, error) {
rawLayerOptions := ctx.String(FlagLayerOptions)
rawLayerOptions := ctx.String(KeyLayerOptions)
layerOptions := store.LayerOptions{}

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/client"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag"
layerFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/layer/flag"
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
"forge.cadoles.com/cadoles/bouncer/internal/format"
@ -20,18 +21,9 @@ func UpdateCommand() *cli.Command {
Name: "update",
Usage: "Update layer",
Flags: layerFlag.WithLayerFlags(
&cli.BoolFlag{
Name: "enabled",
Usage: "Enable or disable proxy",
},
&cli.IntFlag{
Name: "weight",
Usage: "Set `WEIGHT` as proxy's weight",
},
&cli.StringFlag{
Name: "options",
Usage: "Set `OPTIONS` as proxy's options",
},
flag.LayerEnabled(),
flag.LayerWeight(),
flag.LayerOptions(),
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
@ -53,22 +45,22 @@ func UpdateCommand() *cli.Command {
opts := &client.UpdateLayerOptions{}
if ctx.IsSet("options") {
if ctx.IsSet(flag.KeyLayerOptions) {
var options store.LayerOptions
if err := json.Unmarshal([]byte(ctx.String("options")), &options); err != nil {
return errors.Wrap(err, "could not parse options")
if err := json.Unmarshal([]byte(ctx.String(flag.KeyLayerOptions)), &options); err != nil {
return errors.Wrap(err, "could not parse layer's options")
}
opts.Options = &options
}
if ctx.IsSet("weight") {
weight := ctx.Int("weight")
if ctx.IsSet(flag.KeyLayerWeight) {
weight := ctx.Int(flag.KeyLayerWeight)
opts.Weight = &weight
}
if ctx.IsSet("enabled") {
enabled := ctx.Bool("enabled")
if ctx.IsSet(flag.KeyLayerEnabled) {
enabled := ctx.Bool(flag.KeyLayerEnabled)
opts.Enabled = &enabled
}

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/client"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
"forge.cadoles.com/cadoles/bouncer/internal/format"
"github.com/pkg/errors"
@ -18,17 +19,8 @@ func CreateCommand() *cli.Command {
Name: "create",
Usage: "Create proxy",
Flags: proxyFlag.WithProxyFlags(
&cli.StringFlag{
Name: "to",
Usage: "Set `TO` as proxy's destination url",
Value: "",
Required: true,
},
&cli.StringSliceFlag{
Name: "from",
Usage: "Set `FROM` as proxy's patterns to match incoming requests",
Value: cli.NewStringSlice("*"),
},
flag.ProxyTo(true),
flag.ProxyFrom(),
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
@ -43,12 +35,12 @@ func CreateCommand() *cli.Command {
return errors.Wrap(err, "'to' parameter should be a valid url")
}
to, err := url.Parse(ctx.String("to"))
to, err := url.Parse(ctx.String(flag.KeyProxyTo))
if err != nil {
return errors.Wrap(err, "'to' parameter should be a valid url")
}
from := ctx.StringSlice("from")
from := ctx.StringSlice(flag.KeyProxyFrom)
client := client.New(baseFlags.ServerURL, client.WithToken(token))

View File

@ -7,16 +7,9 @@ import (
"github.com/urfave/cli/v2"
)
const FlagProxyName = "proxy-name"
func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
baseFlags := clientFlag.ComposeFlags(
&cli.StringFlag{
Name: FlagProxyName,
Usage: "use `PROXY_NAME` as targeted proxy",
Value: "",
Required: true,
},
ProxyName(),
)
flags = append(flags, baseFlags...)
@ -24,8 +17,58 @@ func WithProxyFlags(flags ...cli.Flag) []cli.Flag {
return flags
}
const KeyProxyName = "proxy-name"
func ProxyName() cli.Flag {
return &cli.StringFlag{
Name: KeyProxyName,
Usage: "use `PROXY_NAME` as targeted proxy",
Value: "",
Required: true,
}
}
const KeyProxyTo = "proxy-to"
func ProxyTo(required bool) cli.Flag {
return &cli.StringFlag{
Name: KeyProxyTo,
Usage: "Set `PROXY_TO` as proxy's destination url",
Value: "",
Required: required,
}
}
const KeyProxyFrom = "proxy-from"
func ProxyFrom() cli.Flag {
return &cli.StringSliceFlag{
Name: KeyProxyFrom,
Usage: "Set `PROXY_FROM` as proxy's patterns to match incoming requests",
Value: cli.NewStringSlice("*"),
}
}
const KeyProxyWeight = "proxy-weight"
func ProxyWeight() cli.Flag {
return &cli.IntFlag{
Name: KeyProxyWeight,
Usage: "Set `PROXY_WEIGHT` as proxy's weight",
}
}
const KeyProxyEnabled = "proxy-enabled"
func ProxyEnabled() cli.Flag {
return &cli.BoolFlag{
Name: KeyProxyEnabled,
Usage: "Enable or disable proxy",
}
}
func AssertProxyName(ctx *cli.Context) (store.ProxyName, error) {
rawProxyName := ctx.String(FlagProxyName)
rawProxyName := ctx.String(KeyProxyName)
name, err := store.ValidateName(rawProxyName)
if err != nil {

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/client"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr"
clientFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/flag"
"forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
proxyFlag "forge.cadoles.com/cadoles/bouncer/internal/command/admin/proxy/flag"
"forge.cadoles.com/cadoles/bouncer/internal/format"
"github.com/pkg/errors"
@ -18,22 +19,10 @@ func UpdateCommand() *cli.Command {
Name: "update",
Usage: "Update proxy",
Flags: proxyFlag.WithProxyFlags(
&cli.StringFlag{
Name: "to",
Usage: "Set `TO` as proxy's destination url",
},
&cli.StringSliceFlag{
Name: "from",
Usage: "Set `FROM` as proxy's patterns to match incoming requests",
},
&cli.BoolFlag{
Name: "enabled",
Usage: "Enable or disable proxy",
},
&cli.IntFlag{
Name: "weight",
Usage: "Set `WEIGHT` as proxy's weight",
},
flag.ProxyTo(false),
flag.ProxyFrom(),
flag.ProxyEnabled(),
flag.ProxyWeight(),
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
@ -50,27 +39,29 @@ func UpdateCommand() *cli.Command {
opts := &client.UpdateProxyOptions{}
if ctx.IsSet("to") {
to := ctx.String("to")
if ctx.IsSet(flag.KeyProxyTo) {
to := ctx.String(flag.KeyProxyTo)
if _, err := url.Parse(to); err != nil {
return errors.Wrap(err, "'to' parameter should be a valid url")
return errors.Wrapf(err, "'%s' parameter should be a valid url", flag.KeyProxyTo)
}
opts.To = &to
}
from := ctx.StringSlice("from")
if from != nil {
opts.From = from
if ctx.IsSet(flag.KeyProxyFrom) {
from := ctx.StringSlice(flag.KeyProxyFrom)
if from != nil {
opts.From = from
}
}
if ctx.IsSet("weight") {
weight := ctx.Int("weight")
if ctx.IsSet(flag.KeyProxyWeight) {
weight := ctx.Int(flag.KeyProxyWeight)
opts.Weight = &weight
}
if ctx.IsSet("enabled") {
enabled := ctx.Bool("enabled")
if ctx.IsSet(flag.KeyProxyEnabled) {
enabled := ctx.Bool(flag.KeyProxyEnabled)
opts.Enabled = &enabled
}

View File

@ -1,16 +1,11 @@
package proxy
import (
"context"
"fmt"
"strings"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/queue"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
@ -33,7 +28,7 @@ func RunCommand() *cli.Command {
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
layers, err := initDirectorLayers(ctx.Context, conf)
layers, err := setup.GetLayers(ctx.Context, conf)
if err != nil {
return errors.Wrap(err, "could not initialize director layers")
}
@ -64,36 +59,3 @@ func RunCommand() *cli.Command {
},
}
}
func initDirectorLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
layers := make([]director.Layer, 0)
queue, err := initQueueLayer(ctx, conf)
if err != nil {
return nil, errors.Wrap(err, "could not initialize queue layer")
}
layers = append(layers, queue)
return layers, nil
}
func initQueueLayer(ctx context.Context, conf *config.Config) (*queue.Queue, error) {
adapter, err := setup.NewQueueAdapter(ctx, conf.Redis)
if err != nil {
return nil, errors.WithStack(err)
}
options := []queue.OptionFunc{
queue.WithTemplateDir(string(conf.Layers.Queue.TemplateDir)),
}
if conf.Layers.Queue.DefaultKeepAlive != nil {
options = append(options, queue.WithDefaultKeepAlive(time.Duration(*conf.Layers.Queue.DefaultKeepAlive)))
}
return queue.New(
adapter,
options...,
), nil
}

View File

@ -6,7 +6,7 @@ import (
"strings"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/queue"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)

View File

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

View File

@ -1,20 +0,0 @@
package queue
import (
_ "embed"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"github.com/pkg/errors"
)
//go:embed schema/layer-options.json
var rawLayerOptionsSchema []byte
func init() {
layerOptionsSchema, err := schema.Parse(rawLayerOptionsSchema)
if err != nil {
panic(errors.Wrap(err, "could not parse queue layer options schema"))
}
schema.RegisterLayerOptionsSchema(LayerType, layerOptionsSchema)
}

View File

@ -7,8 +7,10 @@ import (
"github.com/qri-io/jsonschema"
)
func Parse(data []byte) (*jsonschema.Schema, error) {
var schema jsonschema.Schema
type Schema = jsonschema.Schema
func Parse(data []byte) (*Schema, error) {
var schema Schema
if err := json.Unmarshal(data, &schema); err != nil {
return nil, errors.WithStack(err)
}

View File

@ -1,56 +0,0 @@
package schema
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
)
var defaultRegistry = NewRegistry()
func RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) {
defaultRegistry.RegisterLayerOptionsSchema(layerType, schema)
}
func ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error {
if err := defaultRegistry.ValidateLayerOptions(ctx, layerType, options); err != nil {
return errors.WithStack(err)
}
return nil
}
type Registry struct {
layerOptionSchemas map[store.LayerType]*jsonschema.Schema
}
func (r *Registry) RegisterLayerOptionsSchema(layerType store.LayerType, schema *jsonschema.Schema) {
r.layerOptionSchemas[layerType] = schema
}
func (r *Registry) ValidateLayerOptions(ctx context.Context, layerType store.LayerType, options *store.LayerOptions) error {
schema, exists := r.layerOptionSchemas[layerType]
if !exists {
return errors.WithStack(ErrSchemaNotFound)
}
rawOptions := func(opts *store.LayerOptions) map[string]any {
return *opts
}(options)
state := schema.Validate(ctx, rawOptions)
if len(*state.Errs) > 0 {
return errors.WithStack(NewInvalidDataError(*state.Errs...))
}
return nil
}
func NewRegistry() *Registry {
return &Registry{
layerOptionSchemas: make(map[store.LayerType]*jsonschema.Schema),
}
}

View File

@ -0,0 +1,17 @@
package schema
import (
"context"
"github.com/pkg/errors"
)
func Validate(ctx context.Context, schema *Schema, data map[string]any) error {
state := schema.Validate(ctx, data)
if len(*state.Errs) > 0 {
return errors.WithStack(NewInvalidDataError(*state.Errs...))
}
return nil
}

View File

@ -0,0 +1,43 @@
package setup
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
)
var defaultRegistry = NewRegistry()
func RegisterLayer(layerType store.LayerType, setupFunc LayerSetupFunc, rawOptionsSchema []byte) {
defaultRegistry.RegisterLayer(layerType, setupFunc, rawOptionsSchema)
}
func GetLayerOptionsSchema(layerType store.LayerType) (*jsonschema.Schema, error) {
schema, err := defaultRegistry.GetLayerOptionsSchema(layerType)
if err != nil {
return nil, errors.WithStack(err)
}
return schema, nil
}
func GetLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
layers, err := defaultRegistry.GetLayers(ctx, conf)
if err != nil {
return nil, errors.WithStack(err)
}
return layers, nil
}
func GetLayerTypes() []store.LayerType {
return defaultRegistry.GetLayerTypes()
}
func LayerTypeExists(layerType store.LayerType) bool {
return defaultRegistry.LayerTypeExists(layerType)
}

5
internal/setup/error.go Normal file
View File

@ -0,0 +1,5 @@
package setup
import "errors"
var ErrNotFound = errors.New("not found")

View File

@ -0,0 +1,45 @@
package setup
import (
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue"
queueRedis "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue/redis"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
func init() {
RegisterLayer(queue.LayerType, setupQueueLayer, queue.RawLayerOptionsSchema)
}
func setupQueueLayer(conf *config.Config) (director.Layer, error) {
adapter, err := newQueueAdapter(conf.Redis)
if err != nil {
return nil, errors.WithStack(err)
}
options := []queue.OptionFunc{
queue.WithTemplateDir(string(conf.Layers.Queue.TemplateDir)),
}
if conf.Layers.Queue.DefaultKeepAlive != nil {
options = append(options, queue.WithDefaultKeepAlive(time.Duration(*conf.Layers.Queue.DefaultKeepAlive)))
}
return queue.New(
adapter,
options...,
), nil
}
func newQueueAdapter(redisConf config.RedisConfig) (queue.Adapter, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: redisConf.Adresses,
MasterName: string(redisConf.Master),
})
return queueRedis.NewAdapter(rdb, 2), nil
}

View File

@ -1,20 +0,0 @@
package setup
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/queue"
"github.com/redis/go-redis/v9"
queueRedis "forge.cadoles.com/cadoles/bouncer/internal/queue/redis"
)
func NewQueueAdapter(ctx context.Context, redisConf config.RedisConfig) (queue.Adapter, error) {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: redisConf.Adresses,
MasterName: string(redisConf.Master),
})
return queueRedis.NewAdapter(rdb, 2), nil
}

View File

@ -0,0 +1,80 @@
package setup
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
)
type layerEntry struct {
setup LayerSetupFunc
rawOptionsSchema []byte
}
type Registry struct {
layers map[store.LayerType]layerEntry
}
type LayerSetupFunc func(*config.Config) (director.Layer, error)
func (r *Registry) RegisterLayer(layerType store.LayerType, layerSetup LayerSetupFunc, rawOptionsSchema []byte) {
r.layers[layerType] = layerEntry{
setup: layerSetup,
rawOptionsSchema: rawOptionsSchema,
}
}
func (r *Registry) GetLayerOptionsSchema(layerType store.LayerType) (*schema.Schema, error) {
layerEntry, exists := r.layers[layerType]
if !exists {
return nil, errors.WithStack(ErrNotFound)
}
schema, err := schema.Parse(layerEntry.rawOptionsSchema)
if err != nil {
return nil, errors.WithStack(err)
}
return schema, nil
}
func (r *Registry) GetLayers(ctx context.Context, conf *config.Config) ([]director.Layer, error) {
layers := make([]director.Layer, 0, len(r.layers))
for layerType, layerEntry := range r.layers {
layer, err := layerEntry.setup(conf)
if err != nil {
return nil, errors.Wrapf(err, "could not create layer '%s'", layerType)
}
layers = append(layers, layer)
}
return layers, nil
}
func (r *Registry) LayerTypeExists(layerType store.LayerType) bool {
_, exists := r.layers[layerType]
return exists
}
func (r *Registry) GetLayerTypes() []store.LayerType {
layerTypes := make([]store.LayerType, 0, len(r.layers))
for layerType := range r.layers {
layerTypes = append(layerTypes, layerType)
}
return layerTypes
}
func NewRegistry() *Registry {
return &Registry{
layers: make(map[store.LayerType]layerEntry),
}
}