WIP: add 3 types of server

This commit is contained in:
Umputun
2021-04-02 03:13:49 -05:00
parent 7c08a09053
commit 96e5cd2e6b
62 changed files with 68247 additions and 2 deletions

View File

@@ -2,15 +2,17 @@ package proxy
import (
"context"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/go-pkgz/lgr"
log "github.com/go-pkgz/lgr"
"github.com/go-pkgz/rest"
"github.com/go-pkgz/rest/logger"
"github.com/umputun/docker-proxy/app/proxy/middleware"
)
@@ -23,6 +25,7 @@ type Http struct {
MaxBodySize int64
GzEnabled bool
ProxyHeaders []string
SSLConfig SSLConfig
Version string
}
@@ -39,6 +42,7 @@ func (h *Http) Do(ctx context.Context) error {
httpServer := &http.Server{
Addr: h.Address,
Handler: h.wrap(h.proxyHandler(),
rest.Recoverer(lgr.Default()),
rest.AppInfo("dpx", "umputun", h.Version),
rest.Ping,
logger.New(logger.Prefix("[DEBUG] PROXY")).Handler,
@@ -61,6 +65,83 @@ func (h *Http) Do(ctx context.Context) error {
return httpServer.ListenAndServe()
}
// Run the lister and request's router, activate rest server
func (h *Http) Run(ctx context.Context) {
var httpServer, httpsServer *http.Server
go func() {
<-ctx.Done()
if httpServer != nil {
if err := httpServer.Close(); err != nil {
log.Printf("[ERROR] failed to close proxy http server, %v", err)
}
}
if httpsServer != nil {
if err := httpsServer.Close(); err != nil {
log.Printf("[ERROR] failed to close proxy https server, %v", err)
}
}
}()
handler := h.wrap(h.proxyHandler(),
rest.Recoverer(lgr.Default()),
rest.AppInfo("dpx", "umputun", h.Version),
rest.Ping,
logger.New(logger.Prefix("[DEBUG] PROXY")).Handler,
rest.SizeLimit(h.MaxBodySize),
middleware.Headers(h.ProxyHeaders...),
h.gzipHandler(),
)
switch h.SSLConfig.SSLMode {
case None:
log.Printf("[INFO] activate http proxy server on %s", h.Address)
httpServer = h.makeHTTPServer(h.Address, handler)
httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
err := httpServer.ListenAndServe()
log.Printf("[WARN] http server terminated, %s", err)
case Static:
log.Printf("[INFO] activate https server in 'static' mode on %s", h.Address)
httpsServer = h.makeHTTPSServer(h.Address, handler)
httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
httpServer = h.makeHTTPServer(h.toHttp(h.Address), h.httpToHTTPSRouter())
httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
go func() {
log.Printf("[INFO] activate http redirect server on %s", h.toHttp(h.Address))
err := httpServer.ListenAndServe()
log.Printf("[WARN] http redirect server terminated, %s", err)
}()
err := httpServer.ListenAndServeTLS(h.SSLConfig.Cert, h.SSLConfig.Key)
log.Printf("[WARN] https server terminated, %s", err)
case Auto:
log.Printf("[INFO] activate https server in 'auto' mode on %s", h.Address)
m := h.makeAutocertManager()
httpsServer = h.makeHTTPSAutocertServer(h.Address, handler, m)
httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
httpServer = h.makeHTTPServer(h.toHttp(h.Address), h.httpChallengeRouter(m))
httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
go func() {
log.Printf("[INFO] activate http challenge server on port %s", h.toHttp(h.Address))
err := httpServer.ListenAndServe()
log.Printf("[WARN] http challenge server terminated, %s", err)
}()
err := httpsServer.ListenAndServeTLS("", "")
log.Printf("[WARN] https server terminated, %s", err)
}
}
func (h *Http) toHttp(address string) string {
return strings.Replace(address, ":443", ":80", 1)
}
func (h *Http) gzipHandler() func(next http.Handler) http.Handler {
gzHandler := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -118,7 +199,10 @@ func (h *Http) proxyHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
server := strings.Split(r.Host, ":")[0]
server := r.URL.Hostname()
if server == "" {
server = strings.Split(r.Host, ":")[0]
}
u, ok := h.Match(server, r.URL.Path)
if !ok {
assetsHandler.ServeHTTP(w, r)
@@ -135,3 +219,13 @@ func (h *Http) proxyHandler() http.HandlerFunc {
reverseProxy.ServeHTTP(w, r.WithContext(ctx))
}
}
func (h *Http) makeHTTPServer(addr string, router http.Handler) *http.Server {
return &http.Server{
Addr: addr,
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 30 * time.Second,
}
}

108
app/proxy/ssl.go Normal file
View File

@@ -0,0 +1,108 @@
package proxy
import (
"crypto/tls"
"fmt"
"net/http"
"strings"
log "github.com/go-pkgz/lgr"
"golang.org/x/crypto/acme/autocert"
R "github.com/go-pkgz/rest"
)
// sslMode defines ssl mode for rest server
type sslMode int8
const (
// None defines to run http server only
None sslMode = iota
// Static defines to run both https and http server. Redirect http to https
Static
// Auto defines to run both https and http server. Redirect http to https. Https server with autocert support
Auto
)
// SSLConfig holds all ssl params for rest server
type SSLConfig struct {
SSLMode sslMode
Cert string
Key string
Port int
ACMELocation string
ACMEEmail string
FQDNs []string
}
// httpToHTTPSRouter creates new router which does redirect from http to https server
// with default middlewares. Used in 'static' ssl mode.
func (h *Http) httpToHTTPSRouter() http.Handler {
log.Printf("[DEBUG] create https-to-http redirect routes")
return h.wrap(h.redirectHandler(), R.Recoverer(log.Default()))
}
// httpChallengeRouter creates new router which performs ACME "http-01" challenge response
// with default middlewares. This part is necessary to obtain certificate from LE.
// If it receives not a acme challenge it performs redirect to https server.
// Used in 'auto' ssl mode.
func (h *Http) httpChallengeRouter(m *autocert.Manager) http.Handler {
log.Printf("[DEBUG] create http-challenge routes")
return h.wrap(m.HTTPHandler(h.redirectHandler()), R.Recoverer(log.Default()))
}
func (h *Http) redirectHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := strings.Split(r.Host, ":")[0]
newURL := fmt.Sprintf("https://%s:443%s", server, r.URL.Path)
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusTemporaryRedirect)
})
}
func (h *Http) makeAutocertManager() *autocert.Manager {
return &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(h.SSLConfig.ACMELocation),
HostPolicy: autocert.HostWhitelist(h.SSLConfig.FQDNs...),
Email: h.SSLConfig.ACMEEmail,
}
}
// makeHTTPSAutoCertServer makes https server with autocert mode (LE support)
func (h *Http) makeHTTPSAutocertServer(address string, router http.Handler, m *autocert.Manager) *http.Server {
server := h.makeHTTPServer(address, router)
cfg := h.makeTLSConfig()
cfg.GetCertificate = m.GetCertificate
server.TLSConfig = cfg
return server
}
// makeHTTPSServer makes https server for static mode
func (h *Http) makeHTTPSServer(address string, router http.Handler) *http.Server {
server := h.makeHTTPServer(address, router)
server.TLSConfig = h.makeTLSConfig()
return server
}
func (h *Http) makeTLSConfig() *tls.Config {
return &tls.Config{
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
tls.CurveP384,
},
}
}

