diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b0f546 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.bak +netmaker-gui +config/* +data/* diff --git a/README.md b/README.md index 6091a69..76bb3a0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ # netmaker-gui -Alternative UI for netmaker (see github.com/gravitl/netmaker +A responsive alternative UI for netmaker (https://github.com/gravitl/netmaker) + +Built with go and html/templates. +Missing following features compared to netmaker-ui (https://github.com/gravitl/netmaker-ui) +- DNS +- Netmaker v0.8.0 changes: in particular Relay Gateways + +You can use netmaker-gui at the same time as netmaker-ui. For example, one one running as dashboard.netmaker.example.com and the other at control.netmaker.example.com + + + + +## Installation: +To use along side of your existing netmaker installation insert the following to your docker-compose.yml file + +``` +netmaker-gui + container-name: netmaker-gui + image: nusak/netmaker-gui:v0.1.0 + restart: unless-stopped + ports: + - "8080:8080" + environment: + DATABASE: sqlite + volumes: + - sqldata:/data +``` + +and add an appropriate entry to your proxy relay. + +## Screenshots +### Browser + + +### Mobile + + +## Third Party Tools +- CSS - W3Schools https://w3.schools.com/w3css +- Icons - Material Icons https://fonts.google.com/icons +- Netmaker https://github.com/gravitl/netmaker diff --git a/go.mod b/go.mod index 75e9e34..351d3f0 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/mattkasun/netmaker-gui go 1.16 require ( - github.com/aws/aws-sdk-go v1.34.28 github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.7.4 github.com/go-playground/validator/v10 v10.9.0 // indirect - github.com/gravitl/netmaker v0.7.1 + github.com/gravitl/netmaker v0.7.3 github.com/rqlite/gorqlite v0.0.0-20210804113434-b4935d2eab04 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect diff --git a/go.sum b/go.sum index e924e0a..bbce16a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= @@ -89,14 +88,12 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= -github.com/gravitl/netmaker v0.7.1 h1:RTgGCookqpuqFP+Q91bZV2slrnpNeXGgrKHJN8o/GqY= -github.com/gravitl/netmaker v0.7.1/go.mod h1:oV1K5PZY3llJnTIBpeoLZ02evXD95zQFM0hk7MnFU2U= +github.com/gravitl/netmaker v0.7.3 h1:FtKGXca6O/KuJhavl2GZixfSlyxtkwAtCqcwf91mkT4= +github.com/gravitl/netmaker v0.7.3/go.mod h1:oV1K5PZY3llJnTIBpeoLZ02evXD95zQFM0hk7MnFU2U= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw= @@ -121,6 +118,7 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= diff --git a/handlers.go b/handlers.go index ea7cdc5..15277aa 100644 --- a/handlers.go +++ b/handlers.go @@ -5,12 +5,15 @@ import ( "net/http" "net/url" "strconv" + "strings" + "time" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" controller "github.com/gravitl/netmaker/controllers" "github.com/gravitl/netmaker/functions" "github.com/gravitl/netmaker/models" + "github.com/skip2/go-qrcode" ) func ProcessLogin(c *gin.Context) { @@ -19,18 +22,26 @@ func ProcessLogin(c *gin.Context) { AuthRequest.UserName = c.PostForm("user") AuthRequest.Password = c.PostForm("pass") session := sessions.Default(c) - jwt, err := controller.VerifyAuthRequest(AuthRequest) + //don't need the jwt + _, err := controller.VerifyAuthRequest(AuthRequest) if err != nil { - fmt.Println("error verifying AuthRequest: ", jwt, err) - fmt.Println("setting session err to: ", err) - session.Set("error", err) + fmt.Println("error verifying AuthRequest: ", err) + session.Set("message", err.Error()) session.Set("loggedIn", false) c.HTML(http.StatusUnauthorized, "Login", gin.H{"message": err}) } else { session.Set("loggedIn", true) - session.Set("token", jwt) + //init message + session.Set("message", "") + session.Options(sessions.Options{MaxAge: 1800}) + user, err := controller.GetUser(AuthRequest.UserName) + if err != nil { + fmt.Println("err retrieving user: ", err) + } + session.Set("username", user.UserName) + session.Set("isAdmin", user.IsAdmin) + session.Set("networks", user.Networks) session.Save() - fmt.Println("Successful login:\n", session.Get("loggedIn"), "\njwt:\n", jwt) location := url.URL{Path: "/"} c.Redirect(http.StatusFound, location.RequestURI()) } @@ -62,7 +73,7 @@ func NewUser(c *gin.Context) { func DisplayLanding(c *gin.Context) { var Data PageData - Data.Init("Networks") + Data.Init("Networks", c) c.HTML(http.StatusOK, "layout", Data) } @@ -200,6 +211,17 @@ func UpdateNetwork(c *gin.Context) { c.Redirect(http.StatusFound, location.RequestURI()) } +func RefreshKeys(c *gin.Context) { + net := c.Param("net") + _, err := controller.KeyUpdate(net) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} + func NewKey(c *gin.Context) { var key models.AccessKey var err error @@ -280,3 +302,355 @@ func DeleteUser(c *gin.Context) { location := url.URL{Path: "/"} c.Redirect(http.StatusFound, location.RequestURI()) } + +func EditUser(c *gin.Context) { + session := sessions.Default(c) + username := session.Get("username").(string) + user, err := controller.GetUser(username) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.Abort() + } + c.HTML(http.StatusOK, "EditUser", user) +} + +func UpdateUser(c *gin.Context) { + var new models.User + username := c.Param("user") + user, err := controller.GetUserInternal(username) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.Abort() + return + } + new.UserName = c.PostForm("username") + new.Password = c.PostForm("password") + _, err = controller.UpdateUser(new, user) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.Abort() + return + } + session := sessions.Default(c) + session.Set("message", "user has been updated") + session.Save() + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} + +func EditNode(c *gin.Context) { + network := c.PostForm("network") + mac := c.PostForm("mac") + var node models.Node + node, err := controller.GetNode(mac, network) + if err != nil { + fmt.Println("error getting node details \n", err) + c.JSON(http.StatusBadRequest, err) + } + c.HTML(http.StatusOK, "EditNode", node) +} + +func DeleteNode(c *gin.Context) { + mac := c.PostForm("mac") + net := c.PostForm("net") + fmt.Println("deleting node ", mac, net) + err := controller.DeleteNode(mac+"###"+net, false) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.Abort() + } + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} + +func UpdateNode(c *gin.Context) { + + var node *models.Node + if err := c.ShouldBind(&node); err != nil { + fmt.Println("should bind") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + net := c.Param("net") + mac := c.Param("mac") + fmt.Printf("=============%T %T %T %v %v %v", net, mac, node, net, mac, node) + oldnode, err := models.GetNode(mac, net) + if err != nil { + fmt.Println("Get node with mac ", mac, " and Network ", net) + //c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusBadRequest, node) + return + } + if err = oldnode.Update(node); err != nil { + fmt.Println("update network") + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) + +} + +func NodeHealth(c *gin.Context) { + nodes, err := models.GetAllNodes() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + var response []NodeStatus + var nodeHealth NodeStatus + for _, node := range nodes { + nodeHealth.Mac = node.MacAddress + lastupdate := time.Now().Sub(time.Unix(node.LastCheckIn, 0)) + if lastupdate.Minutes() > 15.0 { + nodeHealth.Status = "Dead: Node last checked in more than 15 minutes ago" + nodeHealth.Color = "w3-deep-orange" + } else if lastupdate.Minutes() > 5.0 { + nodeHealth.Status = "Warning: Node last checked in more than 5 minutes ago" + nodeHealth.Color = "w3-khaki" + } else { + nodeHealth.Status = "Healthy: Node checked in within the last 5 minutes" + nodeHealth.Color = "w3-teal" + } + response = append(response, nodeHealth) + } + c.JSON(http.StatusOK, response) + return +} + +func ProcessEgress(c *gin.Context) { + var egress models.EgressGatewayRequest + egress.NodeID = c.Param("mac") + egress.NetID = c.Param("net") + node, err := controller.GetNode(egress.NodeID, egress.NetID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + egress.Ranges = strings.Split(c.PostForm("ranges"), ",") + egress.Interface = c.PostForm("interface") + + _, err = controller.CreateEgressGateway(egress) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + session := sessions.Default(c) + session.Set("message", node.Name+" is now a gateway") + session.Save() + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} + +func CreateEgress(c *gin.Context) { + net := c.Param("net") + mac := c.Param("mac") + node, err := controller.GetNode(mac, net) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.HTML(http.StatusOK, "Egress", node) +} + +func DeleteEgress(c *gin.Context) { + net := c.Param("net") + mac := c.Param("mac") + _, err := controller.DeleteEgressGateway(net, mac) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Ingress Gateway Created"}) +} + +func CreateIngress(c *gin.Context) { + net := c.Param("net") + mac := c.Param("mac") + _, err := controller.CreateIngressGateway(net, mac) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Ingress Gateway Created"}) +} + +func DeleteIngress(c *gin.Context) { + net := c.Param("net") + mac := c.Param("mac") + _, err := controller.DeleteIngressGateway(net, mac) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Ingress Gateway Created"}) +} + +func CreateIngressClient(c *gin.Context) { + var client models.ExtClient + client.Network = c.Param("net") + client.IngressGatewayID = c.Param("mac") + + node, err := functions.GetNodeByMacAddress(client.Network, client.IngressGatewayID) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + client.IngressGatewayEndpoint = node.Endpoint + ":" + strconv.FormatInt(int64(node.ListenPort), 10) + + err = controller.CreateExtClient(client) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + session := sessions.Default(c) + session.Set("message", "external client has been created") + session.Save() + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} + +func DeleteIngressClient(c *gin.Context) { + net := c.Param("net") + id := c.Param("id") + err := controller.DeleteExtClient(net, id) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + session := sessions.Default(c) + session.Set("message", "external client "+id+" @ "+net+" has been deleted") + session.Save() + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} + +func EditIngressClient(c *gin.Context) { + net := c.Param("net") + id := c.Param("id") + client, err := controller.GetExtClient(id, net) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.HTML(http.StatusOK, "EditExtClient", client) +} + +func GetQR(c *gin.Context) { + net := c.Param("net") + id := c.Param("id") + config, err := GetConf(net, id) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + b, err := qrcode.Encode(config, qrcode.Medium, 220) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Header("Content-Type", "image/png") + c.Data(http.StatusOK, "application/octet-strean", b) +} + +func GetConf(net, id string) (string, error) { + client, err := controller.GetExtClient(id, net) + if err != nil { + return "", err + } + gwnode, err := functions.GetNodeByMacAddress(client.Network, client.IngressGatewayID) + if err != nil { + return "", err + } + network, err := functions.GetParentNetwork(client.Network) + if err != nil { + return "", err + } + keepalive := "" + if network.DefaultKeepalive != 0 { + keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive)) + } + gwendpoint := gwnode.Endpoint + ":" + strconv.Itoa(int(gwnode.ListenPort)) + newAllowedIPs := network.AddressRange + if egressGatewayRanges, err := client.GetEgressRangesOnNetwork(); err == nil { + for _, egressGatewayRange := range egressGatewayRanges { + newAllowedIPs += "," + egressGatewayRange + } + } + defaultDNS := "" + if network.DefaultExtClientDNS != "" { + defaultDNS = "DNS = " + network.DefaultExtClientDNS + } + + config := fmt.Sprintf(`[Interface] +Address = %s +PrivateKey = %s +%s + +[Peer] +PublicKey = %s +AllowedIPs = %s +Endpoint = %s +%s + +`, client.Address+"/32", + client.PrivateKey, + defaultDNS, + gwnode.PublicKey, + newAllowedIPs, + gwendpoint, + keepalive) + + return config, nil +} + +func GetClientConfig(c *gin.Context) { + net := c.Param("net") + id := c.Param("id") + config, err := GetConf(net, id) + b := []byte(config) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + filename := id + ".conf" + //c.FileAttachment(filepath, filename) + c.Header("Content-Description", "File Transfer") + c.Header("Content-Disposition", "attachment: filename="+filename) + c.Data(http.StatusOK, "application/octet-stream", b) +} + +func UpdateClient(c *gin.Context) { + net := c.Param("net") + id := c.Param("id") + newid := c.PostForm("newid") + + client, err := controller.GetExtClient(id, net) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + _, err = controller.UpdateExtClient(newid, net, client) + if err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + session := sessions.Default(c) + session.Set("message", "External client has been updated") + session.Save() + location := url.URL{Path: "/"} + c.Redirect(http.StatusFound, location.RequestURI()) +} diff --git a/helpers.go b/helpers.go deleted file mode 100644 index 70bd636..0000000 --- a/helpers.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "net/http" - - "github.com/gravitl/netmaker/models" -) - -var backendURL = "https://api.netmaker.nusak.ca" - -func API(data interface{}, method, url, authorization string) (*http.Response, error) { - var request *http.Request - var err error - if data != "" { - payload, err := json.Marshal(data) - if err != nil { - return nil, err - } - request, err = http.NewRequest(method, backendURL+url, bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - request.Header.Set("Content-Type", "application/json") - } else { - request, err = http.NewRequest(method, backendURL+url, nil) - if err != nil { - return nil, err - } - } - if authorization != "" { - request.Header.Set("Authorization", "Bearer "+authorization) - } - client := http.Client{} - return client.Do(request) -} - -//func GetNetSummary() ([]NetSummary, error) { -// var network NetSummary -// var result []NetSummary -// //response, err := API("", http.MethodGet, "/api/networks", "secretkey") -// networks, err := models.GetNetworks() -// if err != nil { -// return result, err -// } -// //err = json.NewDecoder(response.Body).Decode(&body) -// //if err != nil { -// // return result, err -// //} -// for _, net := range networks { -// fmt.Println(net.NodesLastModified, net.NetworkLastModified) -// network.ID = net.NetID -// network.Name = net.DisplayName -// network.Address = net.AddressRange -// network.Keys = net.AccessKeys -// network.NodeModified = time.Unix(net.NodesLastModified, 0).Format(time.UnixDate) -// network.NetModified = time.Unix(net.NetworkLastModified, 0).Format(time.UnixDate) -// result = append(result, network) -// } -// return result, err -//} -// -func GetNodeSummary() ([]NodeSummary, error) { - var body []models.Node - var node NodeSummary - var result []NodeSummary - response, err := API("", http.MethodGet, "/api/nodes", "secretkey") - if err != nil { - return result, err - } - err = json.NewDecoder(response.Body).Decode(&body) - if err != nil { - return result, err - } - for _, net := range body { - node.Name = net.Name - node.Network = net.Network - node.PublicIP = net.Endpoint - node.SubNet = net.Address - result = append(result, node) - } - - return result, err -} - -//func GetNetDetails(net string) (models.Network, error) { -// var body models.Network -// response, err := API("", http.MethodGet, "/api/networks/"+net, "secretkey") -// -// if err != nil { -// return body, err -// } -// err = json.NewDecoder(response.Body).Decode(&body) -// return body, err -//} diff --git a/html/buttonbar.html b/html/buttonbar.html index e4e7caa..67debbb 100644 --- a/html/buttonbar.html +++ b/html/buttonbar.html @@ -6,14 +6,14 @@ - + {{/*NewNetwork*/}}