package msgdata import ( "errors" "fmt" "strings" "github.com/google/shlex" "edge-infra.dev/pkg/sds/emergencyaccess/eaconst" ) // Represents all supported operator intervention request messages. // A request message contains request data to be interpreted by // a target remote agent, and a bag of attributes. type Request interface { // Add an attribute to the request message. If the key is already set to any // value the new value will have no affect and the old value will be used AddAttribute(key, val string) // Returns a deep copy of the request message attributes. Attributes() map[string]string // Returns the "command" value in the request that requires authorization. // For a command this would be the first word of the string. // For an executable this would be the name of the executable file. CommandToBeAuthorized() string // Returns a JSON representation of the request data. The underlying structure // will depend on the message version and request type. Data() ([]byte, error) // Returns the request message type ["command", "executable"] RequestType() eaconst.RequestType } // Represents a request that is expected to contain artifact contents. type Artifactor interface { // Update the "contents" field of the request data. This value is expected // to be a base64-encoded string. If the field is already populated, it // will be overwritten. WriteContents(contents string) } // Takes a request data (in JSON form) and attributes and returns a structured // request in the form of the Request interface. Attributes must contain the // request message version and type in order to accurately parse the data. func NewRequest(data []byte, attributes map[string]string) (Request, error) { version, ok := attributes[eaconst.VersionKey] if !ok { return nil, errors.New("failed to find version attribute") } requestType, ok := attributes[eaconst.RequestTypeKey] if !ok { return nil, errors.New("failed to find requestType attribute") } switch version { case string(eaconst.MessageVersion1_0): return assembleV1_0Request(data, attributes) case string(eaconst.MessageVersion2_0): if requestType == string(eaconst.Command) { return assembleV2_0CommandRequest(data, attributes) } if requestType == string(eaconst.Executable) { return assembleV2_0ExecutableRequest(data, attributes) } return nil, fmt.Errorf("received version 2.0 message with unsupported request type %q", requestType) default: return nil, fmt.Errorf("received unsupported request message version %q", version) } } func determineRequestType(payload string) eaconst.RequestType { switch { case strings.HasPrefix(payload, eaconst.ExecutableIdentifier): return eaconst.Executable default: return eaconst.Command } } func deepCopyMap(m map[string]string) map[string]string { newMap := make(map[string]string) for k, v := range m { newMap[k] = v } return newMap } func stripExecutablePrefix(s string) string { after, _ := strings.CutPrefix(s, eaconst.ExecutableIdentifier) return after } func validateAttributes(attributes map[string]string, expectedVersion, expectedType string) error { var err error if actualVersion := attributes[eaconst.VersionKey]; actualVersion != expectedVersion { err = errors.Join(err, fmt.Errorf("version attribute %q should be %q", actualVersion, expectedVersion)) } if actualType := attributes[eaconst.RequestTypeKey]; actualType != expectedType { err = errors.Join(err, fmt.Errorf("type attribute %q should be %q", actualType, expectedType)) } return err } func parsePayload(payload string) (name string, args []string, err error) { if payload == "" { return "", nil, errors.New("payload cannot be empty") } cmdAndArgs, err := shlex.Split(payload) if err != nil { return "", nil, fmt.Errorf("failed to parse payload: %w", err) } return cmdAndArgs[0], cmdAndArgs[1:], nil }