Files
godoxy/src/route/http_route.go
2024-08-01 10:06:42 +08:00

167 lines
3.5 KiB
Go
Executable File

package route
import (
"crypto/tls"
"fmt"
"net"
"time"
"net/http"
"net/url"
"strings"
"github.com/sirupsen/logrus"
E "github.com/yusing/go-proxy/error"
P "github.com/yusing/go-proxy/proxy"
PT "github.com/yusing/go-proxy/proxy/fields"
F "github.com/yusing/go-proxy/utils/functional"
)
type (
HTTPRoute struct {
Alias PT.Alias `json:"alias"`
Subroutes HTTPSubroutes `json:"subroutes"`
mux *http.ServeMux
}
HTTPSubroute struct {
TargetURL URL `json:"targetURL"`
Path PathKey `json:"path"`
proxy *P.ReverseProxy
}
URL struct {
*url.URL
}
PathKey = string
SubdomainKey = string
HTTPSubroutes = map[PathKey]HTTPSubroute
)
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
var tr *http.Transport
if entry.NoTLSVerify {
tr = transportNoTLS
} else {
tr = transport
}
rp := P.NewReverseProxy(entry.URL, tr, entry)
httpRoutes.Lock()
var r *HTTPRoute
r, ok := httpRoutes.UnsafeGet(entry.Alias.String())
if !ok {
r = &HTTPRoute{
Alias: entry.Alias,
Subroutes: make(HTTPSubroutes),
mux: http.NewServeMux(),
}
httpRoutes.UnsafeSet(entry.Alias.String(), r)
}
path := entry.Path.String()
if _, exists := r.Subroutes[path]; exists {
httpRoutes.Unlock()
return nil, E.Duplicated("path", path).Subject(entry.Alias)
}
r.mux.HandleFunc(path, rp.ServeHTTP)
if err := recover(); err != nil {
httpRoutes.Unlock()
switch t := err.(type) {
case error:
// NOTE: likely path pattern error
return nil, E.From(t).Subject(entry.Alias)
default:
return nil, E.From(fmt.Errorf("%v", t)).Subject(entry.Alias)
}
}
sr := HTTPSubroute{
TargetURL: URL{entry.URL},
proxy: rp,
Path: path,
}
rewrite := rp.Rewrite
if logrus.GetLevel() == logrus.DebugLevel {
l := logrus.WithField("alias", entry.Alias)
sr.proxy.Rewrite = func(pr *P.ProxyRequest) {
l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
l.Debug("request headers: ", pr.In.Header)
rewrite(pr)
}
} else {
sr.proxy.Rewrite = rewrite
}
r.Subroutes[path] = sr
httpRoutes.Unlock()
return r, E.Nil()
}
func (r *HTTPRoute) Start() E.NestedError {
httpRoutes.Set(r.Alias.String(), r)
return E.Nil()
}
func (r *HTTPRoute) Stop() E.NestedError {
httpRoutes.Delete(r.Alias.String())
return E.Nil()
}
func (r *HTTPRoute) GetSubroute(path PathKey) (HTTPSubroute, bool) {
sr, ok := r.Subroutes[path]
return sr, ok
}
func (u URL) MarshalText() (text []byte, err error) {
return []byte(u.String()), nil
}
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
mux, err := findMux(r.Host, PathKey(r.URL.Path))
if err != nil {
err = E.Failure("request").
Subjectf("%s %s%s", r.Method, r.Host, r.URL.Path).
With(err)
http.Error(w, err.Error(), http.StatusNotFound)
logrus.Error(err)
return
}
mux.ServeHTTP(w, r)
}
func findMux(host string, path PathKey) (*http.ServeMux, error) {
sd := strings.Split(host, ".")[0]
if r, ok := httpRoutes.UnsafeGet(sd); ok {
return r.mux, nil
}
return nil, E.NotExists("route", fmt.Sprintf("subdomain: %s, path: %s", sd, path))
}
// TODO: default + per proxy
var (
transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 60 * time.Second,
KeepAlive: 60 * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
}
transportNoTLS = func() *http.Transport {
var clone = transport.Clone()
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
return clone
}()
)