2021-04-01 02:37:28 -05:00
|
|
|
package proxy
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-04-05 03:37:28 -05:00
|
|
|
"fmt"
|
2021-04-04 02:57:34 -05:00
|
|
|
"net"
|
2021-04-01 02:37:28 -05:00
|
|
|
"net/http"
|
|
|
|
"net/http/httputil"
|
|
|
|
"net/url"
|
2021-04-03 01:20:24 -05:00
|
|
|
"regexp"
|
|
|
|
"strconv"
|
2021-04-02 00:07:36 -05:00
|
|
|
"strings"
|
2021-04-05 03:37:28 -05:00
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
2021-04-01 02:37:28 -05:00
|
|
|
"time"
|
|
|
|
|
2021-04-02 03:13:49 -05:00
|
|
|
"github.com/go-pkgz/lgr"
|
|
|
|
log "github.com/go-pkgz/lgr"
|
2021-04-01 02:37:28 -05:00
|
|
|
"github.com/go-pkgz/rest"
|
2021-04-03 00:22:54 -05:00
|
|
|
R "github.com/go-pkgz/rest"
|
2021-04-01 02:37:28 -05:00
|
|
|
"github.com/go-pkgz/rest/logger"
|
2021-04-03 00:22:54 -05:00
|
|
|
"github.com/pkg/errors"
|
2021-04-05 03:37:28 -05:00
|
|
|
"github.com/umputun/reproxy/app/discovery"
|
2021-04-01 02:37:28 -05:00
|
|
|
)
|
|
|
|
|
2021-04-03 01:20:24 -05:00
|
|
|
// Http is a proxy server for both http and https
|
2021-04-01 02:37:28 -05:00
|
|
|
type Http struct {
|
|
|
|
Matcher
|
|
|
|
Address string
|
|
|
|
TimeOut time.Duration
|
|
|
|
AssetsLocation string
|
|
|
|
AssetsWebRoot string
|
|
|
|
MaxBodySize int64
|
|
|
|
GzEnabled bool
|
2021-04-01 02:53:42 -05:00
|
|
|
ProxyHeaders []string
|
2021-04-02 03:13:49 -05:00
|
|
|
SSLConfig SSLConfig
|
2021-04-01 02:37:28 -05:00
|
|
|
Version string
|
|
|
|
}
|
|
|
|
|
2021-04-03 01:20:24 -05:00
|
|
|
// Matcher source info (server and route) to the destination url
|
|
|
|
// If no match found return ok=false
|
2021-04-01 02:37:28 -05:00
|
|
|
type Matcher interface {
|
2021-04-02 00:07:36 -05:00
|
|
|
Match(srv, src string) (string, bool)
|
2021-04-04 15:55:06 -05:00
|
|
|
Servers() (servers []string)
|
2021-04-05 03:37:28 -05:00
|
|
|
Mappers() (mappers []discovery.UrlMapper)
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
2021-04-03 00:22:54 -05:00
|
|
|
// Run the lister and request's router, activate rest server
|
|
|
|
func (h *Http) Run(ctx context.Context) error {
|
2021-04-01 02:37:28 -05:00
|
|
|
|
2021-04-03 00:22:54 -05:00
|
|
|
if h.AssetsLocation != "" {
|
|
|
|
log.Printf("[DEBUG] assets file server enabled for %s, webroot %s", h.AssetsLocation, h.AssetsWebRoot)
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
2021-04-02 03:13:49 -05:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2021-04-03 00:22:54 -05:00
|
|
|
handler := R.Wrap(h.proxyHandler(),
|
|
|
|
R.Recoverer(lgr.Default()),
|
|
|
|
R.AppInfo("dpx", "umputun", h.Version),
|
|
|
|
R.Ping,
|
2021-04-05 03:37:28 -05:00
|
|
|
h.healthMiddleware,
|
2021-04-02 03:13:49 -05:00
|
|
|
logger.New(logger.Prefix("[DEBUG] PROXY")).Handler,
|
2021-04-03 00:22:54 -05:00
|
|
|
R.SizeLimit(h.MaxBodySize),
|
|
|
|
R.Headers(h.ProxyHeaders...),
|
2021-04-02 03:13:49 -05:00
|
|
|
h.gzipHandler(),
|
|
|
|
)
|
|
|
|
|
2021-04-04 15:55:06 -05:00
|
|
|
h.SSLConfig.FQDNs = h.Servers() // fill all servers
|
2021-04-02 03:13:49 -05:00
|
|
|
switch h.SSLConfig.SSLMode {
|
2021-04-03 00:22:54 -05:00
|
|
|
case SSLNone:
|
2021-04-02 03:13:49 -05:00
|
|
|
log.Printf("[INFO] activate http proxy server on %s", h.Address)
|
|
|
|
httpServer = h.makeHTTPServer(h.Address, handler)
|
|
|
|
httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
2021-04-03 00:22:54 -05:00
|
|
|
return httpServer.ListenAndServe()
|
|
|
|
case SSLStatic:
|
2021-04-02 03:13:49 -05:00
|
|
|
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")
|
|
|
|
|
2021-04-03 01:20:24 -05:00
|
|
|
httpServer = h.makeHTTPServer(h.toHttp(h.Address, h.SSLConfig.RedirHttpPort), h.httpToHTTPSRouter())
|
2021-04-02 03:13:49 -05:00
|
|
|
httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
|
|
|
|
|
|
|
go func() {
|
2021-04-03 01:20:24 -05:00
|
|
|
log.Printf("[INFO] activate http redirect server on %s", h.toHttp(h.Address, h.SSLConfig.RedirHttpPort))
|
2021-04-02 03:13:49 -05:00
|
|
|
err := httpServer.ListenAndServe()
|
|
|
|
log.Printf("[WARN] http redirect server terminated, %s", err)
|
|
|
|
}()
|
2021-04-03 00:22:54 -05:00
|
|
|
return httpServer.ListenAndServeTLS(h.SSLConfig.Cert, h.SSLConfig.Key)
|
|
|
|
case SSLAuto:
|
2021-04-02 03:13:49 -05:00
|
|
|
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")
|
|
|
|
|
2021-04-03 01:20:24 -05:00
|
|
|
httpServer = h.makeHTTPServer(h.toHttp(h.Address, h.SSLConfig.RedirHttpPort), h.httpChallengeRouter(m))
|
2021-04-02 03:13:49 -05:00
|
|
|
httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
|
|
|
|
|
|
|
|
go func() {
|
2021-04-03 01:20:24 -05:00
|
|
|
log.Printf("[INFO] activate http challenge server on port %s", h.toHttp(h.Address, h.SSLConfig.RedirHttpPort))
|
2021-04-02 03:13:49 -05:00
|
|
|
err := httpServer.ListenAndServe()
|
|
|
|
log.Printf("[WARN] http challenge server terminated, %s", err)
|
|
|
|
}()
|
|
|
|
|
2021-04-03 00:22:54 -05:00
|
|
|
return httpsServer.ListenAndServeTLS("", "")
|
2021-04-02 03:13:49 -05:00
|
|
|
}
|
2021-04-03 00:22:54 -05:00
|
|
|
return errors.Errorf("unknown SSL type %v", h.SSLConfig.SSLMode)
|
2021-04-02 03:13:49 -05:00
|
|
|
}
|
|
|
|
|
2021-04-03 01:20:24 -05:00
|
|
|
func (h *Http) toHttp(address string, httpPort int) string {
|
|
|
|
rx := regexp.MustCompile(`(.*):(\d*)`)
|
|
|
|
return rx.ReplaceAllString(address, "$1:") + strconv.Itoa(httpPort)
|
2021-04-02 03:13:49 -05:00
|
|
|
}
|
|
|
|
|
2021-04-01 02:37:28 -05:00
|
|
|
func (h *Http) gzipHandler() func(next http.Handler) http.Handler {
|
|
|
|
if h.GzEnabled {
|
2021-04-04 02:39:42 -05:00
|
|
|
return R.Gzip()
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
2021-04-03 00:22:54 -05:00
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Http) proxyHandler() http.HandlerFunc {
|
|
|
|
type contextKey string
|
|
|
|
|
|
|
|
reverseProxy := &httputil.ReverseProxy{
|
|
|
|
Director: func(r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
uu := ctx.Value(contextKey("url")).(*url.URL)
|
|
|
|
r.URL.Path = uu.Path
|
|
|
|
r.URL.Host = uu.Host
|
|
|
|
r.URL.Scheme = uu.Scheme
|
|
|
|
r.Header.Add("X-Forwarded-Host", uu.Host)
|
|
|
|
r.Header.Add("X-Origin-Host", r.Host)
|
2021-04-04 02:57:34 -05:00
|
|
|
h.setXRealIP(r)
|
2021-04-01 02:37:28 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
reverseProxy.Transport = http.DefaultTransport
|
|
|
|
reverseProxy.Transport.(*http.Transport).ResponseHeaderTimeout = h.TimeOut
|
|
|
|
|
|
|
|
// default assetsHandler disabled, returns error on missing matches
|
|
|
|
assetsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
log.Printf("[WARN] mo match for %s", r.URL)
|
|
|
|
http.Error(w, "Server error", http.StatusBadGateway)
|
|
|
|
})
|
|
|
|
|
|
|
|
if h.AssetsLocation != "" && h.AssetsWebRoot != "" {
|
|
|
|
fs, err := rest.FileServer(h.AssetsWebRoot, h.AssetsLocation)
|
|
|
|
if err == nil {
|
|
|
|
assetsHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
fs.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
2021-04-02 03:13:49 -05:00
|
|
|
server := r.URL.Hostname()
|
|
|
|
if server == "" {
|
|
|
|
server = strings.Split(r.Host, ":")[0]
|
|
|
|
}
|
2021-04-02 00:07:36 -05:00
|
|
|
u, ok := h.Match(server, r.URL.Path)
|
2021-04-01 02:37:28 -05:00
|
|
|
if !ok {
|
|
|
|
assetsHandler.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
uu, err := url.Parse(u)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "Server error", http.StatusBadGateway)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := context.WithValue(r.Context(), contextKey("url"), uu) // set destination url in request's context
|
|
|
|
reverseProxy.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
}
|
|
|
|
}
|
2021-04-02 03:13:49 -05:00
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
2021-04-04 02:57:34 -05:00
|
|
|
|
|
|
|
func (h *Http) setXRealIP(r *http.Request) {
|
|
|
|
|
|
|
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
userIP := net.ParseIP(ip)
|
|
|
|
if userIP == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
r.Header.Add("X-Real-IP", ip)
|
|
|
|
}
|
2021-04-05 03:37:28 -05:00
|
|
|
|
|
|
|
func (h *Http) healthMiddleware(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/health") {
|
|
|
|
h.healthHandler(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
return http.HandlerFunc(fn)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Http) healthHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
// runs pings in parallel
|
|
|
|
check := func(mappers []discovery.UrlMapper) (ok bool, valid int, total int, errs []string) {
|
|
|
|
outCh := make(chan error, 8)
|
|
|
|
var pinged int32
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, m := range mappers {
|
|
|
|
if m.PingURL == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
wg.Add(1)
|
|
|
|
go func(m discovery.UrlMapper) {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
atomic.AddInt32(&pinged, 1)
|
|
|
|
client := http.Client{Timeout: 100 * time.Millisecond}
|
|
|
|
resp, err := client.Get(m.PingURL)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[WARN] failed to ping for health %s, %v", m.PingURL, err)
|
|
|
|
outCh <- fmt.Errorf("%s, %v", m.PingURL, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
log.Printf("[WARN] failed ping status for health %s (%s)", m.PingURL, resp.Status)
|
|
|
|
outCh <- fmt.Errorf("%s, %s", m.PingURL, resp.Status)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}(m)
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
wg.Wait()
|
|
|
|
close(outCh)
|
|
|
|
}()
|
|
|
|
|
|
|
|
for e := range outCh {
|
|
|
|
errs = append(errs, e.Error())
|
|
|
|
}
|
|
|
|
return len(errs) == 0, int(atomic.LoadInt32(&pinged)) - len(errs), len(mappers), errs
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
|
|
|
ok, valid, total, errs := check(h.Mappers())
|
|
|
|
if !ok {
|
|
|
|
w.WriteHeader(http.StatusExpectationFailed)
|
|
|
|
_, err := fmt.Fprintf(w, `{"status": "failed", "passed": %d, "failed":%d, "errors": "%+v"}`, valid, total-valid, errs)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[WARN] failed %v", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
_, err := fmt.Fprintf(w, `{"status": "ok", "services": %d}`, valid)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[WARN] failed to send halth, %v", err)
|
|
|
|
}
|
|
|
|
}
|