Browse Source

Merge remote-tracking branch 'tc/master' into acme

Alan Yeung 1 year ago
parent
commit
12f0f9a59d

+ 7 - 3
api.go

@@ -6,6 +6,7 @@ import (
 
 	"imuslab.com/zoraxy/mod/auth"
 	"imuslab.com/zoraxy/mod/netstat"
+	"imuslab.com/zoraxy/mod/netutils"
 	"imuslab.com/zoraxy/mod/utils"
 )
 
@@ -55,6 +56,7 @@ func initAPIs() {
 
 	//TLS / SSL config
 	authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
+	authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
 	authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
 	authRouter.HandleFunc("/api/cert/list", handleListCertificate)
 	authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
@@ -82,9 +84,9 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
 
 	//Path Blocker APIs
-	authRouter.HandleFunc("/api/pathblock/add", pathBlockHandler.HandleAddBlockingPath)
-	authRouter.HandleFunc("/api/pathblock/list", pathBlockHandler.HandleListBlockingPath)
-	authRouter.HandleFunc("/api/pathblock/remove", pathBlockHandler.HandleRemoveBlockingPath)
+	authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
+	authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
+	authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
 
 	//Statistic & uptime monitoring API
 	authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
@@ -131,6 +133,8 @@ func initAPIs() {
 
 	//Network utilities
 	authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
+	authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
+	authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
 	authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
 	authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
 	authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)

+ 27 - 0
cert.go

@@ -130,6 +130,33 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// Handle the GET and SET of reverse proxy TLS versions
+func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
+	newState, err := utils.PostPara(r, "set")
+	if err != nil {
+		//GET
+		var reqLatestTLS bool = false
+		if sysdb.KeyExists("settings", "forceLatestTLS") {
+			sysdb.Read("settings", "forceLatestTLS", &reqLatestTLS)
+		}
+
+		js, _ := json.Marshal(reqLatestTLS)
+		utils.SendJSONResponse(w, string(js))
+	} else {
+		if newState == "true" {
+			sysdb.Write("settings", "forceLatestTLS", true)
+			log.Println("Updating minimum TLS version to v1.2 or above")
+			dynamicProxyRouter.UpdateTLSVersion(true)
+		} else if newState == "false" {
+			sysdb.Write("settings", "forceLatestTLS", false)
+			log.Println("Updating minimum TLS version to v1.0 or above")
+			dynamicProxyRouter.UpdateTLSVersion(false)
+		} else {
+			utils.SendErrorResponse(w, "invalid state given")
+		}
+	}
+}
+
 // Handle upload of the certificate
 func handleCertUpload(w http.ResponseWriter, r *http.Request) {
 	// check if request method is POST

+ 1 - 1
go.mod

@@ -13,6 +13,6 @@ require (
 	github.com/microcosm-cc/bluemonday v1.0.24
 	github.com/oschwald/geoip2-golang v1.8.0
 	github.com/satori/go.uuid v1.2.0
-	golang.org/x/net v0.10.0 // indirect
+	golang.org/x/net v0.10.0
 	golang.org/x/sys v0.8.0
 )

+ 3 - 3
main.go

@@ -21,7 +21,7 @@ import (
 	"imuslab.com/zoraxy/mod/geodb"
 	"imuslab.com/zoraxy/mod/mdns"
 	"imuslab.com/zoraxy/mod/netstat"
-	"imuslab.com/zoraxy/mod/pathblock"
+	"imuslab.com/zoraxy/mod/pathrule"
 	"imuslab.com/zoraxy/mod/sshprox"
 	"imuslab.com/zoraxy/mod/statistic"
 	"imuslab.com/zoraxy/mod/statistic/analytic"
@@ -41,7 +41,7 @@ var (
 	name        = "Zoraxy"
 	version     = "2.6.4"
 	nodeUUID    = "generic"
-	development = true //Set this to false to use embedded web fs
+	development = false //Set this to false to use embedded web fs
 	bootTime    = time.Now().Unix()
 
 	/*
@@ -58,7 +58,7 @@ var (
 	authAgent          *auth.AuthAgent         //Authentication agent
 	tlsCertManager     *tlscert.Manager        //TLS / SSL management
 	redirectTable      *redirection.RuleTable  //Handle special redirection rule sets
-	pathBlockHandler   *pathblock.Handler      //Handle specific path blocking or custom headers
+	pathRuleHandler    *pathrule.Handler       //Handle specific path blocking or custom headers
 	geodbStore         *geodb.Store            //GeoIP database, also handle black list and whitelist features
 	netstatBuffers     *netstat.NetStatBuffers //Realtime graph buffers
 	statisticCollector *statistic.Collector    //Collecting statistic from visitors

+ 0 - 1
mod/dynamicproxy/Server.go

@@ -115,7 +115,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		h.proxyRequest(w, r, targetProxyEndpoint)
 	} else if !strings.HasSuffix(proxyingPath, "/") {
 		potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
-
 		if potentialProxtEndpoint != nil {
 			//Missing tailing slash. Redirect to target proxy endpoint
 			http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)

+ 19 - 4
mod/dynamicproxy/dynamicproxy.go

@@ -45,6 +45,13 @@ func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
 	router.Restart()
 }
 
+// Update TLS Version in runtime. Will restart proxy server if running.
+// Set this to true to force TLS 1.2 or above
+func (router *Router) UpdateTLSVersion(requireLatest bool) {
+	router.Option.ForceTLSLatest = requireLatest
+	router.Restart()
+}
+
 // Update https redirect, which will require updates
 func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
 	router.Option.ForceHttpsRedirect = useRedirect
@@ -62,9 +69,13 @@ func (router *Router) StartProxyService() error {
 		return errors.New("Reverse proxy router root not set")
 	}
 
+	minVersion := tls.VersionTLS10
+	if router.Option.ForceTLSLatest {
+		minVersion = tls.VersionTLS12
+	}
 	config := &tls.Config{
 		GetCertificate: router.Option.TlsManager.GetCert,
-		//MinVersion:     tls.VersionTLS12,
+		MinVersion:     uint16(minVersion),
 	}
 
 	if router.Option.UseTls {
@@ -172,18 +183,22 @@ func (router *Router) StopProxyService() error {
 }
 
 // Restart the current router if it is running.
-// Startup the server if it is not running initially
 func (router *Router) Restart() error {
 	//Stop the router if it is already running
+	var err error = nil
 	if router.Running {
 		err := router.StopProxyService()
 		if err != nil {
 			return err
 		}
+
+		// Start the server
+		err = router.StartProxyService()
+		if err != nil {
+			return err
+		}
 	}
 
-	//Start the server
-	err := router.StartProxyService()
 	return err
 }
 

+ 6 - 5
mod/dynamicproxy/typedef.go

@@ -22,13 +22,14 @@ type ProxyHandler struct {
 }
 
 type RouterOption struct {
-	HostUUID           string
-	Port               int
-	UseTls             bool
-	ForceHttpsRedirect bool
+	HostUUID           string //The UUID of Zoraxy, use for heading mod
+	Port               int    //Incoming port
+	UseTls             bool   //Use TLS to serve incoming requsts
+	ForceTLSLatest     bool   //Force TLS1.2 or above
+	ForceHttpsRedirect bool   //Force redirection of http to https endpoint
 	TlsManager         *tlscert.Manager
 	RedirectRuleTable  *redirection.RuleTable
-	GeodbStore         *geodb.Store
+	GeodbStore         *geodb.Store //GeoIP blacklist and whitelist
 	StatisticCollector *statistic.Collector
 }
 

+ 69 - 0
mod/netutils/netutils.go

@@ -0,0 +1,69 @@
+package netutils
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"imuslab.com/zoraxy/mod/utils"
+)
+
+/*
+	This script handles basic network utilities like
+	- traceroute
+	- ping
+*/
+
+func HandleTraceRoute(w http.ResponseWriter, r *http.Request) {
+	targetIpOrDomain, err := utils.GetPara(r, "target")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
+		return
+	}
+
+	maxhopsString, err := utils.GetPara(r, "maxhops")
+	if err != nil {
+		maxhopsString = "64"
+	}
+
+	maxHops, err := strconv.Atoi(maxhopsString)
+	if err != nil {
+		maxHops = 64
+	}
+
+	results, err := TraceRoute(targetIpOrDomain, maxHops)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal(results)
+	utils.SendJSONResponse(w, string(js))
+}
+
+func TraceRoute(targetIpOrDomain string, maxHops int) ([]string, error) {
+	return traceroute(targetIpOrDomain, maxHops)
+}
+
+func HandlePing(w http.ResponseWriter, r *http.Request) {
+	targetIpOrDomain, err := utils.GetPara(r, "target")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
+		return
+	}
+
+	results := []string{}
+	for i := 0; i < 4; i++ {
+		realIP, pingTime, ttl, err := PingIP(targetIpOrDomain)
+		if err != nil {
+			results = append(results, "Reply from "+realIP+": "+err.Error())
+		} else {
+			results = append(results, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl))
+		}
+	}
+
+	js, _ := json.Marshal(results)
+	utils.SendJSONResponse(w, string(js))
+
+}

