feat: verify RpcPath on startup (#2159)

* feat: verify RpcPath on startup

* feat: support http header Grpc-Timeout
This commit is contained in:
Kevin Wan 2022-07-17 12:37:23 +08:00 committed by GitHub
parent b206dd28a3
commit 557383fbbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 31 deletions

View File

@ -21,8 +21,8 @@ type (
Method string Method string
// Path is the HTTP path. // Path is the HTTP path.
Path string Path string
// Rpc is the gRPC rpc method, with format of package.service/method // RpcPath is the gRPC rpc method, with format of package.service/method
Rpc string RpcPath string
} }
// upstream is the configuration for upstream. // upstream is the configuration for upstream.

View File

@ -0,0 +1,34 @@
package internal
import (
"fmt"
"github.com/fullstorydev/grpcurl"
"github.com/jhump/protoreflect/desc"
)
// GetMethods returns all methods of the given grpcurl.DescriptorSource.
func GetMethods(source grpcurl.DescriptorSource) ([]string, error) {
svcs, err := source.ListServices()
if err != nil {
return nil, err
}
var methods []string
for _, svc := range svcs {
d, err := source.FindSymbol(svc)
if err != nil {
return nil, err
}
switch val := d.(type) {
case *desc.ServiceDescriptor:
svcMethods := val.GetMethods()
for _, method := range svcMethods {
methods = append(methods, fmt.Sprintf("%s/%s", svc, method.GetName()))
}
}
}
return methods, nil
}

View File

@ -0,0 +1,29 @@
package internal
import (
"encoding/base64"
"io/ioutil"
"os"
"testing"
"github.com/fullstorydev/grpcurl"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/hash"
)
const b64pb = `CpgBCgtoZWxsby5wcm90bxIFaGVsbG8iHQoHUmVxdWVzdBISCgRwaW5nGAEgASgJUgRwaW5nIh4KCFJlc3BvbnNlEhIKBHBvbmcYASABKAlSBHBvbmcyMAoFSGVsbG8SJwoEUGluZxIOLmhlbGxvLlJlcXVlc3QaDy5oZWxsby5SZXNwb25zZUIJWgcuL2hlbGxvYgZwcm90bzM=`
func TestGetMethods(t *testing.T) {
tmpfile, err := ioutil.TempFile(os.TempDir(), hash.Md5Hex([]byte(b64pb)))
assert.Nil(t, err)
b, err := base64.StdEncoding.DecodeString(b64pb)
assert.Nil(t, err)
assert.Nil(t, ioutil.WriteFile(tmpfile.Name(), b, os.ModeTemporary))
defer os.Remove(tmpfile.Name())
source, err := grpcurl.DescriptorSourceFromProtoSets(tmpfile.Name())
assert.Nil(t, err)
methods, err := GetMethods(source)
assert.Nil(t, err)
assert.EqualValues(t, []string{"hello.Hello/Ping"}, methods)
}

View File

