Compare commits
10 Commits
change-con
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ebc95b9d8 | |||
| c5c6058c66 | |||
|
|
cd5be4ee7a | ||
|
|
d4d7d3e067 | ||
|
|
ea8f84f0d7 | ||
|
|
e90c211d0f | ||
|
|
f4ca559a26 | ||
|
|
28d9c58a66 | ||
|
|
572e1177ef | ||
|
|
66f2811fff |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
*.crt
|
||||
*.key
|
||||
bin/
|
||||
data.json
|
||||
data.json
|
||||
*.json
|
||||
config.yaml
|
||||
|
||||
|
||||
66
README.md
66
README.md
@@ -4,10 +4,68 @@ Reverse Proxy for routing subdomains to different ports on same host machine
|
||||
|
||||
## Configuration
|
||||
```csv
|
||||
test.pablu.de;8181
|
||||
manga.pablu.de;8282
|
||||
pablu.de;8080
|
||||
<Url>;<local Port>
|
||||
server:
|
||||
port: 80
|
||||
ssl:
|
||||
enabled: false
|
||||
certFile: server.crt
|
||||
keyFile: server.key
|
||||
acme:
|
||||
enabled: false
|
||||
email: user@host.dev
|
||||
keyFile: userKey.key
|
||||
caDirUrl: https://localhost:14000/dir
|
||||
tlsAlpn01Port: 5001
|
||||
http01Port: 5002
|
||||
renewTime: 30s
|
||||
|
||||
logging:
|
||||
level: info
|
||||
# Pretty print for human consumption otherwise json
|
||||
pretty: true
|
||||
# Log incoming requests
|
||||
requests: true
|
||||
# Log to file aswell as stderr
|
||||
file:
|
||||
enabled: false
|
||||
maxAge: 14
|
||||
maxBackups: 10
|
||||
path: ~/logs/router
|
||||
|
||||
rateLimit:
|
||||
enabled: false
|
||||
# How many requests per ip adress are allowed
|
||||
bucketSize: 50
|
||||
# How many requests per ip address are refilled
|
||||
refillSize: 50
|
||||
# How often requests per ip address are refilled
|
||||
refillTime: 30s
|
||||
# How often Ip Addresses get cleaned up (only ip addresses with max allowed requests are cleaned up)
|
||||
cleanupTime: 45s
|
||||
|
||||
# Experimental
|
||||
metrics:
|
||||
enabled: false
|
||||
flushInterval: 1h
|
||||
bufferSize: 512
|
||||
file: ~/metrics.json
|
||||
|
||||
hosts:
|
||||
- remotes:
|
||||
- 127.0.0.1
|
||||
port: 3000
|
||||
domains:
|
||||
- api.hitstar.xyz
|
||||
|
||||
- remotes:
|
||||
- 127.0.0.1
|
||||
port: 5173
|
||||
domains:
|
||||
- localhost
|
||||
- hitstar.xyz
|
||||
# api.hitstar.xyz must be declared before rewrite can be used
|
||||
rewrite:
|
||||
"/api": api.hitstar.xyz
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
41
acme/acme.go
41
acme/acme.go
@@ -19,7 +19,8 @@ import (
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
domainrouter "github.com/pablu23/domain-router"
|
||||
domainrouter "git.pablu.de/pablu/domain-router"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Acme struct {
|
||||
@@ -32,7 +33,8 @@ type Acme struct {
|
||||
}
|
||||
|
||||
type CertDomainStorage struct {
|
||||
Domains map[string]time.Time
|
||||
IsUserRegistered bool
|
||||
Domains map[string]time.Time
|
||||
}
|
||||
|
||||
func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
@@ -93,12 +95,6 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Registration = reg
|
||||
|
||||
domains := make([]string, 0)
|
||||
for _, host := range config.Hosts {
|
||||
domains = append(domains, host.Domains...)
|
||||
@@ -113,6 +109,7 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
renewTicker: time.NewTicker(d),
|
||||
}
|
||||
|
||||
isUserRegistered := false
|
||||
_, err = os.Stat("data.json")
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
@@ -127,6 +124,7 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isUserRegistered = data.IsUserRegistered
|
||||
|
||||
mustRenew := false
|
||||
for _, domain := range domains {
|
||||
@@ -135,6 +133,9 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
mustRenew = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
mustRenew = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +144,24 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if !isUserRegistered {
|
||||
log.Debug().Str("user", user.Email).Msg("Registering new User")
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.Registration = reg
|
||||
} else {
|
||||
log.Debug().Str("user", user.Email).Msg("Resolving registration by Key")
|
||||
reg, err := client.Registration.ResolveAccountByKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.Registration = reg
|
||||
}
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: true,
|
||||
@@ -169,8 +188,10 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
dataDomains[domain] = now
|
||||
}
|
||||
|
||||
// User registration is hella scuffed
|
||||
data := CertDomainStorage{
|
||||
Domains: dataDomains,
|
||||
Domains: dataDomains,
|
||||
IsUserRegistered: true,
|
||||
}
|
||||
|
||||
file, err := os.Create("data.json")
|
||||
@@ -187,11 +208,11 @@ func SetupAcme(config *domainrouter.Config) (*Acme, error) {
|
||||
}
|
||||
|
||||
func (a *Acme) RenewAcme() error {
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: a.domains,
|
||||
Bundle: true,
|
||||
}
|
||||
|
||||
certificates, err := a.client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
domainrouter "github.com/pablu23/domain-router"
|
||||
"github.com/pablu23/domain-router/acme"
|
||||
"github.com/pablu23/domain-router/middleware"
|
||||
domainrouter "git.pablu.de/pablu/domain-router"
|
||||
"git.pablu.de/pablu/domain-router/acme"
|
||||
"git.pablu.de/pablu/domain-router/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
@@ -37,17 +42,38 @@ func main() {
|
||||
},
|
||||
}
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
router := domainrouter.New(config, client)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", router.ServeHTTP)
|
||||
|
||||
pipeline := configureMiddleware(config)
|
||||
|
||||
pipeline.Manage()
|
||||
server := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", config.Server.Port),
|
||||
Handler: pipeline(mux),
|
||||
Addr: fmt.Sprintf(":%d", config.Server.Port),
|
||||
// this is rather bad looking
|
||||
Handler: pipeline.Use()(mux),
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-sigs
|
||||
log.Info().Msg("Stopping server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
server.Shutdown(ctx)
|
||||
log.Info().Msg("Http Server stopped")
|
||||
log.Info().Msg("Stopping pipeline")
|
||||
pipeline.Stop(ctx)
|
||||
log.Info().Msg("Pipeline stopped")
|
||||
}()
|
||||
|
||||
if config.Server.Ssl.Enabled {
|
||||
server.TLSConfig = &tls.Config{
|
||||
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
@@ -71,16 +97,23 @@ func main() {
|
||||
}
|
||||
log.Info().Int("port", config.Server.Port).Str("cert", config.Server.Ssl.CertFile).Str("key", config.Server.Ssl.KeyFile).Msg("Starting server")
|
||||
err := server.ListenAndServeTLS("", "")
|
||||
log.Fatal().Err(err).Str("cert", config.Server.Ssl.CertFile).Str("key", config.Server.Ssl.KeyFile).Int("port", config.Server.Port).Msg("Could not start server")
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal().Err(err).Str("cert", config.Server.Ssl.CertFile).Str("key", config.Server.Ssl.KeyFile).Int("port", config.Server.Port).Msg("Could not start server")
|
||||
}
|
||||
} else {
|
||||
log.Info().Int("port", config.Server.Port).Msg("Starting server")
|
||||
err := server.ListenAndServe()
|
||||
log.Fatal().Err(err).Int("port", config.Server.Port).Msg("Could not start server")
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal().Err(err).Int("port", config.Server.Port).Msg("Could not start server")
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
log.Info().Msg("Server shutdown completly, have a nice day")
|
||||
}
|
||||
|
||||
func configureMiddleware(config *domainrouter.Config) middleware.Middleware {
|
||||
middlewares := make([]middleware.Middleware, 0)
|
||||
func configureMiddleware(config *domainrouter.Config) *middleware.Pipeline {
|
||||
pipeline := middleware.NewPipeline()
|
||||
|
||||
if config.RateLimit.Enabled {
|
||||
refillTicker, err := time.ParseDuration(config.RateLimit.RefillTicker)
|
||||
@@ -93,15 +126,22 @@ func configureMiddleware(config *domainrouter.Config) middleware.Middleware {
|
||||
log.Fatal().Err(err).Str("cleanup", config.RateLimit.CleanupTicker).Msg("Could not parse cleanup Ticker")
|
||||
}
|
||||
limiter := middleware.NewLimiter(config.RateLimit.BucketSize, config.RateLimit.BucketRefill, refillTicker, cleanupTicker)
|
||||
limiter.Start()
|
||||
middlewares = append(middlewares, limiter.RateLimiter)
|
||||
pipeline.AddMiddleware(limiter)
|
||||
}
|
||||
|
||||
if config.Logging.Requests {
|
||||
middlewares = append(middlewares, middleware.RequestLogger)
|
||||
pipeline.AddMiddleware(&middleware.RequestLogger{})
|
||||
}
|
||||
|
||||
if config.Metrics.Enabled {
|
||||
flushInterval, err := time.ParseDuration(config.Metrics.FlushInterval)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Str("flush_interval", config.Metrics.FlushInterval).Msg("Could not parse FlushInterval")
|
||||
}
|
||||
metrics := middleware.NewMetrics(config.Metrics.BufferSize, flushInterval, config.Metrics.File)
|
||||
pipeline.AddMiddleware(metrics)
|
||||
}
|
||||
|
||||
pipeline := middleware.Pipeline(middlewares...)
|
||||
return pipeline
|
||||
}
|
||||
|
||||
|
||||
16
config.go
16
config.go
@@ -19,10 +19,12 @@ type Config struct {
|
||||
} `yaml:"ssl"`
|
||||
} `yaml:"server"`
|
||||
Hosts []struct {
|
||||
Port int `yaml:"port"`
|
||||
Remotes []string `yaml:"remotes"`
|
||||
Domains []string `yaml:"domains"`
|
||||
Secure bool `yaml:"secure"`
|
||||
Port int `yaml:"port"`
|
||||
Remotes []string `yaml:"remotes"`
|
||||
Domains []string `yaml:"domains"`
|
||||
Secure bool `yaml:"secure"`
|
||||
Rewrite map[string]string `yaml:"rewrite"`
|
||||
AdditionalHeaders map[string]string `yaml:"extraHeaders"`
|
||||
} `yaml:"hosts"`
|
||||
RateLimit struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
@@ -42,4 +44,10 @@ type Config struct {
|
||||
MaxBackups int `yamls:"maxBackups"`
|
||||
} `yaml:"file"`
|
||||
} `yaml:"logging"`
|
||||
Metrics struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
File string `yaml:"file"`
|
||||
BufferSize int `yaml:"bufferSize"`
|
||||
FlushInterval string `yaml:"flushInterval"`
|
||||
} `yaml:"metrics"`
|
||||
}
|
||||
|
||||
85
config.yaml
85
config.yaml
@@ -1,85 +0,0 @@
|
||||
server:
|
||||
port: 443
|
||||
ssl:
|
||||
enabled: true
|
||||
certFile: server.crt
|
||||
keyFile: server.key
|
||||
acme:
|
||||
enabled: false
|
||||
email: me@pablu.de
|
||||
keyFile: userKey.key
|
||||
caDirUrl: https://192.168.2.154:14000/dir
|
||||
tlsAlpn01Port: 5001
|
||||
http01Port: 5002
|
||||
renewTime: 30s
|
||||
|
||||
|
||||
logging:
|
||||
level: trace
|
||||
# Pretty print for human consumption otherwise json
|
||||
pretty: true
|
||||
# Log incoming requests
|
||||
requests: true
|
||||
# Log to file aswell as stderr
|
||||
file:
|
||||
enabled: false
|
||||
maxAge: 14
|
||||
maxBackups: 10
|
||||
path: ~/logs/router
|
||||
|
||||
|
||||
rateLimit:
|
||||
enabled: false
|
||||
# How many requests per ip adress are allowed
|
||||
bucketSize: 50
|
||||
# How many requests per ip address are refilled
|
||||
refillSize: 50
|
||||
# How often requests per ip address are refilled
|
||||
refillTime: 30s
|
||||
# How often Ip Addresses get cleaned up (only ip addresses with max allowed requests are cleaned up)
|
||||
cleanupTime: 45s
|
||||
|
||||
|
||||
hosts:
|
||||
# Remote address to request
|
||||
- remotes:
|
||||
- localhost
|
||||
# Port on which to request
|
||||
port: 8181
|
||||
# Domains which get redirected to host
|
||||
domains:
|
||||
- localhost
|
||||
- test.localhost
|
||||
|
||||
- remotes:
|
||||
- localhost
|
||||
port: 8282
|
||||
domains:
|
||||
- private.localhost
|
||||
|
||||
- remotes:
|
||||
- localhost
|
||||
port: 5173
|
||||
domains:
|
||||
- hitstar.localhost
|
||||
- hipstar.localhost
|
||||
|
||||
- remotes:
|
||||
- 127.0.0.1
|
||||
port: 46009
|
||||
domains:
|
||||
- chat.localhost
|
||||
|
||||
- remotes:
|
||||
- localhost
|
||||
port: 8080
|
||||
domains:
|
||||
- gorilla.localhost
|
||||
|
||||
- remotes:
|
||||
- www.google.com
|
||||
port: 443
|
||||
# Uses https under the hood to communicate with the remote host
|
||||
secure: true
|
||||
domains:
|
||||
- google.localhost
|
||||
61
example.config.yaml
Normal file
61
example.config.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
server:
|
||||
port: 80
|
||||
ssl:
|
||||
enabled: false
|
||||
certFile: server.crt
|
||||
keyFile: server.key
|
||||
acme:
|
||||
enabled: false
|
||||
email: user@host.dev
|
||||
keyFile: userKey.key
|
||||
caDirUrl: https://localhost:14000/dir
|
||||
tlsAlpn01Port: 5001
|
||||
http01Port: 5002
|
||||
renewTime: 30s
|
||||
|
||||
logging:
|
||||
level: info
|
||||
# Pretty print for human consumption otherwise json
|
||||
pretty: true
|
||||
# Log incoming requests
|
||||
requests: true
|
||||
# Log to file aswell as stderr
|
||||
file:
|
||||
enabled: false
|
||||
maxAge: 14
|
||||
maxBackups: 10
|
||||
path: ~/logs/router
|
||||
|
||||
rateLimit:
|
||||
enabled: false
|
||||
# How many requests per ip adress are allowed
|
||||
bucketSize: 50
|
||||
# How many requests per ip address are refilled
|
||||
refillSize: 50
|
||||
# How often requests per ip address are refilled
|
||||
refillTime: 30s
|
||||
# How often Ip Addresses get cleaned up (only ip addresses with max allowed requests are cleaned up)
|
||||
cleanupTime: 45s
|
||||
|
||||
# Experimental
|
||||
metrics:
|
||||
enabled: false
|
||||
flushInterval: 1h
|
||||
bufferSize: 512
|
||||
file: ~/metrics.json
|
||||
|
||||
hosts:
|
||||
- remotes:
|
||||
- 127.0.0.1
|
||||
port: 3000
|
||||
domains:
|
||||
- api.hitstar.xyz
|
||||
|
||||
- remotes:
|
||||
- 127.0.0.1
|
||||
port: 5173
|
||||
domains:
|
||||
- localhost
|
||||
- hitstar.xyz
|
||||
rewrite:
|
||||
"/api": api.hitstar.xyz
|
||||
5
go.mod
5
go.mod
@@ -1,4 +1,4 @@
|
||||
module github.com/pablu23/domain-router
|
||||
module git.pablu.de/pablu/domain-router
|
||||
|
||||
go 1.23.0
|
||||
|
||||
@@ -7,7 +7,6 @@ toolchain go1.24.4
|
||||
require github.com/rs/zerolog v1.34.0
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/miekg/dns v1.1.67 // indirect
|
||||
@@ -17,11 +16,9 @@ require (
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego v2.7.2+incompatible
|
||||
github.com/go-acme/lego/v4 v4.24.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -1,33 +1,33 @@
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/go-acme/lego v2.7.2+incompatible h1:ThhpPBgf6oa9X/vRd0kEmWOsX7+vmYdckmGZSb+FEp0=
|
||||
github.com/go-acme/lego v2.7.2+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-acme/lego/v4 v4.24.0 h1:pe0q49JKxfSGEP3lkgkMVQrZM1KbD+e0dpJ2McYsiVw=
|
||||
github.com/go-acme/lego/v4 v4.24.0/go.mod h1:hkstZY6D0jylIrZbuNmEQrWQxTIfaJH7prwaWvKDOjw=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
|
||||
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
@@ -40,7 +40,6 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
@@ -48,10 +47,9 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -9,7 +10,16 @@ import (
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func RequestLogger(next http.Handler) http.Handler {
|
||||
type RequestLogger struct{}
|
||||
|
||||
func (_ *RequestLogger) Stop(ctx context.Context) {
|
||||
log.Info().Msg("Stopped Logging")
|
||||
}
|
||||
|
||||
func (_ *RequestLogger) Manage() {
|
||||
}
|
||||
|
||||
func (_ *RequestLogger) Use(next http.Handler) http.Handler {
|
||||
log.Info().Msg("Enabling Logging")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
@@ -22,6 +32,7 @@ func RequestLogger(next http.Handler) http.Handler {
|
||||
Str("uri", r.RequestURI).
|
||||
Str("method", r.Method).
|
||||
Str("uuid", uuid).
|
||||
Str("remote_address", r.RemoteAddr).
|
||||
Msg("Received Request")
|
||||
|
||||
next.ServeHTTP(lrw, r)
|
||||
|
||||
152
middleware/metrics.go
Normal file
152
middleware/metrics.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
c chan RequestMetric
|
||||
endpointMetrics []EndpointMetrics
|
||||
ticker *time.Ticker
|
||||
file string
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
type EndpointMetrics struct {
|
||||
Host string
|
||||
Endpoint string
|
||||
AbsoluteDuration time.Duration
|
||||
Calls uint64
|
||||
}
|
||||
|
||||
type RequestMetric struct {
|
||||
Start time.Time
|
||||
Stop time.Time
|
||||
Host string
|
||||
Method string
|
||||
Uri string
|
||||
Status int
|
||||
Size int
|
||||
}
|
||||
|
||||
func NewMetrics(bufferSize int, flushTimeout time.Duration, file string) *Metrics {
|
||||
return &Metrics{
|
||||
c: make(chan RequestMetric, bufferSize),
|
||||
ticker: time.NewTicker(flushTimeout),
|
||||
file: file,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) Use(next http.Handler) http.Handler {
|
||||
log.Info().Msg("Enabling Request Metrics")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rm := RequestMetric{
|
||||
Start: start,
|
||||
Host: r.Host,
|
||||
Method: r.Method,
|
||||
Uri: r.URL.Path,
|
||||
}
|
||||
|
||||
log.Trace().Any("request_metric", rm).Msg("RequestMetric created")
|
||||
next.ServeHTTP(w, r)
|
||||
rm.Stop = time.Now()
|
||||
log.Trace().Any("request_metric", rm).Msg("RequestMetric finished")
|
||||
|
||||
m.c <- rm
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Metrics) Manage() {
|
||||
for {
|
||||
select {
|
||||
case rm := <-m.c:
|
||||
m.calculateDuration(rm)
|
||||
case <-m.ticker.C:
|
||||
m.Flush()
|
||||
case <-m.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) calculateDuration(rm RequestMetric) {
|
||||
duration := rm.Stop.Sub(rm.Start)
|
||||
|
||||
// TODO: Replace this with a hash probably
|
||||
index := slices.IndexFunc(m.endpointMetrics, func(e EndpointMetrics) bool {
|
||||
if strings.EqualFold(e.Host, rm.Host) && strings.EqualFold(e.Endpoint, rm.Uri) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
var in EndpointMetrics
|
||||
if index >= 0 {
|
||||
in = m.endpointMetrics[index]
|
||||
} else {
|
||||
in = EndpointMetrics{
|
||||
Host: rm.Host,
|
||||
Endpoint: rm.Uri,
|
||||
AbsoluteDuration: time.Duration(0),
|
||||
Calls: 0,
|
||||
}
|
||||
}
|
||||
|
||||
in.AbsoluteDuration += duration
|
||||
in.Calls += 1
|
||||
|
||||
if index >= 0 {
|
||||
m.endpointMetrics[index] = in
|
||||
} else {
|
||||
m.endpointMetrics = append(m.endpointMetrics, in)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) Flush() {
|
||||
file, err := os.Create(m.file)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("file", m.file).Msg("Could not open file for flushing")
|
||||
return
|
||||
}
|
||||
|
||||
a := make([]EndpointMetrics, len(m.endpointMetrics))
|
||||
copy(a, m.endpointMetrics)
|
||||
slices.SortStableFunc(a, func(e1 EndpointMetrics, e2 EndpointMetrics) int {
|
||||
return cmp.Compare(e1.Calls, e2.Calls)
|
||||
})
|
||||
|
||||
err = json.NewEncoder(file).Encode(a)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("file", m.file).Msg("Could not json Encode to file")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("file", m.file).Int("count", len(a)).Msg("Completed Metrics flush")
|
||||
}
|
||||
|
||||
func (m *Metrics) Stop(ctx context.Context) {
|
||||
log.Info().Msg("Stopping Request Metrics")
|
||||
for len(m.c) > 0 {
|
||||
select {
|
||||
case rm := <-m.c:
|
||||
m.calculateDuration(rm)
|
||||
case <-ctx.Done():
|
||||
m.stop <- struct{}{}
|
||||
log.Warn().Msg("Hard Stopped Request Metrics")
|
||||
return
|
||||
}
|
||||
}
|
||||
m.Flush()
|
||||
m.stop <- struct{}{}
|
||||
log.Info().Msg("Stopped Request Metrics")
|
||||
}
|
||||
@@ -1,18 +1,57 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type Middleware func(http.Handler) http.Handler
|
||||
type Middleware interface {
|
||||
Use(http.Handler) http.Handler
|
||||
Manage()
|
||||
Stop(context.Context)
|
||||
}
|
||||
|
||||
func Pipeline(funcs ...Middleware) Middleware {
|
||||
type Pipeline struct {
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
func NewPipeline() *Pipeline {
|
||||
return &Pipeline{}
|
||||
}
|
||||
|
||||
func (p *Pipeline) AddMiddleware(m Middleware) {
|
||||
p.middleware = append(p.middleware, m)
|
||||
}
|
||||
|
||||
func (p *Pipeline) Use() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
for _, m := range slices.Backward(funcs) {
|
||||
next = m(next)
|
||||
for _, m := range slices.Backward(p.middleware) {
|
||||
next = m.Use(next)
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pipeline) Stop(ctx context.Context) {
|
||||
for _, m := range p.middleware {
|
||||
m.Stop(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pipeline) Manage() {
|
||||
for _, m := range p.middleware {
|
||||
go m.Manage()
|
||||
}
|
||||
}
|
||||
|
||||
// func Pipeline(funcs ...Middleware) func(http.Handler) http.Handler {
|
||||
// return func(next http.Handler) http.Handler {
|
||||
// for _, m := range slices.Backward(funcs) {
|
||||
// next = m.Use(next)
|
||||
// }
|
||||
//
|
||||
// return next
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,10 +19,11 @@ type Limiter struct {
|
||||
bucketRefill int
|
||||
rwLock *sync.RWMutex
|
||||
rateChannel chan string
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func NewLimiter(maxRequests int, refills int, refillInterval time.Duration, cleanupInterval time.Duration) Limiter {
|
||||
return Limiter{
|
||||
func NewLimiter(maxRequests int, refills int, refillInterval time.Duration, cleanupInterval time.Duration) *Limiter {
|
||||
return &Limiter{
|
||||
currentBuckets: make(map[string]*atomic.Int64),
|
||||
bucketSize: maxRequests,
|
||||
refillTicker: time.NewTicker(refillInterval),
|
||||
@@ -32,14 +34,15 @@ func NewLimiter(maxRequests int, refills int, refillInterval time.Duration, clea
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Limiter) Start() {
|
||||
go l.Manage()
|
||||
}
|
||||
|
||||
func (l *Limiter) UpdateCleanupTime(new time.Duration) {
|
||||
l.cleanupTicker.Reset(new)
|
||||
}
|
||||
|
||||
func (l *Limiter) Stop(ctx context.Context) {
|
||||
l.stop <- struct{}{}
|
||||
log.Info().Msg("Stopped Ratelimits")
|
||||
}
|
||||
|
||||
func (l *Limiter) Manage() {
|
||||
for {
|
||||
select {
|
||||
@@ -77,6 +80,8 @@ func (l *Limiter) Manage() {
|
||||
l.rwLock.Unlock()
|
||||
duration := time.Since(start)
|
||||
log.Debug().Str("duration", duration.String()).Int("deleted_buckets", deletedBuckets).Msg("Cleaned up Buckets")
|
||||
case <-l.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +98,7 @@ func (l *Limiter) AddIfExists(ip string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *Limiter) RateLimiter(next http.Handler) http.Handler {
|
||||
func (l *Limiter) Use(next http.Handler) http.Handler {
|
||||
log.Info().Int("bucket_size", l.bucketSize).Int("bucket_refill", l.bucketRefill).Msg("Enabling Ratelimits")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
addr := strings.Split(r.RemoteAddr, ":")[0]
|
||||
|
||||
47
router.go
47
router.go
@@ -24,17 +24,28 @@ type Router struct {
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
Port int
|
||||
Remotes []string
|
||||
Secure bool
|
||||
Current *atomic.Uint32
|
||||
Port int
|
||||
Remotes []string
|
||||
Secure bool
|
||||
Current *atomic.Uint32
|
||||
Rewrites map[string]*Host
|
||||
AdditionalHeaders map[string]string
|
||||
}
|
||||
|
||||
func New(config *Config, client *http.Client) Router {
|
||||
m := make(map[string]Host)
|
||||
for _, host := range config.Hosts {
|
||||
for _, domain := range host.Domains {
|
||||
m[domain] = Host{host.Port, host.Remotes, host.Secure, &atomic.Uint32{}}
|
||||
curr := Host{host.Port, host.Remotes, host.Secure, &atomic.Uint32{}, make(map[string]*Host), host.AdditionalHeaders}
|
||||
m[domain] = curr
|
||||
|
||||
for subUrl, rewriteHost := range host.Rewrite {
|
||||
rewrite, ok := m[rewriteHost]
|
||||
if !ok {
|
||||
panic("WIP: Rewrite location has to be defined before rewrite")
|
||||
}
|
||||
curr.Rewrites[subUrl] = &rewrite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +98,26 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
for subUrl, rewriteHost := range host.Rewrites {
|
||||
if !strings.HasPrefix(r.URL.Path, subUrl) {
|
||||
break
|
||||
}
|
||||
|
||||
slicedPath, _ := strings.CutPrefix(r.URL.Path, subUrl)
|
||||
|
||||
log.Info().
|
||||
Str("old_host", strings.Join(host.Remotes, ", ")).
|
||||
Str("new_host", strings.Join(rewriteHost.Remotes, ", ")).
|
||||
Str("sub_url", subUrl).
|
||||
Str("requested_path", r.URL.Path).
|
||||
Str("new_path", slicedPath).
|
||||
Msg("Rewriting matched url path to different remote")
|
||||
|
||||
r.URL.Path = slicedPath
|
||||
host = *rewriteHost
|
||||
break
|
||||
}
|
||||
|
||||
remote := host.Remotes[host.Current.Load()]
|
||||
go router.roundRobin(&host)
|
||||
|
||||
@@ -171,7 +202,11 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
removeHopByHopHeaders(res.Header)
|
||||
|
||||
copyHeader(w.Header(), res.Header)
|
||||
resultHeader := w.Header()
|
||||
copyHeader(resultHeader, res.Header)
|
||||
for name, val := range host.AdditionalHeaders {
|
||||
resultHeader.Add(name, val)
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
err = router.copyResponse(w, res.Body)
|
||||
|
||||
Reference in New Issue
Block a user