// The MIT License // // Copyright (c) 2021 Temporal Technologies Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package converter import ( "bytes" "compress/zlib" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "path" "strings" "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" commonpb "go.temporal.io/api/common/v1" ) // PayloadEncoder is an encoder that encodes or decodes the given payload. // // These can be used (and even chained) in NewEncodingDataConverter. For // example, NewZlibEncoder returns a PayloadEncoder that can be used for // compression. type PayloadEncoder interface { // Encode optionally encodes the given payload which is guaranteed to never // be nil. The byte slices of the payload's metadata or data should never be // mutated directly, but they can be referenced or replaced. Encode(*commonpb.Payload) error // Decode optionally decodes the given payload which is guaranteed to never // be nil. The byte slices of the payload's metadata or data should never be // mutated directly, but they can be referenced or replaced. // // For compatibility reasons, implementers should take care not to decode // payloads that were not previously encoded. Decode(*commonpb.Payload) error } // ZlibEncoderOptions are options for NewZlibEncoder. All fields are optional. type ZlibEncoderOptions struct { // If true, the zlib encoder will encode the contents even if there is no size // benefit. Otherwise, the zlib encoder will only use the encoded value if it // is smaller. AlwaysEncode bool } type zlibEncoder struct{ options ZlibEncoderOptions } // NewZlibEncoder creates a PayloadEncoder for use in NewEncodingDataConverter // to support zlib payload compression. // // While this serves as a reasonable example of a compression encoder, callers // may prefer alternative compression algorithms for lots of small payloads. func NewZlibEncoder(options ZlibEncoderOptions) PayloadEncoder { return &zlibEncoder{options} } func (z *zlibEncoder) Encode(p *commonpb.Payload) error { // Marshal and write b, err := proto.Marshal(p) if err != nil { return err } var buf bytes.Buffer w := zlib.NewWriter(&buf) _, err = w.Write(b) if closeErr := w.Close(); closeErr != nil && err == nil { err = closeErr } if err != nil { return err } // Only set if smaller than original amount or has option to always encode if buf.Len() < len(b) || z.options.AlwaysEncode { p.Metadata = map[string][]byte{MetadataEncoding: []byte("binary/zlib")} p.Data = buf.Bytes() } return nil } func (*zlibEncoder) Decode(p *commonpb.Payload) error { // Only if it's our encoding if string(p.Metadata[MetadataEncoding]) != "binary/zlib" { return nil } r, err := zlib.NewReader(bytes.NewReader(p.Data)) if err != nil { return err } // Read all and unmarshal b, err := ioutil.ReadAll(r) if closeErr := r.Close(); closeErr != nil && err == nil { err = closeErr } if err != nil { return err } p.Reset() return proto.Unmarshal(b, p) } // EncodingDataConverter is a DataConverter that wraps an underlying data // converter and supports chained encoding of just the payload without regard // for serialization to/from actual types. type EncodingDataConverter struct { parent DataConverter encoders []PayloadEncoder } var _ DataConverter = &EncodingDataConverter{} // NewEncodingDataConverter wraps the given parent DataConverter and performs // encoding/decoding on the payload via the given encoders. When encoding for // ToPayload(s), the encoders are applied last to first meaning the earlier // encoders wrap the later ones. When decoding for FromPayload(s) and // ToString(s), the encoders are applied first to last to reverse the effect. func NewEncodingDataConverter(parent DataConverter, encoders ...PayloadEncoder) *EncodingDataConverter { return &EncodingDataConverter{parent, encoders} } // ToPayload implements DataConverter.ToPayload performing encoding on the // result of the parent's ToPayload call. func (e *EncodingDataConverter) ToPayload(value interface{}) (*commonpb.Payload, error) { payload, err := e.parent.ToPayload(value) if payload == nil || err != nil { return payload, err } return e.toPayload(payload) } // ToPayloads implements DataConverter.ToPayloads performing encoding on the // result of the parent's ToPayloads call. func (e *EncodingDataConverter) ToPayloads(value ...interface{}) (*commonpb.Payloads, error) { payloads, err := e.parent.ToPayloads(value...) if payloads == nil || err != nil { return payloads, err } newPayloads := &commonpb.Payloads{Payloads: make([]*commonpb.Payload, len(payloads.Payloads))} for i, payload := range payloads.Payloads { if newPayloads.Payloads[i], err = e.toPayload(payload); err != nil { return nil, err } } return newPayloads, nil } func (e *EncodingDataConverter) toPayload(payload *commonpb.Payload) (*commonpb.Payload, error) { if payload == nil { return nil, nil } // Clone to not affect caller payload = partiallyClonePayload(payload) // Iterate backwards converting for i := len(e.encoders) - 1; i >= 0; i-- { if err := e.encoders[i].Encode(payload); err != nil { return nil, err } } return payload, nil } // FromPayload implements DataConverter.FromPayload performing decoding on the // given payload before sending to the parent FromPayload. func (e *EncodingDataConverter) FromPayload(payload *commonpb.Payload, valuePtr interface{}) error { payload, err := e.fromPayload(payload) if err != nil { return err } return e.parent.FromPayload(payload, valuePtr) } // FromPayloads implements DataConverter.FromPayloads performing decoding on the // given payloads before sending to the parent FromPayloads. func (e *EncodingDataConverter) FromPayloads(payloads *commonpb.Payloads, valuePtrs ...interface{}) error { if payloads == nil { return e.parent.FromPayloads(payloads, valuePtrs...) } newPayloads := &commonpb.Payloads{Payloads: make([]*commonpb.Payload, len(payloads.Payloads))} for i, payload := range payloads.Payloads { var err error if newPayloads.Payloads[i], err = e.fromPayload(payload); err != nil { return err } } return e.parent.FromPayloads(newPayloads, valuePtrs...) } func (e *EncodingDataConverter) fromPayload(payload *commonpb.Payload) (*commonpb.Payload, error) { // If the payload is not the expected encoding, do not convert it if payload == nil { return nil, nil } // Clone to not affect caller payload = partiallyClonePayload(payload) // Iterate forwards converting for _, encoder := range e.encoders { if err := encoder.Decode(payload); err != nil { return nil, err } } return payload, nil } // ToString implements DataConverter.ToString performing decoding on the given // payload before sending to the parent ToString. func (e *EncodingDataConverter) ToString(input *commonpb.Payload) string { input, err := e.fromPayload(input) if err != nil { return err.Error() } return e.parent.ToString(input) } // ToStrings implements DataConverter.ToStrings using ToString for each value. func (e *EncodingDataConverter) ToStrings(input *commonpb.Payloads) []string { if input == nil { return nil } strs := make([]string, len(input.Payloads)) for i, payload := range input.Payloads { strs[i] = e.ToString(payload) } return strs } // Only copies metadata in shallow way, not byte slice func partiallyClonePayload(p *commonpb.Payload) *commonpb.Payload { ret := &commonpb.Payload{Metadata: make(map[string][]byte, len(p.Metadata)), Data: p.Data} for k, v := range p.Metadata { ret.Metadata[k] = v } return ret } const remotePayloadEncoderEncodePath = "/encode" const remotePayloadEncoderDecodePath = "/decode" type encoderHTTPHandler struct { encoder PayloadEncoder } // ServeHTTP implements the http.Handler interface. func (e *encoderHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.NotFound(w, r) return } path := r.URL.Path if !strings.HasSuffix(path, remotePayloadEncoderEncodePath) && !strings.HasSuffix(path, remotePayloadEncoderDecodePath) { http.NotFound(w, r) return } var p commonpb.Payload if r.Body == nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } err := jsonpb.Unmarshal(r.Body, &p) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } switch { case strings.HasSuffix(path, remotePayloadEncoderEncodePath): err = e.encoder.Encode(&p) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } case strings.HasSuffix(path, remotePayloadEncoderDecodePath): err = e.encoder.Decode(&p) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } default: http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(p) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } } // NewPayloadEncoderHTTPHandler creates a http.Handler for a PayloadEncoder. // This can be used to provide a remote data converter. func NewPayloadEncoderHTTPHandler(e PayloadEncoder) http.Handler { return &encoderHTTPHandler{encoder: e} } // RemotePayloadEncoderOptions are options for NewRemotePayloadEncoder. // Client is optional. type RemotePayloadEncoderOptions struct { Endpoint string Client http.Client } type remotePayloadEncoder struct { options RemotePayloadEncoderOptions } // NewRemotePayloadEncoder creates a PayloadEncoder that uses a remote endpoint to encode/decode. func NewRemotePayloadEncoder(options RemotePayloadEncoderOptions) PayloadEncoder { return &remotePayloadEncoder{options} } func (rdc *remotePayloadEncoder) sendHTTP(endpoint string, p *commonpb.Payload) error { payload, err := json.Marshal(p) if err != nil { return fmt.Errorf("unable to marshal payload: %w", err) } req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload)) if err != nil { return fmt.Errorf("unable to build request: %w", err) } req.Header.Set("Content-Type", "application/json") response, err := rdc.options.Client.Do(req) if err != nil { return err } defer func() { _ = response.Body.Close() }() if response.StatusCode == 200 { err = jsonpb.Unmarshal(response.Body, p) if err != nil { return fmt.Errorf("unable to unmarshal payload: %w", err) } return nil } message, _ := io.ReadAll(response.Body) return fmt.Errorf("%s: %s", http.StatusText(response.StatusCode), message) } // Encode sends a payload to remote payload encoder and returns the encoded payload. func (rdc *remotePayloadEncoder) Encode(p *commonpb.Payload) error { return rdc.sendHTTP(path.Join(rdc.options.Endpoint, remotePayloadEncoderEncodePath), p) } // Decode sends a payload to a remote payload encoder and returns the decoded payload. func (rdc *remotePayloadEncoder) Decode(p *commonpb.Payload) error { return rdc.sendHTTP(path.Join(rdc.options.Endpoint, remotePayloadEncoderDecodePath), p) }