Repository: go-kratos/kratos (go)

Issue:
kratos http parse pb message body panic
#### What happened:

使用下列的pb生成 http代码， 使用 json payload正常， 但使用pb序列化后， 解析 request panic

``` protobuf
// 客户端
message Client {
  option (google.api.resource) = {
    type: "v1.greeter.test.example.com/Client"
    pattern: "clients/{client_int32}"
  };

  // 资源名称
  string name = 1;

  // id
  int32 id = 2;

  // 状态
  ClientStatus status = 3 [(google.api.field_behavior) = OUTPUT_ONLY];

  string display_name = 4 ;
}
```
``` protobuf
service ExampleService {
  // 创建Client
  // (-- api-linter: core::0133::method-signature=disabled
  //     aip.dev/not-precedent: 顶层资源无需parent --)
  rpc CreateClient(CreateClientRequest) returns (CreateClientResponse) {
    option (google.api.method_signature) = "client";
    option (google.api.http) = {
      post: "/api/v1/clients"
      body: "client"
    };
  }
}

// 创建Client请求
message CreateClientRequest {
  // 创建Client
  Client client = 1 [(google.api.field_behavior) = REQUIRED];
}

// 创建Client响应
message CreateClientResponse {
  // 创建Client响应
  Client client = 1 [(google.api.field_behavior) = REQUIRED];
}
```

从json转成pb，后端无需修改代码， 前端修改 `Content-Type : application/proto`.   具体的代码实现在  `github.com/go-kratos/kratos/v2/transport/http/codec.go`

``` go
// CodecForRequest get encoding.Codec via http.Request
func CodecForRequest(r *http.Request, name string) (encoding.Codec, bool) {
	for _, accept := range r.Header[name] {
		codec := encoding.GetCodec(httputil.ContentSubtype(accept))
		if codec != nil {
			return codec, true
		}
	}
	return encoding.GetCodec("json"), false
}
```

## 原因

panic 日志

```
2023/04/12 14:16:11 http: panic serving 192.168.105.20:49928: interface conversion: **v1.Client is not protoreflect.ProtoMessage: missing method ProtoReflect
goroutine 107 [running]:
net/http.(*conn).serve.func1()
	/data/go1.19/src/net/http/server.go:1850 +0x148
panic({0x13f86a0, 0xc0004aa480})
	/data/go1.19/src/runtime/panic.go:890 +0x267
github.com/go-kratos/kratos/v2/encoding/proto.codec.Unmarshal({}, {0xc000570400, 0x13, 0x200}, {0x134db20, 0xc0004aa478})
	/data/common/kratos-layout/vendor/github.com/go-kratos/kratos/v2/encoding/proto/proto.go:26 +0x67
github.com/go-kratos/kratos/v2/transport/http.DefaultRequestDecoder(0xc00050cc00, {0x134db20, 0xc0004aa478})
	/data/common/kratos-layout/vendor/github.com/go-kratos/kratos/v2/transport/http/codec.go:72 +0x286
github.com/go-kratos/kratos/v2/transport/http.(*wrapper).Bind(0xc000518980, {0x134db20, 0xc0004aa478})
	/data/common/kratos-layout/vendor/github.com/go-kratos/kratos/v2/transport/http/context.go:99 +0x62
go.example.com/apis/lixin/test/kratos-layout/v1._KratoLayoutService_CreateClient0_HTTP_Handler.func1({0x7f03b05effc8, 0xc000518980})
```
`github.com/go-kratos/kratos/v2/encoding/proto/proto.go`
``` go
func (codec) Unmarshal(data []byte, v interface{}) error {
       // v 是一个二级指针 (**Client)
       // v 是一个二级指针 (**Client)
       // v 是一个二级指针 (**Client)
	return proto.Unmarshal(data, v.(proto.Message))
}
```

生成的Go代码如下

``` go
// 创建Client请求
type CreateClientRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	// 注意这里是指针
	// 注意这里是指针
	// 注意这里是指针
	Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"`
}

