| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- package api
- import (
- "crypto/rand"
- "encoding/hex"
- "encoding/json"
- "net/http"
- "proxmox-router/internal/aerotun"
- "proxmox-router/internal/auth"
- "time"
- )
- type aeroIdentityDraft struct {
- private, public, session string
- expires time.Time
- }
- func (s *Server) handleCreateAeroIdentityDraft(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- private, public, err := aerotun.GenerateIdentity()
- if err != nil {
- http.Error(w, "failed to generate identity", http.StatusInternalServerError)
- return
- }
- b := make([]byte, 32)
- if _, err = rand.Read(b); err != nil {
- http.Error(w, "failed to generate identity token", http.StatusInternalServerError)
- return
- }
- token := hex.EncodeToString(b)
- session := getSessionFromContext(r.Context()).Token
- s.aeroDraftMu.Lock()
- if s.aeroDrafts == nil {
- s.aeroDrafts = make(map[string]aeroIdentityDraft)
- }
- now := time.Now()
- count := 0
- for id, d := range s.aeroDrafts {
- if now.After(d.expires) {
- delete(s.aeroDrafts, id)
- } else if d.session == session {
- count++
- }
- }
- if count >= 16 {
- s.aeroDraftMu.Unlock()
- http.Error(w, "too many pending identities; close an open form and try again", http.StatusTooManyRequests)
- return
- }
- s.aeroDrafts[token] = aeroIdentityDraft{private: private, public: public, session: session, expires: now.Add(2 * time.Hour)}
- s.aeroDraftMu.Unlock()
- w.Header().Set("Cache-Control", "no-store")
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]string{"draft_id": token, "public_key": public})
- }
- func (s *Server) handleDeleteAeroIdentityDraft(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- s.aeroDraftMu.Lock()
- if d, ok := s.aeroDrafts[r.PathValue("id")]; ok && d.session == getSessionFromContext(r.Context()).Token {
- delete(s.aeroDrafts, r.PathValue("id"))
- }
- s.aeroDraftMu.Unlock()
- w.WriteHeader(http.StatusNoContent)
- }
- func (s *Server) handleListAeroTUNTunnels(w http.ResponseWriter, r *http.Request) {
- tunnels := s.vpnMgr.AeroTUN.List()
- for i := range tunnels {
- tunnels[i] = tunnels[i].PublicView()
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(tunnels)
- }
- func (s *Server) handleGetAeroTUNStats(w http.ResponseWriter, r *http.Request) {
- stats := s.vpnMgr.AeroTUN.GetSummaryStats()
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(stats)
- }
- func requireAeroTUNAdmin(w http.ResponseWriter, r *http.Request) bool {
- sess := getSessionFromContext(r.Context())
- if sess == nil || sess.Role != auth.RoleFull {
- http.Error(w, "AeroTUN administration requires full privileges", http.StatusForbidden)
- return false
- }
- return true
- }
- func (s *Server) handleGenerateAeroTUNKey(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- w.Header().Set("Cache-Control", "no-store")
- key, err := aerotun.GenerateSecureKey()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]string{"key": key})
- }
- func (s *Server) handleSaveAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- var req struct {
- aerotun.TunnelInstance
- IdentityDraftID string `json:"identity_draft_id"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid request body", http.StatusBadRequest)
- return
- }
- inst := req.TunnelInstance
- if req.IdentityDraftID != "" {
- if inst.ID != "" || inst.Protocol != 3 || inst.PrivateKey != "" {
- http.Error(w, "identity draft is only valid for a new v3 tunnel", http.StatusBadRequest)
- return
- }
- s.aeroDraftMu.Lock()
- defer s.aeroDraftMu.Unlock()
- d, ok := s.aeroDrafts[req.IdentityDraftID]
- if !ok || d.session != getSessionFromContext(r.Context()).Token || time.Now().After(d.expires) {
- http.Error(w, "identity draft expired or is unavailable; reopen the form to create a new key", http.StatusUnprocessableEntity)
- return
- }
- inst.PrivateKey, inst.PublicKey = d.private, d.public
- }
- saved, err := s.vpnMgr.AeroTUN.Save(inst)
- if req.IdentityDraftID != "" {
- if err == nil {
- delete(s.aeroDrafts, req.IdentityDraftID)
- } else if existing, ok := s.vpnMgr.AeroTUN.Get(saved.ID); ok && existing.PublicKey == inst.PublicKey {
- delete(s.aeroDrafts, req.IdentityDraftID)
- }
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(saved.PublicView())
- }
- func (s *Server) handleDeleteAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- id := r.PathValue("id")
- if id == "" {
- http.Error(w, "missing tunnel ID", http.StatusBadRequest)
- return
- }
- if err := s.vpnMgr.AeroTUN.Delete(id); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
- }
- func (s *Server) handleSetAeroTUNTunnelState(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- id := r.PathValue("id")
- if id == "" {
- http.Error(w, "missing tunnel ID", http.StatusBadRequest)
- return
- }
- var req struct {
- Disabled bool `json:"disabled"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON", http.StatusBadRequest)
- return
- }
- if err := s.vpnMgr.AeroTUN.ToggleState(id, req.Disabled); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
- }
- func (s *Server) handleStartAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- id := r.PathValue("id")
- if id == "" {
- http.Error(w, "missing tunnel ID", http.StatusBadRequest)
- return
- }
- if err := s.vpnMgr.AeroTUN.StartTunnel(id); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
- }
- func (s *Server) handleStopAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
- if !requireAeroTUNAdmin(w, r) {
- return
- }
- id := r.PathValue("id")
- if id == "" {
- http.Error(w, "missing tunnel ID", http.StatusBadRequest)
- return
- }
- if err := s.vpnMgr.AeroTUN.StopTunnel(id); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
- }
|