2021-04-01 09:37:28 +02:00
package main
import (
"context"
"fmt"
2021-04-06 05:12:06 +02:00
"io"
2021-04-07 06:50:04 +02:00
"io/ioutil"
2021-04-01 09:37:28 +02:00
"os"
"os/signal"
"runtime"
2021-04-12 09:07:35 +02:00
"strings"
2021-04-01 09:37:28 +02:00
"syscall"
"time"
docker "github.com/fsouza/go-dockerclient"
2021-04-03 07:22:54 +02:00
log "github.com/go-pkgz/lgr"
2021-04-01 09:37:28 +02:00
"github.com/pkg/errors"
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"
"github.com/umputun/reproxy/app/proxy"
2021-04-01 09:37:28 +02:00
)
var opts struct {
2021-04-13 04:54:59 +02:00
Listen string ` short:"l" long:"listen" env:"LISTEN" default:"127.0.0.1:8080" description:"listen on host:port" `
MaxSize int64 ` short:"m" long:"max" env:"MAX_SIZE" default:"64000" description:"max response size" `
GzipEnabled bool ` short:"g" long:"gzip" env:"GZIP" description:"enable gz compression" `
ProxyHeaders [ ] string ` short:"x" long:"header" env:"HEADER" description:"proxy headers" env-delim:"," `
2021-04-01 09:37:28 +02:00
2021-04-03 07:22:54 +02:00
SSL struct {
2021-04-09 09:28:13 +02:00
Type string ` long:"type" env:"TYPE" description:"ssl (auto) support" choice:"none" choice:"static" choice:"auto" default:"none" ` //nolint
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-04-09 22:05:22 +02:00
RedirHTTPPort int ` long:"http-port" env:"HTTP_PORT" default:"80" description:"http port for redirect to https and acme challenge test" `
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 {
Location string ` short:"a" long:"location" env:"LOCATION" default:"" description:"assets location" `
WebRoot string ` long:"root" env:"ROOT" default:"/" description:"assets web root" `
} ` 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" `
MaxSize int ` long:"max-size" env:"MAX_SIZE" default:"100" description:"maximum size in megabytes before it gets rotated" `
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 {
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" `
2021-04-10 09:07:42 +02:00
Network string ` long:"network" env:"NETWORK" default:"" description:"docker network" `
2021-04-03 09:31:48 +02:00
Excluded [ ] string ` long:"exclude" env:"EXCLUDE" description:"excluded containers" env-delim:"," `
2021-04-12 09:07:35 +02:00
AutoAPI bool ` long:"auto" env:"AUTO" description:"enable automatic routing (without labels)" `
2021-04-01 09:37:28 +02:00
} ` group:"docker" namespace:"docker" env-namespace:"DOCKER" `
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-07 06:18:27 +02:00
NoSignature bool ` long:"no-signature" env:"NO_SIGNATURE" description:"disable 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 )
}
os . Exit ( 1 )
}
setupLog ( opts . Dbg )
catchSignal ( )
providers , err := makeProviders ( )
if err != nil {
log . Fatalf ( "[ERROR] failed to make providers, %v" , err )
}
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 {
log . Fatalf ( "[ERROR] discovery failed, %v" , e )
}
} ( )
}
2021-04-01 09:37:28 +02:00
2021-04-03 07:22:54 +02:00
sslConfig , err := makeSSLConfig ( )
if err != nil {
log . Fatalf ( "[ERROR] failed to make config of ssl server params, %v" , err )
}
2021-04-09 22:05:22 +02:00
defer func ( ) {
if x := recover ( ) ; x != nil {
log . Printf ( "[WARN] run time panic:\n%v" , x )
panic ( x )
}
} ( )
2021-04-07 06:18:27 +02:00
accessLog := makeAccessLogWriter ( )
2021-04-06 05:12:06 +02:00
defer func ( ) {
if err := accessLog . Close ( ) ; err != nil {
log . Printf ( "[WARN] can't close access log, %v" , err )
}
} ( )
2021-04-01 09:37:28 +02:00
px := & proxy . Http {
2021-04-07 06:18:27 +02:00
Version : revision ,
Matcher : svc ,
Address : opts . Listen ,
MaxBodySize : opts . MaxSize ,
AssetsLocation : opts . Assets . Location ,
AssetsWebRoot : opts . Assets . WebRoot ,
GzEnabled : opts . GzipEnabled ,
SSLConfig : sslConfig ,
ProxyHeaders : opts . ProxyHeaders ,
AccessLog : accessLog ,
2021-04-13 19:45:49 +02:00
StdOutEnabled : opts . Logger . StdOut ,
2021-04-07 06:18:27 +02:00
DisableSignature : opts . NoSignature ,
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-04-01 09:37:28 +02:00
}
2021-04-03 07:22:54 +02:00
if err := px . Run ( context . Background ( ) ) ; err != nil {
2021-04-09 22:05:22 +02:00
log . Fatalf ( "[ERROR] proxy server failed, %v" , err ) //nolint gocritic
2021-04-01 09:37:28 +02:00
}
}
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 {
client , err := docker . NewClient ( opts . Docker . Host )
if err != nil {
return nil , errors . Wrapf ( err , "failed to make docker client %s" , err )
}
2021-04-12 09:57:13 +02:00
if opts . Docker . AutoAPI {
log . Printf ( "[INFO] auto-api enabled for docker" )
}
res = append ( res , & provider . Docker { DockerClient : client , Excludes : opts . Docker . Excluded ,
Network : opts . Docker . Network , AutoAPI : opts . Docker . AutoAPI } )
2021-04-01 09:37:28 +02:00
}
2021-04-10 21:34:28 +02:00
if len ( res ) == 0 && opts . Assets . Location == "" {
2021-04-01 09:37:28 +02:00
return nil , errors . Errorf ( "no providers enabled" )
}
return res , nil
}
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-04-09 22:05:22 +02:00
config . 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-04-09 09:28:13 +02:00
config . FQDNs = opts . SSL . FQDNs
2021-04-09 22:05:22 +02:00
config . RedirHTTPPort = opts . SSL . RedirHTTPPort
2021-04-03 07:22:54 +02:00
}
return config , err
}
2021-04-01 09:37:28 +02:00
2021-04-07 06:18:27 +02:00
func makeAccessLogWriter ( ) ( accessLog io . WriteCloser ) {
2021-04-06 05:12:06 +02:00
if ! opts . Logger . Enabled {
2021-04-07 06:50:04 +02:00
return nopWriteCloser { ioutil . Discard }
2021-04-06 05:12:06 +02:00
}
log . Printf ( "[INFO] logger enabled for %s" , opts . Logger . FileName )
return & lumberjack . Logger {
Filename : opts . Logger . FileName ,
MaxSize : opts . Logger . MaxSize ,
2021-04-07 06:18:27 +02:00
MaxBackups : opts . Logger . MaxBackups ,
2021-04-06 05:12:06 +02:00
Compress : true ,
LocalTime : true ,
}
}
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
}
func catchSignal ( ) {
// catch SIGQUIT and print stack traces
sigChan := make ( chan os . Signal )
go func ( ) {
for range sigChan {
log . Print ( "[INFO] SIGQUIT detected" )
stacktrace := make ( [ ] byte , 8192 )
length := runtime . Stack ( stacktrace , true )
if length > 8192 {
length = 8192
}
fmt . Println ( string ( stacktrace [ : length ] ) )
}
} ( )
signal . Notify ( sigChan , syscall . SIGQUIT )
}