diff --git a/handler/routes.go b/handler/routes.go index c03ab7f..6422ac8 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -199,6 +199,15 @@ func UsersSettings() echo.HandlerFunc { } } +// ServersPage renders the server list/create page +func ServersPage() echo.HandlerFunc { + return func(c echo.Context) error { + return c.Render(http.StatusOK, "servers.html", map[string]interface{}{ + "baseData": model.BaseData{Active: "servers", CurrentUser: currentUser(c), Admin: isAdmin(c)}, + }) + } +} + // UpdateUser to update user information func UpdateUser(db store.IStore) echo.HandlerFunc { return func(c echo.Context) error { @@ -256,6 +265,19 @@ func UpdateUser(db store.IStore) echo.HandlerFunc { user.Admin = admin } + // only an admin may change which servers a user can access + if isAdmin(c) { + if rawIDs, ok := data["server_ids"].([]interface{}); ok { + serverIDs := make([]string, 0, len(rawIDs)) + for _, v := range rawIDs { + if id, ok := v.(string); ok && util.ValidateRecordID(id) { + serverIDs = append(serverIDs, id) + } + } + user.ServerIDs = serverIDs + } + } + if err := db.DeleteUser(previousUsername); err != nil { return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) } @@ -308,6 +330,14 @@ func CreateUser(db store.IStore) echo.HandlerFunc { user.Admin = admin + if rawIDs, ok := data["server_ids"].([]interface{}); ok { + for _, v := range rawIDs { + if id, ok := v.(string); ok && util.ValidateRecordID(id) { + user.ServerIDs = append(user.ServerIDs, id) + } + } + } + if err := db.SaveUser(user); err != nil { return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) } @@ -476,6 +506,83 @@ func GetServerClients(db store.IStore) echo.HandlerFunc { } } +// CreateServer handler creates a new WireGuard server (step 3 of the +// multi-server extension). Admin-only. Generates a fresh key pair, +// validates the ID/interface name/subnet, and stores the server plus its +// per-server settings. +func CreateServer(db store.IStore) echo.HandlerFunc { + return func(c echo.Context) error { + type createServerRequest struct { + ID string `json:"id"` + Name string `json:"name"` + Interface string `json:"interface"` + Addresses []string `json:"addresses"` + ListenPort int `json:"listen_port"` + } + var req createServerRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"}) + } + + if !util.ValidateRecordID(req.ID) { + return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid server ID (letters, digits, - and _ only)"}) + } + if !util.ValidateInterfaceName(req.Interface) { + return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid interface name (max 15 chars, letters/digits/-/_ only)"}) + } + if !util.ValidateServerAddresses(req.Addresses) { + return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Address ranges must be in CIDR format"}) + } + if req.ListenPort <= 0 || req.ListenPort > 65535 { + return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid listen port"}) + } + if req.Name == "" { + req.Name = req.ID + } + + if _, err := db.GetServerByID(req.ID); err == nil { + return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "A server with this ID already exists"}) + } + + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + log.Error("Cannot generate wireguard key pair: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) + } + + server := model.Server{ + ID: req.ID, + Name: req.Name, + KeyPair: &model.ServerKeypair{ + PrivateKey: key.String(), + PublicKey: key.PublicKey().String(), + UpdatedAt: time.Now().UTC(), + }, + Interface: &model.ServerInterface{ + Name: req.Interface, + Addresses: req.Addresses, + ListenPort: req.ListenPort, + UpdatedAt: time.Now().UTC(), + }, + } + + if err := db.CreateServer(server); err != nil { + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot create server: %v", err)}) + } + + settings := model.ServerSetting{ + ConfigFilePath: fmt.Sprintf("/etc/wireguard/%s.conf", req.Interface), + UpdatedAt: time.Now().UTC(), + } + if err := db.SaveServerSettings(req.ID, settings); err != nil { + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Server created but settings failed: %v", err)}) + } + + log.Infof("Created server %s (%s)", req.ID, req.Name) + return c.JSON(http.StatusOK, server) + } +} + // NewClient handler func NewClient(db store.IStore) echo.HandlerFunc { return func(c echo.Context) error { diff --git a/main.go b/main.go index 2bf23c7..c5800b4 100644 --- a/main.go +++ b/main.go @@ -251,7 +251,9 @@ func main() { app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession) + app.GET(util.BasePath+"/servers-settings", handler.ServersPage(), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) app.GET(util.BasePath+"/servers", handler.ListServers(db), handler.ValidSession) + app.POST(util.BasePath+"/servers", handler.CreateServer(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.GET(util.BasePath+"/servers/:id/clients", handler.GetServerClients(db), handler.ValidSession, handler.RequireServerAccess(db)) app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) diff --git a/router/router.go b/router/router.go index 59d352e..aab48ec 100644 --- a/router/router.go +++ b/router/router.go @@ -111,6 +111,11 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo log.Fatal(err) } + tmplServersString, err := util.StringFromEmbedFile(tmplDir, "servers.html") + if err != nil { + log.Fatal(err) + } + // create template list funcs := template.FuncMap{ "StringsJoin": strings.Join, @@ -125,6 +130,7 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString)) templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString)) + templates["servers.html"] = template.Must(template.New("servers").Funcs(funcs).Parse(tmplBaseString + tmplServersString)) lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")) if err != nil { diff --git a/templates/base.html b/templates/base.html index 3640699..26c4530 100644 --- a/templates/base.html +++ b/templates/base.html @@ -139,6 +139,14 @@ +