Implemented command response webhook as a pubsub listener. (#315)

Closes #223
See #216
This commit is contained in:
Robin van Boven
2017-11-20 00:13:10 +01:00
committed by Victor Vrantchan
parent 30876f2767
commit 0663d4780c
9 changed files with 161 additions and 33 deletions

View File

@@ -18,7 +18,7 @@ type ConnectService interface {
// Acknowledge acknowledges a response sent by a device and returns
// the next payload if one is available.
Acknowledge(ctx context.Context, req mdm.Response) (payload []byte, err error)
Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error)
}
type connectSvc struct {
@@ -37,7 +37,7 @@ func New(queue Queue, pub pubsub.Publisher) (ConnectService, error) {
}, nil
}
func (svc *connectSvc) Acknowledge(ctx context.Context, req mdm.Response) (payload []byte, err error) {
func (svc *connectSvc) Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error) {
event := NewEvent(req)
msg, err := MarshalEvent(event)
if err != nil {
@@ -47,7 +47,7 @@ func (svc *connectSvc) Acknowledge(ctx context.Context, req mdm.Response) (paylo
return nil, errors.Wrap(err, "publish connect Response on pubsub")
}
cmd, err := svc.queue.Next(ctx, req)
cmd, err := svc.queue.Next(ctx, req.MDMResponse)
if err != nil {
return nil, errors.Wrap(err, "calling Next with mdm response")
}

View File

@@ -7,8 +7,9 @@ import (
"github.com/micromdm/mdm"
)
type mdmConnectRequest struct {
mdm.Response
type MDMConnectRequest struct {
Raw []byte
MDMResponse mdm.Response
}
type mdmConnectResponse struct {
@@ -24,8 +25,8 @@ type Endpoints struct {
func MakeConnectEndpoint(svc ConnectService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(mdmConnectRequest)
payload, err := svc.Acknowledge(ctx, req.Response)
req := request.(MDMConnectRequest)
payload, err := svc.Acknowledge(ctx, req)
return mdmConnectResponse{payload: payload, Err: err}, nil
}
}

View File

@@ -14,13 +14,15 @@ type Event struct {
ID string
Time time.Time
Response mdm.Response
Raw []byte
}
func NewEvent(resp mdm.Response) *Event {
func NewEvent(resp MDMConnectRequest) *Event {
event := Event{
ID: uuid.NewV4().String(),
Time: time.Now().UTC(),
Response: resp,
Response: resp.MDMResponse,
Raw: resp.Raw,
}
return &event
}
@@ -40,6 +42,7 @@ func MarshalEvent(e *Event) ([]byte, error) {
Id: e.ID,
Time: e.Time.UnixNano(),
Response: response,
Raw: e.Raw,
})
}
@@ -61,6 +64,7 @@ func UnmarshalEvent(data []byte, e *Event) error {
RequestType: r.GetRequestType(),
CommandUUID: r.GetCommandUuid(),
}
e.Raw = pb.Raw
return nil
}

View File

@@ -32,6 +32,7 @@ type Event struct {
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Time int64 `protobuf:"varint,2,opt,name=time" json:"time,omitempty"`
Response *Response `protobuf:"bytes,3,opt,name=response" json:"response,omitempty"`
Raw []byte `protobuf:"bytes,4,opt,name=raw,proto3" json:"raw,omitempty"`
}
func (m *Event) Reset() { *m = Event{} }
@@ -60,6 +61,13 @@ func (m *Event) GetResponse() *Response {
return nil
}
func (m *Event) GetRaw() []byte {
if m != nil {
return m.Raw
}
return nil
}
type Response struct {
Udid string `protobuf:"bytes,1,opt,name=udid" json:"udid,omitempty"`
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId" json:"user_id,omitempty"`
@@ -116,19 +124,20 @@ func init() {
func init() { proto.RegisterFile("connect.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 213 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8e, 0xb1, 0x52, 0xc3, 0x30,
0x10, 0x44, 0x47, 0x4e, 0x62, 0xe2, 0x4b, 0xa0, 0xb8, 0x22, 0xa8, 0x34, 0xa9, 0x5c, 0xb9, 0x08,
0xdf, 0x40, 0x41, 0xab, 0x81, 0xda, 0x13, 0x7c, 0x57, 0xa8, 0xb0, 0x64, 0xa4, 0x13, 0x33, 0xf9,
0x10, 0xfe, 0x97, 0xb1, 0x10, 0x1e, 0xba, 0x7b, 0xbb, 0x3b, 0xbb, 0x07, 0xf7, 0xa3, 0x77, 0x8e,
0x47, 0xe9, 0xe7, 0xe0, 0xc5, 0xe3, 0xb1, 0x60, 0xa6, 0xf3, 0x00, 0xbb, 0x97, 0x2f, 0x76, 0x82,
0x0f, 0x50, 0x59, 0xd2, 0xaa, 0x55, 0x5d, 0x63, 0x2a, 0x4b, 0x88, 0xb0, 0x15, 0x3b, 0xb1, 0xae,
0x5a, 0xd5, 0x6d, 0x4c, 0xbe, 0xf1, 0x02, 0xfb, 0xc0, 0x71, 0xf6, 0x2e, 0xb2, 0xde, 0xb4, 0xaa,
0x3b, 0x5c, 0x4e, 0xfd, 0xff, 0xb6, 0xde, 0x14, 0xd7, 0xac, 0xb9, 0xf3, 0xb7, 0x82, 0xfd, 0x9f,
0xbc, 0x94, 0x26, 0x5a, 0x67, 0xf2, 0x8d, 0x8f, 0x70, 0x97, 0x22, 0x87, 0xc1, 0x52, 0xde, 0x6a,
0x4c, 0xbd, 0xe0, 0x2b, 0xe1, 0x09, 0xea, 0x28, 0x57, 0x49, 0x31, 0x6f, 0x35, 0xa6, 0x10, 0x3e,
0xc1, 0x31, 0xf0, 0x67, 0xe2, 0x28, 0x83, 0xdc, 0x66, 0xd6, 0xdb, 0xec, 0x1e, 0x8a, 0xf6, 0x76,
0x9b, 0x79, 0x89, 0x8c, 0x7e, 0x9a, 0xae, 0x8e, 0x86, 0x94, 0x2c, 0xe9, 0xdd, 0x6f, 0xa4, 0x68,
0xef, 0xc9, 0xd2, 0x47, 0x9d, 0x3f, 0x7e, 0xfe, 0x09, 0x00, 0x00, 0xff, 0xff, 0x70, 0xa9, 0xb3,
0xa5, 0x1e, 0x01, 0x00, 0x00,
// 225 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xb1, 0x4e, 0xc3, 0x30,
0x10, 0x86, 0xe5, 0xa4, 0x0d, 0xcd, 0x35, 0x20, 0x74, 0x43, 0xf1, 0x18, 0x3a, 0x65, 0xca, 0x50,
0x9e, 0x81, 0x81, 0xd5, 0x82, 0x39, 0x0a, 0xf1, 0x0d, 0x1e, 0x62, 0xa7, 0xf6, 0x19, 0xd4, 0x07,
0xe1, 0x7d, 0x51, 0x8c, 0xa9, 0xba, 0xfd, 0xff, 0xef, 0x4f, 0xfe, 0x74, 0x70, 0x3f, 0x39, 0x6b,
0x69, 0xe2, 0x7e, 0xf1, 0x8e, 0x1d, 0x36, 0xb9, 0xa6, 0x76, 0x3c, 0xc3, 0xf6, 0xf5, 0x8b, 0x2c,
0xe3, 0x03, 0x14, 0x46, 0x4b, 0xd1, 0x8a, 0xae, 0x56, 0x85, 0xd1, 0x88, 0xb0, 0x61, 0x33, 0x93,
0x2c, 0x5a, 0xd1, 0x95, 0x2a, 0x65, 0x3c, 0xc1, 0xce, 0x53, 0x58, 0x9c, 0x0d, 0x24, 0xcb, 0x56,
0x74, 0xfb, 0xd3, 0xa1, 0xbf, 0xfd, 0xad, 0x57, 0xf9, 0x55, 0x5d, 0x39, 0x7c, 0x84, 0xd2, 0x8f,
0xdf, 0x72, 0xd3, 0x8a, 0xae, 0x51, 0x6b, 0x3c, 0xfe, 0x08, 0xd8, 0xfd, 0x83, 0xab, 0x26, 0xea,
0xab, 0x38, 0x65, 0x7c, 0x82, 0xbb, 0x18, 0xc8, 0x0f, 0x46, 0x27, 0x7b, 0xad, 0xaa, 0xb5, 0xbe,
0x69, 0x3c, 0x40, 0x15, 0x78, 0xe4, 0x18, 0x92, 0xbd, 0x56, 0xb9, 0xe1, 0x33, 0x34, 0x9e, 0xce,
0x91, 0x02, 0x0f, 0x7c, 0x59, 0x28, 0xc9, 0x6a, 0xb5, 0xcf, 0xdb, 0xfb, 0x65, 0xa1, 0x15, 0x99,
0xdc, 0x3c, 0x8f, 0x56, 0x0f, 0x31, 0x1a, 0x2d, 0xb7, 0x7f, 0x48, 0xde, 0x3e, 0xa2, 0xd1, 0x9f,
0x55, 0xba, 0xe1, 0xe5, 0x37, 0x00, 0x00, 0xff, 0xff, 0x0e, 0x78, 0x53, 0x38, 0x30, 0x01, 0x00,
0x00,
}

View File

@@ -6,6 +6,7 @@ message Event {
string id = 1;
int64 time = 2;
Response response = 3;
bytes raw = 4;
}
message Response {

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/go-kit/kit/log"
"github.com/micromdm/mdm"
)
type loggingMiddleware struct {
@@ -20,14 +19,14 @@ func NewLoggingService(svc ConnectService, logger log.Logger) loggingMiddleware
}
}
func (mw loggingMiddleware) Acknowledge(ctx context.Context, req mdm.Response) (payload []byte, err error) {
func (mw loggingMiddleware) Acknowledge(ctx context.Context, req MDMConnectRequest) (payload []byte, err error) {
defer func(begin time.Time) {
_ = mw.logger.Log(
"method", "Acknowledge",
"udid", req.UDID,
"command_uuid", req.CommandUUID,
"status", req.Status,
"request_type", req.RequestType,
"udid", req.MDMResponse.UDID,
"command_uuid", req.MDMResponse.CommandUUID,
"status", req.MDMResponse.Status,
"request_type", req.MDMResponse.RequestType,
"err", err,
"took", time.Since(begin),
)

View File

@@ -3,10 +3,12 @@ package connect
import (
"context"
"fmt"
"io/ioutil"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/groob/plist"
"github.com/micromdm/mdm"
)
type HTTPHandlers struct {
@@ -30,9 +32,21 @@ type errorer interface {
}
func decodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req mdmConnectRequest
err := plist.NewDecoder(r.Body).Decode(&req)
return req, err
var res mdm.Response
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
defer r.Body.Close()
err = plist.Unmarshal(body, &res)
if err != nil {
return nil, err
}
req := MDMConnectRequest{MDMResponse: res, Raw: body}
return req, nil
}
// According to the MDM Check-in protocol, the server must respond with 200 OK

View File

@@ -59,6 +59,7 @@ import (
nanopush "github.com/micromdm/micromdm/push"
"github.com/micromdm/micromdm/queue"
"github.com/micromdm/micromdm/user"
"github.com/micromdm/micromdm/webhook"
)
const homePage = `<!doctype html>
@@ -97,6 +98,7 @@ func serve(args []string) error {
flRepoPath = flagset.String("filerepo", "", "path to http file repo")
flDepSim = flagset.String("depsim", "", "use depsim URL")
flExamples = flagset.Bool("examples", false, "prints some example usage")
flCommandWebhookURL = flagset.String("command-webhook-url", "", "URL to send command responses as raw plists.")
)
flagset.Usage = usageFor(flagset, "micromdm serve [flags]")
if err := flagset.Parse(args); err != nil {
@@ -131,6 +133,9 @@ func serve(args []string) error {
APNSPrivateKeyPath: *flAPNSKeyPath,
depsim: *flDepSim,
tlsCertPath: *flTLSCert,
CommandWebhookURL: *flCommandWebhookURL,
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"
@@ -146,6 +151,7 @@ func serve(args []string) error {
sm.setupCheckinService()
sm.setupPushService(logger)
sm.setupCommandService()
sm.setupWebhooks()
sm.setupCommandQueue(logger)
sm.setupDEPSync()
if sm.err != nil {
@@ -181,6 +187,8 @@ func serve(args []string) error {
stdlog.Fatal(err)
}
sm.startWebhooks()
ctx := context.Background()
httpLogger := log.With(logger, "transport", "http")
@@ -478,6 +486,7 @@ type config struct {
scepDepot *boltdepot.Depot
profileDB *profile.DB
configDB *configsvc.DB
CommandWebhookURL string
// TODO: refactor enroll service and remove the need to reference
// this on-disk cert. but it might be useful to keep the PEM
@@ -493,6 +502,9 @@ type config struct {
commandService command.Service
configService configsvc.Service
responseWebhook *webhook.CommandWebhook
webhooksHTTPClient *http.Client
err error
}
@@ -510,6 +522,34 @@ func (c *config) setupCommandService() {
c.commandService, c.err = command.New(c.db, c.pubclient)
}
func (c *config) setupWebhooks() {
if c.err != nil {
return
}
if c.CommandWebhookURL == "" {
return
}
h, err := webhook.NewCommandWebhook(c.webhooksHTTPClient, connect.ConnectTopic, c.CommandWebhookURL)
if err != nil {
c.err = err
return
}
c.responseWebhook = h
}
func (c *config) startWebhooks() {
if c.err != nil {
return
}
if c.responseWebhook != nil {
c.responseWebhook.StartListener(c.pubclient)
}
}
func (c *config) setupCommandQueue(logger log.Logger) {
if c.err != nil {
return

60
webhook/command.go Normal file
View File

@@ -0,0 +1,60 @@
package webhook
import (
"bytes"
"context"
"fmt"
"net/http"
"github.com/micromdm/micromdm/connect"
"github.com/micromdm/micromdm/pubsub"
"github.com/pkg/errors"
)
const contentType = "application/x-apple-aspen-mdm"
type CommandWebhook struct {
Topic string
CallbackURL string
HTTPClient *http.Client
}
func NewCommandWebhook(httpClient *http.Client, topic, callbackURL string) (*CommandWebhook, error) {
if topic == "" {
return nil, errors.New("webhook: topic should not be empty")
}
if callbackURL == "" {
return nil, errors.New("webhook: callbackURL should not be empty")
}
return &CommandWebhook{HTTPClient: httpClient, Topic: topic, CallbackURL: callbackURL}, nil
}
func (cw CommandWebhook) StartListener(sub pubsub.Subscriber) error {
connectEvents, err := sub.Subscribe(context.TODO(), "commandWebhook", cw.Topic)
if err != nil {
return errors.Wrapf(err,
"subscribing commandWebhook to %s topic", cw.Topic)
}
go func() {
for {
select {
case event := <-connectEvents:
var ev connect.Event
if err := connect.UnmarshalEvent(event.Message, &ev); err != nil {
fmt.Println(err)
continue
}
_, err := cw.HTTPClient.Post(cw.CallbackURL, contentType, bytes.NewBuffer(ev.Raw))
if err != nil {
fmt.Printf("error sending command response: %s\n", err)
}
}
}
}()
return nil
}