diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d72de94 --- /dev/null +++ b/go.mod @@ -0,0 +1,55 @@ +module github.com/cloudwego/kitex-tests + +go 1.17 + +require ( + github.com/apache/thrift v0.13.0 + github.com/bytedance/gopkg v0.0.0-20230728082804-614d0af6619b + github.com/cloudwego/fastpb v0.0.4 + github.com/cloudwego/kitex v0.8.1-0.20240108100713-a6d5d904434d + github.com/cloudwego/netpoll v0.5.1 + google.golang.org/grpc v1.36.1 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/choleraehyq/pid v0.0.17 // indirect + github.com/cloudwego/configmanager v0.2.0 // indirect + github.com/cloudwego/dynamicgo v0.1.6 // indirect + github.com/cloudwego/frugal v0.1.12 // indirect + github.com/cloudwego/localsession v0.0.2 // indirect + github.com/cloudwego/thriftgo v0.3.5 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/pprof v0.0.0-20220608213341-c488b8fa1db3 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect + github.com/jhump/protoreflect v1.8.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/gls v0.0.0-20220109145502-612d0167dce5 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oleiade/lane v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + github.com/tidwall/gjson v1.9.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.2.0 // indirect + golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 // indirect + golang.org/x/text v0.6.0 // indirect + google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace ( + github.com/apache/thrift => github.com/apache/thrift v0.13.0 + github.com/cloudwego/kitex => github.com/felix021/kitex v0.8.1-0.20240111074411-c4fd7c65edf8 +) diff --git a/grpc/tls/cert/gen.sh b/grpc/tls/cert/gen.sh new file mode 100644 index 0000000..69104a0 --- /dev/null +++ b/grpc/tls/cert/gen.sh @@ -0,0 +1,43 @@ +#! /bin/bash +# Copyright 2024 CloudWeGo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +rm -f *.pem +rm -f *.srl + +# 1. Generate CA's private key and self-signed certificate +openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Tech School/OU=Education/CN=*.techschool.guru/emailAddress=techschool.guru@gmail.com" + +echo "CA's self-signed certificate" +openssl x509 -in ca-cert.pem -noout -text + +# 2. Generate web server's private key and certificate signing request (CSR) +openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=FR/ST=Ile de France/L=Paris/O=PC Book/OU=Computer/CN=*.pcbook.com/emailAddress=pcbook@gmail.com" + +# 3. Use CA's private key to sign web server's CSR and get back the signed certificate +openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem + +echo "Server's signed certificate" +openssl x509 -in server-cert.pem -noout -text + +# 4. Generate client's private key and certificate signing request (CSR) +openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=FR/ST=Alsace/L=Strasbourg/O=PC Client/OU=Computer/CN=*.pcclient.com/emailAddress=pcclient@gmail.com" + +# 5. Use CA's private key to sign client's CSR and get back the signed certificate +openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem + +echo "Client's signed certificate" +openssl x509 -in client-cert.pem -noout -text \ No newline at end of file diff --git a/grpc/tls/grpc_server.go b/grpc/tls/grpc_server.go new file mode 100644 index 0000000..08494e7 --- /dev/null +++ b/grpc/tls/grpc_server.go @@ -0,0 +1,130 @@ +// Copyright 2024 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "io/ioutil" + "net" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + grpc_demo "github.com/cloudwego/kitex-tests/grpc_gen/protobuf/grpc_demo_2" +) + +func RunGRPCTLSServer(hostport string) (*grpc.Server, error) { + tlsCredentials, err := serverLoadTLSCredentials() + if err != nil { + return nil, err + } + cred := grpc.Creds(tlsCredentials) + + svr := grpc.NewServer(cred) + ms := &GrpcServiceA{} + grpc_demo.RegisterServiceAServer(svr, ms) + listener, err := net.Listen("tcp", hostport) + if err != nil { + return nil, err + } + go svr.Serve(listener) + return svr, nil +} + +type GrpcServiceA struct { + grpc_demo.UnimplementedServiceAServer +} + +func (s *GrpcServiceA) CallUnary(ctx context.Context, req *grpc_demo.Request) (*grpc_demo.Reply, error) { + res := &grpc_demo.Reply{Message: req.Name + " Hello!"} + return res, nil +} + +func (s *GrpcServiceA) CallClientStream(stream grpc_demo.ServiceA_CallClientStreamServer) error { + var msgs []string + for { + req, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + return err + } + msgs = append(msgs, req.Name) + } + return stream.SendAndClose(&grpc_demo.Reply{Message: "all message: " + strings.Join(msgs, ", ")}) +} +func (s *GrpcServiceA) CallServerStream(req *grpc_demo.Request, stream grpc_demo.ServiceA_CallServerStreamServer) error { + resp := &grpc_demo.Reply{} + for i := 0; i < 3; i++ { + resp.Message = fmt.Sprintf("%v-%d", req.Name, i) + err := stream.Send(resp) + if err != nil { + return err + } + } + return nil +} +func (s *GrpcServiceA) CallBidiStream(stream grpc_demo.ServiceA_CallBidiStreamServer) error { + for { + recv, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + return err + } + resp := &grpc_demo.Reply{} + resp.Message = recv.Name + err = stream.Send(resp) + if err != nil { + return err + } + } + return nil +} + +func serverLoadTLSCredentials() (credentials.TransportCredentials, error) { + // Load certificate of the CA who signed client's certificate + pemClientCA, err := ioutil.ReadFile("cert/ca-cert.pem") + if err != nil { + return nil, err + } + + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(pemClientCA) { + return nil, fmt.Errorf("failed to add client CA's certificate") + } + + // Load server's certificate and private key + serverCert, err := tls.LoadX509KeyPair("cert/server-cert.pem", "cert/server-key.pem") + if err != nil { + return nil, err + } + + // Create the credentials and return it + config := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.RequireAndVerifyClientCert, // mTLS + ClientCAs: certPool, + } + + return credentials.NewTLS(config), nil +} diff --git a/grpc/tls/kitex_client.go b/grpc/tls/kitex_client.go new file mode 100644 index 0000000..c6d27bf --- /dev/null +++ b/grpc/tls/kitex_client.go @@ -0,0 +1,169 @@ +// Copyright 2024 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "io/ioutil" + "strconv" + + "github.com/cloudwego/kitex/client" + "github.com/cloudwego/kitex/client/callopt" + "github.com/cloudwego/kitex/transport" + + "github.com/cloudwego/kitex-tests/kitex_gen/protobuf/grpc_demo" + "github.com/cloudwego/kitex-tests/kitex_gen/protobuf/grpc_demo/servicea" +) + +type ClientWrapper struct { + client servicea.Client +} + +func GetClient(hostport string, clientOptions ...client.Option) (*ClientWrapper, error) { + opts := append(clientOptions, client.WithTransportProtocol(transport.GRPC), client.WithHostPorts(hostport)) + client, err := servicea.NewClient("kitex-test", opts...) + if err != nil { + return nil, err + } + return &ClientWrapper{client: client}, nil +} + +func (c *ClientWrapper) RunUnary(callOptions ...callopt.Option) (*grpc_demo.Reply, error) { + req := &grpc_demo.Request{Name: "Kitex"} + ctx := context.Background() + return c.client.CallUnary(ctx, req, callOptions...) +} + +func (c *ClientWrapper) RunClientStream(callOptions ...callopt.Option) (*grpc_demo.Reply, error) { + + ctx := context.Background() + streamCli, err := c.client.CallClientStream(ctx) + if err != nil { + return nil, err + } + for i := 0; i < 3; i++ { + req := &grpc_demo.Request{Name: "kitex-" + strconv.Itoa(i)} + err = streamCli.SendMsg(req) + if err != nil { + return nil, err + } + } + return streamCli.CloseAndRecv() +} + +func (c *ClientWrapper) RunServerStream(callOptions ...callopt.Option) ([]*grpc_demo.Reply, error) { + ctx := context.Background() + req := &grpc_demo.Request{Name: "kitex"} + streamCli, err := c.client.CallServerStream(ctx, req) + if err != nil { + return nil, err + } + replies := []*grpc_demo.Reply{} + for { + reply, er := streamCli.Recv() + if er != nil { + if er != io.EOF { + return nil, er + } + break + } + replies = append(replies, reply) + } + return replies, nil +} + +func (c *ClientWrapper) RunBidiStream(callOptions ...callopt.Option) ([]*grpc_demo.Reply, error) { + + ctx := context.Background() + stream, err := c.client.CallBidiStream(ctx) + if err != nil { + return nil, err + } + errChan := make(chan error) + go func() { + for i := 0; i < 3; i++ { + req := &grpc_demo.Request{Name: "kitex-" + strconv.Itoa(i)} + err = stream.Send(req) + if err != nil { + errChan <- err + return + } + } + er := stream.Close() + if er != nil { + errChan <- er + return + } + errChan <- nil + }() + repliesChan := make(chan []*grpc_demo.Reply) + errRecvChan := make(chan error) + go func() { + replies := []*grpc_demo.Reply{} + for { + reply, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + errRecvChan <- err + return + } + replies = append(replies, reply) + } + repliesChan <- replies + }() + err = <-errChan + if err != nil { + return nil, err + } + select { + case replies := <-repliesChan: + return replies, nil + case err = <-errRecvChan: + return nil, err + } +} + +func clientLoadTLSCredentials() (*tls.Config, error) { + // Load certificate of the CA who signed server's certificate + pemServerCA, err := ioutil.ReadFile("cert/ca-cert.pem") + if err != nil { + return nil, err + } + + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(pemServerCA) { + return nil, fmt.Errorf("failed to add server CA's certificate") + } + + // Load client's certificate and private key + clientCert, err := tls.LoadX509KeyPair("cert/client-cert.pem", "cert/client-key.pem") + if err != nil { + return nil, err + } + + // Create the credentials and return it + config := &tls.Config{ + Certificates: []tls.Certificate{clientCert}, + RootCAs: certPool, + InsecureSkipVerify: true, + } + return config, nil +} diff --git a/grpc/tls/tls_test.go b/grpc/tls/tls_test.go new file mode 100644 index 0000000..02aeff2 --- /dev/null +++ b/grpc/tls/tls_test.go @@ -0,0 +1,91 @@ +// Copyright 2024 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tls + +import ( + "context" + "errors" + "log" + "os/exec" + "path/filepath" + "testing" + "time" + + client_opt "github.com/cloudwego/kitex/client" + "github.com/cloudwego/kitex/pkg/endpoint" + "github.com/cloudwego/kitex/pkg/rpcinfo" + + "github.com/cloudwego/kitex-tests/pkg/test" +) + +func TestMain(m *testing.M) { + err := generateCert() + if err != nil { + log.Printf("TLS test failed, error: %v", err) + return + } + m.Run() +} + +// Only Kitex gPRC client support TLS now. +func TestKitexClientGRPCTLSWithGRPCServer(t *testing.T) { + hostport := "localhost:9020" + svr, err := RunGRPCTLSServer(hostport) + test.Assert(t, err == nil, err) + defer svr.Stop() + + time.Sleep(time.Second) + cfg, err := clientLoadTLSCredentials() + test.Assert(t, err == nil, err) + client, err := GetClient(hostport, client_opt.WithMiddleware(ServiceNameMW), client_opt.WithGRPCTLSConfig(cfg)) + test.Assert(t, err == nil, err) + resp, err := client.RunUnary() + test.Assert(t, err == nil, err) + test.Assert(t, resp != nil && resp.Message == "Kitex Hello!") + resp, err = client.RunClientStream() + test.Assert(t, err == nil, err) + test.Assert(t, resp != nil && resp.Message == "all message: kitex-0, kitex-1, kitex-2") + respArr, err := client.RunServerStream() + test.Assert(t, err == nil, err) + test.Assert(t, len(respArr) == 3 && respArr[0].Message == "kitex-0" && respArr[1].Message == "kitex-1" && respArr[2].Message == "kitex-2") + respArr, err = client.RunBidiStream() + test.Assert(t, err == nil, err) + test.Assert(t, len(respArr) == 3 && respArr[0].Message == "kitex-0" && respArr[1].Message == "kitex-1" && respArr[2].Message == "kitex-2") +} + +// For modifying package name to avoid collision +func ServiceNameMW(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request, response interface{}) error { + + ri := rpcinfo.GetRPCInfo(ctx) + ink := ri.Invocation() + if ink, ok := ink.(rpcinfo.InvocationSetter); ok { + ink.SetPackageName("grpc_demo_2") + } else { + return errors.New("the interface Invocation doesn't implement InvocationSetter") + } + return next(ctx, request, response) + } +} + +func generateCert() error { + cmd := exec.Command("/bin/bash", "gen.sh") + path, err := filepath.Abs("cert") + if err != nil { + return err + } + cmd.Dir = path + return cmd.Run() +}