internal/cli/http.go (view raw)
1package cli
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log/slog"
9 "net"
10 "net/http"
11 "os"
12 "path/filepath"
13 "time"
14
15 "postern/internal/model"
16)
17
18// HTTPService implements the cli.Service interface for serving HTTP endpoints.
19type HTTPService struct {
20 cmd *Command
21 socketPath string
22 srv *http.Server
23}
24
25// NewHTTPService creates a new HTTP service that serves REST endpoints.
26func NewHTTPService(socketPath string, cmd *Command) *HTTPService {
27 return &HTTPService{
28 cmd: cmd,
29 socketPath: socketPath,
30 }
31}
32
33// Start starts the HTTP server.
34func (h *HTTPService) Start() error {
35 mux := h.HTTPMux()
36
37 // Ensure parent dir exists
38 if err := os.MkdirAll(filepath.Dir(h.socketPath), 0700); err != nil {
39 return fmt.Errorf("creating socket dir: %w", err)
40 }
41 // Remove stale socket from ungraceful shutdown
42 _ = os.Remove(h.socketPath)
43
44 listener, err := net.Listen("unix", h.socketPath)
45 if err != nil {
46 return fmt.Errorf("listening on unix socket: %w", err)
47 }
48 if err := os.Chmod(h.socketPath, 0660); err != nil {
49 return fmt.Errorf("setting socket permissions: %w", err)
50 }
51
52 h.srv = &http.Server{
53 Handler: mux,
54 ReadHeaderTimeout: 5 * time.Second,
55 ReadTimeout: 10 * time.Second,
56 WriteTimeout: 30 * time.Second,
57 IdleTimeout: 120 * time.Second,
58 }
59
60 slog.Info("starting HTTP server on unix socket", "path", h.socketPath)
61 err = h.srv.Serve(listener)
62 if err != nil && !errors.Is(err, http.ErrServerClosed) {
63 return err
64 }
65 return nil
66}
67
68// Stop stops the HTTP server gracefully.
69func (h *HTTPService) Stop() error {
70 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
71 defer cancel()
72
73 slog.Info("shutting down HTTP server")
74 return h.srv.Shutdown(ctx)
75}
76
77// Status returns the status of the HTTP service.
78func (h *HTTPService) Status() string {
79 if h.srv != nil {
80 return "running"
81 }
82 return "stopped"
83}
84
85// HTTPMux creates and returns the HTTP ServeMux with all routes.
86func (h *HTTPService) HTTPMux() *http.ServeMux {
87 mux := http.NewServeMux()
88
89 // User endpoints
90 mux.HandleFunc("GET /users", h.listUsers)
91 mux.HandleFunc("POST /users", h.createUser)
92 mux.HandleFunc("DELETE /users/{username}", h.deleteUser)
93 mux.HandleFunc("POST /users/{username}/addresses", h.addUserAddress)
94 mux.HandleFunc("POST /users/{username}/rotate-password", h.rotateUserPassword)
95
96 // SMTP auth endpoints
97 mux.HandleFunc("GET /smtp-auth", h.listSMTPAuth)
98 mux.HandleFunc("POST /smtp-auth", h.createSMTPAuth)
99 mux.HandleFunc("DELETE /smtp-auth/{name}", h.deleteSMTPAuth)
100
101 // DKIM endpoints
102 mux.HandleFunc("GET /dkim", h.listDKIM)
103 mux.HandleFunc("POST /dkim", h.addDKIM)
104 mux.HandleFunc("GET /dkim/{selector}/{domain}", h.getDKIM)
105 mux.HandleFunc("POST /dkim/{selector}/{domain}/enable", h.enableDKIM)
106 mux.HandleFunc("POST /dkim/{selector}/{domain}/disable", h.disableDKIM)
107 mux.HandleFunc("DELETE /dkim/{selector}/{domain}", h.deleteDKIM)
108
109 // Prune endpoint
110 mux.HandleFunc("POST /prune", h.pruneMails)
111
112 // KV endpoints
113 mux.HandleFunc("GET /kv", h.kvList)
114 mux.HandleFunc("GET /kv/{key...}", h.kvGet)
115 mux.HandleFunc("DELETE /kv/{key...}", h.kvPop)
116
117 return mux
118}
119
120// Helper to write JSON responses
121func writeJSON(w http.ResponseWriter, status int, v interface{}) {
122 w.Header().Set("Content-Type", "application/json")
123 w.WriteHeader(status)
124 if err := json.NewEncoder(w).Encode(v); err != nil {
125 slog.Error("failed to encode JSON response", "error", err)
126 }
127}
128
129// Helper to write error responses
130func writeError(w http.ResponseWriter, err error) {
131 switch {
132 case errors.Is(err, model.ErrAlreadyExists):
133 writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
134 case errors.Is(err, model.ErrNotFound):
135 writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
136 case errors.Is(err, model.ErrInvalidInput):
137 writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
138 default:
139 writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
140 }
141}
142
143func (h *HTTPService) listUsers(w http.ResponseWriter, r *http.Request) {
144 users, err := h.cmd.ListUsers(r.Context())
145 if err != nil {
146 writeError(w, err)
147 return
148 }
149 writeJSON(w, http.StatusOK, users)
150}
151
152func (h *HTTPService) createUser(w http.ResponseWriter, r *http.Request) {
153 var req struct {
154 Username string `json:"username"`
155 }
156 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
157 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
158 return
159 }
160 if req.Username == "" {
161 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
162 return
163 }
164
165 user, err := h.cmd.CreateUser(r.Context(), req.Username)
166 if err != nil {
167 writeError(w, err)
168 return
169 }
170
171 writeJSON(w, http.StatusCreated, user)
172}
173
174func (h *HTTPService) deleteUser(w http.ResponseWriter, r *http.Request) {
175 username := r.PathValue("username")
176 if username == "" {
177 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
178 return
179 }
180
181 err := h.cmd.RemoveUser(r.Context(), username)
182 if err != nil {
183 writeError(w, err)
184 return
185 }
186
187 w.WriteHeader(http.StatusNoContent)
188}
189
190func (h *HTTPService) addUserAddress(w http.ResponseWriter, r *http.Request) {
191 username := r.PathValue("username")
192 if username == "" {
193 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
194 return
195 }
196
197 var req struct {
198 Address string `json:"address"`
199 }
200 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
201 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
202 return
203 }
204 if req.Address == "" {
205 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "address is required"})
206 return
207 }
208
209 err := h.cmd.AddUserAddress(r.Context(), username, req.Address)
210 if err != nil {
211 writeError(w, err)
212 return
213 }
214
215 w.WriteHeader(http.StatusCreated)
216}
217
218func (h *HTTPService) rotateUserPassword(w http.ResponseWriter, r *http.Request) {
219 username := r.PathValue("username")
220 if username == "" {
221 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
222 return
223 }
224
225 user, err := h.cmd.RotateUserPassword(r.Context(), username)
226 if err != nil {
227 writeError(w, err)
228 return
229 }
230
231 writeJSON(w, http.StatusOK, user)
232}
233
234func (h *HTTPService) listSMTPAuth(w http.ResponseWriter, r *http.Request) {
235 auths, err := h.cmd.ListSMTPAuth(r.Context())
236 if err != nil {
237 writeError(w, err)
238 return
239 }
240 writeJSON(w, http.StatusOK, auths)
241}
242
243func (h *HTTPService) createSMTPAuth(w http.ResponseWriter, r *http.Request) {
244 var req struct {
245 Name string `json:"name"`
246 }
247 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
248 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
249 return
250 }
251 if req.Name == "" {
252 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
253 return
254 }
255
256 auth, err := h.cmd.CreateSMTPAuth(r.Context(), req.Name)
257 if err != nil {
258 writeError(w, err)
259 return
260 }
261
262 writeJSON(w, http.StatusCreated, auth)
263}
264
265func (h *HTTPService) deleteSMTPAuth(w http.ResponseWriter, r *http.Request) {
266 name := r.PathValue("name")
267 if name == "" {
268 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
269 return
270 }
271
272 err := h.cmd.RemoveSMTPAuth(r.Context(), name)
273 if err != nil {
274 writeError(w, err)
275 return
276 }
277
278 w.WriteHeader(http.StatusNoContent)
279}
280
281func (h *HTTPService) listDKIM(w http.ResponseWriter, r *http.Request) {
282 dkims, err := h.cmd.ListDKIM(r.Context())
283 if err != nil {
284 writeError(w, err)
285 return
286 }
287 writeJSON(w, http.StatusOK, dkims)
288}
289
290func (h *HTTPService) addDKIM(w http.ResponseWriter, r *http.Request) {
291 var req struct {
292 Selector string `json:"selector"`
293 Domain string `json:"domain"`
294 }
295 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
296 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
297 return
298 }
299 if req.Selector == "" {
300 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
301 return
302 }
303 if req.Domain == "" {
304 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
305 return
306 }
307
308 record, err := h.cmd.AddDKIM(r.Context(), req.Selector, req.Domain)
309 if err != nil {
310 writeError(w, err)
311 return
312 }
313
314 writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
315}
316
317func (h *HTTPService) getDKIM(w http.ResponseWriter, r *http.Request) {
318 selector := r.PathValue("selector")
319 domain := r.PathValue("domain")
320 if selector == "" {
321 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
322 return
323 }
324 if domain == "" {
325 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
326 return
327 }
328
329 record, err := h.cmd.GetDKIM(r.Context(), selector, domain)
330 if err != nil {
331 writeError(w, err)
332 return
333 }
334
335 writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
336}
337
338func (h *HTTPService) enableDKIM(w http.ResponseWriter, r *http.Request) {
339 selector := r.PathValue("selector")
340 domain := r.PathValue("domain")
341 if selector == "" {
342 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
343 return
344 }
345 if domain == "" {
346 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
347 return
348 }
349
350 // Parse force query parameter
351 forceStr := r.URL.Query().Get("force")
352 force := forceStr != "false" // Default to true if not explicitly set to "false"
353
354 err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, true, force)
355 if err != nil {
356 writeError(w, err)
357 return
358 }
359
360 w.WriteHeader(http.StatusNoContent)
361}
362
363func (h *HTTPService) disableDKIM(w http.ResponseWriter, r *http.Request) {
364 selector := r.PathValue("selector")
365 domain := r.PathValue("domain")
366 if selector == "" {
367 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
368 return
369 }
370 if domain == "" {
371 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
372 return
373 }
374
375 err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, false, true)
376 if err != nil {
377 writeError(w, err)
378 return
379 }
380
381 w.WriteHeader(http.StatusNoContent)
382}
383
384func (h *HTTPService) deleteDKIM(w http.ResponseWriter, r *http.Request) {
385 selector := r.PathValue("selector")
386 domain := r.PathValue("domain")
387 if selector == "" {
388 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
389 return
390 }
391 if domain == "" {
392 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
393 return
394 }
395
396 err := h.cmd.RemoveDKIM(r.Context(), selector, domain)
397 if err != nil {
398 writeError(w, err)
399 return
400 }
401
402 w.WriteHeader(http.StatusNoContent)
403}
404
405func (h *HTTPService) pruneMails(w http.ResponseWriter, r *http.Request) {
406 count, err := h.cmd.PruneMails(r.Context())
407 if err != nil {
408 writeError(w, err)
409 return
410 }
411
412 writeJSON(w, http.StatusOK, map[string]int{"pruned": count})
413}
414
415func (h *HTTPService) kvList(w http.ResponseWriter, r *http.Request) {
416 pairs, err := h.cmd.KVList(r.Context())
417 if err != nil {
418 writeError(w, err)
419 return
420 }
421 writeJSON(w, http.StatusOK, pairs)
422}
423
424func (h *HTTPService) kvGet(w http.ResponseWriter, r *http.Request) {
425 key := r.PathValue("key")
426 if key == "" {
427 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key is required"})
428 return
429 }
430
431 val, err := h.cmd.KVGet(r.Context(), key)
432 if err != nil {
433 writeError(w, err)
434 return
435 }
436
437 writeJSON(w, http.StatusOK, map[string]any{"value": val})
438}
439
440func (h *HTTPService) kvPop(w http.ResponseWriter, r *http.Request) {
441 key := r.PathValue("key")
442 if key == "" {
443 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key is required"})
444 return
445 }
446
447 val, err := h.cmd.KVPop(r.Context(), key)
448 if err != nil {
449 writeError(w, err)
450 return
451 }
452
453 writeJSON(w, http.StatusOK, map[string]any{"value": val})
454}