484 lines
14 KiB
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
|