diff --git a/src/api.go b/src/api.go index f75cfa4..41f08c4 100644 --- a/src/api.go +++ b/src/api.go @@ -8,6 +8,7 @@ import ( "imuslab.com/zoraxy/mod/acme/acmedns" "imuslab.com/zoraxy/mod/acme/acmewizard" "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/dynamicproxy/domainsniff" "imuslab.com/zoraxy/mod/ipscan" "imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/netutils" @@ -33,7 +34,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) - authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS) + authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS) authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet) authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener) diff --git a/src/cert.go b/src/cert.go index ce9f83b..a887151 100644 --- a/src/cert.go +++ b/src/cert.go @@ -177,7 +177,10 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) { // Handle front-end toggling TLS mode func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { - currentTlsSetting := false + currentTlsSetting := true //Default to true + if dynamicProxyRouter.Option != nil { + currentTlsSetting = dynamicProxyRouter.Option.UseTls + } if sysdb.KeyExists("settings", "usetls") { sysdb.Read("settings", "usetls", ¤tTlsSetting) } diff --git a/src/def.go b/src/def.go index a834a6d..48303cb 100644 --- a/src/def.go +++ b/src/def.go @@ -43,7 +43,7 @@ const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" SYSTEM_VERSION = "3.1.4" - DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ + DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ /* System Constants */ DATABASE_PATH = "sys.db" @@ -55,6 +55,7 @@ const ( MDNS_IDENTIFY_VENDOR = "imuslab.com" MDNS_SCAN_TIMEOUT = 30 /* Seconds */ MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */ + GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */ ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json" CSRF_COOKIENAME = "zoraxy_csrf" LOG_PREFIX = "zr" @@ -71,27 +72,29 @@ const ( ) /* System Startup Flags */ -var webUIPort = flag.String("port", ":8000", "Management web interface listening port") -var noauth = flag.Bool("noauth", false, "Disable authentication for management interface") -var showver = flag.Bool("version", false, "Show version of this server") -var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") -var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder") -var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)") -var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node") -var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port") -var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode") -var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)") -var acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)") -var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)") -var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters") -var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder") -var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected") +var ( + webUIPort = flag.String("port", ":8000", "Management web interface listening port") + noauth = flag.Bool("noauth", false, "Disable authentication for management interface") + showver = flag.Bool("version", false, "Show version of this server") + allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") + allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder") + mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)") + ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node") + ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port") + runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode") + acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)") + acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)") + enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)") + staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters") + allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder") + enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected") +) /* Global Variables and Handlers */ var ( - nodeUUID = "generic" //System uuid, in uuidv4 format, load from database on startup + nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup bootTime = time.Now().Unix() - requireAuth = true /* Require authentication for webmin panel */ + requireAuth = true //Require authentication for webmin panel, override from flag /* Binary Embedding File System @@ -131,5 +134,5 @@ var ( AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only SystemWideLogger *logger.Logger //Logger for Zoraxy - LogViewer *logviewer.Viewer + LogViewer *logviewer.Viewer //Log viewer HTTP handlers ) diff --git a/src/mod/auth/auth.go b/src/mod/auth/auth.go index 725a839..350a972 100644 --- a/src/mod/auth/auth.go +++ b/src/mod/auth/auth.go @@ -210,8 +210,8 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error { } session.Values["authenticated"] = false session.Values["username"] = nil - session.Save(r, w) - return nil + session.Options.MaxAge = -1 + return session.Save(r, w) } // Get the current session username from request @@ -339,6 +339,7 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool { if err != nil { return false } + // Check if user is authenticated if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { return false diff --git a/src/mod/dynamicproxy/access.go b/src/mod/dynamicproxy/access.go index 26b9741..de8f453 100644 --- a/src/mod/dynamicproxy/access.go +++ b/src/mod/dynamicproxy/access.go @@ -1,7 +1,6 @@ package dynamicproxy import ( - "log" "net/http" "os" "path/filepath" @@ -16,7 +15,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID) if err != nil { //Unable to load access rule. Target rule not found? - log.Println("[Proxy] Unable to load access rule: " + ruleID) + h.Parent.Option.Logger.PrintAndLog("proxy-access", "Unable to load access rule: "+ruleID, err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("500 - Internal Server Error")) return true diff --git a/src/mod/dynamicproxy/domainsniff/domainsniff.go b/src/mod/dynamicproxy/domainsniff/domainsniff.go index 971e47a..32504f0 100644 --- a/src/mod/dynamicproxy/domainsniff/domainsniff.go +++ b/src/mod/dynamicproxy/domainsniff/domainsniff.go @@ -10,8 +10,14 @@ package domainsniff */ import ( "crypto/tls" + "encoding/json" + "fmt" "net" + "net/http" + "strings" "time" + + "imuslab.com/zoraxy/mod/utils" ) // Check if the domain is reachable and return err if not reachable @@ -27,30 +33,114 @@ func DomainReachableWithError(domain string) error { } // Check if a domain have TLS but it is self-signed or expired -func DomainIsSelfSigned(domain string) (bool, error) { +// Return false if sniff error +func DomainIsSelfSigned(domain string) bool { + //Extract the domain from URl in case the user input the full URL + host, port, err := net.SplitHostPort(domain) + if err != nil { + host = domain + } else { + domain = host + ":" + port + } + if !strings.Contains(domain, ":") { + domain = domain + ":443" + } + //Get the certificate conn, err := net.Dial("tcp", domain) if err != nil { - return false, err + return false } defer conn.Close() + //Connect with TLS using secure verify + tlsConn := tls.Client(conn, nil) + err = tlsConn.Handshake() + if err == nil { + //This is a valid certificate + fmt.Println() + return false + } + //Connect with TLS using insecure skip verify config := &tls.Config{ InsecureSkipVerify: true, } - tlsConn := tls.Client(conn, config) + tlsConn = tls.Client(conn, config) err = tlsConn.Handshake() - if err != nil { - return false, err - } - - //Check if the certificate is self-signed - cert := tlsConn.ConnectionState().PeerCertificates[0] - return cert.Issuer.CommonName == cert.Subject.CommonName, nil + //If the handshake is successful, this is a self-signed certificate + return err == nil } // Check if domain reachable func DomainReachable(domain string) bool { return DomainReachableWithError(domain) == nil } + +// Check if domain is served by a web server using HTTPS +func DomainUsesTLS(targetURL string) bool { + //Check if the site support https + httpsUrl := fmt.Sprintf("https://%s", targetURL) + httpUrl := fmt.Sprintf("http://%s", targetURL) + + client := http.Client{Timeout: 5 * time.Second} + + resp, err := client.Head(httpsUrl) + if err == nil && resp.StatusCode == http.StatusOK { + return true + } + + resp, err = client.Head(httpUrl) + if err == nil && resp.StatusCode == http.StatusOK { + return false + } + + //If the site is not reachable, return false + return false +} + +/* + Request Handlers +*/ +//Check if site support TLS +//Pass in ?selfsignchk=true to also check for self-signed certificate +func HandleCheckSiteSupportTLS(w http.ResponseWriter, r *http.Request) { + targetURL, err := utils.PostPara(r, "url") + if err != nil { + utils.SendErrorResponse(w, "invalid url given") + return + } + + //If the selfsign flag is set, also chec for self-signed certificate + _, err = utils.PostBool(r, "selfsignchk") + if err == nil { + //Return the https and selfsign status + type result struct { + Protocol string `json:"protocol"` + SelfSign bool `json:"selfsign"` + } + + scanResult := result{Protocol: "http", SelfSign: false} + + if DomainUsesTLS(targetURL) { + scanResult.Protocol = "https" + if DomainIsSelfSigned(targetURL) { + scanResult.SelfSign = true + } + } + + js, _ := json.Marshal(scanResult) + utils.SendJSONResponse(w, string(js)) + return + } + + if DomainUsesTLS(targetURL) { + js, _ := json.Marshal("https") + utils.SendJSONResponse(w, string(js)) + return + } else { + js, _ := json.Marshal("http") + utils.SendJSONResponse(w, string(js)) + return + } +} diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 17f8523..26e337c 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -291,7 +291,7 @@ func (router *Router) Restart() error { return err } - time.Sleep(300 * time.Millisecond) + time.Sleep(800 * time.Millisecond) // Start the server err = router.StartProxyService() if err != nil { diff --git a/src/mod/geodb/geodb.go b/src/mod/geodb/geodb.go index 07d4571..6432b40 100644 --- a/src/mod/geodb/geodb.go +++ b/src/mod/geodb/geodb.go @@ -3,6 +3,7 @@ package geodb import ( _ "embed" "net/http" + "time" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/netutils" @@ -15,17 +16,22 @@ var geoipv4 []byte //Geodb dataset for ipv4 var geoipv6 []byte //Geodb dataset for ipv6 type Store struct { - geodb [][]string //Parsed geodb list - geodbIpv6 [][]string //Parsed geodb list for ipv6 - geotrie *trie - geotrieIpv6 *trie - sysdb *database.Database - option *StoreOptions + geodb [][]string //Parsed geodb list + geodbIpv6 [][]string //Parsed geodb list for ipv6 + geotrie *trie + geotrieIpv6 *trie + sysdb *database.Database + slowLookupCacheIpv4 map[string]string //Cache for slow lookup + slowLookupCacheIpv6 map[string]string //Cache for slow lookup + cacheClearTicker *time.Ticker //Ticker for clearing cache + cacheClearTickerStopChan chan bool //Stop channel for cache clear ticker + option *StoreOptions } type StoreOptions struct { - AllowSlowIpv4LookUp bool - AllowSloeIpv6Lookup bool + AllowSlowIpv4LookUp bool + AllowSlowIpv6Lookup bool + SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval } type CountryInfo struct { @@ -50,18 +56,44 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) { } var ipv6Trie *trie - if !option.AllowSloeIpv6Lookup { + if !option.AllowSlowIpv6Lookup { ipv6Trie = constrctTrieTree(parsedGeoDataIpv6) } - return &Store{ - geodb: parsedGeoData, - geotrie: ipv4Trie, - geodbIpv6: parsedGeoDataIpv6, - geotrieIpv6: ipv6Trie, - sysdb: sysdb, - option: option, - }, nil + if option.SlowLookupCacheClearInterval == 0 { + option.SlowLookupCacheClearInterval = 15 * time.Minute + } + + //Create a new store + thisGeoDBStore := &Store{ + geodb: parsedGeoData, + geotrie: ipv4Trie, + geodbIpv6: parsedGeoDataIpv6, + geotrieIpv6: ipv6Trie, + sysdb: sysdb, + slowLookupCacheIpv4: make(map[string]string), + slowLookupCacheIpv6: make(map[string]string), + cacheClearTicker: time.NewTicker(option.SlowLookupCacheClearInterval), + cacheClearTickerStopChan: make(chan bool), + option: option, + } + + //Start cache clear ticker + if option.AllowSlowIpv4LookUp || option.AllowSlowIpv6Lookup { + go func(store *Store) { + for { + select { + case <-store.cacheClearTickerStopChan: + return + case <-thisGeoDBStore.cacheClearTicker.C: + thisGeoDBStore.slowLookupCacheIpv4 = make(map[string]string) + thisGeoDBStore.slowLookupCacheIpv6 = make(map[string]string) + } + } + }(thisGeoDBStore) + } + + return thisGeoDBStore, nil } func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) { @@ -73,8 +105,12 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) } +// Close the store func (s *Store) Close() { - + if s.option.AllowSlowIpv4LookUp || s.option.AllowSlowIpv6Lookup { + //Stop cache clear ticker + s.cacheClearTickerStopChan <- true + } } func (s *Store) GetRequesterCountryISOCode(r *http.Request) string { diff --git a/src/mod/geodb/geodb_test.go b/src/mod/geodb/geodb_test.go index 3c7ad34..4e3a784 100644 --- a/src/mod/geodb/geodb_test.go +++ b/src/mod/geodb/geodb_test.go @@ -44,6 +44,7 @@ func TestResolveCountryCodeFromIP(t *testing.T) { store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{ false, true, + 0, }) if err != nil { t.Errorf("error creating store: %v", err) diff --git a/src/mod/geodb/slowSearch.go b/src/mod/geodb/slowSearch.go index 6c4c8c7..8ebc977 100644 --- a/src/mod/geodb/slowSearch.go +++ b/src/mod/geodb/slowSearch.go @@ -56,6 +56,12 @@ func (s *Store) slowSearchIpv4(ipAddr string) string { if isReservedIP(ipAddr) { return "" } + + //Check if already in cache + if cc, ok := s.slowLookupCacheIpv4[ipAddr]; ok { + return cc + } + for _, ipRange := range s.geodb { startIp := ipRange[0] endIp := ipRange[1] @@ -63,6 +69,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string { inRange, _ := isIPv4InRange(startIp, endIp, ipAddr) if inRange { + //Add to cache + s.slowLookupCacheIpv4[ipAddr] = cc return cc } } @@ -73,6 +81,12 @@ func (s *Store) slowSearchIpv6(ipAddr string) string { if isReservedIP(ipAddr) { return "" } + + //Check if already in cache + if cc, ok := s.slowLookupCacheIpv6[ipAddr]; ok { + return cc + } + for _, ipRange := range s.geodbIpv6 { startIp := ipRange[0] endIp := ipRange[1] @@ -80,6 +94,8 @@ func (s *Store) slowSearchIpv6(ipAddr string) string { inRange, _ := isIPv6InRange(startIp, endIp, ipAddr) if inRange { + //Add to cache + s.slowLookupCacheIpv6[ipAddr] = cc return cc } } diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 9763dab..f2bf611 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -27,18 +27,18 @@ func ReverseProxtInit() { /* Load Reverse Proxy Global Settings */ - inboundPort := 80 + inboundPort := 443 if sysdb.KeyExists("settings", "inbound") { sysdb.Read("settings", "inbound", &inboundPort) SystemWideLogger.Println("Serving inbound port ", inboundPort) } else { - SystemWideLogger.Println("Inbound port not set. Using default (80)") + SystemWideLogger.Println("Inbound port not set. Using default (443)") } - useTls := false + useTls := true sysdb.Read("settings", "usetls", &useTls) if useTls { - SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS") + SystemWideLogger.Println("TLS mode enabled. Serving proxy request with TLS") } else { SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http") } @@ -59,7 +59,7 @@ func ReverseProxtInit() { SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy") } - listenOnPort80 := false + listenOnPort80 := true sysdb.Read("settings", "listenP80", &listenOnPort80) if listenOnPort80 { SystemWideLogger.Println("Port 80 listener enabled") @@ -67,7 +67,7 @@ func ReverseProxtInit() { SystemWideLogger.Println("Port 80 listener disabled") } - forceHttpsRedirect := false + forceHttpsRedirect := true sysdb.Read("settings", "redirect", &forceHttpsRedirect) if forceHttpsRedirect { SystemWideLogger.Println("Force HTTPS mode enabled") @@ -1085,6 +1085,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) { if dynamicProxyRouter.Running { dynamicProxyRouter.StopProxyService() dynamicProxyRouter.Option.Port = newIncomingPortInt + time.Sleep(1 * time.Second) //Fixed start fail issue dynamicProxyRouter.StartProxyService() } else { //Only change setting but not starting the proxy service diff --git a/src/start.go b/src/start.go index 0be8d8f..13d919d 100644 --- a/src/start.go +++ b/src/start.go @@ -103,8 +103,9 @@ func startupSequence() { //Create a geodb store geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{ - AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup, - AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup, + AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup, + AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup, + SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute, }) if err != nil { panic(err) diff --git a/src/web/components/rules.html b/src/web/components/rules.html index 6ce8ead..f6df6ce 100644 --- a/src/web/components/rules.html +++ b/src/web/components/rules.html @@ -295,15 +295,25 @@ //Automatic check if the site require TLS and check the checkbox if needed function autoCheckTls(targetDomain){ $.cjax({ - url: "/api/proxy/tlscheck", + url: "/api/proxy/tlscheck?selfsignchk=true", data: {url: targetDomain}, success: function(data){ if (data.error != undefined){ msgbox(data.error, false); - }else if (data == "https"){ - $("#reqTls").parent().checkbox("set checked"); - }else if (data == "http"){ - $("#reqTls").parent().checkbox("set unchecked"); + }else{ + //Check if the site require TLS + if (data.protocol == "https"){ + $("#reqTls").parent().checkbox("set checked"); + }else if (data.protocol == "http"){ + $("#reqTls").parent().checkbox("set unchecked"); + } + //Check if the site is using self-signed cert + if (data.selfsign){ + $("#skipTLSValidation").parent().checkbox("set checked"); + }else{ + $("#skipTLSValidation").parent().checkbox("set unchecked"); + } + } } }) diff --git a/src/web/components/status.html b/src/web/components/status.html index abef39e..24d658b 100644 --- a/src/web/components/status.html +++ b/src/web/components/status.html @@ -73,25 +73,27 @@
Inbound Port (Reverse Proxy Listening Port)