package api import ( "bytes" "database/sql" "encoding/json" "errors" "net/http" "strconv" qrcode "github.com/skip2/go-qrcode" "gitea.perlbach24.de/scripte/wireguard-ui-multi/internal/firewall" "gitea.perlbach24.de/scripte/wireguard-ui-multi/internal/server" wg "gitea.perlbach24.de/scripte/wireguard-ui-multi/internal/wireguard" ) func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeErr(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } func idParam(r *http.Request, name string) (int64, error) { return strconv.ParseInt(r.PathValue(name), 10, 64) } // --- Auth --- type loginRequest struct { Username string `json:"username"` Password string `json:"password"` } func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { var req loginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid request body") return } var hash string err := a.db.QueryRow(`SELECT password_hash FROM users WHERE username = ?`, req.Username).Scan(&hash) if errors.Is(err, sql.ErrNoRows) || (err == nil && !CheckPassword(hash, req.Password)) { writeErr(w, http.StatusUnauthorized, "invalid credentials") return } if err != nil { writeErr(w, http.StatusInternalServerError, "login failed") return } sessionToken, csrfToken, err := a.sessions.Create(req.Username) if err != nil { writeErr(w, http.StatusInternalServerError, "could not create session") return } setSessionCookies(w, sessionToken, csrfToken) _ = a.db.LogAudit(req.Username, "login", "session", "") writeJSON(w, http.StatusOK, map[string]string{"csrf_token": csrfToken}) } func (a *API) handleLogout(w http.ResponseWriter, r *http.Request, sess *session) { if c, err := r.Cookie(sessionCookieName); err == nil { a.sessions.Delete(c.Value) } clearSessionCookies(w) _ = a.db.LogAudit(sess.username, "logout", "session", "") w.WriteHeader(http.StatusNoContent) } // --- Servers --- func (a *API) handleListServers(w http.ResponseWriter, r *http.Request, _ *session) { servers, err := a.store.ListServers() if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } type serverStatus struct { *server.Server Status wg.Status `json:"status"` } out := make([]serverStatus, 0, len(servers)) for _, s := range servers { out = append(out, serverStatus{Server: s, Status: wg.GetStatus(s.InterfaceName)}) } writeJSON(w, http.StatusOK, out) } type createServerRequest struct { Name string `json:"name"` InterfaceName string `json:"interface_name"` ListenPort int `json:"listen_port"` AddressRange string `json:"address_range"` DNS string `json:"dns"` MTU int `json:"mtu"` } func (a *API) handleCreateServer(w http.ResponseWriter, r *http.Request, sess *session) { var req createServerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.InterfaceName == "" || req.ListenPort == 0 || req.AddressRange == "" { writeErr(w, http.StatusBadRequest, "name, interface_name, listen_port, address_range required") return } if req.MTU == 0 { req.MTU = 1420 } priv, pub, err := wg.GenerateKeyPair() if err != nil { writeErr(w, http.StatusInternalServerError, "key generation failed") return } srv := &server.Server{ Name: req.Name, InterfaceName: req.InterfaceName, ListenPort: req.ListenPort, PrivateKey: priv, PublicKey: pub, AddressRange: req.AddressRange, DNS: req.DNS, MTU: req.MTU, Enabled: true, } id, err := a.store.CreateServer(srv) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } srv.ID = id if err := wg.WriteConfig(srv, nil); err != nil { writeErr(w, http.StatusInternalServerError, "config write failed: "+err.Error()) return } _ = a.db.LogAudit(sess.username, "server.create", req.Name, "") writeJSON(w, http.StatusCreated, srv) } func (a *API) handleGetServer(w http.ResponseWriter, r *http.Request, _ *session) { id, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } srv, err := a.store.GetServer(id) if errors.Is(err, server.ErrNotFound) { writeErr(w, http.StatusNotFound, "server not found") return } if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, srv) } func (a *API) handleUpdateServer(w http.ResponseWriter, r *http.Request, sess *session) { id, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } srv, err := a.store.GetServer(id) if errors.Is(err, server.ErrNotFound) { writeErr(w, http.StatusNotFound, "server not found") return } else if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } var req createServerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid request body") return } srv.Name, srv.AddressRange, srv.DNS = req.Name, req.AddressRange, req.DNS if req.MTU > 0 { srv.MTU = req.MTU } if req.ListenPort > 0 { srv.ListenPort = req.ListenPort } if err := a.store.UpdateServer(srv); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } peers, _ := a.store.ListPeersByServer(srv.ID) if err := wg.WriteConfig(srv, peers); err != nil { writeErr(w, http.StatusInternalServerError, "config write failed: "+err.Error()) return } _ = a.db.LogAudit(sess.username, "server.update", srv.Name, "") writeJSON(w, http.StatusOK, srv) } func (a *API) handleDeleteServer(w http.ResponseWriter, r *http.Request, sess *session) { id, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } srv, err := a.store.GetServer(id) if err != nil { writeErr(w, http.StatusNotFound, "server not found") return } _ = wg.Down(srv.InterfaceName) if err := a.store.DeleteServer(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } _ = a.db.LogAudit(sess.username, "server.delete", srv.Name, "") w.WriteHeader(http.StatusNoContent) } func (a *API) handleStartServer(w http.ResponseWriter, r *http.Request, sess *session) { a.serverAction(w, r, sess, "server.start", func(srv *server.Server) error { if err := wg.Up(srv.InterfaceName); err != nil { return err } return firewall.RunHook(firewall.HookServerStart, srv.InterfaceName) }) } func (a *API) handleStopServer(w http.ResponseWriter, r *http.Request, sess *session) { a.serverAction(w, r, sess, "server.stop", func(srv *server.Server) error { if err := wg.Down(srv.InterfaceName); err != nil { return err } return firewall.RunHook(firewall.HookServerStop, srv.InterfaceName) }) } func (a *API) handleReloadServer(w http.ResponseWriter, r *http.Request, sess *session) { a.serverAction(w, r, sess, "server.reload", func(srv *server.Server) error { peers, err := a.store.ListPeersByServer(srv.ID) if err != nil { return err } if err := wg.WriteConfig(srv, peers); err != nil { return err } return wg.Reload(srv.InterfaceName, wg.ConfigPath(srv)) }) } func (a *API) serverAction(w http.ResponseWriter, r *http.Request, sess *session, action string, fn func(*server.Server) error) { id, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } srv, err := a.store.GetServer(id) if errors.Is(err, server.ErrNotFound) { writeErr(w, http.StatusNotFound, "server not found") return } else if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } if err := fn(srv); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } _ = a.db.LogAudit(sess.username, action, srv.Name, "") writeJSON(w, http.StatusOK, map[string]string{"status": string(wg.GetStatus(srv.InterfaceName))}) } func (a *API) handleDownloadServerConfig(w http.ResponseWriter, r *http.Request, _ *session) { id, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } srv, err := a.store.GetServer(id) if err != nil { writeErr(w, http.StatusNotFound, "server not found") return } peers, err := a.store.ListPeersByServer(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Disposition", "attachment; filename="+srv.InterfaceName+".conf") _, _ = w.Write([]byte(wg.RenderConfig(srv, peers))) } // --- Peers --- func (a *API) handleListPeers(w http.ResponseWriter, r *http.Request, _ *session) { id, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } peers, err := a.store.ListPeersByServer(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } // never expose private keys in listing responses type safePeer struct { *server.Peer } out := make([]map[string]any, 0, len(peers)) for _, p := range peers { out = append(out, map[string]any{ "id": p.ID, "server_id": p.ServerID, "name": p.Name, "email": p.Email, "public_key": p.PublicKey, "allowed_ips": p.AllowedIPs, "endpoint": p.Endpoint, "persistent_keepalive": p.PersistentKeepalive, "enabled": p.Enabled, "expires_at": p.ExpiresAt, }) } writeJSON(w, http.StatusOK, out) } type createPeerRequest struct { Name string `json:"name"` Email string `json:"email"` AllowedIPs string `json:"allowed_ips"` PersistentKeepalive int `json:"persistent_keepalive"` UsePresharedKey bool `json:"use_preshared_key"` } func (a *API) handleCreatePeer(w http.ResponseWriter, r *http.Request, sess *session) { serverID, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } srv, err := a.store.GetServer(serverID) if errors.Is(err, server.ErrNotFound) { writeErr(w, http.StatusNotFound, "server not found") return } else if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } var req createPeerRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.AllowedIPs == "" { writeErr(w, http.StatusBadRequest, "name and allowed_ips required") return } if req.PersistentKeepalive == 0 { req.PersistentKeepalive = 25 } priv, pub, err := wg.GenerateKeyPair() if err != nil { writeErr(w, http.StatusInternalServerError, "key generation failed") return } var psk string if req.UsePresharedKey { psk, err = wg.GeneratePresharedKey() if err != nil { writeErr(w, http.StatusInternalServerError, "psk generation failed") return } } p := &server.Peer{ ServerID: serverID, Name: req.Name, Email: req.Email, PublicKey: pub, PrivateKey: priv, PresharedKey: psk, AllowedIPs: req.AllowedIPs, PersistentKeepalive: req.PersistentKeepalive, Enabled: true, } id, err := a.store.CreatePeer(p) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } p.ID = id peers, _ := a.store.ListPeersByServer(serverID) if err := wg.WriteConfig(srv, peers); err != nil { writeErr(w, http.StatusInternalServerError, "config write failed: "+err.Error()) return } _ = firewall.RunHook(firewall.HookPeerAdd, srv.InterfaceName, p.PublicKey) _ = a.db.LogAudit(sess.username, "peer.create", p.Name, "server="+srv.Name) writeJSON(w, http.StatusCreated, p) } func (a *API) handleDeletePeer(w http.ResponseWriter, r *http.Request, sess *session) { serverID, err := idParam(r, "id") if err != nil { writeErr(w, http.StatusBadRequest, "invalid id") return } peerID, err := idParam(r, "peerid") if err != nil { writeErr(w, http.StatusBadRequest, "invalid peer id") return } srv, err := a.store.GetServer(serverID) if err != nil { writeErr(w, http.StatusNotFound, "server not found") return } p, err := a.store.GetPeer(peerID) if err != nil { writeErr(w, http.StatusNotFound, "peer not found") return } if err := a.store.DeletePeer(peerID); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } peers, _ := a.store.ListPeersByServer(serverID) if err := wg.WriteConfig(srv, peers); err != nil { writeErr(w, http.StatusInternalServerError, "config write failed: "+err.Error()) return } _ = firewall.RunHook(firewall.HookPeerRemove, srv.InterfaceName, p.PublicKey) _ = a.db.LogAudit(sess.username, "peer.delete", p.Name, "server="+srv.Name) w.WriteHeader(http.StatusNoContent) } func (a *API) handleDownloadPeerConfig(w http.ResponseWriter, r *http.Request, _ *session) { srv, p, err := a.loadServerAndPeer(r) if err != nil { writeErr(w, http.StatusNotFound, err.Error()) return } host := r.URL.Query().Get("host") if host == "" { host = r.Host } w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Disposition", "attachment; filename="+p.Name+".conf") _, _ = w.Write([]byte(wg.RenderClientConfig(srv, p, host))) } func (a *API) handlePeerQRCode(w http.ResponseWriter, r *http.Request, _ *session) { srv, p, err := a.loadServerAndPeer(r) if err != nil { writeErr(w, http.StatusNotFound, err.Error()) return } host := r.URL.Query().Get("host") if host == "" { host = r.Host } png, err := qrcode.Encode(wg.RenderClientConfig(srv, p, host), qrcode.Medium, 256) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "image/png") _, _ = w.Write(bytes.NewBuffer(png).Bytes()) } func (a *API) loadServerAndPeer(r *http.Request) (*server.Server, *server.Peer, error) { serverID, err := idParam(r, "id") if err != nil { return nil, nil, errors.New("invalid id") } peerID, err := idParam(r, "peerid") if err != nil { return nil, nil, errors.New("invalid peer id") } srv, err := a.store.GetServer(serverID) if err != nil { return nil, nil, errors.New("server not found") } p, err := a.store.GetPeer(peerID) if err != nil { return nil, nil, errors.New("peer not found") } return srv, p, nil }