mirror of
https://github.com/kemko/reproxy.git
synced 2026-01-06 18:25:49 +03:00
WIP: add 3 types of server
This commit is contained in:
@@ -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
108
app/proxy/ssl.go
Normal 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
91
app/proxy/ssl_test.go
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user