Optimized upstream & loadbalancer

- Test and optimized load balancer origin picker
- Fixed no active origin cannot load proxy rule bug
- Implemented logger design in websocket proxy module
- Added more quickstart tours
- Fixed #270 (I guess)
- Fixed #90 (I guess)
This commit is contained in:
Toby Chui 2024-08-19 16:10:35 +08:00
parent b558bcbfcf
commit 608cc0c523
12 changed files with 5597 additions and 4415 deletions

View File

@ -83,6 +83,10 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
for _, upstream := range upstreams { for _, upstream := range upstreams {
targets = append(targets, upstream.String()) targets = append(targets, upstream.String())
} }
if len(targets) == 0 {
//No upstream
return "(no upstream config)"
}
return strings.Join(targets, ", ") return strings.Join(targets, ", ")
} }
@ -93,7 +97,7 @@ func (m *RouteManager) Close() {
} }
// Print debug message // Log Println, replace all log.Println or fmt.Println with this
func (m *RouteManager) debugPrint(message string, err error) { func (m *RouteManager) println(message string, err error) {
m.Options.Logger.PrintAndLog("LoadBalancer", message, err) m.Options.Logger.PrintAndLog("LoadBalancer", message, err)
} }

View File

@ -2,8 +2,6 @@ package loadbalance
import ( import (
"errors" "errors"
"fmt"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
) )
@ -29,7 +27,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
//No valid session found. Assign a new upstream //No valid session found. Assign a new upstream
targetOrigin, index, err := getRandomUpstreamByWeight(origins) targetOrigin, index, err := getRandomUpstreamByWeight(origins)
if err != nil { if err != nil {
fmt.Println("Oops. Unable to get random upstream") m.println("Unable to get random upstream", err)
targetOrigin = origins[0] targetOrigin = origins[0]
index = 0 index = 0
} }
@ -44,7 +42,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
var err error var err error
targetOrigin, _, err = getRandomUpstreamByWeight(origins) targetOrigin, _, err = getRandomUpstreamByWeight(origins)
if err != nil { if err != nil {
log.Println(err) m.println("Failed to get next origin", err)
targetOrigin = origins[0] targetOrigin = origins[0]
} }
@ -161,6 +159,7 @@ func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) {
} }
// IntRange returns a random integer in the range from min to max. // IntRange returns a random integer in the range from min to max.
/*
func intRange(min, max int) (int, error) { func intRange(min, max int) (int, error) {
var result int var result int
switch { switch {
@ -175,3 +174,4 @@ func intRange(min, max int) (int, error) {
} }
return result, nil return result, nil
} }
*/

View File

@ -117,7 +117,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession) selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
if err != nil { if err != nil {
http.ServeFile(w, r, "./web/rperror.html") http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error()) h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname()) h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
return return
} }
@ -144,6 +144,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: selectedUpstream.SkipCertValidations, SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck, SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
Logger: h.Parent.Option.Logger,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return
@ -177,11 +178,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
if err != nil { if err != nil {
if errors.As(err, &dnsError) { if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html") http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname()) h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
} else { } else {
http.ServeFile(w, r, "./web/rperror.html") http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error()) //TODO: Take this upstream offline automatically
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname()) h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
} }
} }
@ -212,6 +212,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations, SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
Logger: h.Parent.Option.Logger,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return

View File

