Sfoglia il codice sorgente

Added sync map slow geoip resolve mode

Toby Chui 3 mesi fa
parent
commit
994b20bfa7

+ 4 - 1
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", &currentTlsSetting)
 	}

+ 22 - 19
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
 )

+ 1 - 1
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 {

+ 55 - 18
mod/geodb/geodb.go

@@ -3,6 +3,8 @@ package geodb
 import (
 	_ "embed"
 	"net/http"
+	"sync"
+	"time"
 
 	"imuslab.com/zoraxy/mod/database"
 	"imuslab.com/zoraxy/mod/netutils"
@@ -15,17 +17,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      sync.Map     //Cache for slow lookup, ip -> cc
+	slowLookupCacheIpv6      sync.Map     //Cache for slow lookup ipv6, ip -> cc
+	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 +57,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 = 30 * time.Minute
+	}
+
+	//Create a new store
+	thisGeoDBStore := &Store{
+		geodb:                    parsedGeoData,
+		geotrie:                  ipv4Trie,
+		geodbIpv6:                parsedGeoDataIpv6,
+		geotrieIpv6:              ipv6Trie,
+		sysdb:                    sysdb,
+		slowLookupCacheIpv4:      sync.Map{},
+		slowLookupCacheIpv6:      sync.Map{},
+		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 = sync.Map{}
+					thisGeoDBStore.slowLookupCacheIpv6 = sync.Map{}
+				}
+			}
+		}(thisGeoDBStore)
+	}
+
+	return thisGeoDBStore, nil
 }
 
 func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
@@ -73,8 +106,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 {

+ 22 - 1
mod/geodb/geodb_test.go

@@ -42,8 +42,9 @@ func TestTrieConstruct(t *testing.T) {
 func TestResolveCountryCodeFromIP(t *testing.T) {
 	// Create a new store
 	store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
-		false,
 		true,
+		true,
+		0,
 	})
 	if err != nil {
 		t.Errorf("error creating store: %v", err)
@@ -83,4 +84,24 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
 	if info.CountryIsoCode != expected {
 		t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
 	}
+
+	// Test for issue #401
+	// Create 100 concurrent goroutines to resolve country code for random IP addresses in the test cases above
+	for i := 0; i < 100; i++ {
+		go func() {
+			for _, testcase := range knownIpCountryMap {
+				ip := testcase[0]
+				expected := testcase[1]
+				info, err := store.ResolveCountryCodeFromIP(ip)
+				if err != nil {
+					t.Errorf("error resolving country code for IP %s: %v", ip, err)
+					return
+				}
+				if info.CountryIsoCode != expected {
+					t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
+				}
+			}
+		}()
+	}
+
 }

File diff suppressed because it is too large
+ 236 - 171
mod/geodb/geoipv4.csv


File diff suppressed because it is too large
+ 247 - 174
mod/geodb/geoipv6.csv


+ 36 - 0
mod/geodb/slowSearch.go

@@ -56,6 +56,13 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
 	if isReservedIP(ipAddr) {
 		return ""
 	}
+
+	//Check if already in cache
+	cc := s.GetSlowSearchCachedIpv4(ipAddr)
+	if cc != "" {
+		return cc
+	}
+
 	for _, ipRange := range s.geodb {
 		startIp := ipRange[0]
 		endIp := ipRange[1]
@@ -63,6 +70,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
 
 		inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
 		if inRange {
+			//Add to cache
+			s.slowLookupCacheIpv4.Store(ipAddr, cc)
 			return cc
 		}
 	}
@@ -73,6 +82,13 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
 	if isReservedIP(ipAddr) {
 		return ""
 	}
+
+	//Check if already in cache
+	cc := s.GetSlowSearchCachedIpv6(ipAddr)
+	if cc != "" {
+		return cc
+	}
+
 	for _, ipRange := range s.geodbIpv6 {
 		startIp := ipRange[0]
 		endIp := ipRange[1]
@@ -80,8 +96,28 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
 
 		inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
 		if inRange {
+			//Add to cache
+			s.slowLookupCacheIpv6.Store(ipAddr, cc)
 			return cc
 		}
 	}
 	return ""
 }