func _KratoLayoutService_CreateClient0_HTTP_Handler(srv KratoLayoutServiceHTTPServer) func(ctx http.Context) error {
	return func(ctx http.Context) error {
		var in CreateClientRequest

                // 这里进一步取指针的地址
                // 这里进一步取指针的地址
                // 这里进一步取指针的地址
		if err := ctx.Bind(&in.Client); err != nil {
			return err
		}

                // 。。。。。。。
		return ctx.Result(200, reply)
	}
}
```


![image](https://user-images.githubusercontent.com/2350353/231374214-0c1af92d-498b-47d4-ac65-357ef5e5c0d6.png)


#### What you expected to happen:

#### How to reproduce it (as minimally and precisely as possible):

#### Anything else we need to know?:

#### Environment:
- Kratos version (use `kratos -v`): 	github.com/go-kratos/kratos/v2 v2.5.3
- Go version (use `go version`):   1.19
- OS (e.g: `cat /etc/os-release`):

NAME="Ubuntu"
VERSION="20.04.3 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.3 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal


- Others:


Pull request description:
fix: http parse pb message body panic
fix #2782 


Patch:
diff --git a/encoding/proto/proto.go b/encoding/proto/proto.go
index 0da5e0dc..8c6ae58d 100644
--- a/encoding/proto/proto.go
+++ b/encoding/proto/proto.go
@@ -3,6 +3,9 @@
 package proto
 
 import (
+	"errors"
+	"reflect"
+
 	"google.golang.org/protobuf/proto"
 
 	"github.com/go-kratos/kratos/v2/encoding"
@@ -23,9 +26,26 @@ func (codec) Marshal(v interface{}) ([]byte, error) {
 }
 
 func (codec) Unmarshal(data []byte, v interface{}) error {
-	return proto.Unmarshal(data, v.(proto.Message))
+	pm, err := getProtoMessage(v)
+	if err != nil {
+		return err
+	}
+	return proto.Unmarshal(data, pm)
 }
 
 func (codec) Name() string {
 	return Name
 }
+
+func getProtoMessage(v interface{}) (proto.Message, error) {
+	if msg, ok := v.(proto.Message); ok {
+		return msg, nil
+	}
+	val := reflect.ValueOf(v)
+	if val.Kind() != reflect.Ptr {
+		return nil, errors.New("not proto message")
+	}
+
+	val = val.Elem()
+	return getProtoMessage(val.Interface())
+}


Test patch:
diff --git a/encoding/proto/proto_test.go b/encoding/proto/proto_test.go
index 1d33b339..2c2ffece 100644
--- a/encoding/proto/proto_test.go
+++ b/encoding/proto/proto_test.go
@@ -44,3 +44,61 @@ func TestCodec(t *testing.T) {
 		t.Errorf("Hobby should be %s, but got %s", res.Hobby, model.Hobby)
 	}
 }
+
+func TestCodec2(t *testing.T) {
+	c := new(codec)
+
+	model := testData.TestModel{
+		Id:    1,
+		Name:  "kratos",
+		Hobby: []string{"study", "eat", "play"},
+	}
+
+	m, err := c.Marshal(&model)
+	if err != nil {
+		t.Errorf("Marshal() should be nil, but got %s", err)
+	}
+
+	var res testData.TestModel
+	rp := &res
+
+	err = c.Unmarshal(m, &rp)
+	if err != nil {
+		t.Errorf("Unmarshal() should be nil, but got %s", err)
+	}
+	if !reflect.DeepEqual(res.Id, model.Id) {
+		t.Errorf("ID should be %d, but got %d", res.Id, model.Id)
+	}
+	if !reflect.DeepEqual(res.Name, model.Name) {
+		t.Errorf("Name should be %s, but got %s", res.Name, model.Name)
+	}
+	if !reflect.DeepEqual(res.Hobby, model.Hobby) {
+		t.Errorf("Hobby should be %s, but got %s", res.Hobby, model.Hobby)
+	}
+}
+
+func Test_getProtoMessage(t *testing.T) {
+	p := &testData.TestModel{Id: 1}
+	type args struct {
+		v interface{}
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{name: "test1", args: args{v: &testData.TestModel{}}, wantErr: false},
+		{name: "test2", args: args{v: testData.TestModel{}}, wantErr: true},
+		{name: "test3", args: args{v: &p}, wantErr: false},
+		{name: "test4", args: args{v: 1}, wantErr: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := getProtoMessage(tt.args.v)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("getProtoMessage() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}