1
0
mirror of https://github.com/umputun/reproxy.git synced 2025-06-30 22:13:42 +02:00
Files
reproxy/app/proxy/proxy.go

444 lines
14 KiB
Go
Raw Normal View History

2021-04-01 02:37:28 -05:00
package proxy
import (
"bytes"
2021-04-01 02:37:28 -05:00
"context"
"fmt"
2021-04-05 22:12:06 -05:00
"io"
"math/rand"
2021-04-04 02:57:34 -05:00
"net"
2021-04-01 02:37:28 -05:00
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
2021-04-03 01:20:24 -05:00
"regexp"
"strconv"
2021-04-02 00:07:36 -05:00
"strings"
2021-04-01 02:37:28 -05:00
"time"
2021-04-02 03:13:49 -05:00
log "github.com/go-pkgz/lgr"
2021-04-03 00:22:54 -05:00
R "github.com/go-pkgz/rest"
2021-04-13 12:45:49 -05:00
"github.com/go-pkgz/rest/logger"
2021-04-05 22:12:06 -05:00
2021-04-05 03:37:28 -05:00
"github.com/umputun/reproxy/app/discovery"
"github.com/umputun/reproxy/app/plugin"
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-13 12:45:49 -05:00
type Http struct { // nolint golint
2021-04-01 02:37:28 -05:00
Matcher
Address string
AssetsLocation string
AssetsWebRoot string
Assets404 string
AssetsSPA bool
MaxBodySize int64
GzEnabled bool
ProxyHeaders []string
DropHeader []string
SSLConfig SSLConfig
Version string
AccessLog io.Writer
StdOutEnabled bool
Signature bool
Timeouts Timeouts
CacheControl MiddlewareProvider
Metrics MiddlewareProvider
PluginConductor MiddlewareProvider
Reporter Reporter
LBSelector func(len int) int
OnlyFrom *OnlyFrom
BasicAuthEnabled bool
BasicAuthAllowed []string
ThrottleSystem int
ThrottleUser int
2021-04-01 02:37:28 -05:00
}
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 {
Match(srv, src string) (res discovery.Matches)
2021-04-04 15:55:06 -05:00
Servers() (servers []string)
2021-04-09 15:05:22 -05:00
Mappers() (mappers []discovery.URLMapper)
CheckHealth() (pingResult map[string]error)
2021-04-01 02:37:28 -05:00
}
// MiddlewareProvider interface defines http middleware handler
type MiddlewareProvider interface {
Middleware(next http.Handler) http.Handler
}
// Reporter defines error reporting service
type Reporter interface {
Report(w http.ResponseWriter, code int)
}
2021-04-12 21:54:59 -05:00
// Timeouts consolidate timeouts for both server and transport
type Timeouts struct {
// server timeouts
ReadHeader time.Duration
Write time.Duration
Idle time.Duration
// transport timeouts
Dial time.Duration
KeepAlive time.Duration
IdleConn time.Duration
TLSHandshake time.Duration
ExpectContinue time.Duration
ResponseHeader time.Duration
}
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)
if h.Assets404 != "" {
log.Printf("[DEBUG] assets 404 file enabled for %s", h.Assets404)
}
2021-04-01 02:37:28 -05:00
}
2021-05-28 16:11:16 -05:00
if h.LBSelector == nil {
h.LBSelector = rand.Intn
}
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(log.Default()), // recover on errors
signatureHandler(h.Signature, h.Version), // send app signature
h.OnlyFrom.Handler, // limit source (remote) IPs if defined
h.pingHandler, // respond to /ping
basicAuthHandler(h.BasicAuthEnabled, h.BasicAuthAllowed), // basic auth
h.healthMiddleware, // respond to /health
h.matchHandler, // set matched routes to context
limiterSystemHandler(h.ThrottleSystem), // limit total requests/sec
limiterUserHandler(h.ThrottleUser), // req/seq per user/route match
h.mgmtHandler(), // handles /metrics and /routes for prometheus
h.pluginHandler(), // prc to external plugins
headersHandler(h.ProxyHeaders, h.DropHeader), // add response headers and delete some request headers
accessLogHandler(h.AccessLog), // apache-format log file
stdoutLogHandler(h.StdOutEnabled, logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]")).Handler),
maxReqSizeHandler(h.MaxBodySize), // limit request max size
gzipHandler(h.GzEnabled), // gzip response
2021-04-02 03:13:49 -05:00
)
// no FQDNs defined, use the list of discovered servers
2021-05-05 01:41:23 -05:00
if len(h.SSLConfig.FQDNs) == 0 && h.SSLConfig.SSLMode == SSLAuto {
h.SSLConfig.FQDNs = h.discoveredServers(ctx, 50*time.Millisecond)
}
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-09 15:05:22 -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-09 15:05:22 -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-14 18:58:05 +04:00
return httpsServer.ListenAndServeTLS(h.SSLConfig.Cert, h.SSLConfig.Key)
2021-04-03 00:22:54 -05:00
case SSLAuto:
2021-04-02 03:13:49 -05:00
log.Printf("[INFO] activate https server in 'auto' mode on %s", h.Address)
log.Printf("[DEBUG] FQDNs %v", h.SSLConfig.FQDNs)
2021-04-02 03:13:49 -05:00
m := h.makeAutocertManager()
httpsServer = h.makeHTTPSAutocertServer(h.Address, handler, m)
httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
2021-04-09 15:05:22 -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-09 15:05:22 -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
}
return fmt.Errorf("unknown SSL type %v", h.SSLConfig.SSLMode)
2021-04-02 03:13:49 -05:00
}
type contextKey string
const (
ctxURL = contextKey("url")
ctxMatchType = contextKey("type")
2021-06-01 03:59:23 -05:00
ctxMatch = contextKey("match")
)
2021-04-01 02:37:28 -05:00
func (h *Http) proxyHandler() http.HandlerFunc {
reverseProxy := &httputil.ReverseProxy{
Director: func(r *http.Request) {
ctx := r.Context()
uu := ctx.Value(ctxURL).(*url.URL)
r.Header.Add("X-Forwarded-Host", r.Host)
2021-04-01 02:37:28 -05:00
r.URL.Path = uu.Path
r.URL.Host = uu.Host
r.URL.Scheme = uu.Scheme
2021-04-17 13:11:10 -05:00
r.Host = uu.Host
2021-04-04 02:57:34 -05:00
h.setXRealIP(r)
2021-04-01 02:37:28 -05:00
},
2021-04-09 20:55:21 -05:00
Transport: &http.Transport{
2021-04-12 21:54:59 -05:00
ResponseHeaderTimeout: h.Timeouts.ResponseHeader,
2021-04-09 20:55:21 -05:00
DialContext: (&net.Dialer{
2021-04-12 21:54:59 -05:00
Timeout: h.Timeouts.Dial,
KeepAlive: h.Timeouts.KeepAlive,
2021-04-09 20:55:21 -05:00
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
2021-04-12 21:54:59 -05:00
IdleConnTimeout: h.Timeouts.IdleConn,
TLSHandshakeTimeout: h.Timeouts.TLSHandshake,
ExpectContinueTimeout: h.Timeouts.ExpectContinue,
2021-04-09 20:55:21 -05:00
},
2021-04-13 14:03:25 -05:00
ErrorLog: log.ToStdLogger(log.Default(), "WARN"),
}
assetsHandler := h.assetsHandler()
2021-04-01 02:37:28 -05:00
return func(w http.ResponseWriter, r *http.Request) {
2021-06-01 03:59:23 -05:00
if r.Context().Value(ctxMatch) == nil { // no route match detected by matchHandler
if h.isAssetRequest(r) {
assetsHandler.ServeHTTP(w, r)
return
}
log.Printf("[WARN] no match for %s %s", r.URL.Hostname(), r.URL.Path)
h.Reporter.Report(w, http.StatusBadGateway)
2021-04-01 02:37:28 -05:00
return
}
2021-06-01 03:59:23 -05:00
match := r.Context().Value(ctxMatch).(discovery.MatchedRoute)
matchType := r.Context().Value(ctxMatchType).(discovery.MatchType)
switch matchType {
case discovery.MTProxy:
switch match.Mapper.RedirectType {
case discovery.RTNone:
uu := r.Context().Value(ctxURL).(*url.URL)
log.Printf("[DEBUG] proxy to %s", uu)
reverseProxy.ServeHTTP(w, r)
case discovery.RTPerm:
log.Printf("[DEBUG] redirect (301) to %s", match.Destination)
http.Redirect(w, r, match.Destination, http.StatusMovedPermanently)
case discovery.RTTemp:
log.Printf("[DEBUG] redirect (302) to %s", match.Destination)
http.Redirect(w, r, match.Destination, http.StatusFound)
}
case discovery.MTStatic:
// static match result has webroot:location:[spa:normal], i.e. /www:/var/somedir/:normal
ae := strings.Split(match.Destination, ":")
if len(ae) != 3 { // shouldn't happen
2021-06-01 03:25:35 -05:00
log.Printf("[WARN] unexpected static assets destination: %s", match.Destination)
h.Reporter.Report(w, http.StatusInternalServerError)
return
}
fs, err := h.fileServer(ae[0], ae[1], ae[2] == "spa", nil)
if err != nil {
2021-06-01 03:25:35 -05:00
log.Printf("[WARN] file server error, %v", err)
h.Reporter.Report(w, http.StatusInternalServerError)
return
}
h.CacheControl.Middleware(fs).ServeHTTP(w, r)
2021-04-01 02:37:28 -05:00
}
}
}
2021-04-02 03:13:49 -05:00
// matchHandler is a part of middleware chain. Matches incoming request to one or more matched rules
// and if match found sets it to the request context. Context used by proxy handler as well as by plugin conductor
func (h *Http) matchHandler(next http.Handler) http.Handler {
getMatch := func(mm discovery.Matches, picker func(len int) int) (m discovery.MatchedRoute, ok bool) {
if len(mm.Routes) == 0 {
return m, false
}
2021-05-28 16:11:16 -05:00
var matches []discovery.MatchedRoute
for _, m := range mm.Routes {
if m.Alive {
matches = append(matches, m)
}
}
switch len(matches) {
case 0:
return m, false
case 1:
return matches[0], true
default:
return matches[picker(len(matches))], true
}
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := r.URL.Hostname()
if server == "" {
server = strings.Split(r.Host, ":")[0] // drop port
}
matches := h.Match(server, r.URL.EscapedPath()) // get all matches for the server:path pair
match, ok := getMatch(matches, h.LBSelector)
if ok {
2021-06-01 03:59:23 -05:00
ctx := context.WithValue(r.Context(), ctxMatch, match) // set match info
ctx = context.WithValue(ctx, ctxMatchType, matches.MatchType) // set match type
ctx = context.WithValue(ctx, plugin.CtxMatch, match) // set match info for plugin conductor
if matches.MatchType == discovery.MTProxy {
uu, err := url.Parse(match.Destination)
if err != nil {
log.Printf("[WARN] can't parse destination %s, %v", match.Destination, err)
h.Reporter.Report(w, http.StatusBadGateway)
return
}
ctx = context.WithValue(ctx, ctxURL, uu) // set destination url in request's context
}
r = r.WithContext(ctx)
}
next.ServeHTTP(w, r)
})
}
func (h *Http) assetsHandler() http.HandlerFunc {
if h.AssetsLocation == "" || h.AssetsWebRoot == "" {
return func(writer http.ResponseWriter, request *http.Request) {}
}
var notFound []byte
var err error
if h.Assets404 != "" {
if notFound, err = os.ReadFile(filepath.Join(h.AssetsLocation, h.Assets404)); err != nil {
log.Printf("[WARN] can't read 404 file %s, %v", h.Assets404, err)
notFound = nil
}
}
log.Printf("[DEBUG] shared assets server enabled for %s %s, spa=%v, not-found=%q",
h.AssetsLocation, h.AssetsWebRoot, h.AssetsSPA, h.Assets404)
fs, err := h.fileServer(h.AssetsWebRoot, h.AssetsLocation, h.AssetsSPA, notFound)
if err != nil {
log.Printf("[WARN] can't initialize assets server, %v", err)
return func(writer http.ResponseWriter, request *http.Request) {}
}
return h.CacheControl.Middleware(fs).ServeHTTP
}
func (h *Http) fileServer(assetsWebRoot, assetsLocation string, spa bool, notFound []byte) (http.Handler, error) {
var notFoundReader io.Reader
if notFound != nil {
notFoundReader = bytes.NewReader(notFound)
}
if spa {
return R.NewFileServer(assetsWebRoot, assetsLocation, R.FsOptCustom404(notFoundReader), R.FsOptSPA)
}
return R.NewFileServer(assetsWebRoot, assetsLocation, R.FsOptCustom404(notFoundReader))
}
func (h *Http) isAssetRequest(r *http.Request) bool {
if h.AssetsLocation == "" || h.AssetsWebRoot == "" {
return false
}
root := strings.TrimSuffix(h.AssetsWebRoot, "/")
return r.URL.Path == root || strings.HasPrefix(r.URL.Path, root+"/")
}
2021-04-09 15:05:22 -05:00
func (h *Http) toHTTP(address string, httpPort int) string {
2021-04-05 22:12:06 -05:00
rx := regexp.MustCompile(`(.*):(\d*)`)
return rx.ReplaceAllString(address, "$1:") + strconv.Itoa(httpPort)
}
func (h *Http) pluginHandler() func(next http.Handler) http.Handler {
if h.PluginConductor == nil {
return passThroughHandler
2021-04-13 12:45:49 -05:00
}
log.Printf("[INFO] plugin support enabled")
return h.PluginConductor.Middleware
2021-04-13 12:45:49 -05:00
}
func (h *Http) mgmtHandler() func(next http.Handler) http.Handler {
if h.Metrics == nil {
return passThroughHandler
}
log.Printf("[DEBUG] metrics enabled")
return h.Metrics.Middleware
}
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,
2021-04-12 21:54:59 -05:00
ReadHeaderTimeout: h.Timeouts.ReadHeader,
WriteTimeout: h.Timeouts.Write,
IdleTimeout: h.Timeouts.Idle,
2021-04-02 03:13:49 -05:00
}
}
2021-04-04 02:57:34 -05:00
func (h *Http) setXRealIP(r *http.Request) {
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
// use the left-most non-private client IP address
// if there is no any non-private IP address, use the left-most address
r.Header.Set("X-Real-IP", preferPublicIP(strings.Split(forwarded, ",")))
return
}
ip, _, err := net.SplitHostPort(r.RemoteAddr)
2021-04-04 02:57:34 -05:00
if err != nil {
return
}
userIP := net.ParseIP(ip)
if userIP == nil {
return
}
r.Header.Set("X-Real-IP", ip)
2021-04-04 02:57:34 -05:00
}
// discoveredServers gets the list of servers discovered by providers.
// The underlying discovery is async and may happen not right away.
// We should try to get servers for some time and make sure we have the complete list of servers
// by checking if the number of servers has not changed between two calls.
func (h *Http) discoveredServers(ctx context.Context, interval time.Duration) (servers []string) {
discoveredServers := 0
for i := 0; i < 100; i++ {
select {
case <-ctx.Done():
return nil
default:
}
servers = h.Servers() // fill all discovered if nothing defined
if len(servers) > 0 && len(servers) == discoveredServers {
break
}
discoveredServers = len(servers)
time.Sleep(interval)
}
return servers
}