@ -1,4 +1,4 @@
package gateway package internal
import ( import (
"fmt" "fmt"
@ -11,7 +11,8 @@ const (
metadataPrefix = "gateway-" metadataPrefix = "gateway-"
) )
func buildHeaders(header http.Header) []string { // BuildHeaders builds the headers for the gateway from HTTP headers.
func BuildHeaders(header http.Header) []string {
var headers []string var headers []string
for k, v := range header { for k, v := range header {

View File

@ -1,4 +1,4 @@
package gateway package internal
import ( import (
"net/http/httptest" "net/http/httptest"
@ -10,12 +10,12 @@ import (
func TestBuildHeadersNoValue(t *testing.T) { func TestBuildHeadersNoValue(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest("GET", "/", nil)
req.Header.Add("a", "b") req.Header.Add("a", "b")
assert.Nil(t, buildHeaders(req.Header)) assert.Nil(t, BuildHeaders(req.Header))
} }
func TestBuildHeadersWithValues(t *testing.T) { func TestBuildHeadersWithValues(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest("GET", "/", nil)
req.Header.Add("grpc-metadata-a", "b") req.Header.Add("grpc-metadata-a", "b")
req.Header.Add("grpc-metadata-b", "b") req.Header.Add("grpc-metadata-b", "b")
assert.EqualValues(t, []string{"gateway-A:b", "gateway-B:b"}, buildHeaders(req.Header)) assert.EqualValues(t, []string{"gateway-A:b", "gateway-B:b"}, BuildHeaders(req.Header))
} }

View File

@ -1,4 +1,4 @@
package gateway package internal
import ( import (
"bytes" "bytes"
@ -11,17 +11,8 @@ import (
"github.com/zeromicro/go-zero/rest/pathvar" "github.com/zeromicro/go-zero/rest/pathvar"
) )
func buildJsonRequestParser(m map[string]interface{}, resolver jsonpb.AnyResolver) ( // NewRequestParser creates a new request parser from the given http.Request and resolver.
grpcurl.RequestParser, error) { func NewRequestParser(r *http.Request, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(m); err != nil {
return nil, err
}
return grpcurl.NewJSONRequestParser(&buf, resolver), nil
}
func newRequestParser(r *http.Request, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) {
vars := pathvar.Vars(r) vars := pathvar.Vars(r)
params, err := httpx.GetFormValues(r) params, err := httpx.GetFormValues(r)
if err != nil { if err != nil {
@ -50,3 +41,13 @@ func newRequestParser(r *http.Request, resolver jsonpb.AnyResolver) (grpcurl.Req
return buildJsonRequestParser(m, resolver) return buildJsonRequestParser(m, resolver)
} }
func buildJsonRequestParser(m map[string]interface{}, resolver jsonpb.AnyResolver) (
grpcurl.RequestParser, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(m); err != nil {
return nil, err
}
return grpcurl.NewJSONRequestParser(&buf, resolver), nil
}

View File

@ -1,4 +1,4 @@
package gateway package internal
import ( import (
"net/http/httptest" "net/http/httptest"
@ -11,7 +11,7 @@ import (
func TestNewRequestParserNoVar(t *testing.T) { func TestNewRequestParserNoVar(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest("GET", "/", nil)
parser, err := newRequestParser(req, nil) parser, err := NewRequestParser(req, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, parser) assert.NotNil(t, parser)
} }
@ -19,14 +19,14 @@ func TestNewRequestParserNoVar(t *testing.T) {
func TestNewRequestParserWithVars(t *testing.T) { func TestNewRequestParserWithVars(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest("GET", "/", nil)
req = pathvar.WithVars(req, map[string]string{"a": "b"}) req = pathvar.WithVars(req, map[string]string{"a": "b"})
parser, err := newRequestParser(req, nil) parser, err := NewRequestParser(req, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, parser) assert.NotNil(t, parser)
} }
func TestNewRequestParserNoVarWithBody(t *testing.T) { func TestNewRequestParserNoVarWithBody(t *testing.T) {
req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"}`)) req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"}`))
parser, err := newRequestParser(req, nil) parser, err := NewRequestParser(req, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, parser) assert.NotNil(t, parser)
} }
@ -34,7 +34,7 @@ func TestNewRequestParserNoVarWithBody(t *testing.T) {
func TestNewRequestParserWithVarsWithBody(t *testing.T) { func TestNewRequestParserWithVarsWithBody(t *testing.T) {
req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"}`)) req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"}`))
req = pathvar.WithVars(req, map[string]string{"c": "d"}) req = pathvar.WithVars(req, map[string]string{"c": "d"})
parser, err := newRequestParser(req, nil) parser, err := NewRequestParser(req, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, parser) assert.NotNil(t, parser)
} }
@ -42,14 +42,14 @@ func TestNewRequestParserWithVarsWithBody(t *testing.T) {
func TestNewRequestParserWithVarsWithWrongBody(t *testing.T) { func TestNewRequestParserWithVarsWithWrongBody(t *testing.T) {
req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"`)) req := httptest.NewRequest("GET", "/", strings.NewReader(`{"a": "b"`))
req = pathvar.WithVars(req, map[string]string{"c": "d"}) req = pathvar.WithVars(req, map[string]string{"c": "d"})
parser, err := newRequestParser(req, nil) parser, err := NewRequestParser(req, nil)
assert.NotNil(t, err) assert.NotNil(t, err)
assert.Nil(t, parser) assert.Nil(t, parser)
} }
func TestNewRequestParserWithForm(t *testing.T) { func TestNewRequestParserWithForm(t *testing.T) {
req := httptest.NewRequest("GET", "/val?a=b", nil) req := httptest.NewRequest("GET", "/val?a=b", nil)
parser, err := newRequestParser(req, nil) parser, err := NewRequestParser(req, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, parser) assert.NotNil(t, parser)
} }

View File

@ -0,0 +1,19 @@
package internal
import (
"net/http"
"time"
)
const grpcTimeoutHeader = "Grpc-Timeout"
// GetTimeout returns the timeout from the header, if not set, returns the default timeout.
func GetTimeout(header http.Header, defaultTimeout time.Duration) time.Duration {
if timeout := header.Get(grpcTimeoutHeader); len(timeout) > 0 {
if t, err := time.ParseDuration(timeout); err == nil {
return t
}
}
return defaultTimeout
}

View File

@ -0,0 +1,22 @@
package internal
import (
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGetTimeout(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set(grpcTimeoutHeader, "1s")
timeout := GetTimeout(req.Header, time.Second*5)
assert.Equal(t, time.Second, timeout)
}
func TestGetTimeoutDefault(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
timeout := GetTimeout(req.Header, time.Second*5)
assert.Equal(t, time.Second*5, timeout)
}

View File

@ -35,7 +35,7 @@ Upstreams:
Mapping: Mapping:
- Method: get - Method: get
Path: /pingHello/:ping Path: /pingHello/:ping
Rpc: hello.Hello/Ping RpcPath: hello.Hello/Ping
- Grpc: - Grpc:
Endpoints: Endpoints:
- localhost:8081 - localhost:8081
@ -43,7 +43,7 @@ Upstreams:
Mapping: Mapping:
- Method: post - Method: post
Path: /pingWorld Path: /pingWorld
Rpc: world.World/Ping RpcPath: world.World/Ping
``` ```
## Generate ProtoSet files ## Generate ProtoSet files

View File

@ -2,6 +2,7 @@ package gateway
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -11,6 +12,7 @@ import (
"github.com/jhump/protoreflect/grpcreflect" "github.com/jhump/protoreflect/grpcreflect"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mr" "github.com/zeromicro/go-zero/core/mr"
"github.com/zeromicro/go-zero/gateway/internal"
"github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/httpx"
"github.com/zeromicro/go-zero/zrpc" "github.com/zeromicro/go-zero/zrpc"
@ -58,8 +60,23 @@ func (s *Server) build() error {
return return
} }
methods, err := internal.GetMethods(source)
if err != nil {
cancel(err)
return
}
methodSet := make(map[string]struct{})
for _, m := range methods {
methodSet[m] = struct{}{}
}
resolver := grpcurl.AnyResolverFromDescriptorSource(source) resolver := grpcurl.AnyResolverFromDescriptorSource(source)
for _, m := range up.Mapping { for _, m := range up.Mapping {
if _, ok := methodSet[m.RpcPath]; !ok {
cancel(fmt.Errorf("rpc method %s not found", m.RpcPath))
return
}
writer.Write(rest.Route{ writer.Write(rest.Route{
Method: strings.ToUpper(m.Method), Method: strings.ToUpper(m.Method),
Path: m.Path, Path: m.Path,
@ -82,15 +99,16 @@ func (s *Server) buildHandler(source grpcurl.DescriptorSource, resolver jsonpb.A
Formatter: grpcurl.NewJSONFormatter(true, Formatter: grpcurl.NewJSONFormatter(true,
grpcurl.AnyResolverFromDescriptorSource(source)), grpcurl.AnyResolverFromDescriptorSource(source)),
} }
parser, err := newRequestParser(r, resolver) parser, err := internal.NewRequestParser(r, resolver)
if err != nil { if err != nil {
httpx.Error(w, err) httpx.Error(w, err)
return return
} }
ctx, can := context.WithTimeout(r.Context(), s.timeout) timeout := internal.GetTimeout(r.Header, s.timeout)
ctx, can := context.WithTimeout(r.Context(), timeout)
defer can() defer can()
if err := grpcurl.InvokeRPC(ctx, source, cli.Conn(), m.Rpc, buildHeaders(r.Header), if err := grpcurl.InvokeRPC(ctx, source, cli.Conn(), m.RpcPath, internal.BuildHeaders(r.Header),
handler, parser.Next); err != nil { handler, parser.Next); err != nil {
httpx.Error(w, err) httpx.Error(w, err)
} }