Browse Source

auto update script executed

Toby Chui 1 year ago
parent
commit
3fedd0b429

+ 3 - 0
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"
 )
 
@@ -132,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)

+ 1 - 0
go.mod

@@ -12,5 +12,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
 	golang.org/x/sys v0.8.0
 )

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

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

+ 20 - 71
web/components/redirection.html

@@ -1,17 +1,5 @@
 
 <div class="standardContainer">
-  <div class="ui basic segment">
-      <h2>Path & Redirection</h2>
-      <p>Setup redirection and path specific settings like custom header, contents and status codes</p>
-  </div>
-
-  <div class="ui top attached tabular menu">
-    <a class="pathAndRedirect item active" data-tab="tab_redirect"><i class="ui blue level up alternate icon"></i> Redirect</a>
-    <a class="pathAndRedirect item" data-tab="tab_pathrules"><i class="ui code icon"></i> Path Rules</a>
-    <a class="pathAndRedirect item" data-tab="tab_pathcache"><i class="ui teal database icon"></i> Caches</a>
-  </div>
-
-  <div class="ui bottom attached tab segment active" data-tab="tab_redirect">
     <div class="ui basic segment">
       <h2>Redirection Rules</h2>
       <p>Add exception case for redirecting any matching URLs</p>
@@ -83,74 +71,31 @@
         <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>
     </div>
   </div>
 
-  <!-- Path Rules -->
-  <div class="ui bottom attached tab segment" data-tab="tab_pathrules">
-    <div class="ui basic segment">
-      <h2>Special Path Rules</h2>
-      <p>Advanced customization for response on particular matching path or URL</p>
-    </div>
-    <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>
-  </div>
+  
 
-  <!-- Path Rules -->
+  <!-- Cache Rules -->
   <div class="ui bottom attached tab segment" data-tab="tab_pathcache">
     <p>Work In Progress</p>
   </div>
 </div>
 <script>
-    $('.menu .pathAndRedirect.item').tab();
-    $('.menu .pathAndRedirect.item').addClass("activated"); 
+    $(".advanceSettings").accordion();
     
   /*
     Redirection functions
@@ -211,6 +156,10 @@
         }
     }
 
+    function createAdvanceRules(){
+      showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
+    }
+
     function initRedirectionRuleList(){
         $("#redirectionRuleList").html("");
         $.get("/api/redirect/list", function(data){

+ 2 - 2
web/components/status.html

@@ -78,7 +78,7 @@
             <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 statusAdvanceSettings">
+        <div class="ui accordion advanceSettings">
             <div class="title">
               <i class="dropdown icon"></i>
                 Advance Settings
@@ -147,7 +147,7 @@
 </div>
 <script>
     let loopbackProxiedInterface = false;
-    $(".statusAdvanceSettings").accordion();
+    $(".advanceSettings").accordion();
 
     //Initial the start stop button if this is reverse proxied
     $.get("/api/proxy/requestIsProxied", function(data){

+ 1 - 1
web/index.html

@@ -53,7 +53,7 @@
                         <i class="simplistic lock icon"></i> TLS / SSL certificate
                     </a>
                     <a class="item" tag="redirectset">
-                        <i class="simplistic level up alternate icon"></i> Path & Redirection
+                        <i class="simplistic level up alternate icon"></i> Redirection
                     </a>
                     <a class="item" tag="access">
                         <i class="simplistic ban icon"></i> Access Control

+ 20 - 1
web/main.css

@@ -21,6 +21,11 @@ body{
     background: var(--theme_advance) !important;
 }
 
+.advancezone{
+    background: var(--theme_advance) !important;
+    border-radius: 1em !important;
+}
+
 .menubar{
     width: 100%;
     padding: 0.4em;
@@ -585,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>