benzcloud-server/internal/core/api/api.go

484 lines
14 KiB
Go

package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/benzjeremy/benzcloud-server/internal/core/auth"
"github.com/benzjeremy/benzcloud-server/internal/core/config"
"github.com/benzjeremy/benzcloud-server/internal/core/dns"
"github.com/benzjeremy/benzcloud-server/internal/core/drive"
"github.com/benzjeremy/benzcloud-server/internal/core/nebula"
"github.com/benzjeremy/benzcloud-server/internal/core/plugins"
)
// Server encapsulates the REST API and state references.
type Server struct {
cfg *config.Config
authMgr *auth.Manager
dnsSrv *dns.Server
nebulaMgr *nebula.Manager
driveMgr *drive.DriveManager
pluginMgr *plugins.Manager
mux *http.ServeMux
}
// NewServer creates a new API controller.
func NewServer(
cfg *config.Config,
authMgr *auth.Manager,
dnsSrv *dns.Server,
nebulaMgr *nebula.Manager,
driveMgr *drive.DriveManager,
pluginMgr *plugins.Manager,
) *Server {
s := &Server{
cfg: cfg,
authMgr: authMgr,
dnsSrv: dnsSrv,
nebulaMgr: nebulaMgr,
driveMgr: driveMgr,
pluginMgr: pluginMgr,
mux: http.NewServeMux(),
}
s.registerRoutes()
return s
}
func (s *Server) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
s.mux.ServeHTTP(w, r)
})
}
func (s *Server) registerRoutes() {
s.mux.HandleFunc("/api/status", s.handleStatus)
s.mux.HandleFunc("/api/setup", s.handleSetup)
s.mux.HandleFunc("/api/login", s.handleLogin)
s.mux.HandleFunc("/api/pair", s.handlePair)
s.mux.HandleFunc("/api/users", s.handleUsers)
s.mux.HandleFunc("/api/plugins", s.handlePlugins)
s.mux.HandleFunc("/api/plugins/toggle", s.handlePluginToggle)
s.mux.HandleFunc("/api/drive/files", s.handleDriveList)
s.mux.HandleFunc("/api/drive/upload", s.handleDriveUpload)
s.mux.HandleFunc("/api/drive/download", s.handleDriveDownload)
s.mux.HandleFunc("/api/drive/folder", s.handleDriveFolder)
s.mux.HandleFunc("/api/drive/delete", s.handleDriveDelete)
s.mux.HandleFunc("/api/dns/logs", s.handleDNSLogs)
}
func (s *Server) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}
func (s *Server) jsonError(w http.ResponseWriter, status int, msg string) {
s.jsonResponse(w, status, map[string]string{"error": msg})
}
func (s *Server) authenticate(r *http.Request) (*auth.User, error) {
authHeader := r.Header.Get("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
token = r.URL.Query().Get("token")
}
return s.authMgr.ValidateSession(token)
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
var peers []*nebula.PeerInfo
if s.nebulaMgr != nil {
peers = s.nebulaMgr.GetPeers()
}
var pluginList []*plugins.PluginStatus
if s.pluginMgr != nil {
pluginList = s.pluginMgr.ListPlugins()
}
res := map[string]interface{}{
"system": "benzcloud-server",
"version": "v1.0",
"setup_completed": s.cfg.SetupCompleted,
"base_domain": s.cfg.BaseDomain,
"server_local_ip": s.cfg.ServerLocalIP,
"server_vpn_ip": s.cfg.ServerVPNIP,
"http_port": s.cfg.HTTPPort,
"dns_port": s.cfg.DNSPort,
"vpn_port": s.cfg.VPNPort,
"nebula_running": s.nebulaMgr != nil && s.nebulaMgr.IsRunning(),
"mesh_peers": len(peers),
"users_count": s.authMgr.UserCount(),
"plugins": pluginList,
"timestamp": time.Now().UTC(),
}
s.jsonResponse(w, http.StatusOK, res)
}
func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.jsonError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if s.cfg.SetupCompleted {
s.jsonError(w, http.StatusBadRequest, "Setup is already completed")
return
}
var req struct {
BaseDomain string `json:"base_domain"`
AdminUsername string `json:"admin_username"`
AdminPassword string `json:"admin_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if strings.TrimSpace(req.BaseDomain) == "" || strings.TrimSpace(req.AdminUsername) == "" || len(req.AdminPassword) < 8 {
s.jsonError(w, http.StatusBadRequest, "Base domain, admin username and password (>= 8 chars) required")
return
}
cleanDomain := strings.ToLower(strings.Trim(req.BaseDomain, "."))
s.cfg.BaseDomain = cleanDomain
// 1. Create Admin user
adminPerms := auth.Permissions{
Admin: true,
Drive: true,
Mail: true,
Chat: true,
Web: true,
VPN: true,
DNS: true,
}
adminUser, err := s.authMgr.CreateUser(req.AdminUsername, req.AdminPassword, adminPerms)
if err != nil {
s.jsonError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create admin: %v", err))
return
}
// 2. Initialize Nebula PKI
if err := s.nebulaMgr.InitPKI(s.cfg.ServerVPNIP, s.cfg.ServerLocalIP, s.cfg.VPNPort); err != nil {
s.jsonError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to initialize Nebula PKI: %v", err))
return
}
_ = s.nebulaMgr.StartController()
// 3. Mark setup complete and save config
s.cfg.SetupCompleted = true
if err := s.cfg.Save(); err != nil {
s.jsonError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save config: %v", err))
return
}
// 4. Log admin in
session, err := s.authMgr.Authenticate(req.AdminUsername, req.AdminPassword)
if err != nil {
s.jsonError(w, http.StatusInternalServerError, "Failed to authenticate new admin")
return
}
s.jsonResponse(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Setup completed successfully",
"base_domain": cleanDomain,
"admin_user": adminUser.Username,
"session_token": session.Token,
})
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.jsonError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid payload")
return
}
session, err := s.authMgr.Authenticate(req.Username, req.Password)
if err != nil {
s.jsonError(w, http.StatusUnauthorized, "Invalid credentials")
return
}
user, _ := s.authMgr.ValidateSession(session.Token)
s.jsonResponse(w, http.StatusOK, map[string]interface{}{
"token": session.Token,
"user": user.Username,
"permissions": user.Permissions,
"overlay_ip": user.OverlayIP,
"expires_at": session.ExpiresAt,
})
}
func (s *Server) handlePair(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.jsonError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid payload")
return
}
session, err := s.authMgr.Authenticate(req.Username, req.Password)
if err != nil {
s.jsonError(w, http.StatusUnauthorized, "Pairing failed: Invalid credentials")
return
}
user, err := s.authMgr.ValidateSession(session.Token)
if err != nil {
s.jsonError(w, http.StatusInternalServerError, "User lookup failed")
return
}
certPEM, keyPEM, configYAML, err := s.nebulaMgr.IssueClientCert(
user.Username,
user.OverlayIP,
s.cfg.ServerLocalIP,
s.cfg.VPNPort,
)
if err != nil {
s.jsonError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to issue client mesh certificate: %v", err))
return
}
// Pre-register peer in DNS if needed
s.dnsSrv.RegisterSubdomain("peer-"+user.Username, user.OverlayIP)
s.jsonResponse(w, http.StatusOK, map[string]interface{}{
"status": "paired",
"username": user.Username,
"base_domain": s.cfg.BaseDomain,
"overlay_ip": user.OverlayIP,
"server_vpn_ip": s.cfg.ServerVPNIP,
"cert_pem": string(certPEM),
"key_pem": string(keyPEM),
"client_config_yaml": string(configYAML),
"session_token": session.Token,
"permissions": user.Permissions,
})
}
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Admin {
s.jsonError(w, http.StatusForbidden, "Admin privileges required")
return
}
switch r.Method {
case http.MethodGet:
users := s.authMgr.ListUsers()
s.jsonResponse(w, http.StatusOK, map[string]interface{}{"users": users})
case http.MethodPost:
var req struct {
Username string `json:"username"`
Password string `json:"password"`
Permissions auth.Permissions `json:"permissions"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid payload")
return
}
user, err := s.authMgr.CreateUser(req.Username, req.Password, req.Permissions)
if err != nil {
s.jsonError(w, http.StatusBadRequest, err.Error())
return
}
s.jsonResponse(w, http.StatusCreated, user)
case http.MethodPut:
var req struct {
Username string `json:"username"`
Permissions auth.Permissions `json:"permissions"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid payload")
return
}
if err := s.authMgr.UpdatePermissions(req.Username, req.Permissions); err != nil {
s.jsonError(w, http.StatusBadRequest, err.Error())
return
}
s.jsonResponse(w, http.StatusOK, map[string]bool{"success": true})
default:
s.jsonError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (s *Server) handlePlugins(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil {
s.jsonError(w, http.StatusUnauthorized, "Unauthorized")
return
}
_ = currentUser
s.jsonResponse(w, http.StatusOK, map[string]interface{}{
"plugins": s.pluginMgr.ListPlugins(),
})
}
func (s *Server) handlePluginToggle(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Admin {
s.jsonError(w, http.StatusForbidden, "Admin privileges required")
return
}
var req struct {
ID string `json:"id"`
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid payload")
return
}
if err := s.pluginMgr.SetPluginEnabled(req.ID, req.Enabled); err != nil {
s.jsonError(w, http.StatusInternalServerError, err.Error())
return
}
s.jsonResponse(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handleDriveList(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Drive {
s.jsonError(w, http.StatusForbidden, "Drive access denied")
return
}
folder := r.URL.Query().Get("path")
files, err := s.driveMgr.ListDirectory(currentUser.Username, folder)
if err != nil {
s.jsonError(w, http.StatusInternalServerError, err.Error())
return
}
s.jsonResponse(w, http.StatusOK, map[string]interface{}{"files": files})
}
func (s *Server) handleDriveUpload(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Drive {
s.jsonError(w, http.StatusForbidden, "Drive access denied")
return
}
if r.Method != http.MethodPost {
s.jsonError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
path := r.URL.Query().Get("path")
if path == "" {
s.jsonError(w, http.StatusBadRequest, "Missing path parameter")
return
}
item, err := s.driveMgr.SaveFile(currentUser.Username, path, r.Body)
if err != nil {
s.jsonError(w, http.StatusInternalServerError, err.Error())
return
}
s.jsonResponse(w, http.StatusOK, item)
}
func (s *Server) handleDriveDownload(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Drive {
s.jsonError(w, http.StatusForbidden, "Drive access denied")
return
}
path := r.URL.Query().Get("path")
data, item, err := s.driveMgr.ReadFile(currentUser.Username, path)
if err != nil {
s.jsonError(w, http.StatusNotFound, "File not found")
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", item.Name))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}
func (s *Server) handleDriveFolder(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Drive {
s.jsonError(w, http.StatusForbidden, "Drive access denied")
return
}
var req struct {
Path string `json:"path"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.jsonError(w, http.StatusBadRequest, "Invalid payload")
return
}
if err := s.driveMgr.CreateFolder(currentUser.Username, req.Path); err != nil {
s.jsonError(w, http.StatusInternalServerError, err.Error())
return
}
s.jsonResponse(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handleDriveDelete(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Drive {
s.jsonError(w, http.StatusForbidden, "Drive access denied")
return
}
path := r.URL.Query().Get("path")
if err := s.driveMgr.DeleteFile(currentUser.Username, path); err != nil {
s.jsonError(w, http.StatusInternalServerError, err.Error())
return
}
s.jsonResponse(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handleDNSLogs(w http.ResponseWriter, r *http.Request) {
currentUser, err := s.authenticate(r)
if err != nil || !currentUser.Permissions.Admin {
s.jsonError(w, http.StatusForbidden, "Admin privileges required")
return
}
logs := s.dnsSrv.GetQueryLog()
s.jsonResponse(w, http.StatusOK, map[string]interface{}{"logs": logs})
}
// Ensure unused import warnings are resolved
var _ = io.EOF