aerotun_handlers.go 7.4 KB

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