diff --git a/HISTORY.md b/HISTORY.md index d0fe7105..9ebbacce 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -176,6 +176,14 @@ Here is a preview of what the new Hero handlers look like: Other Improvements: +- [gRPC](https://grpc.io/) features: + - New Router [Wrapper](middleware/grpc). + - New MVC `.Handle(ctrl, mvc.GRPC{...})` option which allows to register gRPC services per-party (without the requirement of a full wrapper) and optionally strict access to gRPC clients only, see the [example here](_examples/mvc/grpc-compatible). + +- Improved logging (with `app.Logger().SetLevel("debug")`) for MVC-registered routes. + +- New `iris.WithLowercaseRouting` option which forces all routes' paths to be lowercase and converts request paths to their lowercase for matching. + - New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/http_request/read-json-struct-validation) example was updated. - A result of can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body. @@ -184,10 +192,16 @@ Other Improvements: - Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. -- Improve Remote Address parsing as requested at: https://github.com/kataras/iris/issues/1453. Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`. +- Improve Remote Address parsing as requested at: [#1453](https://github.com/kataras/iris/issues/1453). Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`. + +- Fix [#1487](https://github.com/kataras/iris/issues/1487). + +- Fix [#1473](https://github.com/kataras/iris/issues/1473). New Context Methods: +- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2 +- `context.IsGRPC() bool` reports whether the request came from a gRPC client - `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too - `context.StopWithStatus(int)` stops the handlers chain and writes the status code - `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response diff --git a/_examples/mvc/grpc-compatible/README.md b/_examples/mvc/grpc-compatible/README.md new file mode 100644 index 00000000..94a84d4e --- /dev/null +++ b/_examples/mvc/grpc-compatible/README.md @@ -0,0 +1,20 @@ +# gRPC Iris Example + +## Generate TLS Keys + +```sh +$ openssl genrsa -out server.key 2048 +$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +``` + +## Install the protoc Go plugin + +```sh +$ go get -u github.com/golang/protobuf/protoc-gen-go +``` + +## Generate proto + +```sh +$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld +``` diff --git a/_examples/mvc/grpc-compatible/grpc-client/main.go b/_examples/mvc/grpc-compatible/grpc-client/main.go index b8a982f8..253b3799 100644 --- a/_examples/mvc/grpc-compatible/grpc-client/main.go +++ b/_examples/mvc/grpc-compatible/grpc-client/main.go @@ -39,7 +39,7 @@ func main() { } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - r, err := c.PostHello(ctx, &pb.HelloRequest{Name: name}) + r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } diff --git a/_examples/mvc/grpc-compatible/helloworld/helloworld.pb.go b/_examples/mvc/grpc-compatible/helloworld/helloworld.pb.go index 94a0a1de..cb1276bc 100644 --- a/_examples/mvc/grpc-compatible/helloworld/helloworld.pb.go +++ b/_examples/mvc/grpc-compatible/helloworld/helloworld.pb.go @@ -1,130 +1,240 @@ +// Copyright 2015 gRPC 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. + // Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.21.0 +// protoc v3.11.1 // source: helloworld.proto package helloworld import ( context "context" - fmt "fmt" - math "math" - proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) -// 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.ProtoPackageIsVersion3 // please upgrade the proto package +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 // The request message containing the user's name. type HelloRequest struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } -func (m *HelloRequest) Reset() { *m = HelloRequest{} } -func (m *HelloRequest) String() string { return proto.CompactTextString(m) } -func (*HelloRequest) ProtoMessage() {} +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_helloworld_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_helloworld_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. func (*HelloRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_17b8c58d586b62f2, []int{0} + return file_helloworld_proto_rawDescGZIP(), []int{0} } -func (m *HelloRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_HelloRequest.Unmarshal(m, b) -} -func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic) -} -func (m *HelloRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_HelloRequest.Merge(m, src) -} -func (m *HelloRequest) XXX_Size() int { - return xxx_messageInfo_HelloRequest.Size(m) -} -func (m *HelloRequest) XXX_DiscardUnknown() { - xxx_messageInfo_HelloRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_HelloRequest proto.InternalMessageInfo - -func (m *HelloRequest) GetName() string { - if m != nil { - return m.Name +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name } return "" } // The response message containing the greetings type HelloReply struct { - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } -func (m *HelloReply) Reset() { *m = HelloReply{} } -func (m *HelloReply) String() string { return proto.CompactTextString(m) } -func (*HelloReply) ProtoMessage() {} +func (x *HelloReply) Reset() { + *x = HelloReply{} + if protoimpl.UnsafeEnabled { + mi := &file_helloworld_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloReply) ProtoMessage() {} + +func (x *HelloReply) ProtoReflect() protoreflect.Message { + mi := &file_helloworld_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. func (*HelloReply) Descriptor() ([]byte, []int) { - return fileDescriptor_17b8c58d586b62f2, []int{1} + return file_helloworld_proto_rawDescGZIP(), []int{1} } -func (m *HelloReply) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_HelloReply.Unmarshal(m, b) -} -func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic) -} -func (m *HelloReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_HelloReply.Merge(m, src) -} -func (m *HelloReply) XXX_Size() int { - return xxx_messageInfo_HelloReply.Size(m) -} -func (m *HelloReply) XXX_DiscardUnknown() { - xxx_messageInfo_HelloReply.DiscardUnknown(m) -} - -var xxx_messageInfo_HelloReply proto.InternalMessageInfo - -func (m *HelloReply) GetMessage() string { - if m != nil { - return m.Message +func (x *HelloReply) GetMessage() string { + if x != nil { + return x.Message } return "" } -func init() { - proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") - proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") +var File_helloworld_proto protoreflect.FileDescriptor + +var file_helloworld_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x22, 0x22, + 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x49, 0x0a, 0x07, 0x47, 0x72, + 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, + 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, + 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, + 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x30, 0x0a, 0x1b, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, + 0x6f, 0x72, 0x6c, 0x64, 0x42, 0x0f, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f, 0x72, 0x6c, 0x64, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } -func init() { proto.RegisterFile("helloworld.proto", fileDescriptor_17b8c58d586b62f2) } +var ( + file_helloworld_proto_rawDescOnce sync.Once + file_helloworld_proto_rawDescData = file_helloworld_proto_rawDesc +) -var fileDescriptor_17b8c58d586b62f2 = []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, +func file_helloworld_proto_rawDescGZIP() []byte { + file_helloworld_proto_rawDescOnce.Do(func() { + file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData) + }) + return file_helloworld_proto_rawDescData +} + +var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_helloworld_proto_goTypes = []interface{}{ + (*HelloRequest)(nil), // 0: helloworld.HelloRequest + (*HelloReply)(nil), // 1: helloworld.HelloReply +} +var file_helloworld_proto_depIdxs = []int32{ + 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest + 1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_helloworld_proto_init() } +func file_helloworld_proto_init() { + if File_helloworld_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_helloworld_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_helloworld_proto_goTypes, + DependencyIndexes: file_helloworld_proto_depIdxs, + MessageInfos: file_helloworld_proto_msgTypes, + }.Build() + File_helloworld_proto = out.File + file_helloworld_proto_rawDesc = nil + file_helloworld_proto_goTypes = nil + file_helloworld_proto_depIdxs = nil } // Reference imports to suppress errors if they are not otherwise used. @@ -140,7 +250,7 @@ const _ = grpc.SupportPackageIsVersion6 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type GreeterClient interface { // Sends a greeting - PostHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) } type greeterClient struct { @@ -151,9 +261,9 @@ func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { return &greeterClient{cc} } -func (c *greeterClient) PostHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { out := new(HelloReply) - err := c.cc.Invoke(ctx, "/helloworld.Greeter/PostHello", in, out, opts...) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) if err != nil { return nil, err } @@ -163,15 +273,15 @@ func (c *greeterClient) PostHello(ctx context.Context, in *HelloRequest, opts .. // GreeterServer is the server API for Greeter service. type GreeterServer interface { // Sends a greeting - PostHello(context.Context, *HelloRequest) (*HelloReply, error) + SayHello(context.Context, *HelloRequest) (*HelloReply, error) } // UnimplementedGreeterServer can be embedded to have forward compatible implementations. type UnimplementedGreeterServer struct { } -func (*UnimplementedGreeterServer) PostHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) { - return nil, status.Errorf(codes.Unimplemented, "method PostHello not implemented") +func (*UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") } func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { @@ -184,14 +294,14 @@ func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(in return nil, err } if interceptor == nil { - return srv.(GreeterServer).PostHello(ctx, in) + return srv.(GreeterServer).SayHello(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/helloworld.Greeter/PostHello", + FullMethod: "/helloworld.Greeter/SayHello", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GreeterServer).PostHello(ctx, req.(*HelloRequest)) + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) } return interceptor(ctx, in, info, handler) } @@ -201,7 +311,7 @@ var _Greeter_serviceDesc = grpc.ServiceDesc{ HandlerType: (*GreeterServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "PostHello", + MethodName: "SayHello", Handler: _Greeter_SayHello_Handler, }, }, diff --git a/_examples/mvc/grpc-compatible/helloworld/helloworld.proto b/_examples/mvc/grpc-compatible/helloworld/helloworld.proto index 180194d0..8de5d08e 100644 --- a/_examples/mvc/grpc-compatible/helloworld/helloworld.proto +++ b/_examples/mvc/grpc-compatible/helloworld/helloworld.proto @@ -1,3 +1,17 @@ +// Copyright 2015 gRPC 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. + syntax = "proto3"; option java_multiple_files = true; @@ -9,7 +23,7 @@ package helloworld; // The greeting service definition. service Greeter { // Sends a greeting - rpc PostHello (HelloRequest) returns (HelloReply) {} + rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. diff --git a/_examples/mvc/grpc-compatible/main.go b/_examples/mvc/grpc-compatible/main.go index b192f64d..576d92e4 100644 --- a/_examples/mvc/grpc-compatible/main.go +++ b/_examples/mvc/grpc-compatible/main.go @@ -6,7 +6,6 @@ import ( pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld" "github.com/kataras/iris/v12" - grpcWrapper "github.com/kataras/iris/v12/middleware/grpc" "github.com/kataras/iris/v12/mvc" "google.golang.org/grpc" @@ -23,7 +22,8 @@ func main() { app := newApp() app.Logger().SetLevel("debug") - // POST: https://localhost/hello + // The Iris server should ran under TLS (it's a gRPC requirement). + // POST: https://localhost:443/helloworld.greeter/sayhello // with request data: {"name": "John"} // and expected output: {"message": "Hello John"} app.Run(iris.TLS(":443", "server.crt", "server.key")) @@ -31,24 +31,28 @@ func main() { func newApp() *iris.Application { app := iris.New() + app.Logger().SetLevel("debug") ctrl := &myController{} // Register gRPC server. grpcServer := grpc.NewServer() pb.RegisterGreeterServer(grpcServer, ctrl) - // Register MVC application controller. - mvc.New(app).Handle(ctrl) + // serviceName := pb.File_helloworld_proto.Services().Get(0).FullName() + + // Register MVC application controller. + mvc.New(app).Handle(ctrl, mvc.GRPC{ + Server: grpcServer, // Required. + ServiceName: "helloworld.Greeter", // Required. + Strict: false, + }) - // Serve the gRPC server under the Iris HTTP webserver one, - // the Iris server should ran under TLS (it's a gRPC requirement). - app.WrapRouter(grpcWrapper.New(grpcServer)) return app } type myController struct{} -// PostHello implements helloworld.GreeterServer -func (c *myController) PostHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { +// SayHello implements helloworld.GreeterServer. +func (c *myController) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } diff --git a/_examples/mvc/grpc-compatible/main_test.go b/_examples/mvc/grpc-compatible/main_test.go index 1b9e7f51..f937b9d9 100644 --- a/_examples/mvc/grpc-compatible/main_test.go +++ b/_examples/mvc/grpc-compatible/main_test.go @@ -10,7 +10,7 @@ func TestGRPCCompatible(t *testing.T) { app := newApp() e := httptest.New(t, app) - e.POST("/hello").WithJSON(map[string]string{"name": "makis"}).Expect(). + e.POST("/helloworld.Greeter/SayHello").WithJSON(map[string]string{"name": "makis"}).Expect(). Status(httptest.StatusOK). JSON().Equal(map[string]string{"message": "Hello makis"}) } diff --git a/_examples/mvc/grpc-compatible/server.keys.md b/_examples/mvc/grpc-compatible/server.keys.md deleted file mode 100644 index 2205e31a..00000000 --- a/_examples/mvc/grpc-compatible/server.keys.md +++ /dev/null @@ -1,4 +0,0 @@ -```sh -$ openssl genrsa -out server.key 2048 -$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -``` diff --git a/configuration.go b/configuration.go index 0ae32744..e1249e6b 100644 --- a/configuration.go +++ b/configuration.go @@ -258,13 +258,21 @@ var WithoutAutoFireStatusCode = func(app *Application) { app.config.DisableAutoFireStatusCode = true } -// WithPathEscape enables the PathEscape setting. +// WithPathEscape sets the EnablePathEscape setting to true. // // See `Configuration`. var WithPathEscape = func(app *Application) { app.config.EnablePathEscape = true } +// WithLowercaseRouting enables for lowercase routing by +// setting the `ForceLowercaseRoutes` to true. +// +// See `Configuration`. +var WithLowercaseRouting = func(app *Application) { + app.config.ForceLowercaseRouting = true +} + // WithOptimizations can force the application to optimize for the best performance where is possible. // // See `Configuration`. @@ -744,7 +752,8 @@ type Configuration struct { // Defaults to false. DisableInterruptHandler bool `json:"disableInterruptHandler,omitempty" yaml:"DisableInterruptHandler" toml:"DisableInterruptHandler"` - // DisablePathCorrection corrects and redirects or executes directly the handler of + // DisablePathCorrection disables the correcting + // and redirecting or executing directly the handler of // the requested path to the registered path // for example, if /home/ path is requested but no handler for this Route found, // then the Router checks if /home handler exists, if yes, @@ -762,9 +771,7 @@ type Configuration struct { // Defaults to false. DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"` - // EnablePathEscape when is true then its escapes the path, the named parameters (if any). - // Change to false it if you want something like this https://github.com/kataras/iris/issues/135 to work - // + // EnablePathEscape when is true then its escapes the path and the named parameters (if any). // When do you need to Disable(false) it: // accepts parameters with slash '/' // Request: http://localhost:8080/details/Project%2FDelta @@ -775,6 +782,12 @@ type Configuration struct { // Defaults to false. EnablePathEscape bool `json:"enablePathEscape,omitempty" yaml:"EnablePathEscape" toml:"EnablePathEscape"` + // ForceLowercaseRouting if enabled, converts all registered routes paths to lowercase + // and it does lowercase the request path too for matching. + // + // Defaults to false. + ForceLowercaseRouting bool `json:"forceLowercaseRouting,omitempty" yaml:"ForceLowercaseRouting" toml:"ForceLowercaseRouting"` + // EnableOptimization when this field is true // then the application tries to optimize for the best performance where is possible. // @@ -900,8 +913,10 @@ func (c Configuration) GetVHost() string { return c.vhost } -// GetDisablePathCorrection returns the Configuration#DisablePathCorrection, -// DisablePathCorrection corrects and redirects the requested path to the registered path +// GetDisablePathCorrection returns the Configuration#DisablePathCorrection. +// DisablePathCorrection disables the correcting +// and redirecting or executing directly the handler of +// the requested path to the registered path // for example, if /home/ path is requested but no handler for this Route found, // then the Router checks if /home handler exists, if yes, // (permanent)redirects the client to the correct path /home. @@ -922,6 +937,11 @@ func (c Configuration) GetEnablePathEscape() bool { return c.EnablePathEscape } +// GetForceLowercaseRouting returns the value of the `ForceLowercaseRouting` setting. +func (c Configuration) GetForceLowercaseRouting() bool { + return c.ForceLowercaseRouting +} + // GetEnableOptimizations returns whether // the application has performance optimizations enabled. func (c Configuration) GetEnableOptimizations() bool { @@ -1079,6 +1099,10 @@ func WithConfiguration(c Configuration) Configurator { main.EnablePathEscape = v } + if v := c.ForceLowercaseRouting; v { + main.ForceLowercaseRouting = v + } + if v := c.EnableOptimizations; v { main.EnableOptimizations = v } @@ -1150,6 +1174,7 @@ func DefaultConfiguration() Configuration { DisableInterruptHandler: false, DisablePathCorrection: false, EnablePathEscape: false, + ForceLowercaseRouting: false, FireMethodNotAllowed: false, DisableBodyConsumptionOnUnmarshal: false, DisableAutoFireStatusCode: false, diff --git a/context/configuration.go b/context/configuration.go index bd5a65a2..c920730b 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -25,7 +25,8 @@ type ConfigurationReadOnly interface { GetDisablePathCorrection() bool // GetDisablePathCorrectionRedirection returns the Configuration#DisablePathCorrectionRedirection field. - // If DisablePathCorrectionRedirection set to true then it will fire the handler of the matching route without + // If DisablePathCorrectionRedirection set to true then it will handle paths as they are. + // it will fire the handler of the matching route without // the last slash ("/") instead of send a redirection status. GetDisablePathCorrectionRedirection() bool @@ -33,6 +34,9 @@ type ConfigurationReadOnly interface { // returns true when its escapes the path, the named parameters (if any). GetEnablePathEscape() bool + // GetForceLowercaseRouting returns the value of the `ForceLowercaseRouting` setting. + GetForceLowercaseRouting() bool + // GetEnableOptimizations returns whether // the application has performance optimizations enabled. GetEnableOptimizations() bool diff --git a/context/context.go b/context/context.go index ab270b70..645f32f8 100644 --- a/context/context.go +++ b/context/context.go @@ -165,7 +165,6 @@ type Context interface { // Router is calling this function to add the route's handler. // If AddHandler called then the handlers will be inserted // to the end of the already-defined route's handler. - // AddHandler(...Handler) // SetHandlers replaces all handlers with the new. SetHandlers(Handlers) @@ -387,6 +386,11 @@ type Context interface { IsMobile() bool // IsScript reports whether a client is a script. IsScript() bool + // IsHTTP2 reports whether the protocol version for incoming request was HTTP/2. + // The client code always uses either HTTP/1.1 or HTTP/2. + IsHTTP2() bool + // IsGRPC reports whether the request came from a gRPC client. + IsGRPC() bool // GetReferrer extracts and returns the information from the "Referer" header as specified // in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy // or by the URL query parameter "referer". @@ -1668,6 +1672,7 @@ func (ctx *context) RequestPath(escape bool) string { if escape { return ctx.request.URL.EscapedPath() // DecodeQuery(ctx.request.URL.EscapedPath()) } + return ctx.request.URL.Path // RawPath returns empty, requesturi can be used instead also. } @@ -1826,6 +1831,17 @@ func (ctx *context) IsScript() bool { return isScriptRegex.MatchString(s) } +// IsHTTP2 reports whether the protocol version for incoming request was HTTP/2. +// The client code always uses either HTTP/1.1 or HTTP/2. +func (ctx *context) IsHTTP2() bool { + return ctx.Request().ProtoMajor == 2 +} + +// IsGRPC reports whether the request came from a gRPC client. +func (ctx *context) IsGRPC() bool { + return ctx.IsHTTP2() && ctx.GetContentTypeRequested() == ContentGRPCHeaderValue +} + type ( // Referrer contains the extracted information from the `GetReferrer` // @@ -3316,6 +3332,8 @@ const ( ContentFormHeaderValue = "application/x-www-form-urlencoded" // ContentFormMultipartHeaderValue header value for post multipart form data. ContentFormMultipartHeaderValue = "multipart/form-data" + // ContentGRPCHeaderValue Content-Type header value for gRPC. + ContentGRPCHeaderValue = "application/grpc" ) // Binary writes out the raw bytes as binary data. diff --git a/context/handler.go b/context/handler.go index 79e810f1..4c117c12 100644 --- a/context/handler.go +++ b/context/handler.go @@ -1,6 +1,7 @@ package context import ( + "path/filepath" "reflect" "runtime" "strings" @@ -27,20 +28,38 @@ type Handler func(Context) // See `Handler` for more. type Handlers []Handler +func valueOf(v interface{}) reflect.Value { + if val, ok := v.(reflect.Value); ok { + return val + } + + return reflect.ValueOf(v) +} + // HandlerName returns the handler's function name. // See `context.HandlerName` to get function name of the current running handler in the chain. -func HandlerName(h Handler) string { - pc := reflect.ValueOf(h).Pointer() +func HandlerName(h interface{}) string { + pc := valueOf(h).Pointer() return runtime.FuncForPC(pc).Name() } // HandlerFileLine returns the handler's file and line information. // See `context.HandlerFileLine` to get the file, line of the current running handler in the chain. -func HandlerFileLine(h Handler) (file string, line int) { - pc := reflect.ValueOf(h).Pointer() +func HandlerFileLine(h interface{}) (file string, line int) { + pc := valueOf(h).Pointer() return runtime.FuncForPC(pc).FileLine(pc) } +// HandlerFileLineRel same as `HandlerFileLine` but it returns the path as relative to the "workingDir". +func HandlerFileLineRel(h interface{}, workingDir string) (string, int) { + file, line := HandlerFileLine(h) + if relFile, err := filepath.Rel(workingDir, file); err == nil { + file = "./" + relFile + } + + return file, line +} + // MainHandlerName tries to find the main handler than end-developer // registered on the provided chain of handlers and returns its function name. func MainHandlerName(handlers Handlers) (name string) { diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 33b7a24f..48ee33ce 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -163,20 +163,14 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl // NewAPIBuilder creates & returns a new builder // which is responsible to build the API and the router handler. func NewAPIBuilder() *APIBuilder { - api := &APIBuilder{ + return &APIBuilder{ macros: macro.Defaults, errorCodeHandlers: defaultErrorCodeHandlers(), errors: errgroup.New("API Builder"), relativePath: "/", routes: new(repository), + apiBuilderDI: &APIContainer{Container: hero.New()}, } - - api.apiBuilderDI = &APIContainer{ - Self: api, - Container: hero.New(), - } - - return api } // ConfigureContainer accepts one or more functions that can be used @@ -187,12 +181,14 @@ func NewAPIBuilder() *APIBuilder { // // It returns the same `APIBuilder` featured with Dependency Injection. func (api *APIBuilder) ConfigureContainer(builder ...func(*APIContainer)) *APIContainer { - for _, b := range builder { - if b == nil { - continue - } + if api.apiBuilderDI.Self == nil { + api.apiBuilderDI.Self = api + } - b(api.apiBuilderDI) + for _, b := range builder { + if b != nil { + b(api.apiBuilderDI) + } } return api.apiBuilderDI @@ -463,7 +459,7 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl subdomain, path := splitSubdomainAndPath(fullpath) // if allowMethods are empty, then simply register with the passed, main, method. - methods = removeDuplString(append(api.allowMethods, methods...)) + methods = removeDuplicates(append(api.allowMethods, methods...)) routes := make([]*Route, len(methods)) @@ -487,7 +483,7 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl return routes } -func removeDuplString(elements []string) (result []string) { +func removeDuplicates(elements []string) (result []string) { seen := make(map[string]struct{}) for v := range elements { @@ -551,15 +547,11 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P allowMethods: allowMethods, handlerExecutionRules: api.handlerExecutionRules, routeRegisterRule: api.routeRegisterRule, - } - - // attach a new Container with correct dynamic path parameter start index for input arguments - // based on the fullpath. - childContainer := api.apiBuilderDI.Container.Clone() - - childAPI.apiBuilderDI = &APIContainer{ - Self: childAPI, - Container: childContainer, + apiBuilderDI: &APIContainer{ + // attach a new Container with correct dynamic path parameter start index for input arguments + // based on the fullpath. + Container: api.apiBuilderDI.Container.Clone(), + }, } return childAPI diff --git a/core/router/api_builder_benchmark_test.go b/core/router/api_builder_benchmark_test.go index 0b697147..1ff511f9 100644 --- a/core/router/api_builder_benchmark_test.go +++ b/core/router/api_builder_benchmark_test.go @@ -84,7 +84,7 @@ func BenchmarkAPIBuilder(b *testing.B) { paths := genPaths(routesLength, 15, 42) api := NewAPIBuilder() - requestHandler := NewDefaultHandler() + requestHandler := NewDefaultHandler(nil) b.ReportAllocs() b.ResetTimer() diff --git a/core/router/handler.go b/core/router/handler.go index 5ca72a87..0bcd88b2 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -17,7 +17,7 @@ import ( // By-default is the router algorithm. type RequestHandler interface { // HandleRequest should handle the request based on the Context. - HandleRequest(context.Context) + HandleRequest(ctx context.Context) // Build should builds the handler, it's being called on router's BuildRouter. Build(provider RoutesProvider) error // RouteExists reports whether a particular route exists. @@ -25,8 +25,9 @@ type RequestHandler interface { } type routerHandler struct { - trees []*trie - hosts bool // true if at least one route contains a Subdomain. + trees []*trie + hosts bool // true if at least one route contains a Subdomain. + config context.ConfigurationReadOnly } var _ RequestHandler = &routerHandler{} @@ -67,9 +68,10 @@ func (h *routerHandler) AddRoute(r *Route) error { // NewDefaultHandler returns the handler which is responsible // to map the request with a route (aka mux implementation). -func NewDefaultHandler() RequestHandler { - h := &routerHandler{} - return h +func NewDefaultHandler(config context.ConfigurationReadOnly) RequestHandler { + return &routerHandler{ + config: config, + } } // RoutesProvider should be implemented by @@ -128,6 +130,11 @@ func (h *routerHandler) Build(provider RoutesProvider) error { }) for _, r := range registeredRoutes { + if h.config != nil && h.config.GetForceLowercaseRouting() { + // only in that state, keep everyting else as end-developer registered. + r.Path = strings.ToLower(r.Path) + } + if r.Subdomain != "" { h.hosts = true } @@ -188,20 +195,21 @@ func bindMultiParamTypesHandler(top *Route, r *Route) { func (h *routerHandler) HandleRequest(ctx context.Context) { method := ctx.Method() path := ctx.Path() - if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrection() { + config := h.config // ctx.Application().GetConfigurationReadOnly() + + if !config.GetDisablePathCorrection() { if len(path) > 1 && strings.HasSuffix(path, "/") { // Remove trailing slash and client-permanent rule for redirection, // if confgiuration allows that and path has an extra slash. // update the new path and redirect. - r := ctx.Request() + u := ctx.Request().URL // use Trim to ensure there is no open redirect due to two leading slashes path = "/" + strings.Trim(path, "/") - - r.URL.Path = path - if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrectionRedirection() { + u.Path = path + if !config.GetDisablePathCorrectionRedirection() { // do redirect, else continue with the modified path without the last "/". - url := r.URL.String() + url := u.String() // Fixes https://github.com/kataras/iris/issues/921 // This is caused for security reasons, imagine a payment shop, @@ -238,7 +246,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { // localhost -> invalid // sub.mydomain.com -> valid // sub.localhost -> valid - serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() + serverHost := config.GetVHost() if serverHost == requestHost { continue // it's not a subdomain, it's a full domain (with .com...) } @@ -266,7 +274,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { break } - if ctx.Application().ConfigurationReadOnly().GetFireMethodNotAllowed() { + if config.GetFireMethodNotAllowed() { for i := range h.trees { t := h.trees[i] // if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not diff --git a/core/router/route.go b/core/router/route.go index d04f763a..c0c583c2 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -14,11 +14,12 @@ import ( // If any of the following fields are changed then the // caller should Refresh the router. type Route struct { - Name string `json:"name"` // "userRoute" - Method string `json:"method"` // "GET" - methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one. - Subdomain string `json:"subdomain"` // "admin." - tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" + Name string `json:"name"` // "userRoute" + Description string `json:"description"` // "lists a user" + Method string `json:"method"` // "GET" + methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one. + Subdomain string `json:"subdomain"` // "admin." + tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" // temp storage, they're appended to the Handlers on build. // Execution happens before Handlers, can be empty. beginHandlers context.Handlers @@ -322,7 +323,11 @@ func (r *Route) Trace() string { if r.Subdomain != "" { printfmt += fmt.Sprintf(" %s", r.Subdomain) } - printfmt += fmt.Sprintf(" %s ", r.Tmpl().Src) + printfmt += fmt.Sprintf(" %s", r.Tmpl().Src) + + if r.Description != "" { + printfmt += fmt.Sprintf(" (%s)", r.Description) + } mainHandlerName := r.MainHandlerName if !strings.HasSuffix(mainHandlerName, ")") { @@ -330,9 +335,9 @@ func (r *Route) Trace() string { } if l := r.RegisteredHandlersLen(); l > 1 { - printfmt += fmt.Sprintf("-> %s and %d more", mainHandlerName, l-1) + printfmt += fmt.Sprintf(" -> %s and %d more", mainHandlerName, l-1) } else { - printfmt += fmt.Sprintf("-> %s", mainHandlerName) + printfmt += fmt.Sprintf(" -> %s", mainHandlerName) } // printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.MainHandlerName) diff --git a/core/router/router_test.go b/core/router/router_test.go index 7ba66bb3..1be02bc1 100644 --- a/core/router/router_test.go +++ b/core/router/router_test.go @@ -1,6 +1,8 @@ package router_test import ( + "net/http" + "strings" "testing" "github.com/kataras/iris/v12" @@ -39,3 +41,34 @@ func TestRouteExists(t *testing.T) { // run the tests httptest.New(t, app, httptest.Debug(false)).Request("GET", "/route-test").Expect().Status(iris.StatusOK) } + +func TestLowercaseRouting(t *testing.T) { + app := iris.New() + app.WrapRouter(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + // test bottom to begin wrapper, the last ones should execute first. + // The ones that are registered at `Build` state, after this `WrapRouter` call. + // So path should be already lowecased. + if expected, got := strings.ToLower(r.URL.Path), r.URL.Path; expected != got { + t.Fatalf("expected path: %s but got: %s", expected, got) + } + next(w, r) + }) + + h := func(ctx iris.Context) { ctx.WriteString(ctx.Path()) } + + // Register routes. + tests := []string{"/", "/lowercase", "/UPPERCASE", "/Title", "/m1xEd2"} + for _, tt := range tests { + app.Get(tt, h) + } + + app.Configure(iris.WithLowercaseRouting) + // Test routes. + e := httptest.New(t, app) + for _, tt := range tests { + s := strings.ToLower(tt) + e.GET(tt).Expect().Status(httptest.StatusOK).Body().Equal(s) + e.GET(s).Expect().Status(httptest.StatusOK).Body().Equal(s) + e.GET(strings.ToUpper(tt)).Expect().Status(httptest.StatusOK).Body().Equal(s) + } +} diff --git a/iris.go b/iris.go index fbf6bb2a..624864d7 100644 --- a/iris.go +++ b/iris.go @@ -765,18 +765,24 @@ func (app *Application) Build() error { if app.I18n.Loaded() { // {{ tr "lang" "key" arg1 arg2 }} app.view.AddFunc("tr", app.I18n.Tr) - app.WrapRouter(app.I18n.Wrapper()) + app.Router.WrapRouter(app.I18n.Wrapper()) } if !app.Router.Downgraded() { // router - if err := app.tryInjectLiveReload(); err != nil { rp.Errf("LiveReload: init: failed: %v", err) } + if app.config.ForceLowercaseRouting { + app.Router.WrapRouter(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + r.URL.Path = strings.ToLower(r.URL.Path) + next(w, r) + }) + } + // create the request handler, the default routing handler - routerHandler := router.NewDefaultHandler() + routerHandler := router.NewDefaultHandler(app.config) err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false) if err != nil { rp.Err(err) diff --git a/mvc/controller.go b/mvc/controller.go index c590bfca..283e4d07 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -2,6 +2,7 @@ package mvc import ( "fmt" + "os" "reflect" "strings" @@ -81,6 +82,9 @@ type ControllerActivator struct { // true if this controller listens and serves to websocket events. servesWebsocket bool + + // true to skip the internal "activate". + activated bool } // NameOf returns the package name + the struct type's name, @@ -96,6 +100,14 @@ func NameOf(v interface{}) string { } func newControllerActivator(app *Application, controller interface{}) *ControllerActivator { + if controller == nil { + return nil + } + + if c, ok := controller.(*ControllerActivator); ok { + return c + } + typ := reflect.TypeOf(controller) c := &ControllerActivator{ @@ -225,6 +237,11 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { return false } +func (c *ControllerActivator) markAsWebsocket() { + c.servesWebsocket = true + c.attachInjector() +} + func (c *ControllerActivator) attachInjector() { if c.injector == nil { partyCountParams := macro.CountParams(c.app.Router.GetRelPath(), *c.app.Router.Macros()) @@ -232,12 +249,18 @@ func (c *ControllerActivator) attachInjector() { } } -func (c *ControllerActivator) markAsWebsocket() { - c.servesWebsocket = true - c.attachInjector() +// Activated can be called to skip the internal method parsing. +func (c *ControllerActivator) Activated() bool { + b := c.activated + c.activated = true + return b } func (c *ControllerActivator) activate() { + if c.Activated() { + return + } + c.parseMethods() } @@ -314,10 +337,16 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override return nil } + wd, _ := os.Getwd() + for _, r := range routes { - // change the main handler's name in order to respect the controller's and give - // a proper debug message. + // change the main handler's name and file:line + // in order to respect the controller's and give + // a proper debug/log message. r.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) + if m, ok := c.Type.MethodByName(funcName); ok { + r.SourceFileName, r.SourceLineNumber = context.HandlerFileLineRel(m.Func, wd) + } } // add this as a reserved method name in order to diff --git a/mvc/grpc.go b/mvc/grpc.go new file mode 100644 index 00000000..f0ee8855 --- /dev/null +++ b/mvc/grpc.go @@ -0,0 +1,66 @@ +package mvc + +import ( + "net/http" + "path" + + "github.com/kataras/iris/v12/context" +) + +// GRPC registers a controller which serves gRPC clients. +// It accepts the controller ptr to a struct value, +// the gRPCServer itself, and a strict option which is explained below. +// +// The differences by a common controller are: +// HTTP verb: only POST (Party.AllowMethods can be used for more), +// method parsing is disabled: path is the function name as it is, +// if 'strictMode' option is true then this controller will only serve gRPC-based clients +// and fires 404 on common HTTP clients, +// otherwise HTTP clients can send and receive JSON (protos contain json struct fields by-default). +type GRPC struct { + // Server is required and should be gRPC Server derives from google's grpc package. + Server http.Handler + // ServiceName is required and should be the name of the service (used to build the gRPC route path), + // e.g. "helloworld.Greeter". + // For a controller's method of "SayHello" and ServiceName "helloworld.Greeter", + // both gRPC and common HTTP request path is: "/helloworld.Greeter/SayHello". + // + // Tip: the ServiceName can be fetched through proto's file descriptor, e.g. + // serviceName := pb.File_helloworld_proto.Services().Get(0).FullName(). + ServiceName string + + // When Strict option is true then this controller will only serve gRPC-based clients + // and fires 404 on common HTTP clients. + Strict bool +} + +// Apply parses the controller's methods and registers gRPC handlers to the application. +func (g GRPC) Apply(c *ControllerActivator) { + defer c.Activated() + + pre := func(ctx context.Context) { + if ctx.IsGRPC() { // gRPC, consumes and produces protobuf. + g.Server.ServeHTTP(ctx.ResponseWriter(), ctx.Request()) + ctx.StopExecution() + return + } + + if g.Strict { + ctx.NotFound() + } else { + // Allow common HTTP clients, consumes and produces JSON. + ctx.Next() + } + } + + for i := 0; i < c.Type.NumMethod(); i++ { + m := c.Type.Method(i) + path := path.Join(g.ServiceName, m.Name) + if route := c.Handle(http.MethodPost, path, m.Name, pre); route != nil { + route.Description = "gRPC" + if g.Strict { + route.Description = "-only" + } + } + } +} diff --git a/mvc/mvc.go b/mvc/mvc.go index 549c99de..6edfb817 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -111,6 +111,15 @@ func (app *Application) Register(dependencies ...interface{}) *Application { return app } +// Option is an interface which does contain a single `Apply` method that accepts +// a `ControllerActivator`. It can be passed on `Application.Handle` method to +// mdoify the behavior right after the `BeforeActivation` state. +// +// See `GRPC` package-level structure too. +type Option interface { + Apply(*ControllerActivator) +} + // Handle serves a controller for the current mvc application's Router. // It accept any custom struct which its functions will be transformed // to routes. @@ -154,9 +163,12 @@ func (app *Application) Register(dependencies ...interface{}) *Application { // Result or (Result, error) // where Get is an HTTP Method func. // +// Default behavior can be changed through second, variadic, variable "options", +// e.g. Handle(controller, GRPC {Server: grpcServer, Strict: true}) +// // Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc -func (app *Application) Handle(controller interface{}) *Application { - app.handle(controller) +func (app *Application) Handle(controller interface{}, options ...Option) *Application { + app.handle(controller, options...) return app } @@ -195,7 +207,7 @@ func (app *Application) GetNamespaces() websocket.Namespaces { return websocket.JoinConnHandlers(app.websocketControllers...).GetNamespaces() } -func (app *Application) handle(controller interface{}) *ControllerActivator { +func (app *Application) handle(controller interface{}, options ...Option) *ControllerActivator { // initialize the controller's activator, nothing too magical so far. c := newControllerActivator(app, controller) @@ -208,6 +220,12 @@ func (app *Application) handle(controller interface{}) *ControllerActivator { before.BeforeActivation(c) } + for _, opt := range options { + if opt != nil { + opt.Apply(c) + } + } + c.activate() if after, okAfter := controller.(interface {