diff --git a/integration/fixtures/grpc/config.toml b/integration/fixtures/grpc/config.toml new file mode 100644 index 000000000..9fc82b676 --- /dev/null +++ b/integration/fixtures/grpc/config.toml @@ -0,0 +1,29 @@ +defaultEntryPoints = ["https"] + +RootCAs = [ """{{ .CertContent }}""" ] + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = """{{ .CertContent }}""" + KeyFile = """{{ .KeyContent }}""" + + +[web] + address = ":8080" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "https://127.0.0.1:{{ .GRPCServerPort }}" + + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host:127.0.0.1" diff --git a/integration/grpc_test.go b/integration/grpc_test.go new file mode 100644 index 000000000..2db8406e8 --- /dev/null +++ b/integration/grpc_test.go @@ -0,0 +1,114 @@ +package integration + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net" + "os" + "time" + + "github.com/containous/traefik/integration/helloworld" + "github.com/containous/traefik/integration/try" + "github.com/go-check/check" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +var LocalhostCert []byte +var LocalhostKey []byte + +// GRPCSuite +type GRPCSuite struct{ BaseSuite } + +type myserver struct{} + +func (suite *GRPCSuite) SetUpSuite(c *check.C) { + var err error + LocalhostCert, err = ioutil.ReadFile("./resources/tls/local.cert") + c.Assert(err, check.IsNil) + LocalhostKey, err = ioutil.ReadFile("./resources/tls/local.key") + c.Assert(err, check.IsNil) +} + +func (s *myserver) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) { + return &helloworld.HelloReply{Message: "Hello " + in.Name}, nil +} + +func startGRPCServer(lis net.Listener) error { + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + if err != nil { + return err + } + + creds := credentials.NewServerTLSFromCert(&cert) + serverOption := grpc.Creds(creds) + + var s *grpc.Server = grpc.NewServer(serverOption) + defer s.Stop() + + helloworld.RegisterGreeterServer(s, &myserver{}) + return s.Serve(lis) +} + +func callHelloClientGRPC() (string, error) { + roots := x509.NewCertPool() + roots.AppendCertsFromPEM(LocalhostCert) + credsClient := credentials.NewClientTLSFromCert(roots, "") + conn, err := grpc.Dial("127.0.0.1:4443", grpc.WithTransportCredentials(credsClient)) + if err != nil { + return "", err + } + + defer conn.Close() + client := helloworld.NewGreeterClient(conn) + + name := "World" + r, err := client.SayHello(context.Background(), &helloworld.HelloRequest{Name: name}) + if err != nil { + return "", err + } + return r.Message, nil +} + +func (suite *GRPCSuite) TestGRPC(c *check.C) { + lis, err := net.Listen("tcp", ":0") + _, port, err := net.SplitHostPort(lis.Addr().String()) + c.Assert(err, check.IsNil) + + go func() { + err := startGRPCServer(lis) + c.Log(err) + c.Assert(err, check.IsNil) + }() + + file := suite.adaptFile(c, "fixtures/grpc/config.toml", struct { + CertContent string + KeyContent string + GRPCServerPort string + }{ + CertContent: string(LocalhostCert), + KeyContent: string(LocalhostKey), + GRPCServerPort: port, + }) + + defer os.Remove(file) + cmd, _ := suite.cmdTraefik(withConfigFile(file)) + + err = cmd.Start() + c.Assert(err, check.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:127.0.0.1")) + c.Assert(err, check.IsNil) + var response string + err = try.Do(1*time.Second, func() error { + response, err = callHelloClientGRPC() + return err + }) + + c.Assert(err, check.IsNil) + c.Assert(response, check.Equals, "Hello World") +} diff --git a/integration/helloworld/helloworld.pb.go b/integration/helloworld/helloworld.pb.go new file mode 100644 index 000000000..64bd1ef3c --- /dev/null +++ b/integration/helloworld/helloworld.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: helloworld.proto + +/* +Package helloworld is a generated protocol buffer package. + +It is generated from these files: + helloworld.proto + +It has these top-level messages: + HelloRequest + HelloReply +*/ +package helloworld + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// The request message containing the user's name. +type HelloRequest struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *HelloRequest) Reset() { *m = HelloRequest{} } +func (m *HelloRequest) String() string { return proto.CompactTextString(m) } +func (*HelloRequest) ProtoMessage() {} +func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *HelloRequest) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +// The response message containing the greetings +type HelloReply struct { + Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` +} + +func (m *HelloReply) Reset() { *m = HelloReply{} } +func (m *HelloReply) String() string { return proto.CompactTextString(m) } +func (*HelloReply) ProtoMessage() {} +func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *HelloReply) GetMessage() string { + if m != nil { + return m.Message + } + return "" +} + +func init() { + proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") + proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for Greeter service + +type GreeterClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc *grpc.ClientConn +} + +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Greeter service + +type GreeterServer interface { + // Sends a greeting + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { + s.RegisterService(&_Greeter_serviceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Greeter_serviceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "helloworld.proto", +} + +func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 175 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, + 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, + 0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, + 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, + 0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, + 0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a, + 0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64, + 0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x14, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0x19, 0x70, 0x49, 0x67, 0xe6, + 0xeb, 0xa5, 0x17, 0x15, 0x24, 0xeb, 0xa5, 0x56, 0x24, 0xe6, 0x16, 0xe4, 0xa4, 0x16, 0x23, 0xa9, + 0x75, 0xe2, 0x07, 0x2b, 0x0e, 0x07, 0xb1, 0x03, 0x40, 0x5e, 0x0a, 0x60, 0x4c, 0x62, 0x03, 0xfb, + 0xcd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00, +} diff --git a/integration/helloworld/helloworld.proto b/integration/helloworld/helloworld.proto new file mode 100644 index 000000000..c8323bd40 --- /dev/null +++ b/integration/helloworld/helloworld.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 8560e24b0..4ff958b1b 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -39,6 +39,7 @@ func init() { check.Suite(&DynamoDBSuite{}) check.Suite(&ErrorPagesSuite{}) check.Suite(&WebsocketSuite{}) + check.Suite(&GRPCSuite{}) } var traefikBinary = "../dist/traefik" diff --git a/integration/resources/tls/README.md b/integration/resources/tls/README.md new file mode 100644 index 000000000..1fb53c461 --- /dev/null +++ b/integration/resources/tls/README.md @@ -0,0 +1,10 @@ +# TLS certificate description + +## local.crt / local.key + +Generate with +```bash +go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,localhost --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +mv cert.pem local.cert +mv key.pem local.key +``` diff --git a/integration/resources/tls/local.cert b/integration/resources/tls/local.cert new file mode 100644 index 000000000..7f1561863 --- /dev/null +++ b/integration/resources/tls/local.cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICEjCCAXugAwIBAgIRAKdsOQKo42YbSWoXHFxiMBwwDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 +MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA1YHPs3TZnMcIOManaIsv1qZ2I9FhbxOwP88tBH751IcYSwF5Qm40MOUq +sWvCMEFa+Bv5ke8E0ybX/za7eWz/hNg+FvTLbi+7c2fSkv+79w9554pzD22fb/mQ +LuYz/r/RpQguJSRh9cgpdRFY/zgzMWCqRGCC+MOin9D25pCAUJ8CAwEAAaNmMGQw +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF +MAMBAf8wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAA +AAABMA0GCSqGSIb3DQEBCwUAA4GBAEw7efpVsoNmJFc3tQAjHw3GW/N7co9iP/nY +g8gBYtbqQQapFCQ7/dPCNm+fZWjx05S9i/4c/c6sl2dR5lJLPtjG/2AY/LPNq7/N +/BU4KPVF+yi21SoK6GG09hnfbeT5xcvQxa+LNFiTNMHwzolgNhxxabxYPaOQZvyD +wZ6WZa5z +-----END CERTIFICATE----- diff --git a/integration/resources/tls/local.key b/integration/resources/tls/local.key new file mode 100644 index 000000000..69200238b --- /dev/null +++ b/integration/resources/tls/local.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDVgc+zdNmcxwg4xqdoiy/WpnYj0WFvE7A/zy0EfvnUhxhLAXlC +bjQw5Sqxa8IwQVr4G/mR7wTTJtf/Nrt5bP+E2D4W9MtuL7tzZ9KS/7v3D3nninMP +bZ9v+ZAu5jP+v9GlCC4lJGH1yCl1EVj/ODMxYKpEYIL4w6Kf0PbmkIBQnwIDAQAB +AoGAPkAO8t/K4u8U8pjZDp4mYItAPsk5g01WQgSBt6Hd7Z1F8/iGBlxjUy49+GTW +xYMIpvZhGa961KWqrHqkxo6FIQmhiuMRfk8JGH0c9ibe7rGP05EsJOWz/X2k//4A +xfl6P5POMNAbyS18dONQSzMYdMgYkuoiGH/A0tXMFRrRNgECQQDWOMXJiNJEHG0J +2ro+S2Mp0KTBMiVWh+Ee/yTgNIkpDXy45mCBx52vBm7/9sIDkol/XWGn/6cYD6bd +eRJIdkNBAkEA/yVbZYHvI4YO6XGdAUBFph+E5yklcXyWEQjKiFGdAEg+M1YRgZVX +0C3DdV34x3gW9wot3xWVtHPNSy3w6cv73wJAWaQoH81BL381oYoFpUumkzjbuHxj +Y3I4od/ibm+NdcBPEJBWkfgV48etay62wQfwwXsyAjrYkRj7mnGvVOMoAQJBAKFm +Dz/SBuVw2yP/E7OD5cslaxwTYjU8+20BI6VCA0/3Yyl0S5SuVSNCn78x17rOk7Bo +RwY0kEPbcUUaZahvuf8CQAQtu9hkyRo8jaEr+XLT3uhyBpon34nrbcSwK11lf7S6 +ZQ60JoL3T0iridIinjDx/yL1A7anOfpSIiilNMFTfPQ= +-----END RSA PRIVATE KEY-----