91
app/proxy/ssl_test.go Normal file
View File

@@ -0,0 +1,91 @@
package proxy
import (
"context"
"crypto/tls"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSSL_Redirect(t *testing.T) {
p := Http{}
ts := httptest.NewServer(p.httpToHTTPSRouter())
defer ts.Close()
client := http.Client{
// prevent http redirect
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
// allow self-signed certificate
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// check http to https redirect response
resp, err := client.Get(strings.Replace(ts.URL, "127.0.0.1", "localhost", 1) + "/blah?param=1")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 307, resp.StatusCode)
assert.Equal(t, "https://localhost:443/blah?param=1", resp.Header.Get("Location"))
}
func TestSSL_ACME_HTTPChallengeRouter(t *testing.T) {
p := Http{
SSLConfig: SSLConfig{
ACMELocation: "acme",
FQDNs: []string{"example.com", "localhost"},
},
}
m := p.makeAutocertManager()
defer os.RemoveAll(p.SSLConfig.ACMELocation)
ts := httptest.NewServer(p.httpChallengeRouter(m))
defer ts.Close()
client := http.Client{
// prevent http redirect
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
lh := strings.Replace(ts.URL, "127.0.0.1", "localhost", 1)
// check http to https redirect response
resp, err := client.Get(lh + "/blah?param=1")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 307, resp.StatusCode)
assert.Equal(t, "https://localhost:443/blah?param=1", resp.Header.Get("Location"))
// check acme http challenge
req, err := http.NewRequest("GET", lh+"/.well-known/acme-challenge/token123", nil)
require.NoError(t, err)
req.Host = "localhost" // for passing hostPolicy check
resp, err = client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 404, resp.StatusCode)
err = m.Cache.Put(context.Background(), "token123+http-01", []byte("token"))
assert.NoError(t, err)
resp, err = client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "token", string(body))
}