New: gRPC MVC features, new WithLowercaseRouting option and add some new context methods

read HISTORY.md


Former-commit-id: 30a16cceb11f754aa32923058abeda1e736350e7
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-04-25 02:30:19 +03:00
parent 0cf5d5a4a3
commit 5d3c96947c
21 changed files with 566 additions and 185 deletions

View File

@ -176,6 +176,14 @@ Here is a preview of what the new Hero handlers look like:
Other Improvements: 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. - 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 <T> 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. - A result of <T> 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. - 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: 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.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.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 - `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response

View File

@ -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
```

View File

@ -39,7 +39,7 @@ func main() {
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() defer cancel()
r, err := c.PostHello(ctx, &pb.HelloRequest{Name: name}) r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil { if err != nil {
log.Fatalf("could not greet: %v", err) log.Fatalf("could not greet: %v", err)
} }

View File

@ -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. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.21.0
// protoc v3.11.1
// source: helloworld.proto // source: helloworld.proto
package helloworld package helloworld
import ( import (
context "context" context "context"
fmt "fmt"
math "math"
proto "github.com/golang/protobuf/proto" proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc" grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes" codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status" 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. const (
var _ = proto.Marshal // Verify that this generated code is sufficiently up-to-date.
var _ = fmt.Errorf _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
var _ = math.Inf // 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 // This is a compile-time assertion that a sufficiently up-to-date version
// is compatible with the proto package it is being compiled against. // of the legacy proto package is being used.
// A compilation error at this line likely means your copy of the const _ = proto.ProtoPackageIsVersion4
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// The request message containing the user's name. // The request message containing the user's name.
type HelloRequest struct { type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` state protoimpl.MessageState
XXX_NoUnkeyedLiteral struct{} `json:"-"` sizeCache protoimpl.SizeCache
XXX_unrecognized []byte `json:"-"` unknownFields protoimpl.UnknownFields
XXX_sizecache int32 `json:"-"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
} }
func (m *HelloRequest) Reset() { *m = HelloRequest{} } func (x *HelloRequest) Reset() {
func (m *HelloRequest) String() string { return proto.CompactTextString(m) } *x = HelloRequest{}
func (*HelloRequest) ProtoMessage() {} 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) { 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 { func (x *HelloRequest) GetName() string {
return xxx_messageInfo_HelloRequest.Unmarshal(m, b) if x != nil {
} return x.Name
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
} }
return "" return ""
} }
// The response message containing the greetings // The response message containing the greetings
type HelloReply struct { type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` state protoimpl.MessageState
XXX_NoUnkeyedLiteral struct{} `json:"-"` sizeCache protoimpl.SizeCache
XXX_unrecognized []byte `json:"-"` unknownFields protoimpl.UnknownFields
XXX_sizecache int32 `json:"-"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
} }
func (m *HelloReply) Reset() { *m = HelloReply{} } func (x *HelloReply) Reset() {
func (m *HelloReply) String() string { return proto.CompactTextString(m) } *x = HelloReply{}
func (*HelloReply) ProtoMessage() {} 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) { 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 { func (x *HelloReply) GetMessage() string {
return xxx_messageInfo_HelloReply.Unmarshal(m, b) if x != nil {
} return x.Message
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
} }
return "" return ""
} }
func init() { var File_helloworld_proto protoreflect.FileDescriptor
proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") 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{ func file_helloworld_proto_rawDescGZIP() []byte {
// 175 bytes of a gzipped FileDescriptorProto file_helloworld_proto_rawDescOnce.Do(func() {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData)
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, return file_helloworld_proto_rawDescData
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, var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64, var file_helloworld_proto_goTypes = []interface{}{
0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x14, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0x19, 0x70, 0x49, 0x67, 0xe6, (*HelloRequest)(nil), // 0: helloworld.HelloRequest
0xeb, 0xa5, 0x17, 0x15, 0x24, 0xeb, 0xa5, 0x56, 0x24, 0xe6, 0x16, 0xe4, 0xa4, 0x16, 0x23, 0xa9, (*HelloReply)(nil), // 1: helloworld.HelloReply
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, 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. // 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. // 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 { type GreeterClient interface {
// Sends a greeting // 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 { type greeterClient struct {
@ -151,9 +261,9 @@ func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc} 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) 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 { if err != nil {
return nil, err 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. // GreeterServer is the server API for Greeter service.
type GreeterServer interface { type GreeterServer interface {
// Sends a greeting // Sends a greeting
PostHello(context.Context, *HelloRequest) (*HelloReply, error) SayHello(context.Context, *HelloRequest) (*HelloReply, error)
} }
// UnimplementedGreeterServer can be embedded to have forward compatible implementations. // UnimplementedGreeterServer can be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct { type UnimplementedGreeterServer struct {
} }
func (*UnimplementedGreeterServer) PostHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) { func (*UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method PostHello not implemented") return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
} }
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { 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 return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(GreeterServer).PostHello(ctx, in) return srv.(GreeterServer).SayHello(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/helloworld.Greeter/PostHello", FullMethod: "/helloworld.Greeter/SayHello",
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { 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) return interceptor(ctx, in, info, handler)
} }
@ -201,7 +311,7 @@ var _Greeter_serviceDesc = grpc.ServiceDesc{
HandlerType: (*GreeterServer)(nil), HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{ Methods: []grpc.MethodDesc{
{ {
MethodName: "PostHello", MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler, Handler: _Greeter_SayHello_Handler,
}, },
}, },

View File

@ -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"; syntax = "proto3";
option java_multiple_files = true; option java_multiple_files = true;
@ -9,7 +23,7 @@ package helloworld;
// The greeting service definition. // The greeting service definition.
service Greeter { service Greeter {
// Sends a greeting // Sends a greeting
rpc PostHello (HelloRequest) returns (HelloReply) {} rpc SayHello (HelloRequest) returns (HelloReply) {}
} }
// The request message containing the user's name. // The request message containing the user's name.

View File

@ -6,7 +6,6 @@ import (
pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld" pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
grpcWrapper "github.com/kataras/iris/v12/middleware/grpc"
"github.com/kataras/iris/v12/mvc" "github.com/kataras/iris/v12/mvc"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -23,7 +22,8 @@ func main() {
app := newApp() app := newApp()
app.Logger().SetLevel("debug") 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"} // with request data: {"name": "John"}
// and expected output: {"message": "Hello John"} // and expected output: {"message": "Hello John"}
app.Run(iris.TLS(":443", "server.crt", "server.key")) app.Run(iris.TLS(":443", "server.crt", "server.key"))
@ -31,24 +31,28 @@ func main() {
func newApp() *iris.Application { func newApp() *iris.Application {
app := iris.New() app := iris.New()
app.Logger().SetLevel("debug")
ctrl := &myController{} ctrl := &myController{}
// Register gRPC server. // Register gRPC server.
grpcServer := grpc.NewServer() grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, ctrl) pb.RegisterGreeterServer(grpcServer, ctrl)
// Register MVC application controller. // serviceName := pb.File_helloworld_proto.Services().Get(0).FullName()
mvc.New(app).Handle(ctrl)
// 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 return app
} }
type myController struct{} type myController struct{}
// PostHello implements helloworld.GreeterServer // SayHello implements helloworld.GreeterServer.
func (c *myController) PostHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { func (c *myController) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
} }

View File

@ -10,7 +10,7 @@ func TestGRPCCompatible(t *testing.T) {
app := newApp() app := newApp()
e := httptest.New(t, app) 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). Status(httptest.StatusOK).
JSON().Equal(map[string]string{"message": "Hello makis"}) JSON().Equal(map[string]string{"message": "Hello makis"})
} }