@ -70,6 +70,11 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime // Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error { func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
if len(endpoint.ActiveOrigins) == 0 {
//There are no active origins. No need to check for ready
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
return nil
}
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) { if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared //This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime") return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -88,6 +88,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: false, SkipTLSValidation: false,
SkipOriginCheck: false, SkipOriginCheck: false,
Logger: nil,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return

View File

@ -3,6 +3,7 @@ package websocketproxy
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -12,6 +13,7 @@ import (
"strings" "strings"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"imuslab.com/zoraxy/mod/info/logger"
) )
var ( var (
@ -54,8 +56,9 @@ type WebsocketProxy struct {
// Additional options for websocket proxy runtime // Additional options for websocket proxy runtime
type Options struct { type Options struct {
SkipTLSValidation bool //Skip backend TLS validation SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check SkipOriginCheck bool //Skip origin check
Logger *logger.Logger //Logger, can be nil
} }
// ProxyHandler returns a new http.Handler interface that reverse proxies the // ProxyHandler returns a new http.Handler interface that reverse proxies the
@ -78,17 +81,26 @@ func NewProxy(target *url.URL, options Options) *WebsocketProxy {
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options} return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
} }
// Utilities function for log printing
func (w *WebsocketProxy) Println(messsage string, err error) {
if w.Options.Logger != nil {
w.Options.Logger.PrintAndLog("websocket", messsage, err)
return
}
log.Println("[websocketproxy] [system:info]"+messsage, err)
}
// ServeHTTP implements the http.Handler that proxies WebSocket connections. // ServeHTTP implements the http.Handler that proxies WebSocket connections.
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if w.Backend == nil { if w.Backend == nil {
log.Println("websocketproxy: backend function is not defined") w.Println("Invalid websocket backend configuration", errors.New("backend function not found"))
http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError)
return return
} }
backendURL := w.Backend(req) backendURL := w.Backend(req)
if backendURL == nil { if backendURL == nil {
log.Println("websocketproxy: backend URL is nil") w.Println("Invalid websocket backend configuration", errors.New("backend URL is nil"))
http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError)
return return
} }
@ -158,13 +170,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01
connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader) connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader)
if err != nil { if err != nil {
log.Printf("websocketproxy: couldn't dial to remote backend url %s", err) w.Println("Couldn't dial to remote backend url "+backendURL.String(), err)
if resp != nil { if resp != nil {
// If the WebSocket handshake fails, ErrBadHandshake is returned // If the WebSocket handshake fails, ErrBadHandshake is returned
// along with a non-nil *http.Response so that callers can handle // along with a non-nil *http.Response so that callers can handle
// redirects, authentication, etcetera. // redirects, authentication, etcetera.
if err := copyResponse(rw, resp); err != nil { if err := copyResponse(rw, resp); err != nil {
log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err) w.Println("Couldn't write response after failed remote backend handshake to "+backendURL.String(), err)
} }
} else { } else {
http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
@ -198,7 +210,7 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Also pass the header that we gathered from the Dial handshake. // Also pass the header that we gathered from the Dial handshake.
connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) connPub, err := upgrader.Upgrade(rw, req, upgradeHeader)
if err != nil { if err != nil {
log.Printf("websocketproxy: couldn't upgrade %s", err) w.Println("Couldn't upgrade incoming request", err)
return return
} }
defer connPub.Close() defer connPub.Close()

View File

@ -31,6 +31,7 @@ func TestProxy(t *testing.T) {
proxy := NewProxy(u, Options{ proxy := NewProxy(u, Options{
SkipTLSValidation: false, SkipTLSValidation: false,
SkipOriginCheck: false, SkipOriginCheck: false,
Logger: nil,
}) })
proxy.Upgrader = upgrader proxy.Upgrader = upgrader

View File

@ -910,7 +910,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
results := []*dynamicproxy.ProxyEndpoint{} results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool { dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint)) thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end //Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{} cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials { for _, user := range thisEndpoint.BasicAuthCredentials {
@ -919,7 +918,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
PasswordHash: "", PasswordHash: "",
}) })
} }
thisEndpoint.BasicAuthCredentials = cleanedCredentials thisEndpoint.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint) results = append(results, thisEndpoint)
return true return true

View File

@ -2,8 +2,8 @@
<div id="quickstart" class="standardContainer"> <div id="quickstart" class="standardContainer">
<div class="ui container"> <div class="ui container">
<h1 class="ui header"> <h1 class="ui header">
<img src="img/res/1F387.png"> <img src="img/res/1F44B.png">
<div class="content"> <div class="content" style="font-weight: lighter;">
Welcome to Zoraxy! Welcome to Zoraxy!
<div class="sub header">What services are you planning to setup today?</div> <div class="sub header">What services are you planning to setup today?</div>
</div> </div>

View File

@ -496,6 +496,7 @@ var tourSteps = {
<br><br>Now, you can try to visit your website with https:// and see your green lock shows up next to your domain name!`, <br><br>Now, you can try to visit your website with https:// and see your green lock shows up next to your domain name!`,
element: "#cert div[tourstep='certTable']", element: "#cert div[tourstep='certTable']",
scrollto: "#cert div[tourstep='certTable']", scrollto: "#cert div[tourstep='certTable']",
tab: "cert",
pos: "bottomright", pos: "bottomright",
callback: function(){ callback: function(){
hideSideWrapperInTourMode(); hideSideWrapperInTourMode();