Browse Source

auto update script executed

Toby Chui 1 year ago
parent
commit
e6d456d57d
10 changed files with 632 additions and 6 deletions
  1. 5 1
      api.go
  2. 1 0
      go.mod
  3. 6 0
      go.sum
  4. 35 1
      helpers.go
  5. 149 0
      mod/ipscan/ipscan.go
  6. 147 0
      mod/netstat/netstat.go
  7. 55 0
      mod/netstat/nic.go
  8. 3 0
      web/components/networktools.html
  9. 40 4
      web/components/utils.html
  10. 191 0
      web/tools/ipscan.html

+ 5 - 1
api.go

@@ -5,6 +5,7 @@ import (
 	"net/http"
 
 	"imuslab.com/zoraxy/mod/auth"
+	"imuslab.com/zoraxy/mod/netstat"
 	"imuslab.com/zoraxy/mod/utils"
 )
 
@@ -72,6 +73,8 @@ func initAPIs() {
 	//Statistic & uptime monitoring API
 	authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
 	authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
+	authRouter.HandleFunc("/api/stats/netstat", netstat.HandleGetNetworkInterfaceStats)
+	authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
 	authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
 
 	//mDNS APIs
@@ -79,7 +82,8 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
 
 	//Network utilities
-	authRouter.HandleFunc("/api/tools/traceroute", HandleTraceRoute)
+	authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
+
 	//If you got APIs to add, append them here
 }
 

+ 1 - 0
go.mod

