diff --git a/cmd/micromdm/serve.go b/cmd/micromdm/serve.go index 68ac7824..e2383208 100644 --- a/cmd/micromdm/serve.go +++ b/cmd/micromdm/serve.go @@ -1,17 +1,13 @@ package main import ( - "bytes" "context" "crypto/subtle" - "crypto/tls" "crypto/x509" "encoding/base64" - "encoding/pem" "flag" "fmt" "io" - "io/ioutil" stdlog "log" "net/http" "net/url" @@ -20,51 +16,39 @@ import ( "strings" "time" - "github.com/RobotsAndPencils/buford/push" "github.com/boltdb/bolt" "github.com/fullsailor/pkcs7" "github.com/go-kit/kit/auth/basic" "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" httptransport "github.com/go-kit/kit/transport/http" "github.com/groob/finalizer/logutil" - "github.com/micromdm/dep" "github.com/micromdm/go4/env" "github.com/micromdm/go4/httputil" "github.com/micromdm/go4/version" - boltdepot "github.com/micromdm/scep/depot/bolt" scep "github.com/micromdm/scep/server" "github.com/pkg/errors" "golang.org/x/crypto/acme/autocert" - "golang.org/x/crypto/pkcs12" "github.com/micromdm/micromdm/dep/depsync" "github.com/micromdm/micromdm/mdm" "github.com/micromdm/micromdm/mdm/enroll" - "github.com/micromdm/micromdm/pkg/crypto" httputil2 "github.com/micromdm/micromdm/pkg/httputil" "github.com/micromdm/micromdm/platform/apns" - apnsbuiltin "github.com/micromdm/micromdm/platform/apns/builtin" "github.com/micromdm/micromdm/platform/appstore" appsbuiltin "github.com/micromdm/micromdm/platform/appstore/builtin" "github.com/micromdm/micromdm/platform/blueprint" blueprintbuiltin "github.com/micromdm/micromdm/platform/blueprint/builtin" "github.com/micromdm/micromdm/platform/command" "github.com/micromdm/micromdm/platform/config" - configbuiltin "github.com/micromdm/micromdm/platform/config/builtin" depapi "github.com/micromdm/micromdm/platform/dep" "github.com/micromdm/micromdm/platform/device" devicebuiltin "github.com/micromdm/micromdm/platform/device/builtin" "github.com/micromdm/micromdm/platform/profile" - profilebuiltin "github.com/micromdm/micromdm/platform/profile/builtin" - "github.com/micromdm/micromdm/platform/pubsub" - "github.com/micromdm/micromdm/platform/pubsub/inmem" - "github.com/micromdm/micromdm/platform/queue" block "github.com/micromdm/micromdm/platform/remove" - blockbuiltin "github.com/micromdm/micromdm/platform/remove/builtin" "github.com/micromdm/micromdm/platform/user" userbuiltin "github.com/micromdm/micromdm/platform/user/builtin" - "github.com/micromdm/micromdm/workflow/webhook" + "github.com/micromdm/micromdm/server" + boltdepot "github.com/micromdm/scep/depot/bolt" ) const homePage = ` @@ -137,17 +121,17 @@ func serve(args []string) error { if err := os.MkdirAll(*flConfigPath, 0755); err != nil { return errors.Wrapf(err, "creating config directory %s", *flConfigPath) } - sm := &server{ - configPath: *flConfigPath, + sm := &server.Server{ + ConfigPath: *flConfigPath, ServerPublicURL: strings.TrimRight(*flServerURL, "/"), APNSCertificatePath: *flAPNSCertPath, APNSPrivateKeyPass: *flAPNSKeyPass, APNSPrivateKeyPath: *flAPNSKeyPath, - depsim: *flDepSim, - tlsCertPath: *flTLSCert, + Depsim: *flDepSim, + TLSCertPath: *flTLSCert, CommandWebhookURL: *flCommandWebhookURL, - webhooksHTTPClient: &http.Client{Timeout: time.Second * 30}, + WebhooksHTTPClient: &http.Client{Timeout: time.Second * 30}, // TODO: we have a static SCEP challenge password here to prevent // being prompted for the SCEP challenge which happens in a "normal" @@ -156,78 +140,60 @@ func serve(args []string) error { SCEPChallenge: "micromdm", } - sm.setupPubSub() - sm.setupBolt() - sm.setupRemoveService() - sm.setupConfigStore() - sm.loadPushCerts() - sm.setupSCEP(logger) - sm.setupPushService(logger) - sm.setupCommandService() - sm.setupWebhooks(logger) - sm.setupCommandQueue(logger) - sm.setupDepClient() - syncer := sm.setupDEPSync(logger) - if sm.err != nil { - stdlog.Fatal(sm.err) + if err := sm.Setup(logger); err != nil { + stdlog.Fatal(err) + } + syncer, err := sm.CreateDEPSyncer(logger) + if err != nil { + stdlog.Fatal(err) } var removeService block.Service { - svc, err := block.New(sm.removeDB) + svc, err := block.New(sm.RemoveDB) if err != nil { stdlog.Fatal(err) } removeService = block.LoggingMiddleware(logger)(svc) } - devDB, err := devicebuiltin.NewDB(sm.db) + devDB, err := devicebuiltin.NewDB(sm.DB) if err != nil { stdlog.Fatal(err) } - devWorker := device.NewWorker(devDB, sm.pubclient, logger) + devWorker := device.NewWorker(devDB, sm.PubClient, logger) go devWorker.Run(context.Background()) - userDB, err := userbuiltin.NewDB(sm.db) + userDB, err := userbuiltin.NewDB(sm.DB) if err != nil { stdlog.Fatal(err) } - userWorker := user.NewWorker(userDB, sm.pubclient, logger) + userWorker := user.NewWorker(userDB, sm.PubClient, logger) go userWorker.Run(context.Background()) - sm.profileDB, err = profilebuiltin.NewDB(sm.db) + bpDB, err := blueprintbuiltin.NewDB(sm.DB, sm.ProfileDB, userDB) if err != nil { stdlog.Fatal(err) } - sm.setupEnrollmentService() - if sm.err != nil { - stdlog.Fatalf("enrollment service: %s", sm.err) - } - - bpDB, err := blueprintbuiltin.NewDB(sm.db, sm.profileDB, userDB) - if err != nil { - stdlog.Fatal(err) - } - - if err := bpDB.StartListener(sm.pubclient, sm.commandService); err != nil { + if err := bpDB.StartListener(sm.PubClient, sm.CommandService); err != nil { stdlog.Fatal(err) } ctx := context.Background() httpLogger := log.With(logger, "transport", "http") - dc := sm.depClient + dc := sm.DEPClient appDB := &appsbuiltin.Repo{Path: *flRepoPath} - scepEndpoints := scep.MakeServerEndpoints(sm.scepService) + scepEndpoints := scep.MakeServerEndpoints(sm.SCEPService) scepComponentLogger := log.With(logger, "component", "scep") scepEndpoints.GetEndpoint = scep.EndpointLoggingMiddleware(scepComponentLogger)(scepEndpoints.GetEndpoint) scepEndpoints.PostEndpoint = scep.EndpointLoggingMiddleware(scepComponentLogger)(scepEndpoints.PostEndpoint) - scepHandler := scep.MakeHTTPHandler(scepEndpoints, sm.scepService, scepComponentLogger) + scepHandler := scep.MakeHTTPHandler(scepEndpoints, sm.SCEPService, scepComponentLogger) - enrollHandlers := enroll.MakeHTTPHandlers(ctx, enroll.MakeServerEndpoints(sm.enrollService, sm.scepDepot), httptransport.ServerErrorLogger(httpLogger)) + enrollHandlers := enroll.MakeHTTPHandlers(ctx, enroll.MakeServerEndpoints(sm.EnrollService, sm.SCEPDepot), httptransport.ServerErrorLogger(httpLogger)) r, options := httputil2.NewRouter(logger) @@ -242,26 +208,26 @@ func serve(args []string) error { }) } - signatureVerifier := &mdmSignatureVerifier{db: sm.scepDepot} - mdmEndpoints := mdm.MakeServerEndpoints(sm.mdmService) + signatureVerifier := &mdmSignatureVerifier{db: sm.SCEPDepot} + mdmEndpoints := mdm.MakeServerEndpoints(sm.MDMService) mdm.RegisterHTTPHandlers(r, mdmEndpoints, signatureVerifier, logger) // API commands. Only handled if the user provides an api key. if *flAPIKey != "" { basicAuthEndpointMiddleware := basic.AuthMiddleware("micromdm", *flAPIKey, "micromdm") - configsvc := config.New(sm.configDB) + configsvc := config.New(sm.ConfigDB) configEndpoints := config.MakeServerEndpoints(configsvc, basicAuthEndpointMiddleware) config.RegisterHTTPHandlers(r, configEndpoints, options...) - apnsEndpoints := apns.MakeServerEndpoints(sm.pushService, basicAuthEndpointMiddleware) + apnsEndpoints := apns.MakeServerEndpoints(sm.APNSPushService, basicAuthEndpointMiddleware) apns.RegisterHTTPHandlers(r, apnsEndpoints, options...) devicesvc := device.New(devDB) deviceEndpoints := device.MakeServerEndpoints(devicesvc, basicAuthEndpointMiddleware) device.RegisterHTTPHandlers(r, deviceEndpoints, options...) - profilesvc := profile.New(sm.profileDB) + profilesvc := profile.New(sm.ProfileDB) profileEndpoints := profile.MakeServerEndpoints(profilesvc, basicAuthEndpointMiddleware) profile.RegisterHTTPHandlers(r, profileEndpoints, options...) @@ -280,10 +246,10 @@ func serve(args []string) error { appEndpoints := appstore.MakeServerEndpoints(appsvc, basicAuthEndpointMiddleware) appstore.RegisterHTTPHandlers(r, appEndpoints, options...) - commandEndpoints := command.MakeServerEndpoints(sm.commandService, basicAuthEndpointMiddleware) + commandEndpoints := command.MakeServerEndpoints(sm.CommandService, basicAuthEndpointMiddleware) command.RegisterHTTPHandlers(r, commandEndpoints, options...) - depsvc := depapi.New(dc, sm.pubclient) + depsvc := depapi.New(dc, sm.PubClient) depEndpoints := depapi.MakeServerEndpoints(depsvc, basicAuthEndpointMiddleware) depapi.RegisterHTTPHandlers(r, depEndpoints, options...) @@ -320,7 +286,7 @@ func serve(args []string) error { logger, *flTLSCert, *flTLSKey, - sm.configPath, + sm.ConfigPath, *flTLS, ) err = httputil.ListenAndServe(serveOpts...) @@ -372,390 +338,6 @@ func printExamples() { fmt.Println(exampleText) } -type server struct { - configPath string - depsim string - pubclient pubsub.PublishSubscriber - db *bolt.DB - pushCert pushServiceCert - ServerPublicURL string - SCEPChallenge string - APNSPrivateKeyPath string - APNSCertificatePath string - APNSPrivateKeyPass string - tlsCertPath string - scepDepot *boltdepot.Depot - profileDB profile.Store - configDB config.Store - removeDB block.Store - CommandWebhookURL string - depClient dep.Client - - // TODO: refactor enroll service and remove the need to reference - // this on-disk cert. but it might be useful to keep the PEM - // around for anyone who will need to export the CA. - scepCACertPath string - - PushService *push.Service // bufford push - pushService apns.Service - mdmService mdm.Service - enrollService enroll.Service - scepService scep.Service - commandService command.Service - configService config.Service - - webhooksHTTPClient *http.Client - - err error -} - -func (c *server) setupPubSub() { - if c.err != nil { - return - } - c.pubclient = inmem.NewPubSub() -} - -func (c *server) setupCommandService() { - if c.err != nil { - return - } - c.commandService, c.err = command.New(c.pubclient) -} - -func (c *server) setupWebhooks(logger log.Logger) { - if c.err != nil { - return - } - - if c.CommandWebhookURL == "" { - return - } - - ctx := context.Background() - ww := webhook.New(c.CommandWebhookURL, c.pubclient, webhook.WithLogger(logger), webhook.WithHTTPClient(c.webhooksHTTPClient)) - go ww.Run(ctx) -} - -func (c *server) setupRemoveService() { - if c.err != nil { - return - } - removeDB, err := blockbuiltin.NewDB(c.db) - if err != nil { - c.err = err - return - } - c.removeDB = removeDB -} - -func (c *server) setupCommandQueue(logger log.Logger) { - if c.err != nil { - return - } - q, err := queue.NewQueue(c.db, c.pubclient) - if err != nil { - c.err = err - return - } - - var mdmService mdm.Service - { - svc := mdm.NewService(c.pubclient, q) - mdmService = svc - mdmService = block.RemoveMiddleware(c.removeDB)(mdmService) - } - c.mdmService = mdmService -} - -func (c *server) setupBolt() { - if c.err != nil { - return - } - dbPath := filepath.Join(c.configPath, "micromdm.db") - db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: time.Second}) - if err != nil { - c.err = errors.Wrap(err, "opening boltdb") - return - } - c.db = db -} - -func (c *server) loadPushCerts() { - if c.APNSCertificatePath == "" && c.APNSPrivateKeyPass == "" && c.APNSPrivateKeyPath == "" { - // this is optional, config could also be provided with mdmctl - return - } - if c.err != nil { - return - } - - if c.APNSPrivateKeyPath == "" { - var pkcs12Data []byte - pkcs12Data, c.err = ioutil.ReadFile(c.APNSCertificatePath) - if c.err != nil { - return - } - c.pushCert.PrivateKey, c.pushCert.Certificate, c.err = - pkcs12.Decode(pkcs12Data, c.APNSPrivateKeyPass) - return - } - - c.pushCert.Certificate, c.err = crypto.ReadPEMCertificateFile(c.APNSCertificatePath) - if c.err != nil { - return - } - - var pemData []byte - pemData, c.err = ioutil.ReadFile(c.APNSPrivateKeyPath) - if c.err != nil { - return - } - - pkeyBlock := new(bytes.Buffer) - pemBlock, _ := pem.Decode(pemData) - if pemBlock == nil { - c.err = errors.New("invalid PEM data for privkey") - return - } - - if x509.IsEncryptedPEMBlock(pemBlock) { - b, err := x509.DecryptPEMBlock(pemBlock, []byte(c.APNSPrivateKeyPass)) - if err != nil { - c.err = fmt.Errorf("decrypting DES private key %s", err) - return - } - pkeyBlock.Write(b) - } else { - pkeyBlock.Write(pemBlock.Bytes) - } - - priv, err := x509.ParsePKCS1PrivateKey(pkeyBlock.Bytes()) - if err != nil { - c.err = fmt.Errorf("parsing pkcs1 private key: %s", err) - return - } - c.pushCert.PrivateKey = priv -} - -type pushServiceCert struct { - *x509.Certificate - PrivateKey interface{} -} - -func (c *server) setupConfigStore() { - if c.err != nil { - return - } - db, err := configbuiltin.NewDB(c.db, c.pubclient) - if err != nil { - c.err = err - return - } - c.configDB = db - c.configService = config.New(db) - -} - -func (c *server) setupPushService(logger log.Logger) { - if c.err != nil { - return - } - - var opts []apns.Option - { - cert, _ := c.configDB.PushCertificate() - if c.pushCert.Certificate != nil && cert == nil { - cert = &tls.Certificate{ - Certificate: [][]byte{c.pushCert.Certificate.Raw}, - PrivateKey: c.pushCert.PrivateKey, - Leaf: c.pushCert.Certificate, - } - } - if cert == nil { - goto after - } - client, err := push.NewClient(*cert) - if err != nil { - c.err = err - return - } - svc := push.NewService(client, push.Production) - opts = append(opts, apns.WithPushService(svc)) - } -after: - - db, err := apnsbuiltin.NewDB(c.db, c.pubclient) - if err != nil { - c.err = err - return - } - - service, err := apns.New(db, c.configDB, c.pubclient, opts...) - if err != nil { - c.err = errors.Wrap(err, "starting micromdm push service") - return - } - c.pushService = apns.LoggingMiddleware( - log.With(level.Info(logger), "component", "apns"), - )(service) - - pushinfoWorker := apns.NewWorker(db, c.pubclient, logger) - go pushinfoWorker.Run(context.Background()) -} - -func (c *server) setupEnrollmentService() { - if c.err != nil { - return - } - - var topicProvider enroll.TopicProvider - if c.pushCert.Certificate != nil { - pushTopic, err := crypto.TopicFromCert(c.pushCert.Certificate) - if err != nil { - c.err = errors.Wrap(err, "get apns topic from certificate") - return - } - topicProvider = staticTopicProvider{topic: pushTopic} - } else { - topicProvider = c.configDB - } - - var SCEPCertificateSubject string - // TODO: clean up order of inputs. Maybe pass *SCEPConfig as an arg? - // but if you do, the packages are coupled, better not. - c.enrollService, c.err = enroll.NewService( - topicProvider, - c.pubclient, - c.scepCACertPath, - c.ServerPublicURL+"/scep", - c.SCEPChallenge, - c.ServerPublicURL, - c.tlsCertPath, - SCEPCertificateSubject, - c.profileDB, - ) -} - -// if the apns-cert flags are specified this provider will be used in the enroll service. -type staticTopicProvider struct{ topic string } - -func (p staticTopicProvider) PushTopic() (string, error) { - return p.topic, nil -} - -func (c *server) setupDepClient() (dep.Client, error) { - if c.err != nil { - return nil, c.err - } - // depsim config - depsim := c.depsim - var conf *dep.Config - - // try getting the oauth config from bolt - tokens, err := c.configDB.DEPTokens() - if err != nil { - return nil, err - } - if len(tokens) >= 1 { - conf = new(dep.Config) - conf.ConsumerSecret = tokens[0].ConsumerSecret - conf.ConsumerKey = tokens[0].ConsumerKey - conf.AccessSecret = tokens[0].AccessSecret - conf.AccessToken = tokens[0].AccessToken - // TODO: handle expiration - } - - // override with depsim keys if specified on CLI - if depsim != "" { - conf = &dep.Config{ - ConsumerKey: "CK_48dd68d198350f51258e885ce9a5c37ab7f98543c4a697323d75682a6c10a32501cb247e3db08105db868f73f2c972bdb6ae77112aea803b9219eb52689d42e6", - ConsumerSecret: "CS_34c7b2b531a600d99a0e4edcf4a78ded79b86ef318118c2f5bcfee1b011108c32d5302df801adbe29d446eb78f02b13144e323eb9aad51c79f01e50cb45c3a68", - AccessToken: "AT_927696831c59ba510cfe4ec1a69e5267c19881257d4bca2906a99d0785b785a6f6fdeb09774954fdd5e2d0ad952e3af52c6d8d2f21c924ba0caf4a031c158b89", - AccessSecret: "AS_c31afd7a09691d83548489336e8ff1cb11b82b6bca13f793344496a556b1f4972eaff4dde6deb5ac9cf076fdfa97ec97699c34d515947b9cf9ed31c99dded6ba", - } - } - - if conf == nil { - return nil, nil - } - - depServerURL := "https://mdmenrollment.apple.com" - if depsim != "" { - depServerURL = depsim - } - client, err := dep.NewClient(conf, dep.ServerURL(depServerURL)) - if err != nil { - return nil, err - } - - c.depClient = client - - return client, nil -} - -func (c *server) setupDEPSync(logger log.Logger) depsync.Syncer { - if c.err != nil { - return nil - } - - client := c.depClient - opts := []depsync.Option{ - depsync.WithLogger(log.With(logger, "component", "depsync")), - } - if client != nil { - opts = append(opts, depsync.WithClient(client)) - } - - var syncer depsync.Syncer - syncer, c.err = depsync.New(c.pubclient, c.db, logger, opts...) - if c.err != nil { - return nil - } - return syncer -} - -func (c *server) setupSCEP(logger log.Logger) { - if c.err != nil { - return - } - - depot, err := boltdepot.NewBoltDepot(c.db) - if err != nil { - c.err = err - return - } - - key, err := depot.CreateOrLoadKey(2048) - if err != nil { - c.err = err - return - } - - caCert, err := depot.CreateOrLoadCA(key, 5, "MicroMDM", "US") - if err != nil { - c.err = err - return - } - - c.scepCACertPath = filepath.Join(c.configPath, "SCEPCACert.pem") - - c.err = crypto.WritePEMCertificateFile(caCert, c.scepCACertPath) - if c.err != nil { - return - } - - opts := []scep.ServiceOption{ - scep.ClientValidity(365), - scep.ChallengePassword(c.SCEPChallenge), - } - c.scepDepot = depot - c.scepService, c.err = scep.NewService(depot, opts...) - if c.err == nil { - c.scepService = scep.NewLoggingService(logger, c.scepService) - } -} - type mdmSignatureVerifier struct { db *boltdepot.Depot } diff --git a/server/server.go b/server/server.go new file mode 100644 index 00000000..7b867ebe --- /dev/null +++ b/server/server.go @@ -0,0 +1,456 @@ +package server + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "time" + + "github.com/RobotsAndPencils/buford/push" + "github.com/boltdb/bolt" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/micromdm/dep" + boltdepot "github.com/micromdm/scep/depot/bolt" + scep "github.com/micromdm/scep/server" + "github.com/pkg/errors" + "golang.org/x/crypto/pkcs12" + + "github.com/micromdm/micromdm/dep/depsync" + "github.com/micromdm/micromdm/mdm" + "github.com/micromdm/micromdm/mdm/enroll" + "github.com/micromdm/micromdm/pkg/crypto" + "github.com/micromdm/micromdm/platform/apns" + apnsbuiltin "github.com/micromdm/micromdm/platform/apns/builtin" + "github.com/micromdm/micromdm/platform/command" + "github.com/micromdm/micromdm/platform/config" + configbuiltin "github.com/micromdm/micromdm/platform/config/builtin" + "github.com/micromdm/micromdm/platform/profile" + profilebuiltin "github.com/micromdm/micromdm/platform/profile/builtin" + "github.com/micromdm/micromdm/platform/pubsub" + "github.com/micromdm/micromdm/platform/pubsub/inmem" + "github.com/micromdm/micromdm/platform/queue" + block "github.com/micromdm/micromdm/platform/remove" + blockbuiltin "github.com/micromdm/micromdm/platform/remove/builtin" + "github.com/micromdm/micromdm/workflow/webhook" +) + +type Server struct { + ConfigPath string + Depsim string + PubClient pubsub.PublishSubscriber + DB *bolt.DB + PushCert pushServiceCert + ServerPublicURL string + SCEPChallenge string + APNSPrivateKeyPath string + APNSCertificatePath string + APNSPrivateKeyPass string + TLSCertPath string + SCEPDepot *boltdepot.Depot + ProfileDB profile.Store + ConfigDB config.Store + RemoveDB block.Store + CommandWebhookURL string + DEPClient dep.Client + + // TODO: refactor enroll service and remove the need to reference + // this on-disk cert. but it might be useful to keep the PEM + // around for anyone who will need to export the CA. + SCEPCACertPath string + + PushService *push.Service // bufford push + APNSPushService apns.Service + CommandService command.Service + MDMService mdm.Service + EnrollService enroll.Service + SCEPService scep.Service + ConfigService config.Service + + WebhooksHTTPClient *http.Client +} + +func (c *Server) Setup(logger log.Logger) error { + if err := c.setupPubSub(); err != nil { + return err + } + + if err := c.setupBolt(); err != nil { + return err + } + + if err := c.setupRemoveService(); err != nil { + return err + } + + if err := c.setupConfigStore(); err != nil { + return err + } + + if err := c.loadPushCerts(); err != nil { + return err + } + + if err := c.setupSCEP(logger); err != nil { + return err + } + + if err := c.setupPushService(logger); err != nil { + return err + } + + if err := c.setupCommandService(); err != nil { + return err + } + + if err := c.setupWebhooks(logger); err != nil { + return err + } + + if err := c.setupCommandQueue(logger); err != nil { + return err + } + + if err := c.setupDepClient(); err != nil { + return err + } + + if err := c.setupProfileDB(); err != nil { + return err + } + + if err := c.setupEnrollmentService(); err != nil { + return err + } + + return nil +} + +func (c *Server) setupProfileDB() error { + profileDB, err := profilebuiltin.NewDB(c.DB) + if err != nil { + return err + } + c.ProfileDB = profileDB + return nil +} + +func (c *Server) setupPubSub() error { + c.PubClient = inmem.NewPubSub() + return nil +} + +func (c *Server) setupWebhooks(logger log.Logger) error { + if c.CommandWebhookURL == "" { + return nil + } + + ctx := context.Background() + ww := webhook.New(c.CommandWebhookURL, c.PubClient, webhook.WithLogger(logger), webhook.WithHTTPClient(c.WebhooksHTTPClient)) + go ww.Run(ctx) + return nil +} + +func (c *Server) setupRemoveService() error { + removeDB, err := blockbuiltin.NewDB(c.DB) + if err != nil { + return err + } + c.RemoveDB = removeDB + return nil +} + +func (c *Server) setupCommandService() error { + commandService, err := command.New(c.PubClient) + if err != nil { + return err + } + c.CommandService = commandService + return nil +} + +func (c *Server) setupCommandQueue(logger log.Logger) error { + q, err := queue.NewQueue(c.DB, c.PubClient) + if err != nil { + return err + } + + var mdmService mdm.Service + { + svc := mdm.NewService(c.PubClient, q) + mdmService = svc + mdmService = block.RemoveMiddleware(c.RemoveDB)(mdmService) + } + c.MDMService = mdmService + + return nil +} + +func (c *Server) setupBolt() error { + dbPath := filepath.Join(c.ConfigPath, "micromdm.db") + db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: time.Second}) + if err != nil { + return errors.Wrap(err, "opening boltdb") + } + c.DB = db + + return nil +} + +func (c *Server) loadPushCerts() error { + if c.APNSCertificatePath == "" && c.APNSPrivateKeyPass == "" && c.APNSPrivateKeyPath == "" { + // this is optional, config could also be provided with mdmctl + return nil + } + var err error + + if c.APNSPrivateKeyPath == "" { + var pkcs12Data []byte + + pkcs12Data, err = ioutil.ReadFile(c.APNSCertificatePath) + if err != nil { + return err + } + c.PushCert.PrivateKey, c.PushCert.Certificate, err = + pkcs12.Decode(pkcs12Data, c.APNSPrivateKeyPass) + return err + } + + c.PushCert.Certificate, err = crypto.ReadPEMCertificateFile(c.APNSCertificatePath) + if err != nil { + return err + } + + var pemData []byte + pemData, err = ioutil.ReadFile(c.APNSPrivateKeyPath) + if err != nil { + return err + } + + pkeyBlock := new(bytes.Buffer) + pemBlock, _ := pem.Decode(pemData) + if pemBlock == nil { + return errors.New("invalid PEM data for privkey") + } + + if x509.IsEncryptedPEMBlock(pemBlock) { + b, err := x509.DecryptPEMBlock(pemBlock, []byte(c.APNSPrivateKeyPass)) + if err != nil { + return fmt.Errorf("decrypting DES private key %s", err) + } + pkeyBlock.Write(b) + } else { + pkeyBlock.Write(pemBlock.Bytes) + } + + priv, err := x509.ParsePKCS1PrivateKey(pkeyBlock.Bytes()) + if err != nil { + return fmt.Errorf("parsing pkcs1 private key: %s", err) + } + c.PushCert.PrivateKey = priv + + return nil +} + +type pushServiceCert struct { + *x509.Certificate + PrivateKey interface{} +} + +func (c *Server) setupConfigStore() error { + db, err := configbuiltin.NewDB(c.DB, c.PubClient) + if err != nil { + return err + } + c.ConfigDB = db + c.ConfigService = config.New(db) + + return nil +} + +func (c *Server) setupPushService(logger log.Logger) error { + var opts []apns.Option + { + cert, _ := c.ConfigDB.PushCertificate() + if c.PushCert.Certificate != nil && cert == nil { + cert = &tls.Certificate{ + Certificate: [][]byte{c.PushCert.Certificate.Raw}, + PrivateKey: c.PushCert.PrivateKey, + Leaf: c.PushCert.Certificate, + } + } + if cert == nil { + goto after + } + client, err := push.NewClient(*cert) + if err != nil { + return err + } + svc := push.NewService(client, push.Production) + opts = append(opts, apns.WithPushService(svc)) + } +after: + + db, err := apnsbuiltin.NewDB(c.DB, c.PubClient) + if err != nil { + return err + } + + service, err := apns.New(db, c.ConfigDB, c.PubClient, opts...) + if err != nil { + return errors.Wrap(err, "starting micromdm push service") + } + c.APNSPushService = apns.LoggingMiddleware( + log.With(level.Info(logger), "component", "apns"), + )(service) + + return nil +} + +func (c *Server) setupEnrollmentService() error { + var topicProvider enroll.TopicProvider + var err error + if c.PushCert.Certificate != nil { + pushTopic, err := crypto.TopicFromCert(c.PushCert.Certificate) + if err != nil { + return errors.Wrap(err, "get apns topic from certificate") + } + topicProvider = staticTopicProvider{topic: pushTopic} + } else { + topicProvider = c.ConfigDB + } + + var SCEPCertificateSubject string + // TODO: clean up order of inputs. Maybe pass *SCEPConfig as an arg? + // but if you do, the packages are coupled, better not. + c.EnrollService, err = enroll.NewService( + topicProvider, + c.PubClient, + c.SCEPCACertPath, + c.ServerPublicURL+"/scep", + c.SCEPChallenge, + c.ServerPublicURL, + c.TLSCertPath, + SCEPCertificateSubject, + c.ProfileDB, + ) + if err != nil { + return err + } + + return nil +} + +// if the apns-cert flags are specified this provider will be used in the enroll service. +type staticTopicProvider struct{ topic string } + +func (p staticTopicProvider) PushTopic() (string, error) { + return p.topic, nil +} + +func (c *Server) setupDepClient() error { + // depsim config + depsim := c.Depsim + var conf *dep.Config + + // try getting the oauth config from bolt + tokens, err := c.ConfigDB.DEPTokens() + if err != nil { + return err + } + if len(tokens) >= 1 { + conf = new(dep.Config) + conf.ConsumerSecret = tokens[0].ConsumerSecret + conf.ConsumerKey = tokens[0].ConsumerKey + conf.AccessSecret = tokens[0].AccessSecret + conf.AccessToken = tokens[0].AccessToken + // TODO: handle expiration + } + + // override with depsim keys if specified on CLI + if depsim != "" { + conf = &dep.Config{ + ConsumerKey: "CK_48dd68d198350f51258e885ce9a5c37ab7f98543c4a697323d75682a6c10a32501cb247e3db08105db868f73f2c972bdb6ae77112aea803b9219eb52689d42e6", + ConsumerSecret: "CS_34c7b2b531a600d99a0e4edcf4a78ded79b86ef318118c2f5bcfee1b011108c32d5302df801adbe29d446eb78f02b13144e323eb9aad51c79f01e50cb45c3a68", + AccessToken: "AT_927696831c59ba510cfe4ec1a69e5267c19881257d4bca2906a99d0785b785a6f6fdeb09774954fdd5e2d0ad952e3af52c6d8d2f21c924ba0caf4a031c158b89", + AccessSecret: "AS_c31afd7a09691d83548489336e8ff1cb11b82b6bca13f793344496a556b1f4972eaff4dde6deb5ac9cf076fdfa97ec97699c34d515947b9cf9ed31c99dded6ba", + } + } + + if conf == nil { + return nil + } + + depServerURL := "https://mdmenrollment.apple.com" + if depsim != "" { + depServerURL = depsim + } + client, err := dep.NewClient(conf, dep.ServerURL(depServerURL)) + if err != nil { + return err + } + + c.DEPClient = client + + return nil +} + +func (c *Server) CreateDEPSyncer(logger log.Logger) (depsync.Syncer, error) { + client := c.DEPClient + opts := []depsync.Option{ + depsync.WithLogger(log.With(logger, "component", "depsync")), + } + if client != nil { + opts = append(opts, depsync.WithClient(client)) + } + + var syncer depsync.Syncer + syncer, err := depsync.New(c.PubClient, c.DB, logger, opts...) + if err != nil { + return nil, err + } + return syncer, nil +} + +func (c *Server) setupSCEP(logger log.Logger) error { + depot, err := boltdepot.NewBoltDepot(c.DB) + if err != nil { + return err + } + + key, err := depot.CreateOrLoadKey(2048) + if err != nil { + return err + } + + caCert, err := depot.CreateOrLoadCA(key, 5, "MicroMDM", "US") + if err != nil { + return err + } + + c.SCEPCACertPath = filepath.Join(c.ConfigPath, "SCEPCACert.pem") + + err = crypto.WritePEMCertificateFile(caCert, c.SCEPCACertPath) + if err != nil { + return err + } + + opts := []scep.ServiceOption{ + scep.ClientValidity(365), + scep.ChallengePassword(c.SCEPChallenge), + } + c.SCEPDepot = depot + c.SCEPService, err = scep.NewService(depot, opts...) + if err != nil { + return err + } + c.SCEPService = scep.NewLoggingService(logger, c.SCEPService) + + return nil +} diff --git a/cmd/micromdm/serve_test.go b/server/server_test.go similarity index 63% rename from cmd/micromdm/serve_test.go rename to server/server_test.go index 923996da..7052ef1d 100644 --- a/cmd/micromdm/serve_test.go +++ b/server/server_test.go @@ -1,4 +1,4 @@ -package main +package server import "testing" @@ -8,24 +8,24 @@ func TestLoadPushCerts(t *testing.T) { p12path := "testdata/pushcert.p12" keysecret := "secret" - cfg := &server{ + s := &Server{ APNSPrivateKeyPath: keypath, APNSCertificatePath: certpath, APNSPrivateKeyPass: keysecret, } // test separate key and cert - cfg.loadPushCerts() - if cfg.err != nil { - t.Fatal(cfg.err) + err := s.loadPushCerts() + if err != nil { + t.Fatal(err) } // test p12 with secret - cfg.APNSCertificatePath = p12path - cfg.APNSPrivateKeyPath = "" - cfg.loadPushCerts() - if cfg.err != nil { - t.Fatal(cfg.err) + s.APNSCertificatePath = p12path + s.APNSPrivateKeyPath = "" + err = s.loadPushCerts() + if err != nil { + t.Fatal(err) } } diff --git a/cmd/micromdm/testdata/ProviderPrivateKey.key b/server/testdata/ProviderPrivateKey.key similarity index 100% rename from cmd/micromdm/testdata/ProviderPrivateKey.key rename to server/testdata/ProviderPrivateKey.key diff --git a/cmd/micromdm/testdata/pushcert.p12 b/server/testdata/pushcert.p12 similarity index 100% rename from cmd/micromdm/testdata/pushcert.p12 rename to server/testdata/pushcert.p12 diff --git a/cmd/micromdm/testdata/pushcert.pem b/server/testdata/pushcert.pem similarity index 100% rename from cmd/micromdm/testdata/pushcert.pem rename to server/testdata/pushcert.pem