mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-07 01:55:48 +08:00
Add raw command and clear device queue endpoints (#864)
This commit is contained in:
47
platform/command/clear.go
Normal file
47
platform/command/clear.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/micromdm/micromdm/mdm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (svc *CommandService) ClearQueue(ctx context.Context, udid string) error {
|
||||
if err := svc.queue.Clear(ctx, mdm.CheckinEvent{Command: mdm.CheckinCommand{UDID: udid}}); err != nil {
|
||||
return errors.Wrap(err, "clearing command queue")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type clearRequest struct {
|
||||
UDID string
|
||||
}
|
||||
|
||||
type clearResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r clearResponse) Failed() error { return r.Err }
|
||||
func (r clearResponse) StatusCode() int { return http.StatusOK }
|
||||
|
||||
func decodeClearRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
return clearRequest{UDID: mux.Vars(r)["udid"]}, nil
|
||||
}
|
||||
|
||||
// MakeClearQueueEndpoint creates an endpoint which clears device queues.
|
||||
func MakeClearQueueEndpoint(svc Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(clearRequest)
|
||||
if req.UDID == "" {
|
||||
return clearResponse{Err: errEmptyRequest}, nil
|
||||
}
|
||||
if err := svc.ClearQueue(ctx, req.UDID); err != nil {
|
||||
return clearResponse{Err: err}, nil
|
||||
}
|
||||
return clearResponse{}, nil
|
||||
}
|
||||
}
|
||||
@@ -61,3 +61,45 @@ func UnmarshalEvent(data []byte, e *Event) error {
|
||||
e.Payload = &payload
|
||||
return nil
|
||||
}
|
||||
|
||||
type RawEvent struct {
|
||||
CommandUUID string
|
||||
Time time.Time
|
||||
DeviceUDID string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// NewRawEvent returns a RawEvent with the current time.
|
||||
func NewRawEvent(cmd *RawCommand) *RawEvent {
|
||||
event := RawEvent{
|
||||
CommandUUID: cmd.CommandUUID,
|
||||
Time: time.Now().UTC(),
|
||||
DeviceUDID: cmd.UDID,
|
||||
Payload: cmd.Raw,
|
||||
}
|
||||
return &event
|
||||
}
|
||||
|
||||
// MarshalRawEvent serializes a RawEvent to a protocol buffer wire format.
|
||||
func MarshalRawEvent(e *RawEvent) ([]byte, error) {
|
||||
return proto.Marshal(&commandproto.Event{
|
||||
Id: e.CommandUUID, // Id isn't used anywhere, so it's repurposed for CommandUUID
|
||||
Time: e.Time.UnixNano(),
|
||||
DeviceUdid: e.DeviceUDID,
|
||||
PayloadBytes: e.Payload,
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalRawEvent parses a protocol buffer representation of data into
|
||||
// the RawEvent.
|
||||
func UnmarshalRawEvent(data []byte, e *RawEvent) error {
|
||||
var pb commandproto.Event
|
||||
if err := proto.Unmarshal(data, &pb); err != nil {
|
||||
return errors.Wrap(err, "unmarshal pb Event")
|
||||
}
|
||||
e.CommandUUID = pb.Id
|
||||
e.Time = time.Unix(0, pb.Time).UTC()
|
||||
e.DeviceUDID = pb.DeviceUdid
|
||||
e.Payload = pb.PayloadBytes
|
||||
return nil
|
||||
}
|
||||
|
||||
49
platform/command/event_test.go
Normal file
49
platform/command/event_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/micromdm/micromdm/platform/command"
|
||||
)
|
||||
|
||||
const testRawCmd = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Command</key>
|
||||
<dict>
|
||||
<key>ManagedOnly</key>
|
||||
<true/>
|
||||
<key>RequestType</key>
|
||||
<string>ProfileList</string>
|
||||
</dict>
|
||||
<key>CommandUUID</key>
|
||||
<string>0001_ProfileList</string>
|
||||
</dict>
|
||||
</plist>`
|
||||
|
||||
func TestRawEvent(t *testing.T) {
|
||||
cmd := &command.RawCommand{
|
||||
UDID: "1234",
|
||||
CommandUUID: "0001_ProfileList",
|
||||
Raw: []byte(testRawCmd),
|
||||
}
|
||||
cmd.Command.RequestType = "ProfileList"
|
||||
|
||||
ev := command.NewRawEvent(cmd)
|
||||
|
||||
buf, err := command.MarshalRawEvent(ev)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal event: %v", err)
|
||||
}
|
||||
|
||||
ev2 := new(command.RawEvent)
|
||||
if err = command.UnmarshalRawEvent(buf, ev2); err != nil {
|
||||
t.Fatalf("could not unmarshal event: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ev, ev2) {
|
||||
t.Error("expected events to be equal")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/groob/plist"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
@@ -14,6 +19,9 @@ import (
|
||||
const (
|
||||
// CommandTopic is a PubSub topic that events are published to.
|
||||
CommandTopic = "mdm.Command"
|
||||
|
||||
// RawCommandTopic is a PubSub topic that events are published to.
|
||||
RawCommandTopic = "mdm.RawCommand"
|
||||
)
|
||||
|
||||
func (svc *CommandService) NewCommand(ctx context.Context, request *mdm.CommandRequest) (*mdm.CommandPayload, error) {
|
||||
@@ -35,6 +43,21 @@ func (svc *CommandService) NewCommand(ctx context.Context, request *mdm.CommandR
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (svc *CommandService) NewRawCommand(ctx context.Context, cmd *RawCommand) error {
|
||||
if cmd == nil {
|
||||
return errors.New("empty RawCommand")
|
||||
}
|
||||
event := NewRawEvent(cmd)
|
||||
msg, err := MarshalRawEvent(event)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling raw mdm command event")
|
||||
}
|
||||
if err := svc.publisher.Publish(context.TODO(), RawCommandTopic, msg); err != nil {
|
||||
return errors.Wrapf(err, "publish raw mdm command on topic: %s", RawCommandTopic)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type newCommandRequest struct {
|
||||
mdm.CommandRequest
|
||||
}
|
||||
@@ -69,3 +92,65 @@ func MakeNewCommandEndpoint(svc Service) endpoint.Endpoint {
|
||||
return newCommandResponse{Payload: payload}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type RawCommand struct {
|
||||
UDID string `json:"udid" plist:"-"`
|
||||
CommandUUID string `json:"command_uuid"`
|
||||
Command struct {
|
||||
RequestType string `json:"request_type"`
|
||||
} `json:"command"`
|
||||
Raw []byte `plist:"-" json:"payload"`
|
||||
}
|
||||
|
||||
type newRawCommandRequest struct {
|
||||
RawCommand
|
||||
}
|
||||
|
||||
type newRawCommandResponse struct {
|
||||
Payload *RawCommand `json:"payload,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r newRawCommandResponse) Failed() error { return r.Err }
|
||||
func (r newRawCommandResponse) StatusCode() int { return http.StatusCreated }
|
||||
|
||||
func decodeNewRawCommandRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
udid, ok := mux.Vars(r)["udid"]
|
||||
if !ok {
|
||||
return nil, errors.New("empty udid")
|
||||
}
|
||||
|
||||
payload, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read payload body: %w", err)
|
||||
}
|
||||
|
||||
// verify body is valid plist and parse CommandUUID and RequestType
|
||||
var req newRawCommandRequest
|
||||
if err := plist.NewXMLDecoder(bytes.NewBuffer(payload)).Decode(&req); err != nil {
|
||||
return nil, fmt.Errorf("parse payload as plist: %w", err)
|
||||
}
|
||||
|
||||
req.UDID = udid
|
||||
req.Raw = payload
|
||||
return req, nil
|
||||
}
|
||||
|
||||
var errMalformedRequest = errors.New("request is malformed")
|
||||
|
||||
// MakeNewRawCommandEndpoint creates an endpoint which creates new raw MDM Commands.
|
||||
func MakeNewRawCommandEndpoint(svc Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(newRawCommandRequest)
|
||||
if req.UDID == "" {
|
||||
return newRawCommandResponse{Err: errEmptyRequest}, nil
|
||||
}
|
||||
if req.CommandUUID == "" || req.Command.RequestType == "" {
|
||||
return newRawCommandResponse{Err: errMalformedRequest}, nil
|
||||
}
|
||||
if err := svc.NewRawCommand(ctx, &req.RawCommand); err != nil {
|
||||
return newRawCommandResponse{Err: err}, nil
|
||||
}
|
||||
return newRawCommandResponse{Payload: &req.RawCommand}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,22 +8,41 @@ import (
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
NewCommandEndpoint endpoint.Endpoint
|
||||
NewCommandEndpoint endpoint.Endpoint
|
||||
NewRawCommandEndpoint endpoint.Endpoint
|
||||
ClearQueueEndpoint endpoint.Endpoint
|
||||
}
|
||||
|
||||
func MakeServerEndpoints(s Service, outer endpoint.Middleware, others ...endpoint.Middleware) Endpoints {
|
||||
return Endpoints{
|
||||
NewCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewCommandEndpoint(s)),
|
||||
NewCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewCommandEndpoint(s)),
|
||||
NewRawCommandEndpoint: endpoint.Chain(outer, others...)(MakeNewRawCommandEndpoint(s)),
|
||||
ClearQueueEndpoint: endpoint.Chain(outer, others...)(MakeClearQueueEndpoint(s)),
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterHTTPHandlers(r *mux.Router, e Endpoints, options ...httptransport.ServerOption) {
|
||||
// POST /v1/commands Add new MDM Command to device queue.
|
||||
|
||||
r.Methods("POST").Path("/v1/commands").Handler(httptransport.NewServer(
|
||||
e.NewCommandEndpoint,
|
||||
decodeNewCommandRequest,
|
||||
httputil.EncodeJSONResponse,
|
||||
options...,
|
||||
))
|
||||
|
||||
// POST /v1/commands/udid Add new MDM Command with raw plist to device queue.
|
||||
r.Methods("POST").Path("/v1/commands/{udid}").Handler(httptransport.NewServer(
|
||||
e.NewRawCommandEndpoint,
|
||||
decodeNewRawCommandRequest,
|
||||
httputil.EncodeJSONResponse,
|
||||
options...,
|
||||
))
|
||||
|
||||
// DELETE /v1/commands/udid Clear device queue.
|
||||
r.Methods("DELETE").Path("/v1/commands/{udid}").Handler(httptransport.NewServer(
|
||||
e.ClearQueueEndpoint,
|
||||
decodeClearRequest,
|
||||
httputil.EncodeJSONResponse,
|
||||
options...,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
mdmsvc "github.com/micromdm/micromdm/mdm"
|
||||
"github.com/micromdm/micromdm/mdm/mdm"
|
||||
"github.com/micromdm/micromdm/platform/pubsub"
|
||||
"golang.org/x/net/context"
|
||||
@@ -9,15 +10,24 @@ import (
|
||||
|
||||
type Service interface {
|
||||
NewCommand(context.Context, *mdm.CommandRequest) (*mdm.CommandPayload, error)
|
||||
NewRawCommand(context.Context, *RawCommand) error
|
||||
ClearQueue(ctx context.Context, udid string) error
|
||||
}
|
||||
|
||||
// Queue is an MDM Command Queue.
|
||||
type Queue interface {
|
||||
Clear(context.Context, mdmsvc.CheckinEvent) error
|
||||
}
|
||||
|
||||
type CommandService struct {
|
||||
publisher pubsub.Publisher
|
||||
queue Queue
|
||||
}
|
||||
|
||||
func New(pub pubsub.Publisher) (*CommandService, error) {
|
||||
func New(pub pubsub.Publisher, queue Queue) (*CommandService, error) {
|
||||
svc := CommandService{
|
||||
publisher: pub,
|
||||
queue: queue,
|
||||
}
|
||||
return &svc, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user