diff --git a/src/accesslist.go b/src/accesslist.go index a52d878..f3366d9 100644 --- a/src/accesslist.go +++ b/src/accesslist.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - strip "github.com/grokify/html-strip-tags-go" + "github.com/microcosm-cc/bluemonday" "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/utils" ) @@ -137,7 +137,8 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) { } comment, _ := utils.PostPara(r, "comment") - comment = strip.StripTags(comment) + p := bluemonday.StrictPolicy() + comment = p.Sanitize(comment) geodbStore.AddCountryCodeToWhitelist(countryCode, comment) @@ -164,7 +165,8 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) { } comment, _ := utils.PostPara(r, "comment") - comment = strip.StripTags(comment) + p := bluemonday.StrictPolicy() + comment = p.Sanitize(comment) geodbStore.AddIPToWhiteList(ipAddr, comment) } diff --git a/src/api.go b/src/api.go index 14df881..458a002 100644 --- a/src/api.go +++ b/src/api.go @@ -163,6 +163,8 @@ func initAPIs() { authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet) authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet) authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend) + authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle) + authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort) //Account Reset http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail) diff --git a/src/go.mod b/src/go.mod index ce1c3b7..f07b4e5 100644 --- a/src/go.mod +++ b/src/go.mod @@ -10,11 +10,10 @@ require ( github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.5.0 github.com/grandcat/zeroconf v1.0.0 - github.com/grokify/html-strip-tags-go v0.1.0 github.com/likexian/whois v1.15.1 github.com/microcosm-cc/bluemonday v1.0.25 - golang.org/x/net v0.14.0 - golang.org/x/sys v0.11.0 - golang.org/x/text v0.12.0 + golang.org/x/net v0.20.0 + golang.org/x/sys v0.16.0 + golang.org/x/text v0.14.0 golang.org/x/tools v0.12.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index cad7eab..d73f2bc 100644 --- a/src/go.sum +++ b/src/go.sum @@ -740,8 +740,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= -github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= -github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -1146,8 +1144,9 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1268,8 +1267,9 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1421,8 +1421,9 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1437,6 +1438,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1455,8 +1457,9 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/src/main.go b/src/main.go index d8be86f..263334c 100644 --- a/src/main.go +++ b/src/main.go @@ -17,6 +17,7 @@ import ( "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/email" + "imuslab.com/zoraxy/mod/forwardproxy" "imuslab.com/zoraxy/mod/ganserv" "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/info/logger" @@ -49,9 +50,9 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file") var ( name = "Zoraxy" - version = "3.0.0" + version = "3.0.1" nodeUUID = "generic" - development = false //Set this to false to use embedded web fs + development = true //Set this to false to use embedded web fs bootTime = time.Now().Unix() /* @@ -79,6 +80,7 @@ var ( acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs + forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser //Helper modules EmailSender *email.Sender //Email sender that handle email sending diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index 321f031..8b70556 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -272,6 +272,14 @@ func removeHeaders(header http.Header, noCache bool) { header.Del("Cache-Control") header.Set("Cache-Control", "no-store") } + + //Hide Go-HTTP-Client UA if the client didnt sent us one + if _, ok := header["User-Agent"]; !ok { + // If the outbound request doesn't have a User-Agent header set, + // don't send the default Go HTTP client User-Agent. + header.Set("User-Agent", "") + } + } func addXForwardedForHeader(req *http.Request) { @@ -365,6 +373,12 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr } } + //if res.StatusCode == 501 || res.StatusCode == 500 { + // fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI) + // fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode) + // fmt.Println(outreq.Header, req.Host) + //} + //Custom header rewriter functions if res.Header.Get("Location") != "" { locationRewrite := res.Header.Get("Location") diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index d1e5bfc..5193d07 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -114,7 +114,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) } h.logRequest(r, true, 101, "subdomain-websocket", target.Domain) - wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations) + wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ + SkipTLSValidation: target.SkipCertValidations, + SkipOriginCheck: target.SkipWebSocketOriginCheck, + }) wspHandler.ServeHTTP(w, r) return } @@ -178,7 +181,10 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String()) } h.logRequest(r, true, 101, "vdir-websocket", target.Domain) - wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations) + wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ + SkipTLSValidation: target.SkipCertValidations, + SkipOriginCheck: target.parent.SkipWebSocketOriginCheck, + }) wspHandler.ServeHTTP(w, r) return } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 0a48e33..d67b808 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -95,9 +95,10 @@ type ProxyEndpoint struct { Domain string //Domain or IP to proxy to //TLS/SSL Related - RequireTLS bool //Target domain require TLS - BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil) - SkipCertValidations bool //Set to true to accept self signed certs + RequireTLS bool //Target domain require TLS + BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil) + SkipCertValidations bool //Set to true to accept self signed certs + SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections //Virtual Directories VirtualDirectories []*VirtualDirectoryEndpoint @@ -115,6 +116,7 @@ type ProxyEndpoint struct { DefaultSiteValue string //Fallback routing target, optional Disabled bool //If the rule is disabled + //Internal Logic Elements parent *Router proxy *dpcore.ReverseProxy `json:"-"` diff --git a/src/mod/forwardproxy/cproxy/LICENSE.md b/src/mod/forwardproxy/cproxy/LICENSE.md new file mode 100644 index 0000000..99ce426 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/LICENSE.md @@ -0,0 +1,25 @@ +MIT License + +Copyright (c) 2022 Smarty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +NOTE: Various optional and subordinate components carry their own licensing +requirements and restrictions. Use of those components is subject to the terms +and conditions outlined the respective license of each component. diff --git a/src/mod/forwardproxy/cproxy/config.go b/src/mod/forwardproxy/cproxy/config.go new file mode 100644 index 0000000..f0d82bf --- /dev/null +++ b/src/mod/forwardproxy/cproxy/config.go @@ -0,0 +1,109 @@ +package cproxy + +import ( + "net/http" + "time" +) + +func New(options ...option) http.Handler { + var this configuration + Options.apply(options...)(&this) + return newHandler(this.Filter, this.ClientConnector, this.ServerConnector, this.Monitor) +} + +var Options singleton + +type singleton struct{} +type option func(*configuration) + +type configuration struct { + DialTimeout time.Duration + Filter Filter + DialAddress string + Dialer Dialer + LogConnections bool + ProxyProtocol bool + Initializer initializer + ClientConnector clientConnector + ServerConnector serverConnector + Monitor monitor + Logger logger +} + +func (singleton) DialTimeout(value time.Duration) option { + return func(this *configuration) { this.DialTimeout = value } +} +func (singleton) Filter(value Filter) option { + return func(this *configuration) { this.Filter = value } +} +func (singleton) ClientConnector(value clientConnector) option { + return func(this *configuration) { this.ClientConnector = value } +} +func (singleton) DialAddress(value string) option { + return func(this *configuration) { this.DialAddress = value } +} +func (singleton) Dialer(value Dialer) option { + return func(this *configuration) { this.Dialer = value } +} +func (singleton) LogConnections(value bool) option { + return func(this *configuration) { this.LogConnections = value } +} +func (singleton) ProxyProtocol(value bool) option { + return func(this *configuration) { this.ProxyProtocol = value } +} +func (singleton) Initializer(value initializer) option { + return func(this *configuration) { this.Initializer = value } +} +func (singleton) ServerConnector(value serverConnector) option { + return func(this *configuration) { this.ServerConnector = value } +} +func (singleton) Monitor(value monitor) option { + return func(this *configuration) { this.Monitor = value } +} +func (singleton) Logger(value logger) option { + return func(this *configuration) { this.Logger = value } +} + +func (singleton) apply(options ...option) option { + return func(this *configuration) { + for _, item := range Options.defaults(options...) { + item(this) + } + + if this.Dialer == nil { + this.Dialer = newDialer(this) + } + + this.Dialer = newRoutingDialer(this) + + if this.ProxyProtocol { + this.Initializer = newProxyProtocolInitializer() + } + + if this.Initializer == nil { + this.Initializer = nop{} + } + + this.Initializer = newLoggingInitializer(this) + + if this.ServerConnector == nil { + this.ServerConnector = newServerConnector(this.Dialer, this.Initializer) + } + } +} +func (singleton) defaults(options ...option) []option { + return append([]option{ + Options.DialTimeout(time.Second * 10), + Options.Filter(newFilter()), + Options.ClientConnector(newClientConnector()), + Options.Initializer(nop{}), + Options.Monitor(nop{}), + Options.Logger(nop{}), + }, options...) +} + +type nop struct{} + +func (nop) Measure(int) {} +func (nop) Printf(string, ...interface{}) {} +func (nop) Initialize(Socket, Socket) bool { return true } diff --git a/src/mod/forwardproxy/cproxy/default_client_connector.go b/src/mod/forwardproxy/cproxy/default_client_connector.go new file mode 100644 index 0000000..6fd284d --- /dev/null +++ b/src/mod/forwardproxy/cproxy/default_client_connector.go @@ -0,0 +1,19 @@ +package cproxy + +import "net/http" + +type defaultClientConnector struct{} + +func newClientConnector() *defaultClientConnector { + return &defaultClientConnector{} +} + +func (this *defaultClientConnector) Connect(response http.ResponseWriter) Socket { + if hijacker, ok := response.(http.Hijacker); !ok { + return nil + } else if socket, _, _ := hijacker.Hijack(); socket == nil { + return nil // this 'else if' exists to avoid the pointer nil != interface nil issue + } else { + return socket + } +} diff --git a/src/mod/forwardproxy/cproxy/default_dialer.go b/src/mod/forwardproxy/cproxy/default_dialer.go new file mode 100644 index 0000000..dce5da2 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/default_dialer.go @@ -0,0 +1,25 @@ +package cproxy + +import ( + "net" + "time" +) + +type defaultDialer struct { + timeout time.Duration + logger logger +} + +func newDialer(config *configuration) *defaultDialer { + return &defaultDialer{timeout: config.DialTimeout, logger: config.Logger} +} + +func (this *defaultDialer) Dial(address string) Socket { + if socket, err := net.DialTimeout("tcp", address, this.timeout); err == nil { + return socket + } else { + this.logger.Printf("[INFO] Unable to establish connection to [%s]: %s", address, err) + } + + return nil +} diff --git a/src/mod/forwardproxy/cproxy/default_filter.go b/src/mod/forwardproxy/cproxy/default_filter.go new file mode 100644 index 0000000..bbee349 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/default_filter.go @@ -0,0 +1,9 @@ +package cproxy + +import "net/http" + +type defaultFilter struct{} + +func newFilter() *defaultFilter { return &defaultFilter{} } + +func (this *defaultFilter) IsAuthorized(http.ResponseWriter, *http.Request) bool { return true } diff --git a/src/mod/forwardproxy/cproxy/default_handler.go b/src/mod/forwardproxy/cproxy/default_handler.go new file mode 100644 index 0000000..a5e72d2 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/default_handler.go @@ -0,0 +1,56 @@ +package cproxy + +import "net/http" + +type defaultHandler struct { + filter Filter + clientConnector clientConnector + serverConnector serverConnector + meter monitor +} + +func newHandler(filter Filter, clientConnector clientConnector, serverConnector serverConnector, meter monitor) *defaultHandler { + return &defaultHandler{ + filter: filter, + clientConnector: clientConnector, + serverConnector: serverConnector, + meter: meter, + } +} + +func (this *defaultHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) { + this.meter.Measure(MeasurementHTTPRequest) + + if request.Method != "CONNECT" { + this.meter.Measure(MeasurementBadMethod) + writeResponseStatus(response, http.StatusMethodNotAllowed) + + } else if !this.filter.IsAuthorized(response, request) { + this.meter.Measure(MeasurementUnauthorizedRequest) + //writeResponseStatus(response, http.StatusUnauthorized) + + } else if client := this.clientConnector.Connect(response); client == nil { + this.meter.Measure(MeasurementClientConnectionFailed) + writeResponseStatus(response, http.StatusNotImplemented) + + } else if connection := this.serverConnector.Connect(client, request.URL.Host); connection == nil { + this.meter.Measure(MeasurementServerConnectionFailed) + _, _ = client.Write(statusBadGateway) + _ = client.Close() + + } else { + this.meter.Measure(MeasurementProxyReady) + _, _ = client.Write(statusReady) + connection.Proxy() + this.meter.Measure(MeasurementProxyComplete) + } +} + +func writeResponseStatus(response http.ResponseWriter, statusCode int) { + http.Error(response, http.StatusText(statusCode), statusCode) +} + +var ( + statusBadGateway = []byte("HTTP/1.1 502 Bad Gateway\r\n\r\n") + statusReady = []byte("HTTP/1.1 200 OK\r\n\r\n") +) diff --git a/src/mod/forwardproxy/cproxy/default_proxy.go b/src/mod/forwardproxy/cproxy/default_proxy.go new file mode 100644 index 0000000..99b3663 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/default_proxy.go @@ -0,0 +1,54 @@ +package cproxy + +import ( + "io" + "sync" +) + +type defaultProxy struct { + client Socket + server Socket + waiter *sync.WaitGroup +} + +func newProxy(client, server Socket) *defaultProxy { + waiter := &sync.WaitGroup{} + waiter.Add(2) // wait on both client->server and server->client streams + + return &defaultProxy{ + waiter: waiter, + client: client, + server: server, + } +} + +func (this *defaultProxy) Proxy() { + go this.streamAndClose(this.client, this.server) + go this.streamAndClose(this.server, this.client) + this.closeSockets() +} + +func (this *defaultProxy) streamAndClose(reader, writer Socket) { + _, _ = io.Copy(writer, reader) + + tryCloseRead(reader) + tryCloseWrite(writer) + + this.waiter.Done() +} +func tryCloseRead(socket Socket) { + if tcp, ok := socket.(tcpSocket); ok { + _ = tcp.CloseRead() + } +} +func tryCloseWrite(socket Socket) { + if tcp, ok := socket.(tcpSocket); ok { + _ = tcp.CloseWrite() + } +} + +func (this *defaultProxy) closeSockets() { + this.waiter.Wait() + _ = this.client.Close() + _ = this.server.Close() +} diff --git a/src/mod/forwardproxy/cproxy/default_server_connector.go b/src/mod/forwardproxy/cproxy/default_server_connector.go new file mode 100644 index 0000000..a934f15 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/default_server_connector.go @@ -0,0 +1,24 @@ +package cproxy + +type defaultServerConnector struct { + dialer Dialer + initializer initializer +} + +func newServerConnector(dialer Dialer, initializer initializer) *defaultServerConnector { + return &defaultServerConnector{dialer: dialer, initializer: initializer} +} + +func (this *defaultServerConnector) Connect(client Socket, serverAddress string) proxy { + server := this.dialer.Dial(serverAddress) + if server == nil { + return nil + } + + if !this.initializer.Initialize(client, server) { + _ = server.Close() + return nil + } + + return newProxy(client, server) +} diff --git a/src/mod/forwardproxy/cproxy/hostname_filter.go b/src/mod/forwardproxy/cproxy/hostname_filter.go new file mode 100644 index 0000000..fb0476c --- /dev/null +++ b/src/mod/forwardproxy/cproxy/hostname_filter.go @@ -0,0 +1,32 @@ +package cproxy + +import "net/http" + +type hostnameFilter struct { + authorized []string +} + +func NewHostnameFilter(authorized []string) Filter { + return &hostnameFilter{authorized: authorized} +} + +func (this hostnameFilter) IsAuthorized(_ http.ResponseWriter, request *http.Request) bool { + if len(this.authorized) == 0 { + return true + } + + host := request.URL.Host + hostLength := len(host) + for _, authorized := range this.authorized { + if authorized[:2] == "*." { + have, want := hostLength, len(authorized)-1 + if have > want && authorized[1:] == host[hostLength-want:] { + return true + } + } else if authorized == host { + return true + } + } + + return false +} diff --git a/src/mod/forwardproxy/cproxy/hostname_suffix_filter.go b/src/mod/forwardproxy/cproxy/hostname_suffix_filter.go new file mode 100644 index 0000000..9c3652d --- /dev/null +++ b/src/mod/forwardproxy/cproxy/hostname_suffix_filter.go @@ -0,0 +1,26 @@ +package cproxy + +import ( + "net/http" + "strings" +) + +type hostnameSuffixFilter struct { + authorized []string +} + +func NewHostnameSuffixFilter(authorized []string) Filter { + return &hostnameSuffixFilter{authorized: authorized} +} + +func (this hostnameSuffixFilter) IsAuthorized(_ http.ResponseWriter, request *http.Request) bool { + host := request.URL.Host + + for _, authorized := range this.authorized { + if strings.HasSuffix(host, authorized) { + return true + } + } + + return false +} diff --git a/src/mod/forwardproxy/cproxy/interfaces.go b/src/mod/forwardproxy/cproxy/interfaces.go new file mode 100644 index 0000000..1ed0093 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/interfaces.go @@ -0,0 +1,67 @@ +package cproxy + +import ( + "io" + "net" + "net/http" +) + +type ( + Filter interface { + IsAuthorized(http.ResponseWriter, *http.Request) bool + } + + clientConnector interface { + Connect(http.ResponseWriter) Socket + } +) + +type ( + Dialer interface { + Dial(string) Socket + } + + serverConnector interface { + Connect(Socket, string) proxy + } + + initializer interface { + Initialize(Socket, Socket) bool + } + + proxy interface { + Proxy() + } +) + +type ( + Socket interface { + io.ReadWriteCloser + RemoteAddr() net.Addr + } + + tcpSocket interface { + Socket + CloseRead() error + CloseWrite() error + } +) + +type ( + monitor interface { + Measure(int) + } + logger interface { + Printf(string, ...interface{}) + } +) + +const ( + MeasurementHTTPRequest int = iota + MeasurementBadMethod + MeasurementUnauthorizedRequest + MeasurementClientConnectionFailed + MeasurementServerConnectionFailed + MeasurementProxyReady + MeasurementProxyComplete +) diff --git a/src/mod/forwardproxy/cproxy/logging_initializer.go b/src/mod/forwardproxy/cproxy/logging_initializer.go new file mode 100644 index 0000000..37d5d72 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/logging_initializer.go @@ -0,0 +1,24 @@ +package cproxy + +type loggingInitializer struct { + logger logger + inner initializer +} + +func newLoggingInitializer(config *configuration) initializer { + if !config.LogConnections { + return config.Initializer + } + + return &loggingInitializer{inner: config.Initializer, logger: config.Logger} +} + +func (this *loggingInitializer) Initialize(client, server Socket) bool { + result := this.inner.Initialize(client, server) + + if !result { + this.logger.Printf("[INFO] Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr()) + } + + return result +} diff --git a/src/mod/forwardproxy/cproxy/proxy_protocol_initializer.go b/src/mod/forwardproxy/cproxy/proxy_protocol_initializer.go new file mode 100644 index 0000000..df65ca2 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/proxy_protocol_initializer.go @@ -0,0 +1,36 @@ +package cproxy + +import ( + "fmt" + "io" + "net" + "strings" +) + +type proxyProtocolInitializer struct{} + +func newProxyProtocolInitializer() *proxyProtocolInitializer { + return &proxyProtocolInitializer{} +} + +func (this *proxyProtocolInitializer) Initialize(client, server Socket) bool { + header := formatHeader(client.RemoteAddr(), server.RemoteAddr()) + _, err := io.WriteString(server, header) + return err == nil +} +func formatHeader(client, server net.Addr) string { + clientAddress, clientPort := parseAddress(client.String()) + serverAddress, serverPort := parseAddress(server.String()) + if strings.Contains(clientAddress, ":") { + return fmt.Sprintf(proxyProtocolIPv6Preamble, clientAddress, serverAddress, clientPort, serverPort) + } + + return fmt.Sprintf(proxyProtocolIPv4Preamble, clientAddress, serverAddress, clientPort, serverPort) +} +func parseAddress(address string) (string, string) { + address, port, _ := net.SplitHostPort(address) + return address, port +} + +const proxyProtocolIPv4Preamble = "PROXY TCP4 %s %s %s %s\r\n" +const proxyProtocolIPv6Preamble = "PROXY TCP6 %s %s %s %s\r\n" diff --git a/src/mod/forwardproxy/cproxy/routing_dialer.go b/src/mod/forwardproxy/cproxy/routing_dialer.go new file mode 100644 index 0000000..6e2a893 --- /dev/null +++ b/src/mod/forwardproxy/cproxy/routing_dialer.go @@ -0,0 +1,18 @@ +package cproxy + +type routingDialer struct { + inner Dialer + targetAddress string +} + +func newRoutingDialer(config *configuration) Dialer { + if len(config.DialAddress) == 0 { + return config.Dialer + } + + return &routingDialer{inner: config.Dialer, targetAddress: config.DialAddress} +} + +func (this *routingDialer) Dial(string) Socket { + return this.inner.Dial(this.targetAddress) +} diff --git a/src/mod/forwardproxy/forwardproxy.go b/src/mod/forwardproxy/forwardproxy.go new file mode 100644 index 0000000..7213983 --- /dev/null +++ b/src/mod/forwardproxy/forwardproxy.go @@ -0,0 +1,137 @@ +package forwardproxy + +import ( + "context" + "encoding/json" + "errors" + "log" + "net/http" + "strconv" + "time" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/forwardproxy/cproxy" + "imuslab.com/zoraxy/mod/info/logger" + "imuslab.com/zoraxy/mod/utils" +) + +type ZrFilter struct { + //To be implemented +} + +type Handler struct { + server *http.Server + handler *http.Handler + running bool + db *database.Database + logger *logger.Logger + Port int +} + +func NewForwardProxy(sysdb *database.Database, port int, logger *logger.Logger) *Handler { + thisFilter := ZrFilter{} + handler := cproxy.New(cproxy.Options.Filter(thisFilter)) + + return &Handler{ + db: sysdb, + server: nil, + handler: &handler, + running: false, + logger: logger, + Port: port, + } +} + +// Start the forward proxy +func (h *Handler) Start() error { + if h.running { + return errors.New("forward proxy already running") + } + server := &http.Server{Addr: ":" + strconv.Itoa(h.Port), Handler: *h.handler} + h.server = server + + go func() { + if err := server.ListenAndServe(); err != nil { + if err != nil { + log.Println(err.Error()) + } + } + }() + + h.running = true + return nil +} + +// Stop the forward proxy +func (h *Handler) Stop() error { + if h.running && h.server != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := h.server.Shutdown(ctx); err != nil { + return err + } + h.running = false + h.server = nil + } + return nil +} + +// Update the port number of the forward proxy +func (h *Handler) UpdatePort(newPort int) error { + h.Stop() + h.Port = newPort + return h.Start() +} + +func (it ZrFilter) IsAuthorized(w http.ResponseWriter, r *http.Request) bool { + return true +} + +// Handle port change of the forward proxy +func (h *Handler) HandlePort(w http.ResponseWriter, r *http.Request) { + port, err := utils.PostInt(r, "port") + if err != nil { + js, _ := json.Marshal(h.Port) + utils.SendJSONResponse(w, string(js)) + } else { + //Update the port + err = h.UpdatePort(port) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy port updated to :"+strconv.Itoa(h.Port), nil) + h.db.Write("fwdproxy", "port", port) + utils.SendOK(w) + } +} + +// Handle power toggle of the forward proxys +func (h *Handler) HandleToogle(w http.ResponseWriter, r *http.Request) { + enabled, err := utils.PostBool(r, "enable") + if err != nil { + //Get the current state of the forward proxy + js, _ := json.Marshal(h.running) + utils.SendJSONResponse(w, string(js)) + } else { + if enabled { + err = h.Start() + if err != nil { + h.logger.PrintAndLog("Forward Proxy", "Unable to start forward proxy server", err) + utils.SendErrorResponse(w, err.Error()) + return + } + h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Started, listening on :"+strconv.Itoa(h.Port), nil) + } else { + err = h.Stop() + if err != nil { + h.logger.PrintAndLog("Forward Proxy", "Unable to stop forward proxy server", err) + utils.SendErrorResponse(w, err.Error()) + return + } + h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Stopped", nil) + } + h.db.Write("fwdproxy", "enabled", enabled) + utils.SendOK(w) + } +} diff --git a/src/mod/sshprox/sshprox.go b/src/mod/sshprox/sshprox.go index 429013c..de96764 100644 --- a/src/mod/sshprox/sshprox.go +++ b/src/mod/sshprox/sshprox.go @@ -85,7 +85,10 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite r.Header.Set("Zr-Origin-Upgrade", "websocket") requestURL = strings.TrimPrefix(requestURL, "/") u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL) - wspHandler := websocketproxy.NewProxy(u, false) + wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ + SkipTLSValidation: false, + SkipOriginCheck: false, + }) wspHandler.ServeHTTP(w, r) return } diff --git a/src/mod/websocketproxy/websocketproxy.go b/src/mod/websocketproxy/websocketproxy.go index c27b3dd..54ab7eb 100644 --- a/src/mod/websocketproxy/websocketproxy.go +++ b/src/mod/websocketproxy/websocketproxy.go @@ -47,19 +47,26 @@ type WebsocketProxy struct { // If nil, DefaultDialer is used. Dialer *websocket.Dialer - Verbal bool - SkipTlsValidation bool + Verbal bool + + Options Options +} + +// Additional options for websocket proxy runtime +type Options struct { + SkipTLSValidation bool //Skip backend TLS validation + SkipOriginCheck bool //Skip origin check } // ProxyHandler returns a new http.Handler interface that reverse proxies the // request to the given target. -func ProxyHandler(target *url.URL, skipTlsValidation bool) http.Handler { - return NewProxy(target, skipTlsValidation) +func ProxyHandler(target *url.URL, options Options) http.Handler { + return NewProxy(target, options) } // NewProxy returns a new Websocket reverse proxy that rewrites the // URL's to the scheme, host and base path provider in target. -func NewProxy(target *url.URL, skipTlsValidation bool) *WebsocketProxy { +func NewProxy(target *url.URL, options Options) *WebsocketProxy { backend := func(r *http.Request) *url.URL { // Shallow copy u := *target @@ -68,7 +75,7 @@ func NewProxy(target *url.URL, skipTlsValidation bool) *WebsocketProxy { u.RawQuery = r.URL.RawQuery return &u } - return &WebsocketProxy{Backend: backend, Verbal: false, SkipTlsValidation: skipTlsValidation} + return &WebsocketProxy{Backend: backend, Verbal: false, Options: options} } // ServeHTTP implements the http.Handler that proxies WebSocket connections. @@ -88,7 +95,7 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { dialer := w.Dialer if w.Dialer == nil { - if w.SkipTlsValidation { + if w.Options.SkipTLSValidation { //Disable TLS secure check if target allow skip verification bypassDialer := websocket.DefaultDialer bypassDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} @@ -171,6 +178,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { upgrader = DefaultUpgrader } + //Fixing issue #107 by bypassing request origin check + if w.Options.SkipOriginCheck { + upgrader.CheckOrigin = func(r *http.Request) bool { + return true + } + } + // Only pass those headers to the upgrader. upgradeHeader := http.Header{} if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { diff --git a/src/mod/websocketproxy/websocketproxy_test.go b/src/mod/websocketproxy/websocketproxy_test.go index 6059261..20c4668 100644 --- a/src/mod/websocketproxy/websocketproxy_test.go +++ b/src/mod/websocketproxy/websocketproxy_test.go @@ -28,7 +28,10 @@ func TestProxy(t *testing.T) { } u, _ := url.Parse(backendURL) - proxy := NewProxy(u, false) + proxy := NewProxy(u, Options{ + SkipTLSValidation: false, + SkipOriginCheck: false, + }) proxy.Upgrader = upgrader mux := http.NewServeMux() diff --git a/src/start.go b/src/start.go index 20a60ed..f1bb2c6 100644 --- a/src/start.go +++ b/src/start.go @@ -12,6 +12,7 @@ import ( "imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/dynamicproxy/redirection" + "imuslab.com/zoraxy/mod/forwardproxy" "imuslab.com/zoraxy/mod/ganserv" "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/info/logger" @@ -219,6 +220,18 @@ func startupSequence() { //Create an analytic loader AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector) + //Create basic forward proxy + sysdb.NewTable("fwdproxy") + fwdProxyEnabled := false + fwdProxyPort := 5587 + sysdb.Read("fwdproxy", "port", &fwdProxyPort) + sysdb.Read("fwdproxy", "enabled", &fwdProxyEnabled) + forwardProxy = forwardproxy.NewForwardProxy(sysdb, fwdProxyPort, SystemWideLogger) + if fwdProxyEnabled { + SystemWideLogger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Listening on :"+strconv.Itoa(forwardProxy.Port), nil) + forwardProxy.Start() + } + /* ACME API @@ -241,4 +254,5 @@ func finalSequence() { //Inject routing rules registerBuildInRoutingRules() + } diff --git a/src/web/components/networktools.html b/src/web/components/networktools.html index 74e0bd1..cc4ea71 100644 --- a/src/web/components/networktools.html +++ b/src/web/components/networktools.html @@ -84,7 +84,23 @@ Paste to Terminal Shift + Insert - +
+

