package grpcserver import ( "context" "encoding/json" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2" internacme "git.wntrmute.dev/kyle/metacrypt/internal/acme" "git.wntrmute.dev/kyle/metacrypt/internal/engine" ) type acmeServer struct { pb.UnimplementedACMEServiceServer s *GRPCServer } func (as *acmeServer) CreateEAB(ctx context.Context, req *pb.CreateEABRequest) (*pb.CreateEABResponse, error) { ti := tokenInfoFromContext(ctx) h, err := as.getOrCreateHandler(req.Mount) if err != nil { return nil, status.Error(codes.NotFound, "mount not found") } cred, err := h.CreateEAB(ctx, ti.Username) if err != nil { as.s.logger.Error("grpc: acme create EAB", "error", err) return nil, status.Error(codes.Internal, "failed to create EAB credentials") } return &pb.CreateEABResponse{Kid: cred.KID, HmacKey: cred.HMACKey}, nil } func (as *acmeServer) SetConfig(ctx context.Context, req *pb.SetConfigRequest) (*pb.SetConfigResponse, error) { if req.DefaultIssuer == "" { return nil, status.Error(codes.InvalidArgument, "default_issuer is required") } // Verify mount exists. if _, err := as.getOrCreateHandler(req.Mount); err != nil { return nil, status.Error(codes.NotFound, "mount not found") } cfg := &internacme.ACMEConfig{DefaultIssuer: req.DefaultIssuer} data, _ := json.Marshal(cfg) barrierPath := "acme/" + req.Mount + "/config.json" if err := as.s.sealMgr.Barrier().Put(ctx, barrierPath, data); err != nil { as.s.logger.Error("grpc: acme set config", "error", err) return nil, status.Error(codes.Internal, "failed to save config") } return &pb.SetConfigResponse{}, nil } func (as *acmeServer) ListAccounts(ctx context.Context, req *pb.ListAccountsRequest) (*pb.ListAccountsResponse, error) { h, err := as.getOrCreateHandler(req.Mount) if err != nil { return nil, status.Error(codes.NotFound, "mount not found") } accounts, err := h.ListAccounts(ctx) if err != nil { as.s.logger.Error("grpc: acme list accounts", "error", err) return nil, status.Error(codes.Internal, "internal error") } pbAccounts := make([]*pb.ACMEAccount, 0, len(accounts)) for _, a := range accounts { contacts := make([]string, len(a.Contact)) copy(contacts, a.Contact) pbAccounts = append(pbAccounts, &pb.ACMEAccount{ Id: a.ID, Status: a.Status, Contact: contacts, MciasUsername: a.MCIASUsername, CreatedAt: timestamppb.New(a.CreatedAt), }) } return &pb.ListAccountsResponse{Accounts: pbAccounts}, nil } func (as *acmeServer) ListOrders(ctx context.Context, req *pb.ListOrdersRequest) (*pb.ListOrdersResponse, error) { h, err := as.getOrCreateHandler(req.Mount) if err != nil { return nil, status.Error(codes.NotFound, "mount not found") } orders, err := h.ListOrders(ctx) if err != nil { as.s.logger.Error("grpc: acme list orders", "error", err) return nil, status.Error(codes.Internal, "internal error") } pbOrders := make([]*pb.ACMEOrder, 0, len(orders)) for _, o := range orders { identifiers := make([]string, 0, len(o.Identifiers)) for _, id := range o.Identifiers { identifiers = append(identifiers, id.Type+":"+id.Value) } pbOrders = append(pbOrders, &pb.ACMEOrder{ Id: o.ID, AccountId: o.AccountID, Status: o.Status, Identifiers: identifiers, CreatedAt: timestamppb.New(o.CreatedAt), ExpiresAt: timestamppb.New(o.ExpiresAt), }) } return &pb.ListOrdersResponse{Orders: pbOrders}, nil } func (as *acmeServer) getOrCreateHandler(mountName string) (*internacme.Handler, error) { as.s.mu.Lock() defer as.s.mu.Unlock() // Verify mount is a CA engine. mount, err := as.s.engines.GetMount(mountName) if err != nil { return nil, err } if mount.Type != engine.EngineTypeCA { return nil, engine.ErrMountNotFound } // Check handler cache on GRPCServer. if h, ok := as.s.acmeHandlers[mountName]; ok { return h, nil } baseURL := as.s.cfg.Server.ExternalURL if baseURL == "" { baseURL = "https://" + as.s.cfg.Server.ListenAddr } h := internacme.NewHandler(mountName, as.s.sealMgr.Barrier(), as.s.engines, baseURL, as.s.logger) as.s.acmeHandlers[mountName] = h return h, nil }