feat: initial commit
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good

This commit is contained in:
2023-04-24 20:52:12 +02:00
commit e66938f1d3
134 changed files with 8507 additions and 0 deletions

View File

@ -0,0 +1,60 @@
package director
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
)
type contextKey string
const (
contextKeyProxy contextKey = "proxy"
contextKeyLayers contextKey = "layers"
)
var (
errContextKeyNotFound = errors.New("context key not found")
errUnexpectedContextValue = errors.New("unexpected context value")
)
func withProxy(ctx context.Context, proxy *store.Proxy) context.Context {
return context.WithValue(ctx, contextKeyProxy, proxy)
}
func ctxProxy(ctx context.Context) (*store.Proxy, error) {
proxy, err := ctxValue[*store.Proxy](ctx, contextKeyProxy)
if err != nil {
return nil, errors.WithStack(err)
}
return proxy, nil
}
func withLayers(ctx context.Context, layers []*store.Layer) context.Context {
return context.WithValue(ctx, contextKeyLayers, layers)
}
func ctxLayers(ctx context.Context) ([]*store.Layer, error) {
layers, err := ctxValue[[]*store.Layer](ctx, contextKeyLayers)
if err != nil {
return nil, errors.WithStack(err)
}
return layers, nil
}
func ctxValue[T any](ctx context.Context, key contextKey) (T, error) {
raw := ctx.Value(key)
if raw == nil {
return *new(T), errors.WithStack(errContextKeyNotFound)
}
value, ok := raw.(T)
if !ok {
return *new(T), errors.WithStack(errUnexpectedContextValue)
}
return value, nil
}

View File