@@ -4,6 +4,7 @@ go 1.16
 
 require (
 	github.com/boltdb/bolt v1.3.1
+	github.com/go-ping/ping v1.1.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.4.2

+ 6 - 0
go.sum

@@ -5,6 +5,9 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
+github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -44,11 +47,13 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -56,6 +61,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 35 - 1
helpers.go

@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"imuslab.com/zoraxy/mod/dynamicproxy"
+	"imuslab.com/zoraxy/mod/ipscan"
 	"imuslab.com/zoraxy/mod/mdns"
 	"imuslab.com/zoraxy/mod/uptime"
 	"imuslab.com/zoraxy/mod/utils"
@@ -156,6 +157,39 @@ func HandleMdnsScanning(w http.ResponseWriter, r *http.Request) {
 }
 
 //handle traceroute tools
-func HandleTraceRoute(w http.ResponseWriter, r *http.Request) {
+func HandleIpScan(w http.ResponseWriter, r *http.Request) {
+	cidr, err := utils.PostPara(r, "cidr")
+	if err != nil {
+		//Ip range mode
+		start, err := utils.PostPara(r, "start")
+		if err != nil {
+			utils.SendErrorResponse(w, "missing start ip")
+			return
+		}
+
+		end, err := utils.PostPara(r, "end")
+		if err != nil {
+			utils.SendErrorResponse(w, "missing end ip")
+			return
+		}
+
+		discoveredHosts, err := ipscan.ScanIpRange(start, end)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		js, _ := json.Marshal(discoveredHosts)
+		utils.SendJSONResponse(w, string(js))
+	} else {
+		//CIDR mode
+		discoveredHosts, err := ipscan.ScanCIDRRange(cidr)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
 
+		js, _ := json.Marshal(discoveredHosts)
+		utils.SendJSONResponse(w, string(js))
+	}
 }

+ 149 - 0
mod/ipscan/ipscan.go

@@ -0,0 +1,149 @@
+package ipscan
+
+import (
+	"bytes"
+	"fmt"
+	"net"
+	"sort"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/go-ping/ping"
+)
+
+/*
+	IP Scanner
+
+	This module scan the given network range and return a list
+	of nearby nodes.
+*/
+
+type DiscoveredHost struct {
+	IP                string
+	Ping              int
+	Hostname          string
+	HttpPortDetected  bool
+	HttpsPortDetected bool
+}
+
+//Scan an IP range given the start and ending ip address
+func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
+	ipStart := net.ParseIP(start)
+	ipEnd := net.ParseIP(end)
+	if ipStart == nil || ipEnd == nil {
+		return nil, fmt.Errorf("Invalid IP address")
+	}
+
+	if bytes.Compare(ipStart, ipEnd) > 0 {
+		return nil, fmt.Errorf("Invalid IP range")
+	}
+
+	var wg sync.WaitGroup
+	hosts := make([]*DiscoveredHost, 0)
+	for ip := ipStart; bytes.Compare(ip, ipEnd) <= 0; inc(ip) {
+		wg.Add(1)
+		thisIp := ip.String()
+		go func(thisIp string) {
+			defer wg.Done()
+			host := &DiscoveredHost{IP: thisIp}
+			if err := host.CheckPing(); err != nil {
+				// skip if the host is unreachable
+				host.Ping = -1
+				hosts = append(hosts, host)
+				return
+			}
+
+			host.CheckHostname()
+			host.CheckPort("http", 80, &host.HttpPortDetected)
+			host.CheckPort("https", 443, &host.HttpsPortDetected)
+			fmt.Println("OK", host)
+			hosts = append(hosts, host)
+
+		}(thisIp)
+	}
+
+	//Wait until all go routine done
+	wg.Wait()
+	sortByIP(hosts)
+	return hosts, nil
+}
+
+func ScanCIDRRange(cidr string) ([]*DiscoveredHost, error) {
+	_, ipNet, err := net.ParseCIDR(cidr)
+	if err != nil {
+		return nil, err
+	}
+
+	ip := ipNet.IP.To4()
+	startIP := net.IPv4(ip[0], ip[1], ip[2], 1).String()
+	endIP := net.IPv4(ip[0], ip[1], ip[2], 254).String()
+
+	return ScanIpRange(startIP, endIP)
+}
+
+func inc(ip net.IP) {
+	for j := len(ip) - 1; j >= 0; j-- {
+		ip[j]++
+		if ip[j] > 0 {
+			break
+		}
+	}
+}
+
+func sortByIP(discovered []*DiscoveredHost) {
+	sort.Slice(discovered, func(i, j int) bool {
+		return discovered[i].IP < discovered[j].IP
+	})
+}
+
+func (host *DiscoveredHost) CheckPing() error {
+	// ping the host and set the ping time in milliseconds
+	pinger, err := ping.NewPinger(host.IP)
+	if err != nil {
+		return err
+	}
+	pinger.Count = 4
+	pinger.Timeout = time.Second
+	pinger.SetPrivileged(true) // This line may help on some systems
+	pinger.Run()
+	stats := pinger.Statistics()
+	if stats.PacketsRecv == 0 {
+		return fmt.Errorf("Host unreachable for " + host.IP)
+	}
+	host.Ping = int(stats.AvgRtt.Milliseconds())
+	return nil
+}
+
+func (host *DiscoveredHost) CheckHostname() {
+	// lookup the hostname for the IP address
+	names, err := net.LookupAddr(host.IP)
+	fmt.Println(names, err)
+	if err == nil && len(names) > 0 {
+		host.Hostname = names[0]
+	}
+}
+
+func (host *DiscoveredHost) CheckPort(protocol string, port int, detected *bool) {
+	// try to connect to the specified port on the host
+	conn, err := net.DialTimeout("tcp", net.JoinHostPort(host.IP, strconv.Itoa(port)), 1*time.Second)
+	if err == nil {
+		conn.Close()
+		*detected = true
+	}
+}
+
+func (host *DiscoveredHost) ScanPorts(startPort, endPort int) []int {
+	var openPorts []int
+
+	for port := startPort; port <= endPort; port++ {
+		target := fmt.Sprintf("%s:%d", host.IP, port)
+		conn, err := net.DialTimeout("tcp", target, time.Millisecond*500)
+		if err == nil {
+			conn.Close()
+			openPorts = append(openPorts, port)
+		}
+	}
+
+	return openPorts
+}

+ 147 - 0
mod/netstat/netstat.go

@@ -0,0 +1,147 @@
+package netstat
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"imuslab.com/zoraxy/mod/utils"
+)
+
+func HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
+	rx, tx, err := GetNetworkInterfaceStats()
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	currnetNetSpec := struct {
+		RX int64
+		TX int64
+	}{
+		rx,
+		tx,
+	}
+
+	js, _ := json.Marshal(currnetNetSpec)
+	utils.SendJSONResponse(w, string(js))
+}
+
+// Get network interface stats, return accumulated rx bits, tx bits and error if any
+func GetNetworkInterfaceStats() (int64, int64, error) {
+	if runtime.GOOS == "windows" {
+		cmd := exec.Command("wmic", "path", "Win32_PerfRawData_Tcpip_NetworkInterface", "Get", "BytesReceivedPersec,BytesSentPersec,BytesTotalPersec")
+		out, err := cmd.Output()
+		if err != nil {
+			return 0, 0, err
+		}
+
+		//Filter out the first line
+
+		lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n")
+		if len(lines) >= 2 && len(lines[1]) >= 0 {
+			dataLine := lines[1]
+			for strings.Contains(dataLine, "  ") {
+				dataLine = strings.ReplaceAll(dataLine, "  ", " ")
+			}
+			dataLine = strings.TrimSpace(dataLine)
+			info := strings.Split(dataLine, " ")
+			if len(info) < 3 {
+				return 0, 0, errors.New("Invalid wmic results")
+			}
+			rxString := info[0]
+			txString := info[1]
+
+			rx := int64(0)
+			tx := int64(0)
+			if s, err := strconv.ParseInt(rxString, 10, 64); err == nil {
+				rx = s
+			}
+
+			if s, err := strconv.ParseInt(txString, 10, 64); err == nil {
+				tx = s
+			}
+
+			//log.Println(rx, tx)
+			return rx * 4, tx * 4, nil
+		} else {
+			//Invalid data
+			return 0, 0, errors.New("Invalid wmic results")
+		}
+
+	} else if runtime.GOOS == "linux" {
+		allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
+		if err != nil {
+			//Permission denied
+			return 0, 0, errors.New("Access denied")
+		}
+
+		if len(allIfaceRxByteFiles) == 0 {
+			return 0, 0, errors.New("No valid iface found")
+		}
+
+		rxSum := int64(0)
+		txSum := int64(0)
+		for _, rxByteFile := range allIfaceRxByteFiles {
+			rxBytes, err := os.ReadFile(rxByteFile)
+			if err == nil {
+				rxBytesInt, err := strconv.Atoi(strings.TrimSpace(string(rxBytes)))
+				if err == nil {
+					rxSum += int64(rxBytesInt)
+				}
+			}
+
+			//Usually the tx_bytes file is nearby it. Read it as well
+			txByteFile := filepath.Join(filepath.Dir(rxByteFile), "tx_bytes")
+			txBytes, err := os.ReadFile(txByteFile)
+			if err == nil {
+				txBytesInt, err := strconv.Atoi(strings.TrimSpace(string(txBytes)))
+				if err == nil {
+					txSum += int64(txBytesInt)
+				}
+			}
+
+		}
+
+		//Return value as bits
+		return rxSum * 8, txSum * 8, nil
+
+	} else if runtime.GOOS == "darwin" {
+		cmd := exec.Command("netstat", "-ib") //get data from netstat -ib
+		out, err := cmd.Output()
+		if err != nil {
+			return 0, 0, err
+		}
+
+		outStrs := string(out)                                                          //byte array to multi-line string
+		for _, outStr := range strings.Split(strings.TrimSuffix(outStrs, "\n"), "\n") { //foreach multi-line string
+			if strings.HasPrefix(outStr, "en") { //search for ethernet interface
+				if strings.Contains(outStr, "<Link#") { //search for the link with <Link#?>
+					outStrSplit := strings.Fields(outStr) //split by white-space
+
+					rxSum, errRX := strconv.Atoi(outStrSplit[6]) //received bytes sum
+					if errRX != nil {
+						return 0, 0, errRX
+					}
+
+					txSum, errTX := strconv.Atoi(outStrSplit[9]) //transmitted bytes sum
+					if errTX != nil {
+						return 0, 0, errTX
+					}
+
+					return int64(rxSum) * 8, int64(txSum) * 8, nil
+				}
+			}
+		}
+
+		return 0, 0, nil //no ethernet adapters with en*/<Link#*>
+	}
+
+	return 0, 0, errors.New("Platform not supported")
+}

