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("GET /users/{username}/addresses", h.listUserAddresses)
95 mux.HandleFunc("DELETE /users/{username}/addresses/{address}", h.deleteUserAddress)
96 mux.HandleFunc("POST /users/{username}/rotate-password", h.rotateUserPassword)
97
98 // SMTP auth endpoints
99 mux.HandleFunc("GET /smtp-auth", h.listSMTPAuth)
100 mux.HandleFunc("POST /smtp-auth", h.createSMTPAuth)
101 mux.HandleFunc("DELETE /smtp-auth/{name}", h.deleteSMTPAuth)
102
103 // DKIM endpoints
104 mux.HandleFunc("GET /dkim", h.listDKIM)
105 mux.HandleFunc("POST /dkim", h.addDKIM)
106 mux.HandleFunc("GET /dkim/{selector}/{domain}", h.getDKIM)
107 mux.HandleFunc("POST /dkim/{selector}/{domain}/enable", h.enableDKIM)
108 mux.HandleFunc("POST /dkim/{selector}/{domain}/disable", h.disableDKIM)
109 mux.HandleFunc("DELETE /dkim/{selector}/{domain}", h.deleteDKIM)
110
111 // Prune endpoint
112 mux.HandleFunc("POST /prune", h.pruneMails)
113
114 // KV endpoints
115 mux.HandleFunc("GET /kv", h.kvList)
116 mux.HandleFunc("GET /kv/{key...}", h.kvGet)
117 mux.HandleFunc("DELETE /kv/{key...}", h.kvPop)
118
119 return mux
120}
121
122// Helper to write JSON responses
123func writeJSON(w http.ResponseWriter, status int, v interface{}) {
124 w.Header().Set("Content-Type", "application/json")
125 w.WriteHeader(status)
126 if err := json.NewEncoder(w).Encode(v); err != nil {
127 slog.Error("failed to encode JSON response", "error", err)
128 }
129}
130
131// Helper to write error responses
132func writeError(w http.ResponseWriter, err error) {
133 switch {
134 case errors.Is(err, model.ErrAlreadyExists):
135 writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
136 case errors.Is(err, model.ErrNotFound):
137 writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
138 case errors.Is(err, model.ErrInvalidInput):
139 writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
140 default:
141 writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
142 }
143}
144
145func (h *HTTPService) listUsers(w http.ResponseWriter, r *http.Request) {
146 users, err := h.cmd.ListUsers(r.Context())
147 if err != nil {
148 writeError(w, err)
149 return
150 }
151 writeJSON(w, http.StatusOK, users)
152}
153
154func (h *HTTPService) createUser(w http.ResponseWriter, r *http.Request) {
155 var req struct {
156 Username string `json:"username"`
157 }
158 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
159 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
160 return
161 }
162 if req.Username == "" {
163 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
164 return
165 }
166
167 user, err := h.cmd.CreateUser(r.Context(), req.Username)
168 if err != nil {
169 writeError(w, err)
170 return
171 }
172
173 writeJSON(w, http.StatusCreated, user)
174}
175
176func (h *HTTPService) deleteUser(w http.ResponseWriter, r *http.Request) {
177 username := r.PathValue("username")
178 if username == "" {
179 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
180 return
181 }
182
183 err := h.cmd.RemoveUser(r.Context(), username)
184 if err != nil {
185 writeError(w, err)
186 return
187 }
188
189 w.WriteHeader(http.StatusNoContent)
190}
191
192func (h *HTTPService) addUserAddress(w http.ResponseWriter, r *http.Request) {
193 username := r.PathValue("username")
194 if username == "" {
195 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
196 return
197 }
198
199 var req struct {
200 Address string `json:"address"`
201 }
202 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
203 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
204 return
205 }
206 if req.Address == "" {
207 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "address is required"})
208 return
209 }
210
211 err := h.cmd.AddUserAddress(r.Context(), username, req.Address)
212 if err != nil {
213 writeError(w, err)
214 return
215 }
216
217 w.WriteHeader(http.StatusCreated)
218}
219
220func (h *HTTPService) listUserAddresses(w http.ResponseWriter, r *http.Request) {
221 username := r.PathValue("username")
222 if username == "" {
223 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
224 return
225 }
226
227 addresses, err := h.cmd.ListUserAddresses(r.Context(), username)
228 if err != nil {
229 writeError(w, err)
230 return
231 }
232 writeJSON(w, http.StatusOK, addresses)
233}
234
235func (h *HTTPService) deleteUserAddress(w http.ResponseWriter, r *http.Request) {
236 username := r.PathValue("username")
237 if username == "" {
238 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
239 return
240 }
241 address := r.PathValue("address")
242 if address == "" {
243 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "address is required"})
244 return
245 }
246
247 err := h.cmd.RemoveUserAddress(r.Context(), username, address)
248 if err != nil {
249 writeError(w, err)
250 return
251 }
252
253 w.WriteHeader(http.StatusNoContent)
254}
255
256func (h *HTTPService) rotateUserPassword(w http.ResponseWriter, r *http.Request) {
257 username := r.PathValue("username")
258 if username == "" {
259 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
260 return
261 }
262
263 user, err := h.cmd.RotateUserPassword(r.Context(), username)
264 if err != nil {
265 writeError(w, err)
266 return
267 }
268
269 writeJSON(w, http.StatusOK, user)
270}
271
272func (h *HTTPService) listSMTPAuth(w http.ResponseWriter, r *http.Request) {
273 auths, err := h.cmd.ListSMTPAuth(r.Context())
274 if err != nil {
275 writeError(w, err)
276 return
277 }
278 writeJSON(w, http.StatusOK, auths)
279}
280
281func (h *HTTPService) createSMTPAuth(w http.ResponseWriter, r *http.Request) {
282 var req struct {
283 Name string `json:"name"`
284 }
285 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
286 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
287 return
288 }
289 if req.Name == "" {
290 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
291 return
292 }
293
294 auth, err := h.cmd.CreateSMTPAuth(r.Context(), req.Name)
295 if err != nil {
296 writeError(w, err)
297 return
298 }
299
300 writeJSON(w, http.StatusCreated, auth)
301}
302
303func (h *HTTPService) deleteSMTPAuth(w http.ResponseWriter, r *http.Request) {
304 name := r.PathValue("name")
305 if name == "" {
306 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
307 return
308 }
309
310 err := h.cmd.RemoveSMTPAuth(r.Context(), name)
311 if err != nil {
312 writeError(w, err)
313 return
314 }
315
316 w.WriteHeader(http.StatusNoContent)
317}
318
319func (h *HTTPService) listDKIM(w http.ResponseWriter, r *http.Request) {
320 dkims, err := h.cmd.ListDKIM(r.Context())
321 if err != nil {
322 writeError(w, err)
323 return
324 }
325 writeJSON(w, http.StatusOK, dkims)
326}
327
328func (h *HTTPService) addDKIM(w http.ResponseWriter, r *http.Request) {
329 var req struct {
330 Selector string `json:"selector"`
331 Domain string `json:"domain"`
332 }
333 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
334 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
335 return
336 }
337 if req.Selector == "" {
338 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
339 return
340 }
341 if req.Domain == "" {
342 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
343 return
344 }
345
346 record, err := h.cmd.AddDKIM(r.Context(), req.Selector, req.Domain)
347 if err != nil {
348 writeError(w, err)
349 return
350 }
351
352 writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
353}
354
355func (h *HTTPService) getDKIM(w http.ResponseWriter, r *http.Request) {
356 selector := r.PathValue("selector")
357 domain := r.PathValue("domain")
358 if selector == "" {
359 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
360 return
361 }
362 if domain == "" {
363 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
364 return
365 }
366
367 record, err := h.cmd.GetDKIM(r.Context(), selector, domain)
368 if err != nil {
369 writeError(w, err)
370 return
371 }
372
373 writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
374}
375
376func (h *HTTPService) enableDKIM(w http.ResponseWriter, r *http.Request) {
377 selector := r.PathValue("selector")
378 domain := r.PathValue("domain")
379 if selector == "" {
380 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
381 return
382 }
383 if domain == "" {
384 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
385 return
386 }
387
388 // Parse force query parameter
389 forceStr := r.URL.Query().Get("force")
390 force := forceStr != "false" // Default to true if not explicitly set to "false"
391
392 err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, true, force)
393 if err != nil {
394 writeError(w, err)
395 return
396 }
397
398 w.WriteHeader(http.StatusNoContent)
399}
400
401func (h *HTTPService) disableDKIM(w http.ResponseWriter, r *http.Request) {
402 selector := r.PathValue("selector")
403 domain := r.PathValue("domain")
404 if selector == "" {
405 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
406 return
407 }
408 if domain == "" {
409 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
410 return
411 }
412
413 err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, false, true)
414 if err != nil {
415 writeError(w, err)
416 return
417 }
418
419 w.WriteHeader(http.StatusNoContent)
420}
421
422func (h *HTTPService) deleteDKIM(w http.ResponseWriter, r *http.Request) {
423 selector := r.PathValue("selector")
424 domain := r.PathValue("domain")
425 if selector == "" {
426 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
427 return
428 }
429 if domain == "" {
430 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
431 return
432 }
433
434 err := h.cmd.RemoveDKIM(r.Context(), selector, domain)
435 if err != nil {
436 writeError(w, err)
437 return
438 }
439
440 w.WriteHeader(http.StatusNoContent)
441}
442
443func (h *HTTPService) pruneMails(w http.ResponseWriter, r *http.Request) {
444 count, err := h.cmd.PruneMails(r.Context())
445 if err != nil {
446 writeError(w, err)
447 return
448 }
449
450 writeJSON(w, http.StatusOK, map[string]int{"pruned": count})
451}
452
453func (h *HTTPService) kvList(w http.ResponseWriter, r *http.Request) {
454 pairs, err := h.cmd.KVList(r.Context())
455 if err != nil {
456 writeError(w, err)
457 return
458 }
459 writeJSON(w, http.StatusOK, pairs)
460}
461
462func (h *HTTPService) kvGet(w http.ResponseWriter, r *http.Request) {
463 key := r.PathValue("key")
464 if key == "" {
465 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key is required"})
466 return
467 }
468
469 val, err := h.cmd.KVGet(r.Context(), key)
470 if err != nil {
471 writeError(w, err)
472 return
473 }
474
475 writeJSON(w, http.StatusOK, map[string]any{"value": val})
476}
477
478func (h *HTTPService) kvPop(w http.ResponseWriter, r *http.Request) {
479 key := r.PathValue("key")
480 if key == "" {
481 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key is required"})
482 return
483 }
484
485 val, err := h.cmd.KVPop(r.Context(), key)
486 if err != nil {
487 writeError(w, err)
488 return
489 }
490
491 writeJSON(w, http.StatusOK, map[string]any{"value": val})
492}