@ -0,0 +1,231 @@
package director
import (
"context"
"net/http"
"net/url"
"sort"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Director struct {
proxyRepository store.ProxyRepository
layerRepository store.LayerRepository
layerRegistry *LayerRegistry
}
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
ctx := r.Context()
proxies, err := d.getProxies(ctx)
if err != nil {
return r, errors.WithStack(err)
}
var match *store.Proxy
MAIN:
for _, p := range proxies {
for _, from := range p.From {
if matches := wildcard.Match(r.Host, from); !matches {
continue
}
match = p
break MAIN
}
}
if match == nil {
return r, nil
}
toURL, err := url.Parse(match.To)
if err != nil {
return r, errors.WithStack(err)
}
r.URL.Host = toURL.Host
r.URL.Scheme = toURL.Scheme
ctx = logger.With(ctx,
logger.F("proxy", match.Name),
logger.F("host", r.Host),
logger.F("remoteAddr", r.RemoteAddr),
)
ctx = withProxy(ctx, match)
layers, err := d.getLayers(ctx, match.Name)
if err != nil {
return r, errors.WithStack(err)
}
ctx = withLayers(ctx, layers)
r = r.WithContext(ctx)
return r, nil
}
func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true))
if err != nil {
return nil, errors.WithStack(err)
}
sort.Sort(store.ByProxyWeight(headers))
proxies := make([]*store.Proxy, 0, len(headers))
for _, h := range headers {
if !h.Enabled {
continue
}
proxy, err := d.proxyRepository.GetProxy(ctx, h.Name)
if err != nil {
return nil, errors.WithStack(err)
}
proxies = append(proxies, proxy)
}
return proxies, nil
}
func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) {
headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true))
if err != nil {
return nil, errors.WithStack(err)
}
sort.Sort(store.ByLayerWeight(headers))
layers := make([]*store.Layer, 0, len(headers))
for _, h := range headers {
if !h.Enabled {
continue
}
layer, err := d.layerRepository.GetLayer(ctx, proxyName, h.Name)
if err != nil {
return nil, errors.WithStack(err)
}
layers = append(layers, layer)
}
return layers, nil
}
func (d *Director) RequestTransformer() proxy.RequestTransformer {
return func(r *http.Request) {
ctx := r.Context()
layers, err := ctxLayers(ctx)
if err != nil {
if errors.Is(err, errContextKeyNotFound) {
return
}
logger.Error(ctx, "could not retrieve layers from context", logger.E(errors.WithStack(err)))
return
}
for _, layer := range layers {
transformerLayer, ok := d.layerRegistry.GetRequestTransformer(layer.Type)
if !ok {
continue
}
transformer := transformerLayer.RequestTransformer(layer)
transformer(r)
}
}
}
func (d *Director) ResponseTransformer() proxy.ResponseTransformer {
return func(r *http.Response) error {
ctx := r.Request.Context()
layers, err := ctxLayers(ctx)
if err != nil {
if errors.Is(err, errContextKeyNotFound) {
return nil
}
return errors.WithStack(err)
}
for _, layer := range layers {
transformerLayer, ok := d.layerRegistry.GetResponseTransformer(layer.Type)
if !ok {
continue
}
transformer := transformerLayer.ResponseTransformer(layer)
if err := transformer(r); err != nil {
return errors.WithStack(err)
}
}
return nil
}
}
func (d *Director) Middleware() proxy.Middleware {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
r, err := d.rewriteRequest(r)
if err != nil {
logger.Error(r.Context(), "could not rewrite request", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
ctx := r.Context()
layers, err := ctxLayers(ctx)
if err != nil {
if errors.Is(err, errContextKeyNotFound) {
return
}
logger.Error(ctx, "could not retrieve proxy and layers from context", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
httpMiddlewares := make([]proxy.Middleware, 0)
for _, layer := range layers {
middleware, ok := d.layerRegistry.GetMiddleware(layer.Type)
if !ok {
continue
}
httpMiddlewares = append(httpMiddlewares, middleware.Middleware(layer))
}
handler := createMiddlewareChain(next, httpMiddlewares)
handler.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director {
registry := NewLayerRegistry(layers...)
return &Director{proxyRepository, layerRepository, registry}
}

View File

@ -0,0 +1,104 @@
package director
import (
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/store"
)
type Layer interface {
LayerType() store.LayerType
}
type MiddlewareLayer interface {
Layer
Middleware(layer *store.Layer) proxy.Middleware
}
type RequestTransformerLayer interface {
Layer
RequestTransformer(layer *store.Layer) proxy.RequestTransformer
}
type ResponseTransformerLayer interface {
Layer
ResponseTransformer(layer *store.Layer) proxy.ResponseTransformer
}
type LayerRegistry struct {
index map[store.LayerType]Layer
}
func (r *LayerRegistry) GetLayer(layerType store.LayerType) (Layer, bool) {
layer, exists := r.index[layerType]
if !exists {
return nil, false
}
return layer, true
}
func (r *LayerRegistry) getLayerAsAny(layerType store.LayerType) (any, bool) {
return r.GetLayer(layerType)
}
func (r *LayerRegistry) GetMiddleware(layerType store.LayerType) (MiddlewareLayer, bool) {
layer, exists := r.getLayerAsAny(layerType)
if !exists {
return nil, false
}
middleware, ok := layer.(MiddlewareLayer)
if !ok {
return nil, false
}
return middleware, true
}
func (r *LayerRegistry) GetResponseTransformer(layerType store.LayerType) (ResponseTransformerLayer, bool) {
layer, exists := r.getLayerAsAny(layerType)
if !exists {
return nil, false
}
transformer, ok := layer.(ResponseTransformerLayer)
if !ok {
return nil, false
}
return transformer, true
}
func (r *LayerRegistry) GetRequestTransformer(layerType store.LayerType) (RequestTransformerLayer, bool) {
layer, exists := r.getLayerAsAny(layerType)
if !exists {
return nil, false
}
transformer, ok := layer.(RequestTransformerLayer)
if !ok {
return nil, false
}
return transformer, true
}
func (r *LayerRegistry) Load(layers ...Layer) {
index := make(map[store.LayerType]Layer)
for _, l := range layers {
layerType := l.LayerType()
index[layerType] = l
}
r.index = index
}
func NewLayerRegistry(layers ...Layer) *LayerRegistry {
registry := &LayerRegistry{
index: make(map[store.LayerType]Layer),
}
registry.Load(layers...)
return registry
}

View File

@ -0,0 +1,18 @@
package director
import (
"net/http"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/util"
)
func createMiddlewareChain(handler http.Handler, middlewares []proxy.Middleware) http.Handler {
util.Reverse(middlewares)
for _, m := range middlewares {
handler = m(handler)
}
return handler
}

42
internal/proxy/init.go Normal file
View File

@ -0,0 +1,42 @@
package proxy
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors"
)
func (s *Server) initRepositories(ctx context.Context) error {
if err := s.initProxyRepository(ctx); err != nil {
return errors.WithStack(err)
}
if err := s.initLayerRepository(ctx); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Server) initProxyRepository(ctx context.Context) error {
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig)
if err != nil {
return errors.WithStack(err)
}
s.proxyRepository = proxyRepository
return nil
}
func (s *Server) initLayerRepository(ctx context.Context) error {
layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig)
if err != nil {
return errors.WithStack(err)
}
s.layerRepository = layerRepository
return nil
}

40
internal/proxy/option.go Normal file
View File

@ -0,0 +1,40 @@
package proxy
import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
)
type Option struct {
ServerConfig config.ProxyServerConfig
RedisConfig config.RedisConfig
DirectorLayers []director.Layer
}
type OptionFunc func(*Option)
func defaultOption() *Option {
return &Option{
ServerConfig: config.NewDefaultProxyServerConfig(),
RedisConfig: config.NewDefaultRedisConfig(),
DirectorLayers: make([]director.Layer, 0),
}
}
func WithServerConfig(conf config.ProxyServerConfig) OptionFunc {
return func(opt *Option) {
opt.ServerConfig = conf
}
}
func WithRedisConfig(conf config.RedisConfig) OptionFunc {
return func(opt *Option) {
opt.RedisConfig = conf
}
}
func WithDirectorLayers(layers ...director.Layer) OptionFunc {
return func(opt *Option) {
opt.DirectorLayers = layers
}
}

118
internal/proxy/server.go Normal file
View File

@ -0,0 +1,118 @@
package proxy
import (
"context"
"fmt"
"log"
"net"
"net/http"
"forge.cadoles.com/Cadoles/go-proxy"
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Server struct {
serverConfig config.ProxyServerConfig
redisConfig config.RedisConfig
directorLayers []director.Layer
proxyRepository store.ProxyRepository
layerRepository store.LayerRepository
}
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
errs := make(chan error)
addrs := make(chan net.Addr)
go s.run(ctx, addrs, errs)
return addrs, errs
}
func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan error) {
defer func() {
close(errs)
close(addrs)
}()
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
if err := s.initRepositories(ctx); err != nil {
errs <- errors.WithStack(err)
return
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.serverConfig.HTTP.Host, s.serverConfig.HTTP.Port))
if err != nil {
errs <- errors.WithStack(err)
return
}
addrs <- listener.Addr()
defer func() {
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
errs <- errors.WithStack(err)
}
}()
go func() {
<-ctx.Done()
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
log.Printf("%+v", errors.WithStack(err))
}
}()
router := chi.NewRouter()
logger.Info(ctx, "http server listening")
director := director.New(
s.proxyRepository,
s.layerRepository,
s.directorLayers...,
)
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
router.Use(director.Middleware())
handler := proxy.New(
proxy.WithRequestTransformers(
director.RequestTransformer(),
),
proxy.WithResponseTransformers(
director.ResponseTransformer(),
),
)
router.Handle("/*", handler)
if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) {
errs <- errors.WithStack(err)
}
logger.Info(ctx, "http server exiting")
}
func NewServer(funcs ...OptionFunc) *Server {
opt := defaultOption()
for _, fn := range funcs {
fn(opt)
}
return &Server{
serverConfig: opt.ServerConfig,
redisConfig: opt.RedisConfig,
directorLayers: opt.DirectorLayers,
}
}