+ 55 - 0
mod/netstat/nic.go

@@ -0,0 +1,55 @@
+package netstat
+
+import (
+	"encoding/json"
+	"net"
+	"net/http"
+
+	"imuslab.com/zoraxy/mod/utils"
+)
+
+type NetworkInterface struct {
+	Name string
+	ID   int
+	IPs  []string
+}
+
+func HandleListNetworkInterfaces(w http.ResponseWriter, r *http.Request) {
+	nic, err := ListNetworkInterfaces()
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal(nic)
+	utils.SendJSONResponse(w, string(js))
+}
+
+func ListNetworkInterfaces() ([]NetworkInterface, error) {
+	var interfaces []NetworkInterface
+
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, iface := range ifaces {
+		var ips []string
+		addrs, err := iface.Addrs()
+		if err != nil {
+			return nil, err
+		}
+
+		for _, addr := range addrs {
+			ips = append(ips, addr.String())
+		}
+
+		interfaces = append(interfaces, NetworkInterface{
+			Name: iface.Name,
+			ID:   iface.Index,
+			IPs:  ips,
+		})
+	}
+
+	return interfaces, nil
+}

+ 3 - 0
web/components/networktools.html

@@ -16,6 +16,9 @@
         <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>
+        <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>
 
     <div class="ui bottom attached tab segment" data-tab="tab2">

