webserver.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. package kyoketsu
  2. import (
  3. "embed"
  4. "encoding/json"
  5. "fmt"
  6. "html/template"
  7. "io"
  8. "log"
  9. "net/http"
  10. "path"
  11. "strings"
  12. "sync"
  13. )
  14. type ScanRequest struct {
  15. IpAddress string `json:"ip_address"`
  16. NetworkAddress string `json:"network_address"`
  17. FqdnPattern string `json:"fqdn_pattern"`
  18. }
  19. // Holding all static web server resources
  20. //
  21. //go:embed html/bootstrap-5.0.2-dist/js/* html/bootstrap-5.0.2-dist/css/* html/* html/templates/*
  22. var content embed.FS
  23. /*
  24. Run a new webserver
  25. :param port: port number to run the webserver on
  26. */
  27. func RunHttpServer(port int, dbhook TopologyDatabaseIO, portmap []int, logStream io.Writer) {
  28. assets := &AssetHandler{Root: content, RelPath: "static", EmbedRoot: "html"}
  29. tmpl, err := template.ParseFS(content, "html/templates/*.html")
  30. if err != nil {
  31. log.Fatal(err)
  32. }
  33. iptable, err := template.ParseFS(content, "html/templates/ip_table.html")
  34. if err != nil {
  35. log.Fatal(err)
  36. }
  37. htmlHndl := &HtmlHandler{Home: tmpl, TableEntry: iptable, DbHook: dbhook, stream: logStream}
  38. execHndl := &ExecutionHandler{DbHook: dbhook, PortMap: portmap, TableEntry: iptable, stream: logStream}
  39. http.Handle("/static/", assets)
  40. http.Handle("/home", htmlHndl)
  41. http.Handle("/subnets", htmlHndl)
  42. http.Handle("/excludefqdn", htmlHndl)
  43. http.Handle("/refresh", execHndl)
  44. log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil))
  45. }
  46. type ExecutionHandler struct {
  47. DbHook TopologyDatabaseIO
  48. TableEntry *template.Template
  49. PortMap []int
  50. stream io.Writer
  51. }
  52. func (e *ExecutionHandler) Log(vals ...string) {
  53. e.stream.Write([]byte("KYOKETSU-WEB LOG ||| " + strings.Join(vals, " ||| ") + "\n"))
  54. }
  55. /*
  56. 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
  57. :param w: an http.ResponseWriter that we will write data back to
  58. :param r: a pointer to the request coming in from the client
  59. */
  60. func (e *ExecutionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  61. e.Log("Recieved: " + r.Method + " on path: " + r.RequestURI)
  62. input, err := e.parseRequest(r)
  63. if err != nil {
  64. http.Error(w, err.Error(), http.StatusInternalServerError)
  65. return
  66. }
  67. e.Log(fmt.Sprintf("Parsed request struct: %+v", input))
  68. subnetMap, err := GetNetworkAddresses(input.IpAddress)
  69. if err != nil {
  70. http.Error(w, "Failed to get network addresses", http.StatusInternalServerError)
  71. return
  72. }
  73. scanned := make(chan Host, 1000)
  74. var wg sync.WaitGroup
  75. var mu sync.Mutex
  76. var errorRaised bool
  77. wg.Add(1)
  78. go e.processScannedData(w, e.TableEntry, scanned, &wg, &mu, &errorRaised)
  79. NetSweep(subnetMap.Ipv4s, subnetMap.Mask, RetrieveScanDirectives(), scanned)
  80. e.Log("Waiting for execution group to return finish.")
  81. wg.Wait()
  82. e.Log("Execution group finished scanning.")
  83. if errorRaised {
  84. http.Error(w, "Error during scan processing. Check logs for details.", http.StatusInternalServerError)
  85. }
  86. }
  87. /*
  88. Parse the request sent in from the client
  89. :param r: pointer to the http.Request coming in from the client
  90. */
  91. func (e *ExecutionHandler) parseRequest(r *http.Request) (ScanRequest, error) {
  92. var input ScanRequest
  93. b, err := io.ReadAll(r.Body)
  94. defer r.Body.Close()
  95. if err != nil {
  96. return input, fmt.Errorf("error reading request body: %w", err)
  97. }
  98. err = json.Unmarshal(b, &input)
  99. if err != nil {
  100. return input, fmt.Errorf("error unmarshalling request body: %w", err)
  101. }
  102. return input, nil
  103. }
  104. /*
  105. Process the data that is created from the kyoketsu Web Scanner, and parse the data into an HTML template
  106. :param w: an http.ResponseWriter to write the template back into
  107. :param templ: a pointer to the html template that will house the data
  108. :param scanned: a channel with 'Host' structs coming through
  109. :param wg: a pointer to a waitgroup that will get decremented when the function exits
  110. :param mu: a pointer to a mutex that will control when the errorRaised singleton is modified
  111. :param errorRaised: a pointer to a boolean that will signify if an error raised whilst processing data
  112. */
  113. func (e *ExecutionHandler) processScannedData(w http.ResponseWriter, templ *template.Template, scanned chan Host, wg *sync.WaitGroup, mu *sync.Mutex, errorRaised *bool) {
  114. defer wg.Done()
  115. for x := range scanned {
  116. if len(x.ListeningPorts) > 0 {
  117. if err := templ.Execute(w, x); err != nil {
  118. mu.Lock()
  119. *errorRaised = true
  120. mu.Unlock()
  121. e.Log(err.Error())
  122. }
  123. host, err := e.DbHook.GetByIP(x.IpAddress)
  124. if err != nil {
  125. if err != ErrNotExists {
  126. mu.Lock()
  127. *errorRaised = true
  128. mu.Unlock()
  129. e.Log(err.Error())
  130. }
  131. if _, err := e.DbHook.Create(x); err != nil {
  132. mu.Lock()
  133. *errorRaised = true
  134. mu.Unlock()
  135. e.Log(err.Error())
  136. }
  137. continue
  138. }
  139. if _, err := e.DbHook.Update(host.Id, x); err != nil {
  140. mu.Lock()
  141. *errorRaised = true
  142. mu.Unlock()
  143. e.Log(err.Error())
  144. }
  145. }
  146. }
  147. }
  148. // handlers //
  149. type HtmlHandler struct {
  150. Home *template.Template // pointer to the HTML homepage
  151. TableEntry *template.Template // pointer to the table entry html template
  152. DbHook TopologyDatabaseIO
  153. stream io.Writer
  154. }
  155. func (h *HtmlHandler) Log(vals ...string) {
  156. h.stream.Write([]byte("KYOKETSU-WEB LOG ||| " + strings.Join(vals, " ||| ") + "\n"))
  157. }
  158. func (h *HtmlHandler) handleHome(w http.ResponseWriter) {
  159. data, err := h.DbHook.All()
  160. if err != nil {
  161. h.Log("Error reading from database: " + err.Error())
  162. http.Error(w, "There was an error reading from the database: "+err.Error(), http.StatusInternalServerError)
  163. }
  164. h.Home.Execute(w, data)
  165. return
  166. }
  167. func (h *HtmlHandler) subnetQueryHandler(w http.ResponseWriter, r *http.Request) {
  168. var req ScanRequest
  169. b, err := io.ReadAll(r.Body)
  170. defer r.Body.Close()
  171. if err != nil {
  172. http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest)
  173. return
  174. }
  175. err = json.Unmarshal(b, &req)
  176. if err != nil {
  177. http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest)
  178. return
  179. }
  180. data, err := h.DbHook.GetByNetwork(req.NetworkAddress)
  181. if err != nil {
  182. http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest)
  183. return
  184. }
  185. for _, host := range data {
  186. h.TableEntry.Execute(w, host)
  187. }
  188. }
  189. func (h *HtmlHandler) fqdnQueryHandler(w http.ResponseWriter, r *http.Request) {
  190. var req ScanRequest
  191. b, err := io.ReadAll(r.Body)
  192. defer r.Body.Close()
  193. if err != nil {
  194. http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest)
  195. return
  196. }
  197. err = json.Unmarshal(b, &req)
  198. if err != nil {
  199. http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest)
  200. return
  201. }
  202. dnsList := strings.Split(req.FqdnPattern, ",")
  203. var ntwrk string
  204. if req.NetworkAddress == "" {
  205. ntwrk = "%"
  206. } else {
  207. ntwrk = req.NetworkAddress
  208. }
  209. h.Log("Query Arguments: " + ntwrk + " " + req.FqdnPattern)
  210. data, err := h.DbHook.FilterDnsPattern(ntwrk, dnsList)
  211. if err != nil {
  212. http.Error(w, "There was an error reading the request: "+err.Error(), http.StatusBadRequest)
  213. return
  214. }
  215. for _, host := range data {
  216. h.TableEntry.Execute(w, host)
  217. }
  218. }
  219. /*
  220. Handler function for HTML serving
  221. :param w: http.ResponseWriter interface for sending data back
  222. :param r: pointer to the http.Request coming in
  223. */
  224. func (h *HtmlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  225. h.Log("Recieved " + r.Method + " on path: " + r.RequestURI)
  226. switch r.RequestURI {
  227. case "/home":
  228. h.handleHome(w)
  229. case "/subnets":
  230. h.subnetQueryHandler(w, r)
  231. case "/excludefqdn":
  232. h.fqdnQueryHandler(w, r)
  233. }
  234. }
  235. type AssetHandler struct {
  236. Root embed.FS // Should be able to use anything that implements the fs.FS interface for serving asset files
  237. EmbedRoot string // This is the root of the embeded file system
  238. RelPath string // The path that will be used for the handler, relative to the root of the webserver (/static, /assets, etc)
  239. }
  240. /*
  241. Handler function to serve out asset files (HTMX, bootstrap, pngs etc)
  242. :param w: http.ResponseWriter interface for sending data back to the caller
  243. :param r: pointer to an http.Request
  244. */
  245. func (a *AssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  246. var uripath string // the path from the request
  247. var pathSp []string // the path from the request split, so that we can point the request path to the embedded fs
  248. var assetPath []string // the cleaned path for the requested asset
  249. var fname string // filename of the requested asset
  250. var ctype string
  251. var b []byte
  252. var err error
  253. uripath = strings.TrimPrefix(r.URL.Path, a.RelPath)
  254. uripath = strings.Trim(uripath, "/")
  255. pathSp = strings.Split(uripath, "/")
  256. fname = pathSp[len(pathSp)-1]
  257. assetPath = append(assetPath, a.EmbedRoot)
  258. for i := 1; i < len(pathSp); i++ {
  259. assetPath = append(assetPath, pathSp[i])
  260. }
  261. b, err = a.Root.ReadFile(path.Join(assetPath...))
  262. if err != nil {
  263. http.Error(w, "Error occured when getting Asset: "+err.Error(), http.StatusBadRequest)
  264. }
  265. switch {
  266. case strings.Contains(fname, "css"):
  267. ctype = "text/css"
  268. case strings.Contains(fname, "js"):
  269. ctype = "text/javascript"
  270. case strings.Contains(fname, "html"):
  271. ctype = "text/html"
  272. case strings.Contains(fname, "json"):
  273. ctype = "application/json"
  274. case strings.Contains(fname, "png"):
  275. ctype = "image/png"
  276. default:
  277. ctype = "text"
  278. }
  279. w.Header().Add("Content-Type", ctype)
  280. fmt.Fprint(w, string(b))
  281. }