netmaker/controllers/extClientHttpController.go
2021-05-27 15:00:56 -04:00

456 lines
14 KiB
Go

package controller
import (
"context"
"encoding/json"
"io"
"errors"
"fmt"
"math/rand"
// "fmt"
"net/http"
"time"
"strconv"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/functions"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/mongoconn"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/skip2/go-qrcode"
)
func extClientHandlers(r *mux.Router) {
r.HandleFunc("/api/extclients", securityCheck(http.HandlerFunc(getAllExtClients))).Methods("GET")
r.HandleFunc("/api/extclients/{network}", securityCheck(http.HandlerFunc(getNetworkExtClients))).Methods("GET")
r.HandleFunc("/api/extclients/{network}/{clientid}", securityCheck(http.HandlerFunc(getExtClient))).Methods("GET")
r.HandleFunc("/api/extclients/{network}/{clientid}/{type}", securityCheck(http.HandlerFunc(getExtClientConf))).Methods("GET")
r.HandleFunc("/api/extclients/{network}/{clientid}", securityCheck(http.HandlerFunc(updateExtClient))).Methods("PUT")
r.HandleFunc("/api/extclients/{network}/{clientid}", securityCheck(http.HandlerFunc(deleteExtClient))).Methods("DELETE")
r.HandleFunc("/api/extclients/{network}/{macaddress}", securityCheck(http.HandlerFunc(createExtClient))).Methods("POST")
}
// TODO: Implement Validation
func ValidateExtClientCreate(networkName string, extclient models.ExtClient) error {
// v := validator.New()
// _ = v.RegisterValidation("macaddress_unique", func(fl validator.FieldLevel) bool {
// var isFieldUnique bool = functions.IsFieldUnique(networkName, "macaddress", extclient.MacAddress)
// return isFieldUnique
// })
// _ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
// _, err := extclient.GetNetwork()
// return err == nil
// })
// err := v.Struct(extclient)
// if err != nil {
// for _, e := range err.(validator.ValidationErrors) {
// fmt.Println(e)
// }
// }
return nil
}
// TODO: Implement Validation
func ValidateExtClientUpdate(networkName string, extclient models.ExtClient) error {
// v := validator.New()
// _ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
// _, err := extclient.GetNetwork()
// return err == nil
// })
// err := v.Struct(extclient)
// if err != nil {
// for _, e := range err.(validator.ValidationErrors) {
// fmt.Println(e)
// }
// }
return nil
}
func checkIngressExists(network string, macaddress string) bool {
node, err := functions.GetNodeByMacAddress(network, macaddress)
if err != nil {
return false
}
return node.IsIngressGateway
}
//Gets all extclients associated with network, including pending extclients
func getNetworkExtClients(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var extclients []models.ExtClient
var params = mux.Vars(r)
extclients, err := GetNetworkExtClients(params["network"])
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
//Returns all the extclients in JSON format
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(extclients)
}
func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
var extclients []models.ExtClient
collection := mongoconn.Client.Database("netmaker").Collection("extclients")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
filter := bson.M{"network": network}
//Filtering out the ID field cuz Dillon doesn't like it. May want to filter out other fields in the future
cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
if err != nil {
return []models.ExtClient{}, err
}
defer cancel()
for cur.Next(context.TODO()) {
//Using a different model for the ReturnExtClient (other than regular extclient).
//Either we should do this for ALL structs (so Networks and Keys)
//OR we should just use the original struct
//My preference is to make some new return structs
//TODO: Think about this. Not an immediate concern. Just need to get some consistency eventually
var extclient models.ExtClient
err := cur.Decode(&extclient)
if err != nil {
return []models.ExtClient{}, err
}
// add item our array of extclients
extclients = append(extclients, extclient)
}
//TODO: Another fatal error we should take care of.
if err := cur.Err(); err != nil {
return []models.ExtClient{}, err
}
return extclients, nil
}
//A separate function to get all extclients, not just extclients for a particular network.
//Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
func getAllExtClients(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
extclients, err := functions.GetAllExtClients()
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
//Return all the extclients in JSON format
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(extclients)
}
//Get an individual extclient. Nothin fancy here folks.
func getExtClient(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
var extclient models.ExtClient
collection := mongoconn.Client.Database("netmaker").Collection("extclients")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
filter := bson.M{"network": params["network"], "clientid": params["clientid"]}
err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&extclient)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
defer cancel()
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(extclient)
}
//Get an individual extclient. Nothin fancy here folks.
func getExtClientConf(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
var extclient models.ExtClient
collection := mongoconn.Client.Database("netmaker").Collection("extclients")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
filter := bson.M{"network": params["network"], "clientid": params["clientid"]}
err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&extclient)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
gwnode, err := functions.GetNodeByMacAddress(extclient.Network, extclient.IngressGatewayID)
if err != nil {
fmt.Println("Could not retrieve Ingress Gateway Node " + extclient.IngressGatewayID)
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
network, err := functions.GetParentNetwork(extclient.Network)
if err != nil {
fmt.Println("Could not retrieve Ingress Gateway Network " + extclient.Network)
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
keepalive := ""
if network.DefaultKeepalive != 0 {
keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
}
gwendpoint := gwnode.Endpoint + ":" + strconv.Itoa(int(gwnode.ListenPort))
config := fmt.Sprintf(`[Interface]
Address = %s
PrivateKey = %s
[Peer]
PublicKey = %s
AllowedIPs = %s
Endpoint = %s
%s
`, extclient.Address + "/32",
extclient.PrivateKey,
gwnode.PublicKey,
network.AddressRange,
gwendpoint,
keepalive)
if params["type"] == "qr" {
bytes, err := qrcode.Encode(config, qrcode.Medium, 220)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
_, err = w.Write(bytes)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
return
}
if params["type"] == "file" {
name := extclient.ClientID + ".conf"
w.Header().Set("Content-Type", "application/config")
w.Header().Set("Content-Disposition", "attachment; filename=\"" + name + "\"")
w.WriteHeader(http.StatusOK)
_, err := fmt.Fprint(w, config)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
}
return
}
defer cancel()
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(extclient)
}
func CreateExtClient(extclient models.ExtClient) error {
if extclient.PrivateKey == "" {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return err
}
extclient.PrivateKey = privateKey.String()
extclient.PublicKey = privateKey.PublicKey().String()
}
if extclient.Address == "" {
newAddress, err := functions.UniqueAddress(extclient.Network)
if err != nil {
return err
}
extclient.Address = newAddress
}
if extclient.ClientID == "" {
clientid := StringWithCharset(7, charset)
clientname := "client-" + clientid
extclient.ClientID = clientname
}
if extclient.ClientID == "" {
cid := StringWithCharset(7, charset)
clientid := "client-" + cid
extclient.ClientID = clientid
}
extclient.LastModified = time.Now().Unix()
collection := mongoconn.Client.Database("netmaker").Collection("extclients")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
// insert our network into the network table
_, err := collection.InsertOne(ctx, extclient)
defer cancel()
if err != nil {
return err
}
err = SetNetworkNodesLastModified(extclient.Network)
return err
}
//This one's a doozy
//To create a extclient
//Must have valid key and be unique
func createExtClient(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
networkName := params["network"]
macaddress := params["macaddress"]
//Check if network exists first
//TODO: This is inefficient. Let's find a better way.
//Just a few rows down we grab the network anyway
ingressExists := checkIngressExists(networkName, macaddress)
if !ingressExists {
returnErrorResponse(w, r, formatError(errors.New("ingress does not exist"), "internal"))
return
}
var extclient models.ExtClient
extclient.Network = networkName
extclient.IngressGatewayID = macaddress
node, err := functions.GetNodeByMacAddress(networkName, macaddress)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
extclient.IngressGatewayEndpoint = node.Endpoint + ":" + strconv.FormatInt(int64(node.ListenPort), 10)
err = json.NewDecoder(r.Body).Decode(&extclient)
if err != nil && !errors.Is(err, io.EOF) {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
err = ValidateExtClientCreate(params["network"], extclient)
if err != nil {
returnErrorResponse(w, r, formatError(err, "badrequest"))
return
}
err = CreateExtClient(extclient)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
w.WriteHeader(http.StatusOK)
}
func updateExtClient(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
var newExtClient models.ExtClient
var oldExtClient models.ExtClient
// we decode our body request params
_ = json.NewDecoder(r.Body).Decode(&newExtClient)
// TODO: Validation for update.
// err := ValidateExtClientUpdate(params["network"], params["clientid"], newExtClient)
// if err != nil {
// returnErrorResponse(w, r, formatError(err, "badrequest"))
// return
// }
collection := mongoconn.Client.Database("netmaker").Collection("extclients")
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
filter := bson.M{"network": params["network"], "clientid": params["clientid"]}
err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&oldExtClient)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
success, err := DeleteExtClient(params["network"], params["clientid"])
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
} else if !success {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
oldExtClient.ClientID = newExtClient.ClientID
CreateExtClient(oldExtClient)
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(oldExtClient)
}
func DeleteExtClient(network string, clientid string) (bool, error) {
deleted := false
collection := mongoconn.Client.Database("netmaker").Collection("extclients")
filter := bson.M{"network": network, "clientid": clientid}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
result, err := collection.DeleteOne(ctx, filter)
deletecount := result.DeletedCount
if deletecount > 0 {
deleted = true
}
defer cancel()
fmt.Println("Deleted extclient client " + clientid + " from network " + network)
return deleted, err
}
//Delete a extclient
//Pretty straightforward
func deleteExtClient(w http.ResponseWriter, r *http.Request) {
// Set header
w.Header().Set("Content-Type", "application/json")
// get params
var params = mux.Vars(r)
success, err := DeleteExtClient(params["network"], params["clientid"])
if err != nil {
returnErrorResponse(w, r, formatError(err, "internal"))
return
} else if !success {
err = errors.New("Could not delete extclient " + params["clientid"])
returnErrorResponse(w, r, formatError(err, "internal"))
return
}
returnSuccessResponse(w, r, params["clientid"]+" deleted.")
}
func StringWithCharset(length int, charset string) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var seededRand *rand.Rand = rand.New(
rand.NewSource(time.Now().UnixNano()))