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