Files
warmbly/internal/api/handler/node_logs.go
T

134 lines
3.6 KiB
Go

package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/warmbly/warmbly/internal/app/nodelogs"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/pkg/nodeevidence"
)
func (h *Handler) logNodeExists(c *gin.Context, id uuid.UUID) bool {
if h.FleetNodeRepo == nil {
errx.JSON(c, errx.ErrServiceDown)
return false
}
_, err := h.FleetNodeRepo.Get(c.Request.Context(), id)
if errors.Is(err, pgx.ErrNoRows) {
errx.JSON(c, errx.ErrNotFound)
return false
}
if err != nil {
errx.JSON(c, errx.ErrServiceDown)
return false
}
return true
}
func (h *Handler) InternalNodeLogs(c *gin.Context) {
c.Header("Cache-Control", "no-store")
if h.NodeLogs == nil {
errx.JSON(c, errx.ErrServiceDown)
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
secret := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if err := h.NodeLogs.Authenticate(c.Request.Context(), id, secret); err != nil {
if errors.Is(err, nodelogs.ErrRateLimited) {
c.Header("Retry-After", "60")
errx.JSON(c, errx.New(errx.TooManyRequests, "node evidence rate exceeded"))
return
}
if errors.Is(err, nodelogs.ErrUnauthorized) {
errx.JSON(c, errx.ErrUnauthorized)
} else {
errx.JSON(c, errx.ErrServiceDown)
}
return
}
if !h.logNodeExists(c, id) {
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 32768)
var batch nodeevidence.Batch
if err := c.ShouldBindJSON(&batch); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid evidence batch"))
return
}
if err := h.NodeLogs.Ingest(c.Request.Context(), id, secret, batch); err != nil {
if errors.Is(err, nodelogs.ErrUnauthorized) {
errx.JSON(c, errx.ErrUnauthorized)
} else if errors.Is(err, nodelogs.ErrInvalid) {
errx.JSON(c, errx.New(errx.BadRequest, "invalid evidence batch"))
} else {
errx.JSON(c, errx.ErrServiceDown)
}
return
}
c.Status(http.StatusNoContent)
}
func (h *Handler) AdminNodeLogs(c *gin.Context) {
c.Header("Cache-Control", "no-store")
if h.NodeLogs == nil {
errx.JSON(c, errx.ErrServiceDown)
return
}
id, ok := h.parseID(c)
if !ok || !h.logNodeExists(c, id) {
return
}
f := nodelogs.Filter{Level: c.Query("level"), Limit: nodelogs.MaxRead}
if f.Level != "" && f.Level != "info" && f.Level != "warn" && f.Level != "error" {
errx.JSON(c, errx.New(errx.BadRequest, "level must be info, warn or error"))
return
}
for key, dest := range map[string]*time.Time{"after": &f.After, "before": &f.Before} {
if value := c.Query(key); value != "" {
at, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "after and before must be RFC3339 timestamps"))
return
}
*dest = at
}
}
if !f.After.IsZero() && !f.Before.IsZero() && f.After.After(f.Before) {
errx.JSON(c, errx.New(errx.BadRequest, "after must not follow before"))
return
}
if value := c.Query("mailbox_id"); value != "" {
id, err := uuid.Parse(value)
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "mailbox_id must be a uuid"))
return
}
f.MailboxID = id
}
if value := c.Query("limit"); value != "" {
n, err := strconv.Atoi(value)
if err != nil || n < 1 || n > nodelogs.MaxRead {
errx.JSON(c, errx.New(errx.BadRequest, "limit must be between 1 and 200"))
return
}
f.Limit = n
}
out, err := h.NodeLogs.History(c.Request.Context(), id, f)
if err != nil {
errx.JSON(c, errx.ErrServiceDown)
return
}
c.JSON(http.StatusOK, out)
}