From 4c051ca37f376be287ce5c37cf349df7cce6bacc Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 3 May 2021 21:40:21 -0500 Subject: [PATCH] detect in-container and set listen address to 0.0.0.0 (#62) * detect in-container and set listen to 0.0.0.0 * simplify default address logic don't change if user defined, use 127.0.0.1:8080 for non-docker and 0.0.0.0:8080 for in-docker only if nothing set * add dynamic default to redir http port * add docs about dynamic defaults * add ssl example * lint: params warn --- README.md | 16 +++++++-- app/main.go | 49 +++++++++++++++++++++++---- app/main_test.go | 59 +++++++++++++++++++++++++++++++++ examples/ssl/README.md | 9 +++++ examples/ssl/docker-compose.yml | 50 ++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 examples/ssl/README.md create mode 100644 examples/ssl/docker-compose.yml diff --git a/README.md b/README.md index 7bf361a..1c284a6 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,16 @@ There are two ways to set cache duration: - `--timeout.*` various timeouts for both server and proxy transport. See `timeout` section in [All Application Options](#all-application-options) +## Default ports + +In order to eliminate the need to pass custom params/environment, the default `--listen` is dynamic and trying to be reasonable and helpful for the typical cases: + +- If anything set by users to `--listen` all the logic below ignored and host:port passed in and used directly. +- If nothing set by users to `--listen` and reproxy runs outside of the docker container, the default is `127.0.0.1:80` for http mode (`ssl.type=none`) and `127.0.0.1:443` for ssl mode (`ssl.type=auto` or `ssl.type=static`). +- If nothing set by users to `--listen` and reproxy runs inside the docker, the default is `0.0.0.0:8080` for http mode, and `0.0.0.0:8443` for ssl mode. + +Another default set in the similar dynamic way is `-ssl.http-port`. For run inside of the docker container it set to `8080` and without to `80`. + ## Ping and health checks reproxy provides 2 endpoints for this purpose: @@ -170,7 +180,8 @@ Reproxy returns 502 (Bad Gateway) error in case if request doesn't match to any ## All Application Options ``` - -l, --listen= listen on host:port (default: 127.0.0.1:8080) [$LISTEN] +Application Options: + -l, --listen= listen on host:port (default: 0.0.0.0:8080/8443 under docker, 127.0.0.1:80/443 without) [$LISTEN] -m, --max= max request size (default: 64000) [$MAX_SIZE] -g, --gzip enable gz compression [$GZIP] -x, --header= proxy headers [$HEADER] @@ -183,7 +194,7 @@ ssl: --ssl.key= path to key.pem file [$SSL_KEY] --ssl.acme-location= dir where certificates will be stored by autocert manager (default: ./var/acme) [$SSL_ACME_LOCATION] --ssl.acme-email= admin email for certificate notifications [$SSL_ACME_EMAIL] - --ssl.http-port= http port for redirect to https and acme challenge test (default: 80) [$SSL_HTTP_PORT] + --ssl.http-port= http port for redirect to https and acme challenge test (default: 8080 under docker, 80 without) [$SSL_HTTP_PORT] --ssl.fqdn= FQDN(s) for ACME certificates [$SSL_ACME_FQDN] assets: @@ -237,7 +248,6 @@ error: Help Options: -h, --help Show this help message - ``` ## Status diff --git a/app/main.go b/app/main.go index f87f3e9..bfc7a2e 100644 --- a/app/main.go +++ b/app/main.go @@ -24,7 +24,7 @@ import ( ) var opts struct { - Listen string `short:"l" long:"listen" env:"LISTEN" default:"127.0.0.1:8080" description:"listen on host:port"` + 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 int64 `short:"m" long:"max" env:"MAX_SIZE" default:"64000" description:"max request 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:","` @@ -35,7 +35,7 @@ var opts struct { 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"` - RedirHTTPPort int `long:"http-port" env:"HTTP_PORT" default:"80" description:"http port for redirect to https and acme challenge test"` + 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)"` FQDNs []string `long:"fqdn" env:"ACME_FQDN" env-delim:"," description:"FQDN(s) for ACME certificates"` } `group:"ssl" namespace:"ssl" env-namespace:"SSL"` @@ -111,7 +111,7 @@ func main() { if err.(*flags.Error).Type != flags.ErrHelp { log.Printf("[ERROR] cli error: %v", err) } - os.Exit(1) + os.Exit(2) } setupLog(opts.Dbg) @@ -194,10 +194,13 @@ func run() error { return fmt.Errorf("failed to make error reporter: %w", err) } + addr := listenAddress(opts.Listen, opts.SSL.Type) + log.Printf("[DEBUG] listen address %s", addr) + px := &proxy.Http{ Version: revision, Matcher: svc, - Address: opts.Listen, + Address: addr, MaxBodySize: opts.MaxSize, AssetsLocation: opts.Assets.Location, AssetsWebRoot: opts.Assets.WebRoot, @@ -286,13 +289,13 @@ func makeSSLConfig() (config proxy.SSLConfig, err error) { config.SSLMode = proxy.SSLStatic config.Cert = opts.SSL.Cert config.Key = opts.SSL.Key - config.RedirHTTPPort = opts.SSL.RedirHTTPPort + config.RedirHTTPPort = redirHTTPPort(opts.SSL.RedirHTTPPort) case "auto": config.SSLMode = proxy.SSLAuto config.ACMELocation = opts.SSL.ACMELocation config.ACMEEmail = opts.SSL.ACMEEmail config.FQDNs = opts.SSL.FQDNs - config.RedirHTTPPort = opts.SSL.RedirHTTPPort + config.RedirHTTPPort = redirHTTPPort(opts.SSL.RedirHTTPPort) } return config, err } @@ -325,6 +328,40 @@ func makeAccessLogWriter() (accessLog io.WriteCloser) { } } +// 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") { + return "0.0.0.0:443" + } + return "127.0.0.1:8443" +} + +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 +} + type nopWriteCloser struct{ io.Writer } func (n nopWriteCloser) Close() error { return nil } diff --git a/app/main_test.go b/app/main_test.go index 2370843..999e748 100644 --- a/app/main_test.go +++ b/app/main_test.go @@ -174,3 +174,62 @@ func waitForHTTPServerStart(port int) { } } } + +func Test_listenAddress(t *testing.T) { + + tbl := []struct { + addr string + sslType string + env string + res string + }{ + {"", "none", "1", "0.0.0.0:8080"}, + {"", "none", "0", "127.0.0.1:80"}, + {"", "auto", "false", "127.0.0.1:8443"}, + {"", "auto", "true", "0.0.0.0:443"}, + {"127.0.0.1:8081", "none", "true", "127.0.0.1:8081"}, + {"192.168.1.1:8081", "none", "false", "192.168.1.1:8081"}, + {"127.0.0.1:8080", "none", "0", "127.0.0.1:8080"}, + {"127.0.0.1:8443", "auto", "true", "127.0.0.1:8443"}, + } + + defer os.Unsetenv("REPROXY_IN_DOCKER") + + for i, tt := range tbl { + t.Run(strconv.Itoa(i), func(t *testing.T) { + assert.NoError(t, os.Unsetenv("REPROXY_IN_DOCKER")) + if tt.env != "" { + assert.NoError(t, os.Setenv("REPROXY_IN_DOCKER", tt.env)) + } + assert.Equal(t, tt.res, listenAddress(tt.addr, tt.sslType)) + }) + } + +} + +func Test_redirHTTPPort(t *testing.T) { + tbl := []struct { + port int + env string + res int + }{ + {0, "1", 8080}, + {0, "0", 80}, + {0, "true", 8080}, + {0, "false", 80}, + {1234, "true", 1234}, + {1234, "false", 1234}, + } + + defer os.Unsetenv("REPROXY_IN_DOCKER") + + for i, tt := range tbl { + t.Run(strconv.Itoa(i), func(t *testing.T) { + assert.NoError(t, os.Unsetenv("REPROXY_IN_DOCKER")) + if tt.env != "" { + assert.NoError(t, os.Setenv("REPROXY_IN_DOCKER", tt.env)) + } + assert.Equal(t, tt.res, redirHTTPPort(tt.port)) + }) + } +} diff --git a/examples/ssl/README.md b/examples/ssl/README.md new file mode 100644 index 0000000..5021d71 --- /dev/null +++ b/examples/ssl/README.md @@ -0,0 +1,9 @@ +# Example of a docker provider with an automatic SSL (Let's Encrypt) + +This example should run on the machine with resolvable FQDN. All files use example.com, make sure you **replace it with your domain**. + +run this example with `docker-compose up` and try to hit containers: + +- `curl https://example.com/api/svc1/123` +- `curl http://example.com/api/svc2/345` +- `curl http://example.com/whoami/test` diff --git a/examples/ssl/docker-compose.yml b/examples/ssl/docker-compose.yml new file mode 100644 index 0000000..fd0067b --- /dev/null +++ b/examples/ssl/docker-compose.yml @@ -0,0 +1,50 @@ +services: + reproxy: + image: umputun/reproxy:master + container_name: reproxy + hostname: reproxy + ports: + - "80:8080" + - "443:8443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./web:/web + environment: + - TZ=America/Chicago + - DOCKER_ENABLED=true + - SSL_TYPE=auto + - SSL_ACME_FQDN=example.com <-- replace it + - HEADER= + X-Frame-Options:SAMEORIGIN, + X-XSS-Protection:1; mode=block;, + Content-Security-Policy:default-src 'self'; style-src 'self' 'unsafe-inline'; + + # automatic destination, will be mapped for ^/api/svc1/(.*) + svc1: + image: ghcr.io/umputun/echo-http + hostname: svc1 + container_name: svc1 + command: --message="hello world from svc1" + labels: + reproxy.route: '^/svc1/(.*)' + reproxy.dest: '/@1' + + + # explicit destination, will be mapped for ^/api/svc2/(.*) + svc2: + image: ghcr.io/umputun/echo-http + hostname: svc2 + container_name: svc2 + command: --message="hello world from svc2" + labels: + reproxy.route: '^/svc2/(.*)' + reproxy.dest: '/@1' + + # explicit destination, routing match defined by lables + whoami: + image: 'containous/whoami' + hostname: whoami + container_name: whoami + labels: + reproxy.route: '^/whoami/(.*)' + reproxy.dest: '/@1' \ No newline at end of file