package magpie import ( "context" "crypto/rsa" "fmt" "io" "net/http" "github.com/gin-contrib/cors" "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/pkg/edge/edgeencrypt" "edge-infra.dev/pkg/lib/fog" ) const ( DecryptionBodySize = 3 // todo calculate this better ChannelParam = "channel" ) type Server struct { cfg *Config router *gin.Engine metric *gin.Engine client client.Client decrypt edgeencrypt.DecryptFunc } func NewDecryptionServer(cfg *Config, cl client.Client, log logr.Logger, decrypt edgeencrypt.DecryptFunc) *Server { router := gin.New() router.ContextWithFallback = true metric := edgeencrypt.MetricServer(router) router.Use(requestid.New(requestid.WithCustomHeaderStrKey(edgeencrypt.CorrelationIDKey))) router.Use(edgeencrypt.RequestLogger(log)) router.Use(gin.Recovery()) // Add CORS middleware around every request corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true corsConfig.AllowHeaders = []string{"*"} router.Use(cors.New(corsConfig)) router.Use(edgeencrypt.MaxRequestBodySize(edgeencrypt.EncryptionDefaultBodySize * DecryptionBodySize)) server := &Server{ cfg: cfg, router: router, metric: metric, decrypt: decrypt, client: cl, } router.GET("/health", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) router.GET("/ready", func(c *gin.Context) { if _, err := edgeencrypt.GetPublicKey(c, cl, cfg.Namespace, cfg.JWTSecret); err != nil { _ = c.Error(fmt.Errorf("failed to get decryption public key: %w", err)) c.AbortWithStatus(http.StatusServiceUnavailable) return } c.String(http.StatusOK, "ok") }) router.Use(edgeencrypt.BearerToken(server.GetPublicKey, edgeencrypt.Decryption)) encrypt := router.Group("/v1/decrypt/:" + ChannelParam) { encrypt.POST("", server.decryptHandler) } router.NoRoute(func(c *gin.Context) { c.AbortWithStatus(404) }) return server } func (s *Server) Start(address, metricAddress string) error { go func() { if err := s.metric.Run(metricAddress); err != nil { panic(fmt.Errorf("failed to start metric server: %w", err)) } }() return s.router.Run(address) } func (s *Server) decryptHandler(c *gin.Context) { ctx := c.Request.Context() log := fog.FromContext(ctx) claims := edgeencrypt.ClaimsFromContext(ctx) if claims == nil { _ = c.AbortWithError(http.StatusUnauthorized, fmt.Errorf("claims not found in context")) return } data, err := io.ReadAll(c.Request.Body) if err != nil { _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("failed to read request body: %w", err)) return } if len(data) == 0 { _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("body is empty")) return } e := &edgeencrypt.EncryptedData{} if err = e.FromDecryptionRequest(data); err != nil { _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("failed to unmarshal encrypted data: %w", err)) return } if err = e.Valid(); err != nil { _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid encrypted data: %w", err)) return } channel := c.Param(ChannelParam) if e.Channel != channel { _ = c.AbortWithError(http.StatusUnauthorized, fmt.Errorf("invalid channel for encrypted data")) return } res, err := edgeencrypt.DecryptData(ctx, e, claims, s.decrypt) if err != nil { _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("failed to decrypt data: %w", err)) return } log.Info("successfully decrypted data") c.Data(http.StatusOK, gin.MIMEPlain, res) } func (s *Server) GetPublicKey(ctx context.Context) (*rsa.PublicKey, error) { pk, err := edgeencrypt.GetPublicKey(ctx, s.client, s.cfg.Namespace, s.cfg.JWTSecret) if err != nil { return nil, fmt.Errorf("failed to get decryption jwt public key: %w", err) } return pk.ToRSAPublicKey() } // swagger:route POST /v1/decrypt/{channel} PostDecrypt // Edge Decryption Service // consumes: // - text/plain // produces: // - text/plain // parameters: // + name: Channel // in: path // description: The encryption channel // required: true // schema: // type: string // + name: Authorization // in: header // description: Bearer token for authentication // required: true // type:string // pattern: ^Bearer\s[\W-.]+$ // + name: Body // in: body // description: The data to decrypt // required: true // schema: // type: string // //Responses: // 200: decryptedTextResponse // 400: description:Bad Request // 401: description:Invalid Credentials // 404: description:Path Not Found // The Decrypted data // swagger:response decryptedTextResponse type decryptedTextResponse struct { //nolint:unused // The Decrypted data // in:body Body string }