package kyoketsu import ( "embed" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "path" "strings" "sync" ) type ScanRequest struct { IpAddress string `json:"ip_address"` NetworkAddress string `json:"network_address"` FqdnPattern string `json:"fqdn_pattern"` } // Holding all static web server resources // //go:embed html/bootstrap-5.0.2-dist/js/* html/bootstrap-5.0.2-dist/css/* html/* html/templates/* var content embed.FS /* Run a new webserver :param port: port number to run the webserver on */ func RunHttpServer(port int, dbhook TopologyDatabaseIO, portmap []int, logStream io.Writer) { assets := &AssetHandler{Root: content, RelPath: "static", EmbedRoot: "html"} tmpl, err := template.ParseFS(content, "html/templates/*.html") if err != nil { log.Fatal(err) } iptable, err := template.ParseFS(content, "html/templates/ip_table.html") if err != nil { log.Fatal(err) } htmlHndl := &HtmlHandler{Home: tmpl, TableEntry: iptable, DbHook: dbhook, stream: logStream} execHndl := &ExecutionHandler{DbHook: dbhook, PortMap: portmap, TableEntry: iptable, stream: logStream} http.Handle("/static/", assets) http.Handle("/home", htmlHndl) http.Handle("/subnets", htmlHndl) http.Handle("/excludefqdn", htmlHndl) http.Handle("/refresh", execHndl) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) } type ExecutionHandler struct { DbHook TopologyDatabaseIO TableEntry *template.Template PortMap []int stream io.Writer } func (e *ExecutionHandler) Log(vals ...string) { e.stream.Write([]byte("KYOKETSU-WEB LOG ||| " + strings.Join(vals, " ||| ") + "\n")) } /* Top level function to be routed to, this will spawn a suite of goroutines that will perform a concurrent scan on hosts and write back HTML data :param w: an http.ResponseWriter that we will write data back to :param r: a pointer to the request coming in from the client */ func (e *ExecutionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { e.Log("Recieved: " + r.Method + " on path: " + r.RequestURI) input, err := e.parseRequest(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } e.Log(fmt.Sprintf("Parsed request struct: %+v", input)) subnetMap, err := GetNetworkAddresses(input.IpAddress) if err != nil { http.Error(w, "Failed to get network addresses", http.StatusInternalServerError) return } scanned := make(chan Host, 1000) var wg sync.WaitGroup var mu sync.Mutex var errorRaised bool wg.Add(1) go e.processScannedData(w, e.TableEntry, scanned, &wg, &mu, &errorRaised) NetSweep(subnetMap.Ipv4s, subnetMap.Mask, RetrieveScanDirectives(), scanned) e.Log("Waiting for execution group to return finish.") wg.Wait() e.Log("Execution group finished scanning.") if errorRaised { http.Error(w, "Error during scan processing. Check logs for details.", http.StatusInternalServerError) } } /* Parse the request sent in from the client :param r: pointer to the http.Request coming in from the client */ func (e *ExecutionHandler) parseRequest(r *http.Request) (ScanRequest, error) { var input ScanRequest b, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { return input, fmt.Errorf("error reading request body: %w", err) } err = json.Unmarshal(b, &input) if err != nil { return input, fmt.Errorf("error unmarshalling request body: %w", err) } return input, nil } /* Process the data that is created from the kyoketsu Web Scanner, and parse the data into an HTML template :param w: an http.ResponseWriter to write the template back into :param templ: a pointer to the html template that will house the data :param scanned: a channel with 'Host' structs coming through :param wg: a pointer to a waitgroup that will get decremented when the function exits :param mu: a pointer to a mutex that will control when the errorRaised singleton is modified :param errorRaised: a pointer to a boolean that will signify if an error raised whilst processing data */ func (e *ExecutionHandler) processScannedData(w http.ResponseWriter, templ *template.Template, scanned chan Host, wg *sync.WaitGroup, mu *sync.Mutex, errorRaised *bool) { defer wg.Done() for x := range scanned { if len(x.ListeningPorts) > 0 { if err := templ.Execute(w, x); err != nil { mu.Lock() *errorRaised = true mu.Unlock() e.Log(err.Error()) } host, err := e.DbHook.GetByIP(x.IpAddress) if err != nil { if err != ErrNotExists { mu.Lock() *errorRaised = true mu.Unlock() e.Log(err.Error()) } if _, err := e.DbHook.Create(x); err != nil { mu.Lock() *errorRaised = true mu.Unlock() e.Log(err.Error()) } continue } if _, err := e.DbHook.Update(host.Id, x); err != nil { mu.Lock() *errorRaised = true mu.Unlock() e.Log(err.Error()) } } } } // handlers // type HtmlHandler struct { Home *template.Template // pointer to the HTML homepage TableEntry *template.Template // pointer to the table entry html template DbHook TopologyDatabaseIO stream io.Writer } func (h *HtmlHandler) Log(vals ...string) { h.stream.Write([]byte("KYOKETSU-WEB LOG ||| " + strings.Join(vals, " ||| ") + "\n")) } func (h *HtmlHandler) handleHome(w http.ResponseWriter) { data, err := h.DbHook.All() if err != nil { h.Log("Error reading from database: " + err.Error()) http.Error(w, "There was an error reading from the database: "+err.Error(), http.StatusInternalServerError) } h.Home.Execute(w, data) return } func (h *HtmlHandler) subnetQueryHandler(w http.ResponseWriter, r *http.Request) { var req ScanRequest b, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest) return } err = json.Unmarshal(b, &req) if err != nil { http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest) return } data, err := h.DbHook.GetByNetwork(req.NetworkAddress) if err != nil { http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest) return } for _, host := range data { h.TableEntry.Execute(w, host) } } func (h *HtmlHandler) fqdnQueryHandler(w http.ResponseWriter, r *http.Request) { var req ScanRequest b, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest) return } err = json.Unmarshal(b, &req) if err != nil { http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest) return } dnsList := strings.Split(req.FqdnPattern, ",") var ntwrk string if req.NetworkAddress == "" { ntwrk = "%" } else { ntwrk = req.NetworkAddress } h.Log("Query Arguments: " + ntwrk + " " + req.FqdnPattern) data, err := h.DbHook.FilterDnsPattern(ntwrk, dnsList) if err != nil { http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest) return } for _, host := range data { h.TableEntry.Execute(w, host) } } /* Handler function for HTML serving :param w: http.ResponseWriter interface for sending data back :param r: pointer to the http.Request coming in */ func (h *HtmlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.Log("Recieved " + r.Method + " on path: " + r.RequestURI) switch r.RequestURI { case "/home": h.handleHome(w) case "/subnets": h.subnetQueryHandler(w, r) case "/excludefqdn": h.fqdnQueryHandler(w, r) } } type AssetHandler struct { Root embed.FS // Should be able to use anything that implements the fs.FS interface for serving asset files EmbedRoot string // This is the root of the embeded file system RelPath string // The path that will be used for the handler, relative to the root of the webserver (/static, /assets, etc) } /* Handler function to serve out asset files (HTMX, bootstrap, pngs etc) :param w: http.ResponseWriter interface for sending data back to the caller :param r: pointer to an http.Request */ func (a *AssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var uripath string // the path from the request var pathSp []string // the path from the request split, so that we can point the request path to the embedded fs var assetPath []string // the cleaned path for the requested asset var fname string // filename of the requested asset var ctype string var b []byte var err error uripath = strings.TrimPrefix(r.URL.Path, a.RelPath) uripath = strings.Trim(uripath, "/") pathSp = strings.Split(uripath, "/") fname = pathSp[len(pathSp)-1] assetPath = append(assetPath, a.EmbedRoot) for i := 1; i < len(pathSp); i++ { assetPath = append(assetPath, pathSp[i]) } b, err = a.Root.ReadFile(path.Join(assetPath...)) if err != nil { http.Error(w, "Error occured when getting Asset: "+err.Error(), http.StatusBadRequest) } switch { case strings.Contains(fname, "css"): ctype = "text/css" case strings.Contains(fname, "js"): ctype = "text/javascript" case strings.Contains(fname, "html"): ctype = "text/html" case strings.Contains(fname, "json"): ctype = "application/json" case strings.Contains(fname, "png"): ctype = "image/png" default: ctype = "text" } w.Header().Add("Content-Type", ctype) fmt.Fprint(w, string(b)) }