+ 28 - 0
mod/netutils/netutils_test.go

@@ -0,0 +1,28 @@
+package netutils_test
+
+import (
+	"testing"
+
+	"imuslab.com/zoraxy/mod/netutils"
+)
+
+func TestHandleTraceRoute(t *testing.T) {
+	results, err := netutils.TraceRoute("imuslab.com", 64)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	t.Log(results)
+}
+
+func TestHandlePing(t *testing.T) {
+	ipOrDomain := "example.com"
+
+	realIP, pingTime, ttl, err := netutils.PingIP(ipOrDomain)
+	if err != nil {
+		t.Fatal("Error:", err)
+		return
+	}
+
+	t.Log(realIP, pingTime, ttl)
+}

+ 48 - 0
mod/netutils/pingip.go

@@ -0,0 +1,48 @@
+package netutils
+
+import (
+	"fmt"
+	"net"
+	"time"
+)
+
+func PingIP(ipOrDomain string) (string, time.Duration, int, error) {
+	ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain)
+	if err != nil {
+		return "", 0, 0, fmt.Errorf("failed to resolve IP address: %v", err)
+	}
+
+	ip := ipAddr.IP.String()
+
+	start := time.Now()
+
+	conn, err := net.Dial("ip:icmp", ip)
+	if err != nil {
+		return ip, 0, 0, fmt.Errorf("failed to establish ICMP connection: %v", err)
+	}
+	defer conn.Close()
+
+	icmpMsg := []byte{8, 0, 0, 0, 0, 1, 0, 0}
+	_, err = conn.Write(icmpMsg)
+	if err != nil {
+		return ip, 0, 0, fmt.Errorf("failed to send ICMP message: %v", err)
+	}
+
+	reply := make([]byte, 1500)
+	err = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
+	if err != nil {
+		return ip, 0, 0, fmt.Errorf("failed to set read deadline: %v", err)
+	}
+
+	_, err = conn.Read(reply)
+	if err != nil {
+		return ip, 0, 0, fmt.Errorf("failed to read ICMP reply: %v", err)
+	}
+
+	elapsed := time.Since(start)
+	pingTime := elapsed.Round(time.Millisecond)
+
+	ttl := int(reply[8])
+
+	return ip, pingTime, ttl, nil
+}