Forward Proxy

+

Setup a basic HTTP forward proxy to access web server in another LAN
+ To enable forward proxy in your domain, add a proxy rule to 127.0.0.1:{selected_port}

+
+
+ +
+ + +
+
+
+ + +
+

Wake On LAN

Wake up a remote server by WOL Magic Packet or an IoT device

@@ -558,6 +574,68 @@ function renderWhoisDomainTable(jsonData) { } +//Forward Proxy +function initForwardProxyInfo(){ + $.get("/api/tools/fwdproxy/enable", function(data){ + if (data == true){ + //Disable the start btn + $("#forwardProxyButtons").find(".startBtn").addClass('disabled'); + $("#forwardProxyButtons").find(".stopBtn").removeClass('disabled'); + }else{ + $("#forwardProxyButtons").find(".startBtn").removeClass('disabled'); + $("#forwardProxyButtons").find(".stopBtn").addClass('disabled'); + } + }); + + $.get("/api/tools/fwdproxy/port", function(data){ + $("#forwardProxyPort").val(data); + }) +} +initForwardProxyInfo(); + +function toggleForwadProxy(enabled){ + $.ajax({ + url: "/api/tools/fwdproxy/enable", + method: "POST", + data: { + "enable": enabled + }, + success: function(data){ + if (data.error != undefined){ + msgbox(data.error, false); + }else{ + msgbox(`Forward proxy ${enabled?"enabled":"disabled"}`) + } + + initForwardProxyInfo(); + } + }) +} + +function updateForwardProxyPort(){ + let newPortNumber = $("#forwardProxyPort").val(); + if (newPortNumber < 1024 || newPortNumber > 65535){ + $("#newPortNumber").parent().addClass('error'); + }else{ + $("#newPortNumber").parent().removeClass('error'); + } + + $.ajax({ + url: "/api/tools/fwdproxy/port", + method: "POST", + data: { + "port": newPortNumber + }, + success: function(data){ + if (data.error != undefined){ + msgbox(data.error, false); + } + msgbox("Forward proxy port updated"); + initForwardProxyInfo(); + } + }); +} + diff --git a/src/web/components/utils.html b/src/web/components/utils.html index 00d56b0..9d84eab 100644 --- a/src/web/components/utils.html +++ b/src/web/components/utils.html @@ -4,12 +4,12 @@

You might find these tools or information helpful when setting up your gateway server

-
+
-
+

IP Address to CIDR

No experience with CIDR notations? Here are some tools you can use to make setting up easier.

@@ -128,7 +128,7 @@
-
+

System Backup & Restore

Options related to system backup, migrate and restore.

@@ -175,7 +175,16 @@