+
+// GetSlowSearchCachedIpv4 return the country code for the given ipv4 address, return empty string if not found
+func (s *Store) GetSlowSearchCachedIpv4(ipAddr string) string {
+	cc, ok := s.slowLookupCacheIpv4.Load(ipAddr)
+	if ok {
+		return cc.(string)
+	}
+	return ""
+}
+
+// GetSlowSearchCachedIpv6 return the country code for the given ipv6 address, return empty string if not found
+func (s *Store) GetSlowSearchCachedIpv6(ipAddr string) string {
+	cc, ok := s.slowLookupCacheIpv6.Load(ipAddr)
+	if ok {
+		return cc.(string)
+	}
+	return ""
+}

+ 7 - 6
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

+ 3 - 2
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)

+ 12 - 10
web/components/status.html

@@ -73,25 +73,27 @@
     <p>Inbound Port (Reverse Proxy Listening Port)</p>
     <div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
         <small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
-        <input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
+        <input type="text" id="incomingPort" placeholder="Incoming Port" value="443">
         <button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
     </div>
     <br>
     <div id="tls" class="ui toggle notloopbackOnly checkbox">
         <input type="checkbox">
-        <label>Use TLS to serve proxy request</label>
+        <label>Use TLS to serve proxy request<br>
+        <small>Also known as HTTPS mode</small></label>
     </div>
     <br>
-    <div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
+    <div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.4em;" >
         <input type="checkbox">
         <label>Enable HTTP server on port 80<br>
-        <small>(Only apply when TLS enabled and not using port 80)</small></label>
+        <small>Accept HTTP requests even if you are using HTTPS mode</small></label>
     </div>
     <br>
     <div tourstep="forceHttpsRedirect" style="display: inline-block;">
-        <div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
+        <div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.4em;">
             <input type="checkbox">
-            <label>Force redirect HTTP request to HTTPS</label>
+            <label>Force redirect HTTP request to HTTPS<br>
+            <small>Redirect web traffic from port 80 to 443, require enabling HTTP server on port 80</small></label>
         </div>
     </div>
     <div class="ui basic segment advanceoptions">
@@ -359,10 +361,10 @@
                     return;
                 }
                 if (enabled){
-                    $("#redirect").show();
+                    //$("#redirect").show();
                     msgbox("Port 80 listener enabled");
                 }else{
-                    $("#redirect").hide();
+                    //$("#redirect").hide();
                     msgbox("Port 80 listener disabled");
                 }
             }