+ 212 - 0
mod/netutils/traceroute.go

@@ -0,0 +1,212 @@
+package netutils
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"time"
+
+	"golang.org/x/net/icmp"
+	"golang.org/x/net/ipv4"
+)
+
+const (
+	protocolICMP = 1
+)
+
+// liveTraceRoute return realtime tracing information to live response handler
+func liveTraceRoute(dst string, maxHops int, liveRespHandler func(string)) error {
+	timeout := time.Second * 3
+	// resolve the host name to an IP address
+	ipAddr, err := net.ResolveIPAddr("ip4", dst)
+	if err != nil {
+		return fmt.Errorf("failed to resolve IP address for %s: %v", dst, err)
+	}
+	// create a socket to listen for incoming ICMP packets
+	conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
+	if err != nil {
+		return fmt.Errorf("failed to create ICMP listener: %v", err)
+	}
+	defer conn.Close()
+	id := os.Getpid() & 0xffff
+	seq := 0
+loop_ttl:
+	for ttl := 1; ttl <= maxHops; ttl++ {
+		// set the TTL on the socket
+		if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
+			return fmt.Errorf("failed to set TTL: %v", err)
+		}
+		seq++
+		// create an ICMP message
+		msg := icmp.Message{
+			Type: ipv4.ICMPTypeEcho,
+			Code: 0,
+			Body: &icmp.Echo{
+				ID:   id,
+				Seq:  seq,
+				Data: []byte("zoraxy_trace"),
+			},
+		}
+		// serialize the ICMP message
+		msgBytes, err := msg.Marshal(nil)
+		if err != nil {
+			return fmt.Errorf("failed to serialize ICMP message: %v", err)
+		}
+		// send the ICMP message
+		start := time.Now()
+		if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
+			//log.Printf("%d: %v", ttl, err)
+			liveRespHandler(fmt.Sprintf("%d: %v", ttl, err))
+			continue loop_ttl
+		}
+		// listen for the reply
+		replyBytes := make([]byte, 1500)
+		if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
+			return fmt.Errorf("failed to set read deadline: %v", err)
+		}
+		for i := 0; i < 3; i++ {
+			n, peer, err := conn.ReadFrom(replyBytes)
+			if err != nil {
+				if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
+					//fmt.Printf("%d: *\n", ttl)
+					liveRespHandler(fmt.Sprintf("%d: *\n", ttl))
+					continue loop_ttl
+				} else {
+					liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
+				}
+				continue
+			}
+			// parse the ICMP message
+			replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
+			if err != nil {
+				liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
+				continue
+			}
+			// check if the reply is an echo reply
+			if replyMsg.Type == ipv4.ICMPTypeEchoReply {
+				echoReply, ok := msg.Body.(*icmp.Echo)
+				if !ok || echoReply.ID != id || echoReply.Seq != seq {
+					continue
+				}
+				liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, peer, time.Since(start)))
+				break loop_ttl
+			}
+			if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
+				echoReply, ok := msg.Body.(*icmp.Echo)
+				if !ok || echoReply.ID != id || echoReply.Seq != seq {
+					continue
+				}
+				var raddr = peer.String()
+				names, _ := net.LookupAddr(raddr)
+				if len(names) > 0 {
+					raddr = names[0] + " (" + raddr + ")"
+				} else {
+					raddr = raddr + " (" + raddr + ")"
+				}
+				liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, raddr, time.Since(start)))
+				continue loop_ttl
+			}
+		}
+
+	}
+	return nil
+}
+
+// Standard traceroute, return results after complete
+func traceroute(dst string, maxHops int) ([]string, error) {
+	results := []string{}
+	timeout := time.Second * 3
+	// resolve the host name to an IP address
+	ipAddr, err := net.ResolveIPAddr("ip4", dst)
+	if err != nil {
+		return results, fmt.Errorf("failed to resolve IP address for %s: %v", dst, err)
+	}
+	// create a socket to listen for incoming ICMP packets
+	conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
+	if err != nil {
+		return results, fmt.Errorf("failed to create ICMP listener: %v", err)
+	}
+	defer conn.Close()
+	id := os.Getpid() & 0xffff
+	seq := 0
+loop_ttl:
+	for ttl := 1; ttl <= maxHops; ttl++ {
+		// set the TTL on the socket
+		if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
+			return results, fmt.Errorf("failed to set TTL: %v", err)
+		}
+		seq++
+		// create an ICMP message
+		msg := icmp.Message{
+			Type: ipv4.ICMPTypeEcho,
+			Code: 0,
+			Body: &icmp.Echo{
+				ID:   id,
+				Seq:  seq,
+				Data: []byte("zoraxy_trace"),
+			},
+		}
+		// serialize the ICMP message
+		msgBytes, err := msg.Marshal(nil)
+		if err != nil {
+			return results, fmt.Errorf("failed to serialize ICMP message: %v", err)
+		}
+		// send the ICMP message
+		start := time.Now()
+		if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
+			//log.Printf("%d: %v", ttl, err)
+			results = append(results, fmt.Sprintf("%d: %v", ttl, err))
+			continue loop_ttl
+		}
+		// listen for the reply
+		replyBytes := make([]byte, 1500)
+		if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
+			return results, fmt.Errorf("failed to set read deadline: %v", err)
+		}
+		for i := 0; i < 3; i++ {
+			n, peer, err := conn.ReadFrom(replyBytes)
+			if err != nil {
+				if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
+					//fmt.Printf("%d: *\n", ttl)
+					results = append(results, fmt.Sprintf("%d: *", ttl))
+					continue loop_ttl
+				} else {
+					results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
+				}
+				continue
+			}
+			// parse the ICMP message
+			replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
+			if err != nil {
+				results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
+				continue
+			}
+			// check if the reply is an echo reply
+			if replyMsg.Type == ipv4.ICMPTypeEchoReply {
+				echoReply, ok := msg.Body.(*icmp.Echo)
+				if !ok || echoReply.ID != id || echoReply.Seq != seq {
+					continue
+				}
+				results = append(results, fmt.Sprintf("%d: %v %v", ttl, peer, time.Since(start)))
+				break loop_ttl
+			}
+			if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
+				echoReply, ok := msg.Body.(*icmp.Echo)
+				if !ok || echoReply.ID != id || echoReply.Seq != seq {
+					continue
+				}
+				var raddr = peer.String()
+				names, _ := net.LookupAddr(raddr)
+				if len(names) > 0 {
+					raddr = names[0] + " (" + raddr + ")"
+				} else {
+					raddr = raddr + " (" + raddr + ")"
+				}
+				results = append(results, fmt.Sprintf("%d: %v %v", ttl, raddr, time.Since(start)))
+				continue loop_ttl
+			}
+		}
+
+	}
+	return results, nil
+}