+ 40 - 4
web/components/utils.html

@@ -1,11 +1,11 @@
 <div class="standardContainer">
     <div class="ui basic segment">
         <h2>Utilities</h2>
-        <p>You might find these tools helpful when setting up your gateway server</p>
+        <p>You might find these tools or information helpful when setting up your gateway server</p>
     </div>
     <div class="ui divider"></div>
     <div class="selfauthOnly">
-        <h3><i class="ui user icon"></i> Account Management</h3>
+        <h3>Account Management</h3>
         <p>Functions to help management the current account</p>
         <div class="ui basic segment">
             <h5><i class="chevron down icon"></i> Change Password</h5>
@@ -31,8 +31,20 @@
         </div>
         <div class="ui divider"></div>
     </div>
-    
-    <h3><i class="ui code icon"></i> IP Address Converter</h3>
+    <h3>Network Interfaces</h3>
+    <p>Network Interface Card (NIC) currently installed on this host</p>
+    <table id="network-interfaces-table" class="ui selectable inverted striped celled table">
+		<thead>
+			<tr>
+				<th>Interface Name</th>
+				<th>ID</th>
+				<th>IP Address</th>
+			</tr>
+		</thead>
+		<tbody></tbody>
+	</table>
+
+    <h3> IP Address to CIDR</h3>
     <p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
     <div class="ui basic segment">
         <h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
@@ -101,6 +113,30 @@
         });
     }
 
+    /*
+        NIC Info
+    */
+
+    function renderNICInfo(){
+        $.get("/api/stats/listnic",function(data){
+            var tbody = document.querySelector("#network-interfaces-table tbody");
+            data.forEach(function(item) {
+                var tr = document.createElement("tr");
+                var name = document.createElement("td");
+                name.textContent = item.Name;
+                var id = document.createElement("td");
+                id.textContent = item.ID;
+                var ips = document.createElement("td");
+                ips.innerHTML = item.IPs.join("<br>");
+                tr.appendChild(name);
+                tr.appendChild(id);
+                tr.appendChild(ips);
+                tbody.appendChild(tr);
+            });
+        });
+    }
+    renderNICInfo();
+
     /*
         IP Address Utilities
     */

+ 191 - 0
web/tools/ipscan.html