@@ -400,10 +402,10 @@
         $.get("/api/proxy/listenPort80", function(data){
             if (data){
                 $("#listenP80").checkbox("set checked");
-                $("#redirect").show();
+                //$("#redirect").show();
             }else{
                 $("#listenP80").checkbox("set unchecked");
-                $("#redirect").hide();
+                //$("#redirect").hide();
             }
 
             $("#listenP80").find("input").on("change", function(){

+ 8 - 3
web/components/vdir.html

@@ -191,14 +191,19 @@
         var targetDomain = $("#virtualDirectoryDomain").val().trim();
         if (targetDomain != ""){
             $.cjax({
-                url: "/api/proxy/tlscheck",
+                url: "/api/proxy/tlscheck?selfsignchk=true",
                 data: {url: targetDomain},
                 success: function(data){
                     if (data.error != undefined){
 
-                    }else if (data == "https"){
+                    }else if (data.protocol == "https"){
                         $("#vdReqTls").parent().checkbox("set checked");
-                    }else if (data == "http"){
+                        if (data.selfsign){
+                            $("#vdSkipTLSValidation").parent().checkbox("set checked");
+                        }else{
+                            $("#vdSkipTLSValidation").parent().checkbox("set unchecked");
+                        }
+                    }else if (data.protocol == "http"){
                         $("#vdReqTls").parent().checkbox("set unchecked");
                     }
                 }

+ 6 - 1
web/darktheme.css

@@ -1125,4 +1125,9 @@ body.darkTheme .ui.celled.compact.table thead th {
     background-color: var(--theme_bg_secondary) !important;
     color: var(--text_color) !important;
     border-color: var(--divider_color) !important;
-}
+}
+
+body.darkTheme .ui.list .list > .item .header, .ui.list > .item .header,
+body.darkTheme .ui.list .list > .item .description, .ui.list > .item .description {
+    color: var(--text_color) !important;
+}

+ 243 - 178
web/snippet/dockerContainersList.html

@@ -1,178 +1,243 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <!-- Notes: This should be open in its original path-->
-    <meta charset="utf-8" />
-    <link rel="stylesheet" href="../script/semantic/semantic.min.css" />
-    <script src="../script/jquery-3.6.0.min.js"></script>
-    <script src="../script/semantic/semantic.min.js"></script>
-  </head>
-  <body>
-    <link rel="stylesheet" href="../darktheme.css">
-    <script src="../script/darktheme.js"></script>
-    <br />
-    <div class="ui container">
-      <div class="field">
-        <div class="ui checkbox">
-          <input type="checkbox" id="showUnexposed" class="hidden" />
-          <label for="showUnexposed"
-            >Show Containers with Unexposed Ports
-            <br />
-            <small
-              >Please make sure Zoraxy and the target container share a
-              network</small
-            >
-          </label>
-        </div>
-      </div>
-      <div class="ui header">
-        <div class="content">
-          List of Docker Containers
-          <div class="sub header">
-            Below is a list of all detected Docker containers currently running
-            on the system.
-          </div>
-        </div>
-      </div>
-      <div id="containersList" class="ui middle aligned divided list active">
-        <div class="ui loader active"></div>
-      </div>
-      <div class="ui horizontal divider"></div>
-      <div id="containersAddedListHeader" class="ui header" hidden>
-        Already added containers:
-      </div>
-      <div
-        id="containersAddedList"
-        class="ui middle aligned divided list"
-      ></div>
-    </div>
-
-    <script>
-      let lines = {};
-      let linesAdded = {};
-
-      document
-        .getElementById("showUnexposed")
-        .addEventListener("change", () => {
-          console.log("showUnexposed", $("#showUnexposed").is(":checked"));
-          $("#containersList").html('<div class="ui loader active"></div>');
-
-          $("#containersAddedList").empty();
-          $("#containersAddedListHeader").attr("hidden", true);
-
-          lines = {};
-          linesAdded = {};
-
-          getDockerContainers();
-        });
-
-      function getDockerContainers() {
-        const hostRequest = $.get("/api/proxy/list?type=host");
-        const dockerRequest = $.get("/api/docker/containers");
-
-        Promise.all([hostRequest, dockerRequest])
-          .then(([hostData, dockerData]) => {
-            if (!dockerData.error && !hostData.error) {
-              const { containers, network } = dockerData;
-
-              const existingTargets = new Set(
-                hostData.flatMap(({ ActiveOrigins }) =>
-                  ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
-                )
-              );
-
-              for (const container of containers) {
-                const Ports = container.Ports;
-                const name = container.Names[0].replace(/^\//, "");
-
-                for (const portObject of Ports) {
-                  let port = portObject.PublicPort;
-                  if (!port) {
-                    if (!$("#showUnexposed").is(":checked")) {
-                      continue;
-                    }
-                    port = portObject.PrivatePort;
-                  }
-                  const key = `${name}-${port}`;
-
-                  // if port is not exposed, use container's name and let docker handle the routing
-                  // BUT this will only work if the container is on the same network as Zoraxy
-                  const targetAddress = portObject.IP || name;
-
-                  if (
-                    existingTargets.has(`${targetAddress}:${port}`) &&
-                    !linesAdded[key]
-                  ) {
-                    linesAdded[key] = {
-                      name,
-                      ip: targetAddress,
-                      port,
-                    };
-                  } else if (!lines[key]) {
-                    lines[key] = {
-                      name,
-                      ip: targetAddress,
-                      port,
-                    };
-                  }
-                }
-              }
-
-              for (const [key, line] of Object.entries(lines)) {
-                $("#containersList").append(
-                  `<div class="item">
-                        <div class="right floated content">
-                          <div class="ui button" onclick="addContainerItem('${key}');">Add</div>
-                        </div>
-                        <div class="content">
-                          <div class="header">${line.name}</div>
-                          <div class="description">
-                            ${line.ip}:${line.port}
-                          </div>
-                        </div>`
-                );
-              }
-
-              for (const [key, line] of Object.entries(linesAdded)) {
-                $("#containersAddedList").append(
-                  `<div class="item">
-                        <div class="content">
-                          <div class="header">${line.name}</div>
-                          <div class="description">
-                            ${line.ip}:${line.port}
-                          </div>
-                        </div>`
-                );
-              }
-
-              Object.entries(linesAdded).length &&
-                $("#containersAddedListHeader").removeAttr("hidden");
-              $("#containersList .loader").removeClass("active");
-            } else {
-              parent.msgbox(
-                `Error loading data: ${dockerData.error || hostData.error}`,
-                false
-              );
-              $("#containersList").html(
-                `<div class="ui basic segment"><i class="ui red times icon"></i> ${
-                  dockerData.error || hostData.error
-                }</div>`
-              );
-            }
-          })
-          .catch((error) => {
-            console.log(error.responseText);
-            parent.msgbox("Error loading data: " + error.message, false);
-          });
-      }
-
-      getDockerContainers();
-
-      function addContainerItem(item) {
-        if (lines[item]) {
-          parent.addContainerItem(lines[item]);
-        }
-      }
-    </script>
-  </body>
-</html>
+<!DOCTYPE html>
+<html>
+  <head>
+    <!-- Notes: This should be open in its original path-->
+    <meta charset="utf-8" />
+    <link rel="stylesheet" href="../script/semantic/semantic.min.css" />
+    <script src="../script/jquery-3.6.0.min.js"></script>
+    <script src="../script/semantic/semantic.min.js"></script>
+  </head>
+  <body>
+    <link rel="stylesheet" href="../darktheme.css">
+    <script src="../script/darktheme.js"></script>
+    <br />
+    <div class="ui container">
+      <div class="ui form">
+        <div class="field">
+          <input
+            id="searchbar"
+            type="text"
+            placeholder="Search..."
+            autocomplete="off"
+          />
+        </div>
+        <div class="field">
+          <div class="ui checkbox">
+            <input type="checkbox" id="showUnexposed" class="hidden" />
+            <label for="showUnexposed"
+              >Show Containers with unexposed ports
+              <br />
+              <small
+                >Please make sure Zoraxy and the target container share a
+                network</small
+              >
+            </label>
+          </div>
+        </div>
+      </div>
+      <div class="ui header">
+        <div class="content">
+          List of Docker Containers
+          <div class="sub header">
+            Below is a list of all detected Docker containers currently running
+            on the system.
+          </div>
+        </div>
+      </div>
+      <div id="containersList" class="ui middle aligned divided list active">
+        <div class="ui loader active"></div>
+      </div>
+      <div class="ui horizontal divider"></div>
+      <div id="containersAddedListHeader" class="ui header" hidden>
+        Already added containers:
+      </div>
+      <div
+        id="containersAddedList"
+        class="ui middle aligned divided list"
+      >
+
+      </div>
+    </div>
+    <script>
+      // debounce function to prevent excessive calls to a function
+      function debounce(func, delay) {
+        let timeout;
+        return (...args) => {
+          clearTimeout(timeout);
+          timeout = setTimeout(() => func(...args), delay);
+        };
+      }
+
+      // wait until DOM is fully loaded before executing script
+      $(document).ready(() => {
+        const $containersList = $("#containersList");
+        const $containersAddedList = $("#containersAddedList");
+        const $containersAddedListHeader = $("#containersAddedListHeader");
+        const $searchbar = $("#searchbar");
+        const $showUnexposed = $("#showUnexposed");
+
+        let lines = {};
+        let linesAdded = {};
+
+        // load showUnexposed checkbox state from local storage
+        function loadShowUnexposedState() {
+          const storedState = localStorage.getItem("showUnexposed");
+          if (storedState !== null) {
+            $showUnexposed.prop("checked", storedState === "true");
+          }
+        }
+
+        // save showUnexposed checkbox state to local storage
+        function saveShowUnexposedState() {
+          localStorage.setItem("showUnexposed", $showUnexposed.prop("checked"));
+        }
+
+        // fetch docker containers
+        function getDockerContainers() {
+          $containersList.html('<div class="ui loader active"></div>');
+          $containersAddedList.empty();
+          $containersAddedListHeader.attr("hidden", true);
+
+          lines = {};
+          linesAdded = {};
+
+          const hostRequest = $.get("/api/proxy/list?type=host");
+          const dockerRequest = $.get("/api/docker/containers");
+
+          Promise.all([hostRequest, dockerRequest])
+            .then(([hostData, dockerData]) => {
+              if (!hostData.error && !dockerData.error) {
+                processDockerData(hostData, dockerData);
+              } else {
+                showError(hostData.error || dockerData.error);
+              }
+            })
+            .catch((error) => {
+              console.error(error);
+              parent.msgbox("Error loading data: " + error.message, false);
+            });
+        }
+
+        // process docker data and update ui
+        function processDockerData(hostData, dockerData) {
+          const { containers } = dockerData;
+          const existingTargets = new Set(
+            hostData.flatMap(({ ActiveOrigins }) =>
+              ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
+            )
+          );
+
+          containers.forEach((container) => {
+            const name = container.Names[0].replace(/^\//, "");
+            container.Ports.forEach((portObject) => {
+              let port = portObject.PublicPort || portObject.PrivatePort;
+              if (!portObject.PublicPort && !$showUnexposed.is(":checked"))
+                return;
+
+              // if port is not exposed, use container's name and let docker handle the routing
+              // BUT this will only work if the container is on the same network as Zoraxy
+              const targetAddress = portObject.IP || name;
+              const key = `${name}-${port}`;
+
+              if (
+                existingTargets.has(`${targetAddress}:${port}`) &&
+                !linesAdded[key]
+              ) {
+                linesAdded[key] = { name, ip: targetAddress, port };
+              } else if (!lines[key]) {
+                lines[key] = { name, ip: targetAddress, port };
+              }
+            });
+          });
+
+          // update ui
+          updateContainersList();
+          updateAddedContainersList();
+        }
+
+        // update containers list
+        function updateContainersList() {
+          $containersList.empty();
+          Object.entries(lines).forEach(([key, line]) => {
+            $containersList.append(`
+              <div class="item">
+                <div class="right floated content">
+                  <div class="ui button add-button" data-key="${key}">Add</div>
+                </div>
+                <div class="content">
+                  <div class="header">${line.name}</div>
+                  <div class="description">${line.ip}:${line.port}</div>
+                </div>
+              </div>
+            `);
+          });
+          $containersList.find(".loader").removeClass("active");
+        }
+
+        // update the added containers list
+        function updateAddedContainersList() {
+          Object.entries(linesAdded).forEach(([key, line]) => {
+            $containersAddedList.append(`
+              <div class="item">
+                <div class="content">
+                  <div class="header">${line.name}</div>
+                  <div class="description">${line.ip}:${line.port}</div>
+                </div>
+              </div>
+            `);
+          });
+          if (Object.keys(linesAdded).length) {
+            $containersAddedListHeader.removeAttr("hidden");
+          }
+        }
+
+        // show error message
+        function showError(error) {
+          $containersList.html(
+            `<div class="ui basic segment"><i class="ui red times icon"></i> ${error}</div>`
+          );
+          parent.msgbox(`Error loading data: ${error}`, false);
+        }
+
+        //
+        // event listeners
+        //
+
+        $showUnexposed.on("change", () => {
+          saveShowUnexposedState(); // save the new state to local storage
+          getDockerContainers();
+        });
+
+        $searchbar.on(
+          "input",
+          debounce(() => {
+            // debounce searchbar input with 300ms delay, then filter list
+            // this prevents excessive calls to the filter function
+            const search = $searchbar.val().toLowerCase();
+            $("#containersList .item").each((index, item) => {
+              const content = $(item).text().toLowerCase();
+              $(item).toggle(content.includes(search));
+            });
+          }, 300)
+        );
+
+        $containersList.on("click", ".add-button", (event) => {
+          const key = $(event.currentTarget).data("key");
+          if (lines[key]) {
+            parent.addContainerItem(lines[key]);
+          }
+        });
+
+        //
+        // initial calls
+        //
+
+        // load state of showUnexposed checkbox
+        loadShowUnexposedState();
+
+        // initial load of docker containers
+        getDockerContainers();
+      });
+    </script>
+  </body>
+</html>

Some files were not shown because too many files changed in this diff