+ 1 - 1
mod/pathblock/handler.go → mod/pathrule/handler.go

@@ -1,4 +1,4 @@
-package pathblock
+package pathrule
 
 import (
 	"encoding/json"

+ 1 - 1
mod/pathblock/pathblock.go → mod/pathrule/pathrule.go

@@ -1,4 +1,4 @@
-package pathblock
+package pathrule
 
 import (
 	"encoding/json"

+ 9 - 0
reverseproxy.go

@@ -38,6 +38,14 @@ func ReverseProxtInit() {
 		log.Println("TLS mode disabled. Serving proxy request with plain http")
 	}
 
+	forceLatestTLSVersion := false
+	sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
+	if forceLatestTLSVersion {
+		log.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
+	} else {
+		log.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
+	}
+
 	forceHttpsRedirect := false
 	sysdb.Read("settings", "redirect", &forceHttpsRedirect)
 	if forceHttpsRedirect {
@@ -50,6 +58,7 @@ func ReverseProxtInit() {
 		HostUUID:           nodeUUID,
 		Port:               inboundPort,
 		UseTls:             useTls,
+		ForceTLSLatest:     forceLatestTLSVersion,
 		ForceHttpsRedirect: forceHttpsRedirect,
 		TlsManager:         tlsCertManager,
 		RedirectRuleTable:  redirectTable,

+ 3 - 3
start.go

@@ -15,7 +15,7 @@ import (
 	"imuslab.com/zoraxy/mod/geodb"
 	"imuslab.com/zoraxy/mod/mdns"
 	"imuslab.com/zoraxy/mod/netstat"
-	"imuslab.com/zoraxy/mod/pathblock"
+	"imuslab.com/zoraxy/mod/pathrule"
 	"imuslab.com/zoraxy/mod/sshprox"
 	"imuslab.com/zoraxy/mod/statistic"
 	"imuslab.com/zoraxy/mod/statistic/analytic"
@@ -101,8 +101,8 @@ func startupSequence() {
 		from file.
 	*/
 
-	pathBlockHandler = pathblock.NewPathBlocker(&pathblock.Options{
-		ConfigFolder: "./rules/pathblock",
+	pathRuleHandler = pathrule.NewPathBlocker(&pathrule.Options{
+		ConfigFolder: "./rules/pathrules",
 	})
 
 	/*

+ 53 - 0
web/components/networktools.html

@@ -12,13 +12,41 @@
     </div>
 
     <div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
+        <!-- MDNS Scanner-->
         <h2>Multicast DNS (mDNS) Scanner</h2>
         <p>Discover mDNS enabled service in this gateway forwarded network</p>
         <button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
         <div class="ui divider"></div>
+        <!-- IP Scanner-->
         <h2>IP Scanner</h2>
         <p>Discover local area network devices by pinging them one by one</p>
         <button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
+        <div class="ui divider"></div>
+        <!-- Traceroute-->
+        <h2>Traceroute / Ping</h2>
+        <p>Trace the network nodes that your packets hops through</p>
+        <div class="ui form">
+            <div class="two fields">
+                <div class="field">
+                    <label>Target domain or IP</label>
+                    <input type="text" id="traceroute_domain" placeholder="1.1.1.1">
+                </div>
+                <div class="field">
+                  <label>Max Hops</label>
+                  <input type="number" min="1" step="1" id="traceroute_maxhops" placeholder="64" value="64">
+                </div>
+            </div>
+            <button class="ui basic button" onclick="traceroute();"><i class="ui blue location arrow icon"></i> Start Tracing</button>
+            <button class="ui basic button" onclick="ping();"><i class="ui teal circle outline icon"></i> Ping</button>
+            <br><br>
+            <div class="field">
+                <label>Results</label>
+                <textarea id="traceroute_results" rows="10" style=""></textarea>
+            </div>
+            
+        </div>
+        <div class=""></div>
+
     </div>
 
     <div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
@@ -435,7 +463,32 @@ function updateMDNSListForWoL(){
 }
 updateMDNSListForWoL();
 
+function traceroute(){
+    let domain = $("#traceroute_domain").val().trim();
+    let maxhops = $("#traceroute_maxhops").val().trim();
+    $("#traceroute_results").val("Loading...");
+    $.get("/api/tools/traceroute?target=" + domain + "&maxhops=" + maxhops, function(data){
+        if (data.error != undefined){
+            $("#traceroute_results").val("");
+            msgbox(data.error, false, 6000);
+        }else{
+            $("#traceroute_results").val(data.join("\n"));
+        }
+    });
+}
 
+function ping(){
+    let domain = $("#traceroute_domain").val().trim();
+    $("#traceroute_results").val("Loading...");
+    $.get("/api/tools/ping?target=" + domain, function(data){
+        if (data.error != undefined){
+            $("#traceroute_results").val("");
+            msgbox(data.error, false, 6000);
+        }else{
+            $("#traceroute_results").val(data.join("\n"));
+        }
+    });
+}
 </script>
 
 

+ 92 - 66
web/components/redirection.html

@@ -1,78 +1,100 @@
+
 <div class="standardContainer">
-  <div class="ui basic segment">
-    <h2>Redirection Rules</h2>
-    <p>Add exception case for redirecting any matching URLs</p>
-  </div>
-  <div style="width: 100%; overflow-x: auto;">
-      <table class="ui sortable unstackable celled table" >
-        <thead>
-            <tr>
-                <th>Redirection URL</th>
-                <th>Destination URL</th>
-                <th class="no-sort">Copy Pathname</th>
-                <th class="no-sort">Status Code</th>
-                <th class="no-sort">Remove</th>
-            </tr>
-        </thead>
-        <tbody id="redirectionRuleList">
-            <tr>
-                <td></td>
-                <td></td>
-                <td></td>
-                <td></td>
-            </tr>
-        </tbody>
-    </table>
-  </div>
-  <div class="ui green message" id="delRuleSucc" style="display:none;">
-    <i class="ui green checkmark icon"></i> Redirection Rule Deleted
-  </div>
-  <div class="ui divider"></div>
-  <h4>Add Redirection Rule</h4>
-  <div class="ui form">
-      <div class="field">
-        <label>Redirection URL (From)</label>
-        <input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
-        <small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
-      </div>
-      <div class="field">
-        <label>Destination URL (To)</label>
-        <input type="text" name="destination-url" placeholder="Destination URL">
-        <small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite</small>
-      </div>
-      <div class="field">
-        <div class="ui checkbox">
-          <input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
-          <label>Forward Pathname</label>
+    <div class="ui basic segment">
+      <h2>Redirection Rules</h2>
+      <p>Add exception case for redirecting any matching URLs</p>
+    </div>
+    <div style="width: 100%; overflow-x: auto;">
+        <table class="ui sortable unstackable celled table" >
+          <thead>
+              <tr>
+                  <th>Redirection URL</th>
+                  <th>Destination URL</th>
+                  <th class="no-sort">Copy Pathname</th>
+                  <th class="no-sort">Status Code</th>
+                  <th class="no-sort">Remove</th>
+              </tr>
+          </thead>
+          <tbody id="redirectionRuleList">
+              <tr>
+                  <td></td>
+                  <td></td>
+                  <td></td>
+                  <td></td>
+              </tr>
+          </tbody>
+      </table>
+    </div>
+    <div class="ui green message" id="delRuleSucc" style="display:none;">
+      <i class="ui green checkmark icon"></i> Redirection Rule Deleted
+    </div>
+    <div class="ui divider"></div>
+    <h4>Add Redirection Rule</h4>
+    <div class="ui form">
+        <div class="field">
+          <label>Redirection URL (From)</label>
+          <input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
+          <small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
         </div>
-        <div class="ui message">
-          <p>Append the current pathname after the redirect destination</p>
-          <i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
-          <i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
+        <div class="field">
+          <label>Destination URL (To)</label>
+          <input type="text" name="destination-url" placeholder="Destination URL">
+          <small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite</small>
         </div>
-      </div>
-      <div class="grouped fields">
-          <label>Redirection Status Code</label>
-          <div class="field">
-            <div class="ui radio checkbox">
-              <input type="radio" name="redirect-type" value="307" checked>
-              <label>Temporary Redirect <br><small>Status Code: 307</small></label>
-            </div>
+        <div class="field">
+          <div class="ui checkbox">
+            <input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
+            <label>Forward Pathname</label>
+          </div>
+          <div class="ui message">
+            <p>Append the current pathname after the redirect destination</p>
+            <i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
+            <i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
           </div>
-          <div class="field">
-            <div class="ui radio checkbox">
-              <input type="radio" name="redirect-type" value="301">
-              <label>Moved Permanently <br><small>Status Code: 301</small></label>
+        </div>
+        <div class="grouped fields">
+            <label>Redirection Status Code</label>
+            <div class="field">
+              <div class="ui radio checkbox">
+                <input type="radio" name="redirect-type" value="307" checked>
+                <label>Temporary Redirect <br><small>Status Code: 307</small></label>
+              </div>
+            </div>
+            <div class="field">
+              <div class="ui radio checkbox">
+                <input type="radio" name="redirect-type" value="301">
+                <label>Moved Permanently <br><small>Status Code: 301</small></label>
+              </div>
+            </div>
+        </div>
+        <button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
+        <div class="ui green message" id="ruleAddSucc" style="display:none;">
+          <i class="ui green checkmark icon"></i> Redirection Rules Added
+        </div>
+        <br><br>
+        <!--
+        <div class="advancezone ui basic segment">
+          <div class="ui accordion advanceSettings">
+            <div class="title">
+              <i class="dropdown icon"></i>
+              Advance Options
+            </div>
+            <div class="content">
+              <p>If you need custom header, content or status code other than basic redirects, you can use the advance path rules editor.</p>
+              <button class="ui black basic button" onclick="createAdvanceRules();"><i class="ui black external icon"></i> Open Advance Rules Editor</button>
             </div>
           </div>
-      </div>
-      <button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
-      <div class="ui green message" id="ruleAddSucc" style="display:none;">
-        <i class="ui green checkmark icon"></i> Redirection Rules Added
-      </div>
+        </div>
+         -->
+    </div>
   </div>
 </div>
 <script>
+    $(".advanceSettings").accordion();
+    
+  /*
+    Redirection functions
+  */
     $(".checkbox").checkbox();
 
     function resetForm() {
@@ -129,6 +151,10 @@
         }
     }
 
+    function createAdvanceRules(){
+      showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
+    }
+
     function initRedirectionRuleList(){
         $("#redirectionRuleList").html("");
         $.get("/api/redirect/list", function(data){

+ 75 - 5
web/components/status.html

@@ -72,11 +72,30 @@
         <label>Use TLS to serve proxy request</label>
     </div>
     <br>
-    <div id="redirect" class="ui toggle notloopbackOnly checkbox" style="margin-top: 0.6em;">
+    <div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
         <input type="checkbox">
         <label>Force redirect HTTP request to HTTPS<br>
             <small>(Only apply when listening port is not 80)</small></label>
     </div>
+    <div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
+        <div class="ui accordion advanceSettings">
+            <div class="title">
+              <i class="dropdown icon"></i>
+                Advance Settings
+            </div>
+            <div class="content">
+                <p>If you have no idea what are these, you can leave them as default :)</p>
+                <div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
+                    <input type="checkbox">
+                    <label>Force TLS v1.2 or above<br>
+                    <small>(Enhance security, but not compatible with legacy browsers)</small></label>
+                </div>
+                <br>
+            </div>
+        </div>
+    </div>
+    
+    
     <br><br>
     <button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
     <button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
@@ -128,6 +147,8 @@
 </div>
 <script>
     let loopbackProxiedInterface = false;
+    $(".advanceSettings").accordion();
+
     //Initial the start stop button if this is reverse proxied
     $.get("/api/proxy/requestIsProxied", function(data){
         if (data == true){
@@ -340,21 +361,50 @@
     }
     initHTTPtoHTTPSRedirectSetting();
 
+    function initTlsVersionSetting(){
+        $.get("/api/cert/tlsRequireLatest", function(data){
+            if (data == true){
+                $("#tlsMinVer").checkbox("set checked");
+            }else{
+                $("#tlsMinVer").checkbox("set unchecked");
+            }
+
+            //Bind events to the checkbox
+            $("#tlsMinVer").find("input").on("change", function(){
+                let thisValue = $("#tlsMinVer").checkbox("is checked");
+                $.ajax({
+                    url: "/api/cert/tlsRequireLatest",
+                    data: {"set": thisValue},
+                    success: function(data){
+                        if (data.error != undefined){
+                            msgbox(data.error, false, 5000);
+                        }else{
+                            msgbox("TLS Version Setting Updated");
+                        }
+                    }
+                })
+            });
+        });
+        
+    }
+    initTlsVersionSetting();
+
     function initTlsSetting(){
         $.get("/api/cert/tls", function(data){
             if (data == true){
                 $("#tls").checkbox("set checked");
             }else{
-                $("#redirect").addClass('disabled');
+                $(".tlsEnabledOnly").addClass('disabled');
+                $(".tlsEnabledOnly").addClass('disabled');
             }
 
             //Initiate the input listener on the checkbox
             $("#tls").find("input").on("change", function(){
                 let thisValue = $("#tls").checkbox("is checked");
                 if (thisValue){
-                    $("#redirect").removeClass('disabled');
+                    $(".tlsEnabledOnly").removeClass('disabled');
                 }else{
-                    $("#redirect").addClass('disabled');
+                    $(".tlsEnabledOnly").addClass('disabled');
                 }
                 $.ajax({
                     url: "/api/cert/tls",
@@ -364,7 +414,27 @@
                             alert(data.error);
                         }else{
                             //Updated
-                            msgbox("Setting Updated");
+                            
+                            //Check for case if the port is invalid default ports
+                            if ($("#incomingPort").val() == "80" && thisValue == true){
+                                confirmBox("Change listen port to :443?", function(choice){
+                                    if (choice == true){
+                                        $("#incomingPort").val("443");
+                                        handlePortChange();
+                                    }
+                                });
+                                   
+                            }else if ($("#incomingPort").val() == "443" && thisValue == false){
+                                confirmBox("Change listen port to :80?", function(choice){
+                                    if (choice == true){
+                                        $("#incomingPort").val("80");
+                                        handlePortChange();
+                                    }
+                                });
+                            }else{
+                                msgbox("Setting Updated");
+                            }
+
                             initRPStaste();
                         }
                     }

+ 69 - 0
web/index.html

@@ -148,6 +148,17 @@
             <p><i class="green check circle icon"></i> There are no message</p>
         </div>
         
+     
+        <div id="confirmBox" style="display:none;">
+            <div class="ui top attached progress">
+                <div class="bar" style="width: 100%; min-width: 0px;"></div>
+            </div>
+            <div class="confirmBoxBody">
+                <button class="ui red basic mini circular icon right floated button" style="margin-left: 0.4em;"><i class="ui times icon"></i></button>
+                <button class="ui green basic mini circular icon right floated button"><i class="ui check icon"></i></button>
+                <div class="questionToConfirm">Confirm Exit?</div>
+            </div>
+        </div>
         <br><br>
         <script>
             $(".year").text(new Date().getFullYear());
@@ -280,6 +291,64 @@
                 $("#messageBox").stop().finish().fadeIn("fast").delay(delayDuration).fadeOut("fast");
             }
 
+            function confirmBox(question_to_confirm, callback, delaytime=15) {
+                
+                var progressBar = $("#confirmBox .bar");
+                var questionElement = $("#confirmBox .questionToConfirm");
+                
+                //Just to make sure there are no animation runnings
+                progressBar.stop();
+
+                // Update the question to confirm
+                questionElement.text(question_to_confirm);
+
+                // Start the progress bar animation
+                progressBar.css("width", "100%");
+                progressBar.animate({ width: "0%", easing: "linear" }, delaytime * 1000, function() {
+                    // Animation complete, invoke the callback with undefined
+                    callback(undefined);
+
+                    //Unset the event listener
+                    $("#confirmBox .ui.green.button").off("click");
+
+                    // Hide the confirm box
+                    $("#confirmBox").hide();
+                });
+
+                // Bind click event to "Yes" button
+                $("#confirmBox .ui.green.button").on("click", function() {
+                    // Stop the progress bar animation
+                    progressBar.stop();
+
+                    // Invoke the callback with true
+                    callback(true);
+
+                    // Hide the confirm box
+                    $("#confirmBox").hide();
+
+                    //Unset the event listener
+                    $("#confirmBox .ui.green.button").off("click");
+                });
+
+                // Bind click event to "No" button
+                $("#confirmBox .ui.red.button").on("click", function() {
+                    // Stop the progress bar animation
+                    progressBar.stop();
+
+                    // Invoke the callback with false
+                    callback(false);
+
+                    // Hide the confirm box
+                    $("#confirmBox").hide();
+
+                     //Unset the event listener
+                     $("#confirmBox .ui.red.button").off("click");
+                });
+
+                // Show the confirm box
+                $("#confirmBox").show().transition('jiggle');
+            }
+
             /*
                 Toggles for side wrapper
             */

+ 53 - 1
web/main.css

@@ -6,6 +6,7 @@
     --theme_lgrey: #f6f6f6;
     --theme_green: #3c9c63;
     --theme_fcolor: #979797;
+    --theme_advance: #f8f8f9;
 }
 body{
     background-color:#f6f6f6;
@@ -16,6 +17,15 @@ body{
     display:none;
 }
 
+.advance{
+    background: var(--theme_advance) !important;
+}
+
+.advancezone{
+    background: var(--theme_advance) !important;
+    border-radius: 1em !important;
+}
+
 .menubar{
     width: 100%;
     padding: 0.4em;
@@ -89,6 +99,34 @@ body{
     z-index: 999;
 }
 
+/* Confirm Box */
+#confirmBox{
+    position: fixed;
+    z-index: 999;
+    bottom: 1em;
+    right: 1em;
+    min-width: 300px;
+    background-color: #ffffff;
+    color: rgb(65, 65, 65);
+    box-shadow: 10px 10px 5px -2px rgba(0,0,0,0.13);
+}
+
+#confirmBox .confirmBoxBody{
+    padding: 1em;
+}
+
+#confirmBox .ui.progress .bar{
+    background: #ffe32b !important;
+}
+
+#confirmBox .confirmBoxBody .button{
+    margin-top: -0.4em;
+}
+
+#confirmBox .questionToConfirm{
+    margin-top: -0.2em;
+}
+
 /* Standard containers */
 .standardContainer{
     position: relative;
@@ -552,4 +590,18 @@ body{
 
 .GANetMember.unauthorized{
     border-left: 6px solid #9c3c3c !important;
-}
+}
+
+/*
+    Network Utilities
+*/
+
+#traceroute_results{
+    resize: none; 
+    background-color: #202020; 
+    color: white;
+}
+
+#traceroute_results::selection {
+    background: #a9d1f3; 
+  }

+ 76 - 0
web/snippet/advancePathRules.html

@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <!-- Notes: This should be open in its original path-->
+        <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>
+        <br>
+        <div class="ui container">
+            <!-- Path Rules -->
+            <div class="ui header">
+                <div class="content">
+                    Special Path Rules
+                    <div class="sub header">Advanced customization for response on particular matching path or URL</div>
+                </div>
+            </div>
+            <h4>Current list of special path rules.</h4>
+            <div style="width: 100%; overflow-x: auto;">
+                <table class="ui sortable unstackable celled table" >
+                    <thead>
+                        <tr>
+                            <th>Matching Path</th>
+                            <th>Status Code</th>
+                            <th class="no-sort">Exact Match</th>
+                            <th class="no-sort">Case Sensitive</th>
+                            <th class="no-sort">Enabled</th>
+                            <th class="no-sort">Actions</th>
+                        </tr>
+                    </thead>
+                    <tbody id="specialPathRules">
+                        <tr>
+                            <td></td>
+                            <td></td>
+                            <td></td>
+                            <td></td>
+                            <td></td>
+                            <td></td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+            <div class="ui divider"></div>
+            <h4>Add Special Path Rule</h4>
+            <div class="ui form">
+                <div class="field">
+                <label>Matching URI</label>
+                <input type="text" name="matchingPath" placeholder="Matching URL">
+                <small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be handled by this rule, e.g. example.com/secret</small>
+                </div>
+                <div class="field">
+                <div class="ui checkbox">
+                    <input type="checkbox" name="exactMatch" tabindex="0" class="hidden">
+                    <label>Require Exact Match</label>
+                </div>
+                <div class="ui message">
+                    <p>Require exactly match but not prefix match (default). Enable this if you only want to block access to a directory but not the resources inside the directory. Assume you have entered a matching URI of <b>example.com/secret/</b> and set it to return 401</p>
+                    <i class="check square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> (content of image.png)<br>
+                    <i class="square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> HTTP 401 
+                </div>
+                </div>
+                <div class="field">
+                <label>Response Status Code</label>
+                <input type="text"name="statusCode" placeholder="500">
+                <small><i class="ui circle info icon"></i> HTTP Status Code to be served by this rule</small>
+                </div>
+            </div>
+            <br><br>
+            <button class="ui basic button iframeOnly"  style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
+        </div>
+        <script>
+
+        </script>
+    </body>
+</html>