@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta name="apple-mobile-web-app-capable" content="yes" />
+        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
+        <meta charset="UTF-8">
+        <meta name="theme-color" content="#4b75ff">
+        <link rel="icon" type="image/png" href="./favicon.png" />
+        <title>IP Scanner | Zoraxy</title>
+        <link rel="stylesheet" href="../script/semantic/semantic.min.css">
+        <script src="../script/jquery-3.6.0.min.js"></script>
+        <script src="../../script/ao_module.js"></script>
+        <script src="../script/semantic/semantic.min.js"></script>
+        <script src="../script/tablesort.js"></script>
+        <link rel="stylesheet" href="../main.css">
+        <style>
+            .offlinehost{
+                display: none;
+            }
+        </style>
+    </head>
+    <body>
+        <div class="ui container">
+            <br>
+            <div class="ui segment">
+                <div style="padding: 1em;">
+                    <div class="ui stackable grid">
+                        <div class="eight wide column">
+                            <h4 class="ui dividing header">Scan IP Range</h4>
+                            <div class="ui form">
+                                <div class="field">
+                                    <label>Start IP</label>
+                                    <input type="text" id="start-ip" name="start-ip">
+                                </div>
+                                <div class="field">
+                                    <label>End IP</label>
+                                    <input type="text" id="end-ip" name="end-ip">
+                                </div>
+                                <button class="ui basic button scanbtn" id="ip-scan-btn"><i class="blue search icon"></i> Scan</button>
+                            </div>
+                            <br>
+                        </div>
+                        <div class="eight wide column">
+                            <h4 class="ui dividing header">Scan CIDR Range</h4>
+                            <div class="ui form">
+                                <div class="field">
+                                    <label>CIDR</label>
+                                    <input type="text" id="cidr" name="cidr">
+                                </div>
+                                <button class="ui basic button scanbtn" id="cidr-scan-btn"><i class="blue search icon"></i> Scan</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <button class="ui basic button" onclick="toggleOfflineHost();"><i class="grey eye icon"></i>View Offline Hosts</button>
+            <div class="ui divider"></div>
+            <br>
+                <div id="scan-results">
+                   
+                </div>
+            <br>
+            <div style="float: right;">
+                <button class="ui basic button" style="margin-right: 1em;" onclick="window.open('', '_self', ''); window.close();"><i class="ui red remove icon"></i> Close</button>
+            </div>
+            <br><br><br>
+        </div>
+        <script>
+            $(document).ready(function() {
+                $('#ip-scan-btn').click(function(event) {
+                    event.preventDefault();
+                    var start = $('#start-ip').val().trim();
+                    var end = $('#end-ip').val().trim();
+                    if (start === '' || end === '') {
+                        alert('Please enter start and end IP range');
+                        return;
+                    }
+
+                    if (!isValidIPv4(start) || !isValidIPv4(end)){
+                        alert('Invalid start or end ip address');
+                        return;
+                    }
+
+                    $(".scanbtn").addClass("disabled");
+                    $("#scan-results").html(`
+                        <div class="ui basic segment" align="center">
+                            <i class="loading spinner icon"></i> Scanning
+                        </div>`);
+                    $.post("/api/tools/ipscan", {start: start, end: end}, function(data) {
+                        displayResults(data);
+                        $(".scanbtn").removeClass("disabled");
+                    });
+                });
+        
+                $('#cidr-scan-btn').click(function(event) {
+                    event.preventDefault();
+                    var cidr = $('#cidr').val().trim();
+                    if (cidr === '') {
+                        alert('Please enter CIDR notation');
+                        return;
+                    }
+
+                    if (!isValidCIDR(cidr)){
+                        alert("Invalid CIDR notation string given");
+                        return;
+                    }
+                    $(".scanbtn").addClass("disabled");
+                    $("#scan-results").html(`
+                        <div class="ui basic segment" align="center">
+                            <i class="loading spinner icon"></i> Scanning
+                        </div>`);
+                    $.post("/api/tools/ipscan", {cidr: cidr}, function(data) {
+                        displayResults(data);
+                        $(".scanbtn").removeClass("disabled");
+                    });
+                });
+
+                function isValidCIDR(str) {
+                    const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
+                    return cidrRegex.test(str);
+                }
+
+
+                function isValidIPv4(ip) {
+                    var regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
+                    if (!regex.test(ip)) {
+                        return false;
+                    }
+                    var parts = ip.split(".");
+                    for (var i = 0; i < parts.length; i++) {
+                        if (parseInt(parts[i], 10) > 255) {
+                        return false;
+                        }
+                    }
+                    return true;
+                }
+
+                function displayResults(data) {
+                    var table = $('<table class="ui celled table"></table>');
+                    var header = $('<thead><tr><th>IP Address</th><th>Ping</th><th>Hostname</th><th>HTTP Detected</th><th>HTTPS Detected</th></tr></thead>');
+                    table.append(header);
+                    var body = $('<tbody></tbody>');
+                    var offlineHostCounter = 0;
+                    for (var i = 0; i < data.length; i++) {
+                        var classname = "offlinehost";
+                        if (data[i].Ping>=0){
+                            classname = "onlinehost";
+                        }else{
+                            offlineHostCounter++;
+                        }
+                        var row = $('<tr class="' + classname + '"></tr>');
+                        var status = data[i].Ping >= 0 ? '<i class="green circle icon"></i>' : '<i class="grey circle icon"></i>';
+                        row.append($('<td>' + status + data[i].IP + '</td>'));
+                        row.append($('<td>' + ((data[i].Ping>=0)?data[i].Ping+"ms":"Host Unreachable") + '</td>'));
+                        row.append($('<td>' + data[i].Hostname + '</td>'));
+                        row.append($('<td>' + (data[i].HttpPortDetected ? '<i class="green check icon"></i>' : '') + '</td>'));
+                        row.append($('<td>' + (data[i].HttpsPortDetected ? '<i class="green check icon"></i>' : '') + '</td>'));
+                        body.append(row);
+                    }
+
+                    if (data.length == 0){
+                        var body = $(`<tbody>
+                        <tr>
+                            <td colspan="5">
+                                <i class="green circle check icon"></i> Discover no devices in given IP range
+                            </td>
+                        </tr>
+                        </tbody>`);
+                    }
+
+                    if (offlineHostCounter == data.length){
+                        //All offline
+                        var body = $(`<tbody>
+                        <tr>
+                            <td colspan="5">
+                                <i class="green circle check icon"></i> All hosts in given IP range are offline
+                            </td>
+                        </tr>
+                        </tbody>`);
+                    }
+                    table.append(body);
+                    $('#scan-results').empty().append(table);
+                }
+            });
+
+            function toggleOfflineHost(){
+                $(".offlinehost").toggle();
+            }
+        </script>
+    </body>
+</html>