View File

@ -1,4 +0,0 @@
```sh
$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
```

View File

@ -258,13 +258,21 @@ var WithoutAutoFireStatusCode = func(app *Application) {
app.config.DisableAutoFireStatusCode = true app.config.DisableAutoFireStatusCode = true
} }
// WithPathEscape enables the PathEscape setting. // WithPathEscape sets the EnablePathEscape setting to true.
// //
// See `Configuration`. // See `Configuration`.
var WithPathEscape = func(app *Application) { var WithPathEscape = func(app *Application) {
app.config.EnablePathEscape = true 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. // WithOptimizations can force the application to optimize for the best performance where is possible.
// //
// See `Configuration`. // See `Configuration`.
@ -744,7 +752,8 @@ type Configuration struct {
// Defaults to false. // Defaults to false.
DisableInterruptHandler bool `json:"disableInterruptHandler,omitempty" yaml:"DisableInterruptHandler" toml:"DisableInterruptHandler"` 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 // the requested path to the registered path
// for example, if /home/ path is requested but no handler for this Route found, // for example, if /home/ path is requested but no handler for this Route found,
// then the Router checks if /home handler exists, if yes, // then the Router checks if /home handler exists, if yes,
@ -762,9 +771,7 @@ type Configuration struct {
// Defaults to false. // Defaults to false.
DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"` DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"`
// EnablePathEscape when is true then its escapes the path, the named parameters (if any). // EnablePathEscape when is true then its escapes the path and the named parameters (if any).
// Change to false it if you want something like this https://github.com/kataras/iris/issues/135 to work
//
// When do you need to Disable(false) it: // When do you need to Disable(false) it:
// accepts parameters with slash '/' // accepts parameters with slash '/'
// Request: http://localhost:8080/details/Project%2FDelta // Request: http://localhost:8080/details/Project%2FDelta
@ -775,6 +782,12 @@ type Configuration struct {
// Defaults to false. // Defaults to false.
EnablePathEscape bool `json:"enablePathEscape,omitempty" yaml:"EnablePathEscape" toml:"EnablePathEscape"` 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 // EnableOptimization when this field is true
// then the application tries to optimize for the best performance where is possible. // 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 return c.vhost
} }
// GetDisablePathCorrection returns the Configuration#DisablePathCorrection, // GetDisablePathCorrection returns the Configuration#DisablePathCorrection.
// DisablePathCorrection corrects and redirects the requested path to the registered path // 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, // for example, if /home/ path is requested but no handler for this Route found,
// then the Router checks if /home handler exists, if yes, // then the Router checks if /home handler exists, if yes,
// (permanent)redirects the client to the correct path /home. // (permanent)redirects the client to the correct path /home.
@ -922,6 +937,11 @@ func (c Configuration) GetEnablePathEscape() bool {
return c.EnablePathEscape return c.EnablePathEscape
} }
// GetForceLowercaseRouting returns the value of the `ForceLowercaseRouting` setting.
func (c Configuration) GetForceLowercaseRouting() bool {
return c.ForceLowercaseRouting
}
// GetEnableOptimizations returns whether // GetEnableOptimizations returns whether
// the application has performance optimizations enabled. // the application has performance optimizations enabled.
func (c Configuration) GetEnableOptimizations() bool { func (c Configuration) GetEnableOptimizations() bool {
@ -1079,6 +1099,10 @@ func WithConfiguration(c Configuration) Configurator {
main.EnablePathEscape = v main.EnablePathEscape = v
} }
if v := c.ForceLowercaseRouting; v {
main.ForceLowercaseRouting = v
}
if v := c.EnableOptimizations; v { if v := c.EnableOptimizations; v {
main.EnableOptimizations = v main.EnableOptimizations = v
} }
@ -1150,6 +1174,7 @@ func DefaultConfiguration() Configuration {
DisableInterruptHandler: false, DisableInterruptHandler: false,
DisablePathCorrection: false, DisablePathCorrection: false,
EnablePathEscape: false, EnablePathEscape: false,
ForceLowercaseRouting: false,
FireMethodNotAllowed: false, FireMethodNotAllowed: false,
DisableBodyConsumptionOnUnmarshal: false, DisableBodyConsumptionOnUnmarshal: false,
DisableAutoFireStatusCode: false, DisableAutoFireStatusCode: false,

View File

@ -25,7 +25,8 @@ type ConfigurationReadOnly interface {
GetDisablePathCorrection() bool GetDisablePathCorrection() bool
// GetDisablePathCorrectionRedirection returns the Configuration#DisablePathCorrectionRedirection field. // 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. // the last slash ("/") instead of send a redirection status.
GetDisablePathCorrectionRedirection() bool GetDisablePathCorrectionRedirection() bool
@ -33,6 +34,9 @@ type ConfigurationReadOnly interface {
// returns true when its escapes the path, the named parameters (if any). // returns true when its escapes the path, the named parameters (if any).
GetEnablePathEscape() bool GetEnablePathEscape() bool
// GetForceLowercaseRouting returns the value of the `ForceLowercaseRouting` setting.
GetForceLowercaseRouting() bool
// GetEnableOptimizations returns whether // GetEnableOptimizations returns whether
// the application has performance optimizations enabled. // the application has performance optimizations enabled.
GetEnableOptimizations() bool GetEnableOptimizations() bool

View File

@ -165,7 +165,6 @@ type Context interface {
// Router is calling this function to add the route's handler. // Router is calling this function to add the route's handler.
// If AddHandler called then the handlers will be inserted // If AddHandler called then the handlers will be inserted
// to the end of the already-defined route's handler. // to the end of the already-defined route's handler.
//
AddHandler(...Handler) AddHandler(...Handler)
// SetHandlers replaces all handlers with the new. // SetHandlers replaces all handlers with the new.
SetHandlers(Handlers) SetHandlers(Handlers)
@ -387,6 +386,11 @@ type Context interface {
IsMobile() bool IsMobile() bool
// IsScript reports whether a client is a script. // IsScript reports whether a client is a script.
IsScript() bool 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 // 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 // in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
// or by the URL query parameter "referer". // or by the URL query parameter "referer".
@ -1668,6 +1672,7 @@ func (ctx *context) RequestPath(escape bool) string {
if escape { if escape {
return ctx.request.URL.EscapedPath() // DecodeQuery(ctx.request.URL.EscapedPath()) return ctx.request.URL.EscapedPath() // DecodeQuery(ctx.request.URL.EscapedPath())
} }
return ctx.request.URL.Path // RawPath returns empty, requesturi can be used instead also. 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) 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 ( type (
// Referrer contains the extracted information from the `GetReferrer` // Referrer contains the extracted information from the `GetReferrer`
// //
@ -3316,6 +3332,8 @@ const (
ContentFormHeaderValue = "application/x-www-form-urlencoded" ContentFormHeaderValue = "application/x-www-form-urlencoded"
// ContentFormMultipartHeaderValue header value for post multipart form data. // ContentFormMultipartHeaderValue header value for post multipart form data.
ContentFormMultipartHeaderValue = "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. // Binary writes out the raw bytes as binary data.

View File

@ -1,6 +1,7 @@
package context package context
import ( import (
"path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
@ -27,20 +28,38 @@ type Handler func(Context)
// See `Handler` for more. // See `Handler` for more.
type Handlers []Handler 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. // HandlerName returns the handler's function name.
// See `context.HandlerName` to get function name of the current running handler in the chain. // See `context.HandlerName` to get function name of the current running handler in the chain.
func HandlerName(h Handler) string { func HandlerName(h interface{}) string {
pc := reflect.ValueOf(h).Pointer() pc := valueOf(h).Pointer()
return runtime.FuncForPC(pc).Name() return runtime.FuncForPC(pc).Name()
} }
// HandlerFileLine returns the handler's file and line information. // 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. // See `context.HandlerFileLine` to get the file, line of the current running handler in the chain.
func HandlerFileLine(h Handler) (file string, line int) { func HandlerFileLine(h interface{}) (file string, line int) {
pc := reflect.ValueOf(h).Pointer() pc := valueOf(h).Pointer()
return runtime.FuncForPC(pc).FileLine(pc) 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 // MainHandlerName tries to find the main handler than end-developer
// registered on the provided chain of handlers and returns its function name. // registered on the provided chain of handlers and returns its function name.
func MainHandlerName(handlers Handlers) (name string) { func MainHandlerName(handlers Handlers) (name string) {

View File

@ -163,20 +163,14 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl
// NewAPIBuilder creates & returns a new builder // NewAPIBuilder creates & returns a new builder
// which is responsible to build the API and the router handler. // which is responsible to build the API and the router handler.
func NewAPIBuilder() *APIBuilder { func NewAPIBuilder() *APIBuilder {
api := &APIBuilder{ return &APIBuilder{
macros: macro.Defaults, macros: macro.Defaults,
errorCodeHandlers: defaultErrorCodeHandlers(), errorCodeHandlers: defaultErrorCodeHandlers(),
errors: errgroup.New("API Builder"), errors: errgroup.New("API Builder"),
relativePath: "/", relativePath: "/",
routes: new(repository), 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 // 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. // It returns the same `APIBuilder` featured with Dependency Injection.
func (api *APIBuilder) ConfigureContainer(builder ...func(*APIContainer)) *APIContainer { func (api *APIBuilder) ConfigureContainer(builder ...func(*APIContainer)) *APIContainer {
for _, b := range builder { if api.apiBuilderDI.Self == nil {
if b == nil { api.apiBuilderDI.Self = api
continue }
}
b(api.apiBuilderDI) for _, b := range builder {
if b != nil {
b(api.apiBuilderDI)
}
} }
return api.apiBuilderDI return api.apiBuilderDI
@ -463,7 +459,7 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl
subdomain, path := splitSubdomainAndPath(fullpath) subdomain, path := splitSubdomainAndPath(fullpath)
// if allowMethods are empty, then simply register with the passed, main, method. // 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)) routes := make([]*Route, len(methods))
@ -487,7 +483,7 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl
return routes return routes
} }
func removeDuplString(elements []string) (result []string) { func removeDuplicates(elements []string) (result []string) {
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for v := range elements { for v := range elements {
@ -551,15 +547,11 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
allowMethods: allowMethods, allowMethods: allowMethods,
handlerExecutionRules: api.handlerExecutionRules, handlerExecutionRules: api.handlerExecutionRules,
routeRegisterRule: api.routeRegisterRule, routeRegisterRule: api.routeRegisterRule,
} apiBuilderDI: &APIContainer{
// attach a new Container with correct dynamic path parameter start index for input arguments
// attach a new Container with correct dynamic path parameter start index for input arguments // based on the fullpath.
// based on the fullpath. Container: api.apiBuilderDI.Container.Clone(),
childContainer := api.apiBuilderDI.Container.Clone() },
childAPI.apiBuilderDI = &APIContainer{
Self: childAPI,
Container: childContainer,
} }
return childAPI return childAPI

View File

@ -84,7 +84,7 @@ func BenchmarkAPIBuilder(b *testing.B) {
paths := genPaths(routesLength, 15, 42) paths := genPaths(routesLength, 15, 42)
api := NewAPIBuilder() api := NewAPIBuilder()
requestHandler := NewDefaultHandler() requestHandler := NewDefaultHandler(nil)
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()

View File

@ -17,7 +17,7 @@ import (
// By-default is the router algorithm. // By-default is the router algorithm.
type RequestHandler interface { type RequestHandler interface {
// HandleRequest should handle the request based on the Context. // 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 should builds the handler, it's being called on router's BuildRouter.
Build(provider RoutesProvider) error Build(provider RoutesProvider) error
// RouteExists reports whether a particular route exists. // RouteExists reports whether a particular route exists.
@ -25,8 +25,9 @@ type RequestHandler interface {
} }
type routerHandler struct { type routerHandler struct {
trees []*trie trees []*trie
hosts bool // true if at least one route contains a Subdomain. hosts bool // true if at least one route contains a Subdomain.
config context.ConfigurationReadOnly
} }
var _ RequestHandler = &routerHandler{} var _ RequestHandler = &routerHandler{}
@ -67,9 +68,10 @@ func (h *routerHandler) AddRoute(r *Route) error {
// NewDefaultHandler returns the handler which is responsible // NewDefaultHandler returns the handler which is responsible
// to map the request with a route (aka mux implementation). // to map the request with a route (aka mux implementation).
func NewDefaultHandler() RequestHandler { func NewDefaultHandler(config context.ConfigurationReadOnly) RequestHandler {
h := &routerHandler{} return &routerHandler{
return h config: config,
}
} }
// RoutesProvider should be implemented by // RoutesProvider should be implemented by
@ -128,6 +130,11 @@ func (h *routerHandler) Build(provider RoutesProvider) error {
}) })
for _, r := range registeredRoutes { 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 != "" { if r.Subdomain != "" {
h.hosts = true h.hosts = true
} }
@ -188,20 +195,21 @@ func bindMultiParamTypesHandler(top *Route, r *Route) {
func (h *routerHandler) HandleRequest(ctx context.Context) { func (h *routerHandler) HandleRequest(ctx context.Context) {
method := ctx.Method() method := ctx.Method()
path := ctx.Path() path := ctx.Path()
if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrection() { config := h.config // ctx.Application().GetConfigurationReadOnly()
if !config.GetDisablePathCorrection() {
if len(path) > 1 && strings.HasSuffix(path, "/") { if len(path) > 1 && strings.HasSuffix(path, "/") {
// Remove trailing slash and client-permanent rule for redirection, // Remove trailing slash and client-permanent rule for redirection,
// if confgiuration allows that and path has an extra slash. // if confgiuration allows that and path has an extra slash.
// update the new path and redirect. // 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 // use Trim to ensure there is no open redirect due to two leading slashes
path = "/" + strings.Trim(path, "/") path = "/" + strings.Trim(path, "/")
u.Path = path
r.URL.Path = path if !config.GetDisablePathCorrectionRedirection() {
if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrectionRedirection() {
// do redirect, else continue with the modified path without the last "/". // 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 // Fixes https://github.com/kataras/iris/issues/921
// This is caused for security reasons, imagine a payment shop, // This is caused for security reasons, imagine a payment shop,
@ -238,7 +246,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
// localhost -> invalid // localhost -> invalid
// sub.mydomain.com -> valid // sub.mydomain.com -> valid
// sub.localhost -> valid // sub.localhost -> valid
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() serverHost := config.GetVHost()
if serverHost == requestHost { if serverHost == requestHost {
continue // it's not a subdomain, it's a full domain (with .com...) 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 break
} }
if ctx.Application().ConfigurationReadOnly().GetFireMethodNotAllowed() { if config.GetFireMethodNotAllowed() {
for i := range h.trees { for i := range h.trees {
t := h.trees[i] t := h.trees[i]
// if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not // if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not

View File

@ -14,11 +14,12 @@ import (
// If any of the following fields are changed then the // If any of the following fields are changed then the
// caller should Refresh the router. // caller should Refresh the router.
type Route struct { type Route struct {
Name string `json:"name"` // "userRoute" Name string `json:"name"` // "userRoute"
Method string `json:"method"` // "GET" Description string `json:"description"` // "lists a user"
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. Method string `json:"method"` // "GET"
Subdomain string `json:"subdomain"` // "admin." 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.
tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" Subdomain string `json:"subdomain"` // "admin."
tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}"
// temp storage, they're appended to the Handlers on build. // temp storage, they're appended to the Handlers on build.
// Execution happens before Handlers, can be empty. // Execution happens before Handlers, can be empty.
beginHandlers context.Handlers beginHandlers context.Handlers
@ -322,7 +323,11 @@ func (r *Route) Trace() string {
if r.Subdomain != "" { if r.Subdomain != "" {
printfmt += fmt.Sprintf(" %s", 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 mainHandlerName := r.MainHandlerName
if !strings.HasSuffix(mainHandlerName, ")") { if !strings.HasSuffix(mainHandlerName, ")") {
@ -330,9 +335,9 @@ func (r *Route) Trace() string {
} }
if l := r.RegisteredHandlersLen(); l > 1 { 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 { } 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) // printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.MainHandlerName)

View File

@ -1,6 +1,8 @@
package router_test package router_test
import ( import (
"net/http"
"strings"
"testing" "testing"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
@ -39,3 +41,34 @@ func TestRouteExists(t *testing.T) {
// run the tests // run the tests
httptest.New(t, app, httptest.Debug(false)).Request("GET", "/route-test").Expect().Status(iris.StatusOK) 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)
}
}

12
iris.go
View File

@ -765,18 +765,24 @@ func (app *Application) Build() error {
if app.I18n.Loaded() { if app.I18n.Loaded() {
// {{ tr "lang" "key" arg1 arg2 }} // {{ tr "lang" "key" arg1 arg2 }}
app.view.AddFunc("tr", app.I18n.Tr) app.view.AddFunc("tr", app.I18n.Tr)
app.WrapRouter(app.I18n.Wrapper()) app.Router.WrapRouter(app.I18n.Wrapper())
} }
if !app.Router.Downgraded() { if !app.Router.Downgraded() {
// router // router
if err := app.tryInjectLiveReload(); err != nil { if err := app.tryInjectLiveReload(); err != nil {
rp.Errf("LiveReload: init: failed: %v", err) 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 // 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) err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false)
if err != nil { if err != nil {
rp.Err(err) rp.Err(err)

View File

@ -2,6 +2,7 @@ package mvc
import ( import (
"fmt" "fmt"
"os"
"reflect" "reflect"
"strings" "strings"
@ -81,6 +82,9 @@ type ControllerActivator struct {
// true if this controller listens and serves to websocket events. // true if this controller listens and serves to websocket events.
servesWebsocket bool servesWebsocket bool
// true to skip the internal "activate".
activated bool
} }
// NameOf returns the package name + the struct type's name, // 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 { func newControllerActivator(app *Application, controller interface{}) *ControllerActivator {
if controller == nil {
return nil
}
if c, ok := controller.(*ControllerActivator); ok {
return c
}
typ := reflect.TypeOf(controller) typ := reflect.TypeOf(controller)
c := &ControllerActivator{ c := &ControllerActivator{
@ -225,6 +237,11 @@ func (c *ControllerActivator) isReservedMethod(name string) bool {
return false return false
} }
func (c *ControllerActivator) markAsWebsocket() {
c.servesWebsocket = true
c.attachInjector()
}
func (c *ControllerActivator) attachInjector() { func (c *ControllerActivator) attachInjector() {
if c.injector == nil { if c.injector == nil {
partyCountParams := macro.CountParams(c.app.Router.GetRelPath(), *c.app.Router.Macros()) partyCountParams := macro.CountParams(c.app.Router.GetRelPath(), *c.app.Router.Macros())
@ -232,12 +249,18 @@ func (c *ControllerActivator) attachInjector() {
} }
} }
func (c *ControllerActivator) markAsWebsocket() { // Activated can be called to skip the internal method parsing.
c.servesWebsocket = true func (c *ControllerActivator) Activated() bool {
c.attachInjector() b := c.activated
c.activated = true
return b
} }
func (c *ControllerActivator) activate() { func (c *ControllerActivator) activate() {
if c.Activated() {
return
}
c.parseMethods() c.parseMethods()
} }
@ -314,10 +337,16 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override
return nil return nil
} }
wd, _ := os.Getwd()
for _, r := range routes { for _, r := range routes {
// change the main handler's name in order to respect the controller's and give // change the main handler's name and file:line
// a proper debug message. // in order to respect the controller's and give
// a proper debug/log message.
r.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) 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 // add this as a reserved method name in order to

66
mvc/grpc.go Normal file
View File

@ -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"
}
}
}
}

View File

@ -111,6 +111,15 @@ func (app *Application) Register(dependencies ...interface{}) *Application {
return app 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. // Handle serves a controller for the current mvc application's Router.
// It accept any custom struct which its functions will be transformed // It accept any custom struct which its functions will be transformed
// to routes. // to routes.
@ -154,9 +163,12 @@ func (app *Application) Register(dependencies ...interface{}) *Application {
// Result or (Result, error) // Result or (Result, error)
// where Get is an HTTP Method func. // 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 // Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc
func (app *Application) Handle(controller interface{}) *Application { func (app *Application) Handle(controller interface{}, options ...Option) *Application {
app.handle(controller) app.handle(controller, options...)
return app return app
} }
@ -195,7 +207,7 @@ func (app *Application) GetNamespaces() websocket.Namespaces {
return websocket.JoinConnHandlers(app.websocketControllers...).GetNamespaces() 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. // initialize the controller's activator, nothing too magical so far.
c := newControllerActivator(app, controller) c := newControllerActivator(app, controller)
@ -208,6 +220,12 @@ func (app *Application) handle(controller interface{}) *ControllerActivator {
before.BeforeActivation(c) before.BeforeActivation(c)
} }
for _, opt := range options {
if opt != nil {
opt.Apply(c)
}
}
c.activate() c.activate()
if after, okAfter := controller.(interface { if after, okAfter := controller.(interface {