sanitize provisioner output strings

The grpc protocol requires strings to be valid utf8, but because
provisioners often don't have control over the command output, invalid
utf8 sequences can make it into the response causing grpc transport
errors.

Replace all invalid utf sequences with the standard utf replacement
character in the provisioner output. The code is a direct copy from the
go1.13 std library, and can be replaced with strings.ToValidUTF8 once
it's available.
This commit is contained in:
James Bardin 2019-11-06 14:27:28 -05:00
parent fa12e9f7d9
commit 49439d02d1
2 changed files with 133 additions and 2 deletions

View File

@ -2,6 +2,8 @@ package plugin
import ( import (
"log" "log"
"strings"
"unicode/utf8"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
proto "github.com/hashicorp/terraform/internal/tfplugin5" proto "github.com/hashicorp/terraform/internal/tfplugin5"
@ -90,7 +92,7 @@ type uiOutput struct {
func (o uiOutput) Output(s string) { func (o uiOutput) Output(s string) {
err := o.srv.Send(&proto.ProvisionResource_Response{ err := o.srv.Send(&proto.ProvisionResource_Response{
Output: s, Output: toValidUTF8(s, string(utf8.RuneError)),
}) })
if err != nil { if err != nil {
log.Printf("[ERROR] %s", err) log.Printf("[ERROR] %s", err)
@ -145,3 +147,55 @@ func (s *GRPCProvisionerServer) Stop(_ context.Context, req *proto.Stop_Request)
return resp, nil return resp, nil
} }
// FIXME: backported from go1.13 strings package, remove once terraform is
// using go >= 1.13
// ToValidUTF8 returns a copy of the string s with each run of invalid UTF-8 byte sequences
// replaced by the replacement string, which may be empty.
func toValidUTF8(s, replacement string) string {
var b strings.Builder
for i, c := range s {
if c != utf8.RuneError {
continue
}
_, wid := utf8.DecodeRuneInString(s[i:])
if wid == 1 {
b.Grow(len(s) + len(replacement))
b.WriteString(s[:i])
s = s[i:]
break
}
}
// Fast path for unchanged input
if b.Cap() == 0 { // didn't call b.Grow above
return s
}
invalid := false // previous byte was from an invalid UTF-8 sequence
for i := 0; i < len(s); {
c := s[i]
if c < utf8.RuneSelf {
i++
invalid = false
b.WriteByte(c)
continue
}
_, wid := utf8.DecodeRuneInString(s[i:])
if wid == 1 {
i++
if !invalid {
invalid = true
b.WriteString(replacement)
}
continue
}
invalid = false
b.WriteString(s[i : i+wid])
i += wid
}
return b.String()
}

View File

@ -1,5 +1,82 @@
package plugin package plugin
import proto "github.com/hashicorp/terraform/internal/tfplugin5" import (
"testing"
"unicode/utf8"
"github.com/golang/mock/gomock"
"github.com/hashicorp/terraform/helper/schema"
proto "github.com/hashicorp/terraform/internal/tfplugin5"
mockproto "github.com/hashicorp/terraform/plugin/mock_proto"
"github.com/hashicorp/terraform/terraform"
context "golang.org/x/net/context"
)
var _ proto.ProvisionerServer = (*GRPCProvisionerServer)(nil) var _ proto.ProvisionerServer = (*GRPCProvisionerServer)(nil)
type validUTF8Matcher string
func (m validUTF8Matcher) Matches(x interface{}) bool {
resp := x.(*proto.ProvisionResource_Response)
return utf8.Valid([]byte(resp.Output))
}
func (m validUTF8Matcher) String() string {
return string(m)
}
func mockProvisionerServer(t *testing.T, c *gomock.Controller) *mockproto.MockProvisioner_ProvisionResourceServer {
server := mockproto.NewMockProvisioner_ProvisionResourceServer(c)
server.EXPECT().Send(
validUTF8Matcher("check for valid utf8"),
).Return(nil)
return server
}
// ensure that a provsioner cannot return invalid utf8 which isn't allowed in
// the grpc protocol.
func TestProvisionerInvalidUTF8(t *testing.T) {
p := &schema.Provisioner{
ConnSchema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
out := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
out.Output("invalid \xc3\x28\n")
return nil
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
srv := mockProvisionerServer(t, ctrl)
cfg := &proto.DynamicValue{
Msgpack: []byte("\x81\xa3foo\x01"),
}
conn := &proto.DynamicValue{
Msgpack: []byte("\x81\xa3foo\xa4host"),
}
provisionerServer := NewGRPCProvisionerServerShim(p)
req := &proto.ProvisionResource_Request{
Config: cfg,
Connection: conn,
}
if err := provisionerServer.ProvisionResource(req, srv); err != nil {
t.Fatal(err)
}
}