aerotun_handlers.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package api
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "encoding/json"
  6. "net/http"
  7. "proxmox-router/internal/aerotun"
  8. "proxmox-router/internal/auth"
  9. "time"
  10. )
  11. type aeroIdentityDraft struct {
  12. private, public, session string
  13. expires time.Time
  14. }
  15. func (s *Server) handleCreateAeroIdentityDraft(w http.ResponseWriter, r *http.Request) {
  16. if !requireAeroTUNAdmin(w, r) {
  17. return
  18. }
  19. private, public, err := aerotun.GenerateIdentity()
  20. if err != nil {
  21. http.Error(w, "failed to generate identity", http.StatusInternalServerError)
  22. return
  23. }
  24. b := make([]byte, 32)
  25. if _, err = rand.Read(b); err != nil {
  26. http.Error(w, "failed to generate identity token", http.StatusInternalServerError)
  27. return
  28. }
  29. token := hex.EncodeToString(b)
  30. session := getSessionFromContext(r.Context()).Token
  31. s.aeroDraftMu.Lock()
  32. if s.aeroDrafts == nil {
  33. s.aeroDrafts = make(map[string]aeroIdentityDraft)
  34. }
  35. now := time.Now()
  36. count := 0
  37. for id, d := range s.aeroDrafts {
  38. if now.After(d.expires) {
  39. delete(s.aeroDrafts, id)
  40. } else if d.session == session {
  41. count++
  42. }
  43. }
  44. if count >= 16 {
  45. s.aeroDraftMu.Unlock()
  46. http.Error(w, "too many pending identities; close an open form and try again", http.StatusTooManyRequests)
  47. return
  48. }
  49. s.aeroDrafts[token] = aeroIdentityDraft{private: private, public: public, session: session, expires: now.Add(2 * time.Hour)}
  50. s.aeroDraftMu.Unlock()
  51. w.Header().Set("Cache-Control", "no-store")
  52. w.Header().Set("Content-Type", "application/json")
  53. _ = json.NewEncoder(w).Encode(map[string]string{"draft_id": token, "public_key": public})
  54. }
  55. func (s *Server) handleDeleteAeroIdentityDraft(w http.ResponseWriter, r *http.Request) {
  56. if !requireAeroTUNAdmin(w, r) {
  57. return
  58. }
  59. s.aeroDraftMu.Lock()
  60. if d, ok := s.aeroDrafts[r.PathValue("id")]; ok && d.session == getSessionFromContext(r.Context()).Token {
  61. delete(s.aeroDrafts, r.PathValue("id"))
  62. }
  63. s.aeroDraftMu.Unlock()
  64. w.WriteHeader(http.StatusNoContent)
  65. }
  66. func (s *Server) handleListAeroTUNTunnels(w http.ResponseWriter, r *http.Request) {
  67. tunnels := s.vpnMgr.AeroTUN.List()
  68. for i := range tunnels {
  69. tunnels[i] = tunnels[i].PublicView()
  70. }
  71. w.Header().Set("Content-Type", "application/json")
  72. _ = json.NewEncoder(w).Encode(tunnels)
  73. }
  74. func (s *Server) handleGetAeroTUNStats(w http.ResponseWriter, r *http.Request) {
  75. stats := s.vpnMgr.AeroTUN.GetSummaryStats()
  76. w.Header().Set("Content-Type", "application/json")
  77. _ = json.NewEncoder(w).Encode(stats)
  78. }
  79. func requireAeroTUNAdmin(w http.ResponseWriter, r *http.Request) bool {
  80. sess := getSessionFromContext(r.Context())
  81. if sess == nil || sess.Role != auth.RoleFull {
  82. http.Error(w, "AeroTUN administration requires full privileges", http.StatusForbidden)
  83. return false
  84. }
  85. return true
  86. }
  87. func (s *Server) handleGenerateAeroTUNKey(w http.ResponseWriter, r *http.Request) {
  88. if !requireAeroTUNAdmin(w, r) {
  89. return
  90. }
  91. w.Header().Set("Cache-Control", "no-store")
  92. key, err := aerotun.GenerateSecureKey()
  93. if err != nil {
  94. http.Error(w, err.Error(), http.StatusInternalServerError)
  95. return
  96. }
  97. w.Header().Set("Content-Type", "application/json")
  98. _ = json.NewEncoder(w).Encode(map[string]string{"key": key})
  99. }
  100. func (s *Server) handleSaveAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
  101. if !requireAeroTUNAdmin(w, r) {
  102. return
  103. }
  104. var req struct {
  105. aerotun.TunnelInstance
  106. IdentityDraftID string `json:"identity_draft_id"`
  107. }
  108. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  109. http.Error(w, "invalid request body", http.StatusBadRequest)
  110. return
  111. }
  112. inst := req.TunnelInstance
  113. if req.IdentityDraftID != "" {
  114. if inst.ID != "" || inst.Protocol != 3 || inst.PrivateKey != "" {
  115. http.Error(w, "identity draft is only valid for a new v3 tunnel", http.StatusBadRequest)
  116. return
  117. }
  118. s.aeroDraftMu.Lock()
  119. defer s.aeroDraftMu.Unlock()
  120. d, ok := s.aeroDrafts[req.IdentityDraftID]
  121. if !ok || d.session != getSessionFromContext(r.Context()).Token || time.Now().After(d.expires) {
  122. http.Error(w, "identity draft expired or is unavailable; reopen the form to create a new key", http.StatusUnprocessableEntity)
  123. return
  124. }
  125. inst.PrivateKey, inst.PublicKey = d.private, d.public
  126. }
  127. saved, err := s.vpnMgr.AeroTUN.Save(inst)
  128. if req.IdentityDraftID != "" {
  129. if err == nil {
  130. delete(s.aeroDrafts, req.IdentityDraftID)
  131. } else if existing, ok := s.vpnMgr.AeroTUN.Get(saved.ID); ok && existing.PublicKey == inst.PublicKey {
  132. delete(s.aeroDrafts, req.IdentityDraftID)
  133. }
  134. }
  135. if err != nil {
  136. http.Error(w, err.Error(), http.StatusInternalServerError)
  137. return
  138. }
  139. w.Header().Set("Content-Type", "application/json")
  140. _ = json.NewEncoder(w).Encode(saved.PublicView())
  141. }
  142. func (s *Server) handleDeleteAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
  143. if !requireAeroTUNAdmin(w, r) {
  144. return
  145. }
  146. id := r.PathValue("id")
  147. if id == "" {
  148. http.Error(w, "missing tunnel ID", http.StatusBadRequest)
  149. return
  150. }
  151. if err := s.vpnMgr.AeroTUN.Delete(id); err != nil {
  152. http.Error(w, err.Error(), http.StatusInternalServerError)
  153. return
  154. }
  155. w.Header().Set("Content-Type", "application/json")
  156. _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
  157. }
  158. func (s *Server) handleSetAeroTUNTunnelState(w http.ResponseWriter, r *http.Request) {
  159. if !requireAeroTUNAdmin(w, r) {
  160. return
  161. }
  162. id := r.PathValue("id")
  163. if id == "" {
  164. http.Error(w, "missing tunnel ID", http.StatusBadRequest)
  165. return
  166. }
  167. var req struct {
  168. Disabled bool `json:"disabled"`
  169. }
  170. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  171. http.Error(w, "invalid JSON", http.StatusBadRequest)
  172. return
  173. }
  174. if err := s.vpnMgr.AeroTUN.ToggleState(id, req.Disabled); err != nil {
  175. http.Error(w, err.Error(), http.StatusInternalServerError)
  176. return
  177. }
  178. w.Header().Set("Content-Type", "application/json")
  179. _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
  180. }
  181. func (s *Server) handleStartAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
  182. if !requireAeroTUNAdmin(w, r) {
  183. return
  184. }
  185. id := r.PathValue("id")
  186. if id == "" {
  187. http.Error(w, "missing tunnel ID", http.StatusBadRequest)
  188. return
  189. }
  190. if err := s.vpnMgr.AeroTUN.StartTunnel(id); err != nil {
  191. http.Error(w, err.Error(), http.StatusInternalServerError)
  192. return
  193. }
  194. w.Header().Set("Content-Type", "application/json")
  195. _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
  196. }
  197. func (s *Server) handleStopAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
  198. if !requireAeroTUNAdmin(w, r) {
  199. return
  200. }
  201. id := r.PathValue("id")
  202. if id == "" {
  203. http.Error(w, "missing tunnel ID", http.StatusBadRequest)
  204. return
  205. }
  206. if err := s.vpnMgr.AeroTUN.StopTunnel(id); err != nil {
  207. http.Error(w, err.Error(), http.StatusInternalServerError)
  208. return
  209. }
  210. w.Header().Set("Content-Type", "application/json")
  211. _ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
  212. }