2021-04-01 09:37:28 +02:00
package main
import (
"context"
2021-04-14 18:28:23 +02:00
"errors"
2021-04-01 09:37:28 +02:00
"fmt"
2021-04-06 05:12:06 +02:00
"io"
2021-05-15 05:01:27 +02:00
"math"
2021-04-16 09:49:00 +02:00
"net/http"
2021-06-01 09:56:39 +02:00
"net/rpc"
2021-04-01 09:37:28 +02:00
"os"
"os/signal"
2021-05-15 05:01:27 +02:00
"strconv"
2021-04-12 09:07:35 +02:00
"strings"
2021-04-01 09:37:28 +02:00
"syscall"
"time"
2021-04-03 07:22:54 +02:00
log "github.com/go-pkgz/lgr"
2021-04-02 07:07:36 +02:00
"github.com/umputun/go-flags"
2021-04-09 22:31:14 +02:00
"gopkg.in/natefinch/lumberjack.v2"
2021-04-02 07:07:36 +02:00
2021-04-03 21:23:23 +02:00
"github.com/umputun/reproxy/app/discovery"
"github.com/umputun/reproxy/app/discovery/provider"
2021-05-12 10:05:12 +02:00
"github.com/umputun/reproxy/app/discovery/provider/consulcatalog"
2021-04-21 02:04:12 +02:00
"github.com/umputun/reproxy/app/mgmt"
2021-06-01 09:56:39 +02:00
"github.com/umputun/reproxy/app/plugin"
2021-04-03 21:23:23 +02:00
"github.com/umputun/reproxy/app/proxy"
2021-04-01 09:37:28 +02:00
)
var opts struct {
2023-11-25 21:54:14 +02:00
Listen string ` short:"l" long:"listen" env:"LISTEN" description:"listen on host:port (default: 0.0.0.0:8080/8443 under docker, 127.0.0.1:80/443 without)" `
MaxSize string ` short:"m" long:"max" env:"MAX_SIZE" default:"64K" description:"max request size" `
GzipEnabled bool ` short:"g" long:"gzip" env:"GZIP" description:"enable gz compression" `
ProxyHeaders [ ] string ` short:"x" long:"header" description:"outgoing proxy headers to add" ` // env HEADER split in code to allow , inside ""
DropHeaders [ ] string ` long:"drop-header" env:"DROP_HEADERS" description:"incoming headers to drop" env-delim:"," `
AuthBasicHtpasswd string ` long:"basic-htpasswd" env:"BASIC_HTPASSWD" description:"htpasswd file for basic auth" `
RemoteLookupHeaders bool ` long:"remote-lookup-headers" env:"REMOTE_LOOKUP_HEADERS" description:"enable remote lookup headers" `
2023-11-27 20:05:17 +02:00
LBType string ` long:"lb-type" env:"LB_TYPE" description:"load balancer type" choice:"random" choice:"failover" choice:"roundrobin" default:"random" ` // nolint
2024-01-07 20:19:50 +02:00
Insecure bool ` long:"insecure" env:"INSECURE" description:"skip SSL certificate verification for the destination host" `
2024-01-25 11:28:54 +02:00
KeepHost bool ` long:"keep-host" env:"KEEP_HOST" description:"pass the Host header from the client as-is, instead of rewriting it" `
2021-04-01 09:37:28 +02:00
2021-04-03 07:22:54 +02:00
SSL struct {
2022-01-06 07:57:05 +02:00
Type string ` long:"type" env:"TYPE" description:"ssl (auto) support" choice:"none" choice:"static" choice:"auto" default:"none" ` // nolint
2021-04-09 09:28:13 +02:00
Cert string ` long:"cert" env:"CERT" description:"path to cert.pem file" `
Key string ` long:"key" env:"KEY" description:"path to key.pem file" `
ACMELocation string ` long:"acme-location" env:"ACME_LOCATION" description:"dir where certificates will be stored by autocert manager" default:"./var/acme" `
ACMEEmail string ` long:"acme-email" env:"ACME_EMAIL" description:"admin email for certificate notifications" `
2021-05-04 04:40:21 +02:00
RedirHTTPPort int ` long:"http-port" env:"HTTP_PORT" description:"http port for redirect to https and acme challenge test (default: 8080 under docker, 80 without)" `
2021-04-09 09:28:13 +02:00
FQDNs [ ] string ` long:"fqdn" env:"ACME_FQDN" env-delim:"," description:"FQDN(s) for ACME certificates" `
2021-04-03 07:22:54 +02:00
} ` group:"ssl" namespace:"ssl" env-namespace:"SSL" `
2021-04-01 09:37:28 +02:00
Assets struct {
2021-04-27 01:51:48 +02:00
Location string ` short:"a" long:"location" env:"LOCATION" default:"" description:"assets location" `
WebRoot string ` long:"root" env:"ROOT" default:"/" description:"assets web root" `
2021-06-08 02:04:18 +02:00
SPA bool ` long:"spa" env:"SPA" description:"spa treatment for assets" `
2021-04-27 01:51:48 +02:00
CacheControl [ ] string ` long:"cache" env:"CACHE" description:"cache duration for assets" env-delim:"," `
2022-01-06 07:57:05 +02:00
NotFound string ` long:"not-found" env:"NOT_FOUND" description:"path to file to serve on 404, relative to location" `
2021-04-01 09:37:28 +02:00
} ` group:"assets" namespace:"assets" env-namespace:"ASSETS" `
2021-04-06 05:12:06 +02:00
Logger struct {
2021-04-13 19:45:49 +02:00
StdOut bool ` long:"stdout" env:"STDOUT" description:"enable stdout logging" `
2021-04-06 05:12:06 +02:00
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable access and error rotated logs" `
FileName string ` long:"file" env:"FILE" default:"access.log" description:"location of access log" `
2021-05-15 05:01:27 +02:00
MaxSize string ` long:"max-size" env:"MAX_SIZE" default:"100M" description:"maximum size before it gets rotated" `
2021-04-06 05:12:06 +02:00
MaxBackups int ` long:"max-backups" env:"MAX_BACKUPS" default:"10" description:"maximum number of old log files to retain" `
} ` group:"logger" namespace:"logger" env-namespace:"LOGGER" `
2021-04-01 09:37:28 +02:00
Docker struct {
2021-04-28 21:00:38 +02:00
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable docker provider" `
Host string ` long:"host" env:"HOST" default:"unix:///var/run/docker.sock" description:"docker host" `
Network string ` long:"network" env:"NETWORK" default:"" description:"docker network" `
Excluded [ ] string ` long:"exclude" env:"EXCLUDE" description:"excluded containers" env-delim:"," `
AutoAPI bool ` long:"auto" env:"AUTO" description:"enable automatic routing (without labels)" `
APIPrefix string ` long:"prefix" env:"PREFIX" description:"prefix for docker source routes" `
2021-04-01 09:37:28 +02:00
} ` group:"docker" namespace:"docker" env-namespace:"DOCKER" `
2021-05-12 10:05:12 +02:00
ConsulCatalog struct {
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable consul catalog provider" `
Address string ` long:"address" env:"ADDRESS" default:"http://127.0.0.1:8500" description:"consul address" `
CheckInterval time . Duration ` long:"interval" env:"INTERVAL" default:"1s" description:"consul catalog check interval" `
} ` group:"consul-catalog" namespace:"consul-catalog" env-namespace:"CONSUL_CATALOG" `
2021-04-01 09:37:28 +02:00
File struct {
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable file provider" `
2021-04-07 06:18:27 +02:00
Name string ` long:"name" env:"NAME" default:"reproxy.yml" description:"file name" `
2021-04-01 09:37:28 +02:00
CheckInterval time . Duration ` long:"interval" env:"INTERVAL" default:"3s" description:"file check interval" `
Delay time . Duration ` long:"delay" env:"DELAY" default:"500ms" description:"file event delay" `
} ` group:"file" namespace:"file" env-namespace:"FILE" `
Static struct {
2021-04-01 09:53:42 +02:00
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable static provider" `
2021-04-12 06:18:06 +02:00
Rules [ ] string ` long:"rule" env:"RULES" description:"routing rules" env-delim:";" `
2021-04-01 09:37:28 +02:00
} ` group:"static" namespace:"static" env-namespace:"STATIC" `
2021-04-13 04:54:59 +02:00
Timeouts struct {
ReadHeader time . Duration ` long:"read-header" env:"READ_HEADER" default:"5s" description:"read header server timeout" `
Write time . Duration ` long:"write" env:"WRITE" default:"30s" description:"write server timeout" `
Idle time . Duration ` long:"idle" env:"IDLE" default:"30s" description:"idle server timeout" `
Dial time . Duration ` long:"dial" env:"DIAL" default:"30s" description:"dial transport timeout" `
KeepAlive time . Duration ` long:"keep-alive" env:"KEEP_ALIVE" default:"30s" description:"keep-alive transport timeout" `
ResponseHeader time . Duration ` long:"resp-header" env:"RESP_HEADER" default:"5s" description:"response header transport timeout" `
IdleConn time . Duration ` long:"idle-conn" env:"IDLE_CONN" default:"90s" description:"idle connection transport timeout" `
TLSHandshake time . Duration ` long:"tls" env:"TLS" default:"10s" description:"TLS hanshake transport timeout" `
ExpectContinue time . Duration ` long:"continue" env:"CONTINUE" default:"1s" description:"expect continue transport timeout" `
} ` group:"timeout" namespace:"timeout" env-namespace:"TIMEOUT" `
2021-04-21 02:04:12 +02:00
Management struct {
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable management API" `
Listen string ` long:"listen" env:"LISTEN" default:"0.0.0.0:8081" description:"listen on host:port" `
} ` group:"mgmt" namespace:"mgmt" env-namespace:"MGMT" `
2021-04-30 11:03:36 +02:00
ErrorReport struct {
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable html errors reporting" `
Template string ` long:"template" env:"TEMPLATE" description:"error message template file" `
} ` group:"error" namespace:"error" env-namespace:"ERROR" `
2021-05-17 01:34:51 +02:00
HealthCheck struct {
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable automatic health-check" `
Interval time . Duration ` long:"interval" env:"INTERVAL" default:"300s" description:"automatic health-check interval" `
} ` group:"health-check" namespace:"health-check" env-namespace:"HEALTH_CHECK" `
2021-07-03 08:23:50 +02:00
Throttle struct {
System int ` long:"system" env:"SYSTEM" default:"0" description:"throttle overall activity'" `
User int ` long:"user" env:"USER" default:"0" description:"limit req/sec per user and per proxy destination" `
} ` group:"throttle" namespace:"throttle" env-namespace:"THROTTLE" `
2021-06-01 09:56:39 +02:00
Plugin struct {
Enabled bool ` long:"enabled" env:"ENABLED" description:"enable plugin support" `
Listen string ` long:"listen" env:"LISTEN" default:"127.0.0.1:8081" description:"registration listen on host:port" `
} ` group:"plugin" namespace:"plugin" env-namespace:"PLUGIN" `
2021-04-13 21:08:15 +02:00
Signature bool ` long:"signature" env:"SIGNATURE" description:"enable reproxy signature headers" `
Dbg bool ` long:"dbg" env:"DEBUG" description:"debug mode" `
2021-04-01 09:37:28 +02:00
}
var revision = "unknown"
func main ( ) {
2021-04-06 05:12:06 +02:00
fmt . Printf ( "reproxy %s\n" , revision )
2021-04-01 09:37:28 +02:00
p := flags . NewParser ( & opts , flags . PrintErrors | flags . PassDoubleDash | flags . HelpFlag )
p . SubcommandsOptional = true
if _ , err := p . Parse ( ) ; err != nil {
if err . ( * flags . Error ) . Type != flags . ErrHelp {
log . Printf ( "[ERROR] cli error: %v" , err )
}
2021-05-04 04:40:21 +02:00
os . Exit ( 2 )
2021-04-01 09:37:28 +02:00
}
setupLog ( opts . Dbg )
2021-04-16 09:49:00 +02:00
2021-04-23 09:02:36 +02:00
log . Printf ( "[DEBUG] options: %+v" , opts )
2021-04-27 01:51:48 +02:00
err := run ( )
if err != nil {
log . Fatalf ( "[ERROR] proxy server failed, %v" , err )
}
}
func run ( ) error {
2021-04-16 09:49:00 +02:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
2021-04-27 01:51:48 +02:00
go func ( ) {
// catch signal and invoke graceful termination
2021-04-16 09:49:00 +02:00
stop := make ( chan os . Signal , 1 )
signal . Notify ( stop , os . Interrupt , syscall . SIGTERM )
<- stop
log . Printf ( "[WARN] interrupt signal" )
cancel ( )
} ( )
2021-04-01 09:37:28 +02:00
2022-08-21 10:41:52 +02:00
defer func ( ) {
// handle panic
if x := recover ( ) ; x != nil {
log . Printf ( "[WARN] run time panic:\n%v" , x )
panic ( x )
}
} ( )
2021-04-01 09:37:28 +02:00
providers , err := makeProviders ( )
if err != nil {
2021-04-27 01:51:48 +02:00
return fmt . Errorf ( "failed to make providers: %w" , err )
2021-04-01 09:37:28 +02:00
}
2021-04-12 09:29:17 +02:00
svc := discovery . NewService ( providers , time . Second )
2021-04-11 21:21:58 +02:00
if len ( providers ) > 0 {
go func ( ) {
if e := svc . Run ( context . Background ( ) ) ; e != nil {
2021-04-27 01:51:48 +02:00
log . Printf ( "[WARN] discovery failed, %v" , e )
2021-04-11 21:21:58 +02:00
}
} ( )
}
2021-05-07 17:36:07 +02:00
if opts . HealthCheck . Enabled {
svc . ScheduleHealthCheck ( context . Background ( ) , opts . HealthCheck . Interval )
}
2021-04-01 09:37:28 +02:00
2021-04-27 01:51:48 +02:00
sslConfig , sslErr := makeSSLConfig ( )
if sslErr != nil {
return fmt . Errorf ( "failed to make config of ssl server params: %w" , sslErr )
2021-04-03 07:22:54 +02:00
}
2021-05-15 05:01:27 +02:00
accessLog , alErr := makeAccessLogWriter ( )
if alErr != nil {
2022-06-24 00:55:01 +02:00
return fmt . Errorf ( "failed to access log: %w" , alErr )
2021-05-15 05:01:27 +02:00
}
2021-04-06 05:12:06 +02:00
defer func ( ) {
2021-04-27 01:51:48 +02:00
if logErr := accessLog . Close ( ) ; logErr != nil {
log . Printf ( "[WARN] can't close access log, %v" , logErr )
2021-04-06 05:12:06 +02:00
}
} ( )
2021-04-27 01:51:48 +02:00
cacheControl , err := proxy . MakeCacheControl ( opts . Assets . CacheControl )
if err != nil {
return fmt . Errorf ( "failed to make cache control: %w" , err )
}
2021-04-30 11:03:36 +02:00
errReporter , err := makeErrorReporter ( )
if err != nil {
return fmt . Errorf ( "failed to make error reporter: %w" , err )
}
2021-05-04 04:40:21 +02:00
addr := listenAddress ( opts . Listen , opts . SSL . Type )
log . Printf ( "[DEBUG] listen address %s" , addr )
2021-05-15 05:01:27 +02:00
maxBodySize , perr := sizeParse ( opts . MaxSize )
if perr != nil {
return fmt . Errorf ( "failed to convert MaxSize: %w" , err )
}
2021-07-08 09:31:27 +02:00
proxyHeaders := opts . ProxyHeaders
if len ( proxyHeaders ) == 0 {
proxyHeaders = splitAtCommas ( os . Getenv ( "HEADER" ) ) // env value may have comma inside "", parsed separately
}
2021-11-07 23:31:33 +02:00
basicAuthAllowed , baErr := makeBasicAuth ( opts . AuthBasicHtpasswd )
if baErr != nil {
return fmt . Errorf ( "failed to load basic auth: %w" , baErr )
}
2021-04-01 09:37:28 +02:00
px := & proxy . Http {
2021-04-27 01:51:48 +02:00
Version : revision ,
Matcher : svc ,
2021-05-04 04:40:21 +02:00
Address : addr ,
2021-05-15 05:01:27 +02:00
MaxBodySize : int64 ( maxBodySize ) ,
2021-04-27 01:51:48 +02:00
AssetsLocation : opts . Assets . Location ,
AssetsWebRoot : opts . Assets . WebRoot ,
2022-01-06 07:57:05 +02:00
Assets404 : opts . Assets . NotFound ,
2021-06-08 02:04:18 +02:00
AssetsSPA : opts . Assets . SPA ,
2021-04-27 01:51:48 +02:00
CacheControl : cacheControl ,
GzEnabled : opts . GzipEnabled ,
SSLConfig : sslConfig ,
2024-01-07 20:19:50 +02:00
Insecure : opts . Insecure ,
2021-07-08 09:31:27 +02:00
ProxyHeaders : proxyHeaders ,
2021-09-11 21:38:56 +02:00
DropHeader : opts . DropHeaders ,
2021-04-27 01:51:48 +02:00
AccessLog : accessLog ,
StdOutEnabled : opts . Logger . StdOut ,
Signature : opts . Signature ,
2021-05-28 23:11:16 +02:00
LBSelector : makeLBSelector ( ) ,
2021-04-13 04:54:59 +02:00
Timeouts : proxy . Timeouts {
ReadHeader : opts . Timeouts . ReadHeader ,
Write : opts . Timeouts . Write ,
Idle : opts . Timeouts . Idle ,
Dial : opts . Timeouts . Dial ,
KeepAlive : opts . Timeouts . KeepAlive ,
IdleConn : opts . Timeouts . IdleConn ,
TLSHandshake : opts . Timeouts . TLSHandshake ,
ExpectContinue : opts . Timeouts . ExpectContinue ,
ResponseHeader : opts . Timeouts . ResponseHeader ,
} ,
2021-11-07 23:31:33 +02:00
Metrics : makeMetrics ( ctx , svc ) ,
Reporter : errReporter ,
PluginConductor : makePluginConductor ( ctx ) ,
ThrottleSystem : opts . Throttle . System * 3 ,
ThrottleUser : opts . Throttle . User ,
BasicAuthEnabled : len ( basicAuthAllowed ) > 0 ,
BasicAuthAllowed : basicAuthAllowed ,
2024-01-25 11:28:54 +02:00
KeepHost : opts . KeepHost ,
2023-11-25 21:54:14 +02:00
OnlyFrom : makeOnlyFromMiddleware ( ) ,
2021-04-01 09:37:28 +02:00
}
2021-04-27 01:51:48 +02:00
err = px . Run ( ctx )
2023-11-25 21:54:14 +02:00
if err != nil && errors . Is ( err , http . ErrServerClosed ) {
2021-11-07 23:31:33 +02:00
log . Printf ( "[WARN] proxy server closed, %v" , err ) // nolint gocritic
2021-04-27 01:51:48 +02:00
return nil
2021-04-01 09:37:28 +02:00
}
2021-04-27 01:51:48 +02:00
return err
2021-04-01 09:37:28 +02:00
}
2021-11-07 23:31:33 +02:00
// makeBasicAuth returns a list of allowed basic auth users and password hashes.
// if no htpasswd file is specified, an empty list is returned.
func makeBasicAuth ( htpasswdFile string ) ( [ ] string , error ) {
var basicAuthAllowed [ ] string
if htpasswdFile != "" {
2021-11-09 20:18:26 +02:00
data , err := os . ReadFile ( htpasswdFile ) //nolint:gosec //read file with opts passed path
2021-11-07 23:31:33 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to read htpasswd file %s: %w" , htpasswdFile , err )
}
basicAuthAllowed = strings . Split ( string ( data ) , "\n" )
for i , v := range basicAuthAllowed {
basicAuthAllowed [ i ] = strings . TrimSpace ( v )
basicAuthAllowed [ i ] = strings . Replace ( basicAuthAllowed [ i ] , "\t" , "" , - 1 )
}
}
return basicAuthAllowed , nil
}
2021-04-12 09:07:35 +02:00
// make all providers. the order is matter, defines which provider will have priority in case of conflicting rules
// static first, file second and docker the last one
2021-04-01 09:37:28 +02:00
func makeProviders ( ) ( [ ] discovery . Provider , error ) {
var res [ ] discovery . Provider
2021-04-12 09:07:35 +02:00
if opts . Static . Enabled {
var msgs [ ] string
for _ , rule := range opts . Static . Rules {
msgs = append ( msgs , "\"" + rule + "\"" )
}
log . Printf ( "[DEBUG] inject static rules: %s" , strings . Join ( msgs , " " ) )
res = append ( res , & provider . Static { Rules : opts . Static . Rules } )
}
2021-04-01 09:37:28 +02:00
if opts . File . Enabled {
res = append ( res , & provider . File {
FileName : opts . File . Name ,
CheckInterval : opts . File . CheckInterval ,
Delay : opts . File . Delay ,
} )
}
if opts . Docker . Enabled {
2021-04-14 23:28:57 +02:00
client := provider . NewDockerClient ( opts . Docker . Host , opts . Docker . Network )
2021-04-12 09:57:13 +02:00
if opts . Docker . AutoAPI {
log . Printf ( "[INFO] auto-api enabled for docker" )
}
2021-04-16 20:33:06 +02:00
const refreshInterval = time . Second * 10 // seems like a reasonable default
2021-04-12 09:57:13 +02:00
res = append ( res , & provider . Docker { DockerClient : client , Excludes : opts . Docker . Excluded ,
2021-04-28 21:00:38 +02:00
AutoAPI : opts . Docker . AutoAPI , APIPrefix : opts . Docker . APIPrefix , RefreshInterval : refreshInterval } )
2021-04-01 09:37:28 +02:00
}
2021-05-12 10:05:12 +02:00
if opts . ConsulCatalog . Enabled {
client := consulcatalog . NewClient ( opts . ConsulCatalog . Address , http . DefaultClient )
res = append ( res , consulcatalog . New ( client , opts . ConsulCatalog . CheckInterval ) )
}
2021-04-10 21:34:28 +02:00
if len ( res ) == 0 && opts . Assets . Location == "" {
2021-04-14 18:28:23 +02:00
return nil , errors . New ( "no providers enabled" )
2021-04-01 09:37:28 +02:00
}
return res , nil
}
2021-06-01 09:56:39 +02:00
func makePluginConductor ( ctx context . Context ) proxy . MiddlewareProvider {
if ! opts . Plugin . Enabled {
return nil
}
conductor := & plugin . Conductor {
Address : opts . Plugin . Listen ,
2024-03-15 23:53:10 +02:00
RPCDialer : plugin . RPCDialerFunc ( func ( _ , address string ) ( plugin . RPCClient , error ) {
2021-06-01 09:56:39 +02:00
return rpc . Dial ( "tcp" , address )
} ) ,
}
go func ( ) {
if err := conductor . Run ( ctx ) ; err != nil {
log . Printf ( "[WARN] plugin conductor error, %v" , err )
}
} ( )
return conductor
}
func makeMetrics ( ctx context . Context , informer mgmt . Informer ) proxy . MiddlewareProvider {
if ! opts . Management . Enabled {
return nil
2021-05-28 23:11:16 +02:00
}
2021-06-01 09:56:39 +02:00
metrics := mgmt . NewMetrics ( )
go func ( ) {
mgSrv := mgmt . Server {
Listen : opts . Management . Listen ,
Informer : informer ,
AssetsLocation : opts . Assets . Location ,
AssetsWebRoot : opts . Assets . WebRoot ,
Version : revision ,
}
if err := mgSrv . Run ( ctx ) ; err != nil {
log . Printf ( "[WARN] management service failed, %v" , err )
}
} ( )
return metrics
2021-05-28 23:11:16 +02:00
}
2021-04-03 07:22:54 +02:00
func makeSSLConfig ( ) ( config proxy . SSLConfig , err error ) {
switch opts . SSL . Type {
case "none" :
config . SSLMode = proxy . SSLNone
case "static" :
if opts . SSL . Cert == "" {
return config , errors . New ( "path to cert.pem is required" )
}
if opts . SSL . Key == "" {
return config , errors . New ( "path to key.pem is required" )
}
config . SSLMode = proxy . SSLStatic
config . Cert = opts . SSL . Cert
config . Key = opts . SSL . Key
2021-05-04 04:40:21 +02:00
config . RedirHTTPPort = redirHTTPPort ( opts . SSL . RedirHTTPPort )
2021-04-03 07:22:54 +02:00
case "auto" :
config . SSLMode = proxy . SSLAuto
config . ACMELocation = opts . SSL . ACMELocation
config . ACMEEmail = opts . SSL . ACMEEmail
2021-05-19 20:39:49 +02:00
config . FQDNs = fqdns ( opts . SSL . FQDNs )
2021-05-04 04:40:21 +02:00
config . RedirHTTPPort = redirHTTPPort ( opts . SSL . RedirHTTPPort )
2022-02-15 10:36:24 +02:00
default :
2022-02-15 11:00:34 +02:00
return config , fmt . Errorf ( "invalid value %q for SSL_TYPE, allowed values are: none, static or auto" , opts . SSL . Type )
2021-04-03 07:22:54 +02:00
}
return config , err
}
2021-04-01 09:37:28 +02:00
2023-11-27 20:05:17 +02:00
func makeLBSelector ( ) proxy . LBSelector {
2021-06-01 09:56:39 +02:00
switch opts . LBType {
case "random" :
2023-11-27 20:05:17 +02:00
return & proxy . RandomSelector { }
2021-06-01 09:56:39 +02:00
case "failover" :
2023-11-27 20:05:17 +02:00
return & proxy . FailoverSelector { }
case "roundrobin" :
return & proxy . RoundRobinSelector { }
2021-06-01 09:56:39 +02:00
default :
2023-11-27 20:05:17 +02:00
return & proxy . FailoverSelector { }
2021-06-01 09:56:39 +02:00
}
}
2023-11-25 21:54:14 +02:00
func makeOnlyFromMiddleware ( ) * proxy . OnlyFrom {
if opts . RemoteLookupHeaders {
return proxy . NewOnlyFrom ( proxy . OFRealIP , proxy . OFForwarded , proxy . OFRemoteAddr )
}
return proxy . NewOnlyFrom ( proxy . OFRemoteAddr )
}
2021-04-30 11:03:36 +02:00
func makeErrorReporter ( ) ( proxy . Reporter , error ) {
result := & proxy . ErrorReporter {
Nice : opts . ErrorReport . Enabled ,
}
if opts . ErrorReport . Template != "" {
2021-11-09 20:18:26 +02:00
data , err := os . ReadFile ( opts . ErrorReport . Template )
2021-04-30 11:03:36 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to load error html template from %s, %w" , opts . ErrorReport . Template , err )
}
result . Template = string ( data )
}
return result , nil
}
2021-05-15 05:01:27 +02:00
func makeAccessLogWriter ( ) ( accessLog io . WriteCloser , err error ) {
2021-04-06 05:12:06 +02:00
if ! opts . Logger . Enabled {
2021-11-09 20:18:26 +02:00
return nopWriteCloser { io . Discard } , nil
2021-04-06 05:12:06 +02:00
}
2021-05-15 05:01:27 +02:00
maxSize , perr := sizeParse ( opts . Logger . MaxSize )
if perr != nil {
return nil , fmt . Errorf ( "can't parse logger MaxSize: %w" , perr )
}
2021-05-15 05:03:16 +02:00
maxSize /= 1048576
2021-05-15 05:01:27 +02:00
log . Printf ( "[INFO] logger enabled for %s, max size %dM" , opts . Logger . FileName , maxSize )
2021-04-06 05:12:06 +02:00
return & lumberjack . Logger {
Filename : opts . Logger . FileName ,
2021-05-15 05:01:27 +02:00
MaxSize : int ( maxSize ) , // in MB
2021-04-07 06:18:27 +02:00
MaxBackups : opts . Logger . MaxBackups ,
2021-04-06 05:12:06 +02:00
Compress : true ,
LocalTime : true ,
2021-05-15 05:01:27 +02:00
} , nil
2021-04-06 05:12:06 +02:00
}
2021-05-04 04:40:21 +02:00
// listenAddress sets default to 127.0.0.0:8080/80443 and, if detected REPROXY_IN_DOCKER env, to 0.0.0.0:80/443
func listenAddress ( addr , sslType string ) string {
// don't set default if any opts.Listen address defined by user
if addr != "" {
return addr
}
// http, set default to 8080 in docker, 80 without
if sslType == "none" {
if v , ok := os . LookupEnv ( "REPROXY_IN_DOCKER" ) ; ok && ( v == "1" || v == "true" ) {
return "0.0.0.0:8080"
}
return "127.0.0.1:80"
}
// https, set default to 8443 in docker, 443 without
if v , ok := os . LookupEnv ( "REPROXY_IN_DOCKER" ) ; ok && ( v == "1" || v == "true" ) {
2021-05-04 04:52:08 +02:00
return "0.0.0.0:8443"
2021-05-04 04:40:21 +02:00
}
2021-05-04 04:52:08 +02:00
return "127.0.0.1:443"
2021-05-04 04:40:21 +02:00
}
func redirHTTPPort ( port int ) int {
// don't set default if any ssl.http-port defined by user
if port != 0 {
return port
}
if v , ok := os . LookupEnv ( "REPROXY_IN_DOCKER" ) ; ok && ( v == "1" || v == "true" ) {
return 8080
}
return 80
}
2021-05-19 20:39:49 +02:00
// fqdns cleans space suffixes and prefixes which can sneak in from docker compose
func fqdns ( inp [ ] string ) ( res [ ] string ) {
for _ , v := range inp {
res = append ( res , strings . TrimSpace ( v ) )
}
return res
}
2021-05-15 05:01:27 +02:00
func sizeParse ( inp string ) ( uint64 , error ) {
if inp == "" {
return 0 , errors . New ( "empty value" )
}
for i , sfx := range [ ] string { "k" , "m" , "g" , "t" } {
if strings . HasSuffix ( inp , strings . ToUpper ( sfx ) ) || strings . HasSuffix ( inp , strings . ToLower ( sfx ) ) {
val , err := strconv . Atoi ( inp [ : len ( inp ) - 1 ] )
if err != nil {
return 0 , fmt . Errorf ( "can't parse %s: %w" , inp , err )
}
return uint64 ( float64 ( val ) * math . Pow ( float64 ( 1024 ) , float64 ( i + 1 ) ) ) , nil
}
}
return strconv . ParseUint ( inp , 10 , 64 )
}
2021-07-08 09:31:27 +02:00
// splitAtCommas split s at commas, ignoring commas in strings.
2021-07-08 23:27:42 +02:00
// Eliminate leading and trailing dbl quotes in each element only if both presented
2021-07-08 09:31:27 +02:00
// based on https://stackoverflow.com/a/59318708
func splitAtCommas ( s string ) [ ] string {
2021-07-08 23:27:42 +02:00
cleanup := func ( s string ) string {
2021-07-08 23:40:14 +02:00
if s == "" {
2021-07-08 23:27:42 +02:00
return s
}
res := strings . TrimSpace ( s )
2021-07-09 22:20:45 +02:00
if res [ 0 ] == '"' && res [ len ( res ) - 1 ] == '"' {
2021-07-08 23:27:42 +02:00
res = strings . TrimPrefix ( res , ` " ` )
res = strings . TrimSuffix ( res , ` " ` )
}
return res
}
2021-07-08 09:31:27 +02:00
var res [ ] string
var beg int
var inString bool
for i := 0 ; i < len ( s ) ; i ++ {
if s [ i ] == ',' && ! inString {
2021-07-08 23:27:42 +02:00
res = append ( res , cleanup ( s [ beg : i ] ) )
2021-07-08 09:31:27 +02:00
beg = i + 1
continue
}
if s [ i ] == '"' {
if ! inString {
inString = true
2021-07-08 23:27:42 +02:00
} else if i > 0 && s [ i - 1 ] != '\\' { // also allow \"
2021-07-08 09:31:27 +02:00
inString = false
}
}
}
2021-07-08 23:27:42 +02:00
res = append ( res , cleanup ( s [ beg : ] ) )
2021-07-08 09:31:27 +02:00
if len ( res ) == 1 && res [ 0 ] == "" {
return [ ] string { }
}
return res
}
2021-04-06 05:12:06 +02:00
type nopWriteCloser struct { io . Writer }
func ( n nopWriteCloser ) Close ( ) error { return nil }
2021-04-03 07:22:54 +02:00
func setupLog ( dbg bool ) {
2021-04-01 09:37:28 +02:00
if dbg {
2021-04-03 07:22:54 +02:00
log . Setup ( log . Debug , log . CallerFile , log . CallerFunc , log . Msec , log . LevelBraces )
return
2021-04-01 09:37:28 +02:00
}
2021-04-03 07:22:54 +02:00
log . Setup ( log . Msec , log . LevelBraces )
2021-04-01 09:37:28 +02:00
}