mirror of
https://github.com/minio/minio.git
synced 2025-01-11 15:03:22 -05:00
add audit/admin trace support for browser requests (#10947)
To support this functionality we had to fork the gorilla/rpc package with relevant changes
This commit is contained in:
parent
7bc47a14cc
commit
86409fa93d
@ -23,5 +23,10 @@ issues:
|
||||
exclude:
|
||||
- should have a package comment
|
||||
- error strings should not be capitalized or end with punctuation or a newline
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg/rpc
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.20.0 # use the fixed version to not introduce new linters unexpectedly
|
||||
|
@ -21,7 +21,7 @@ import storage from 'local-storage-fallback'
|
||||
|
||||
class Web {
|
||||
constructor(endpoint) {
|
||||
const namespace = 'Web'
|
||||
const namespace = 'web'
|
||||
this.JSONrpc = new JSONrpc({
|
||||
endpoint,
|
||||
namespace
|
||||
|
File diff suppressed because one or more lines are too long
@ -1006,6 +1006,15 @@ func mustTrace(entry interface{}, trcAll, errOnly bool) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle browser requests separately filter them and return.
|
||||
if HasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath+"/upload") {
|
||||
if errOnly {
|
||||
return trcInfo.RespInfo.StatusCode >= http.StatusBadRequest
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
trace := trcAll || !HasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath+SlashSeparator)
|
||||
if errOnly {
|
||||
return trace && trcInfo.RespInfo.StatusCode >= http.StatusBadRequest
|
||||
|
@ -28,8 +28,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
jsonrpc "github.com/minio/minio/pkg/rpc"
|
||||
trace "github.com/minio/minio/pkg/trace"
|
||||
)
|
||||
|
||||
@ -83,8 +85,8 @@ func getOpName(name string) (op string) {
|
||||
op = strings.TrimPrefix(name, "github.com/minio/minio/cmd.")
|
||||
op = strings.TrimSuffix(op, "Handler-fm")
|
||||
op = strings.Replace(op, "objectAPIHandlers", "s3", 1)
|
||||
op = strings.Replace(op, "webAPIHandlers", "webui", 1)
|
||||
op = strings.Replace(op, "adminAPIHandlers", "admin", 1)
|
||||
op = strings.Replace(op, "(*webAPIHandlers)", "web", 1)
|
||||
op = strings.Replace(op, "(*storageRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*peerRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*lockRESTServer)", "internal", 1)
|
||||
@ -95,6 +97,69 @@ func getOpName(name string) (op string) {
|
||||
return op
|
||||
}
|
||||
|
||||
// WebTrace gets trace of web request
|
||||
func WebTrace(ri *jsonrpc.RequestInfo) trace.Info {
|
||||
r := ri.Request
|
||||
w := ri.ResponseWriter
|
||||
|
||||
name := ri.Method
|
||||
// Setup a http request body recorder
|
||||
reqHeaders := r.Header.Clone()
|
||||
reqHeaders.Set("Host", r.Host)
|
||||
if len(r.TransferEncoding) == 0 {
|
||||
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
|
||||
} else {
|
||||
reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
|
||||
}
|
||||
|
||||
t := trace.Info{FuncName: name}
|
||||
t.NodeName = r.Host
|
||||
if globalIsDistErasure {
|
||||
t.NodeName = GetLocalPeer(globalEndpoints)
|
||||
}
|
||||
|
||||
// strip port from the host address
|
||||
if host, _, err := net.SplitHostPort(t.NodeName); err == nil {
|
||||
t.NodeName = host
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
rq := trace.RequestInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Proto: r.Proto,
|
||||
Method: r.Method,
|
||||
Path: SlashSeparator + pathJoin(vars["bucket"], vars["object"]),
|
||||
RawQuery: r.URL.RawQuery,
|
||||
Client: handlers.GetSourceIP(r),
|
||||
Headers: reqHeaders,
|
||||
}
|
||||
|
||||
rw, ok := w.(*logger.ResponseWriter)
|
||||
if ok {
|
||||
rs := trace.ResponseInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Headers: rw.Header().Clone(),
|
||||
StatusCode: rw.StatusCode,
|
||||
Body: logger.BodyPlaceHolder,
|
||||
}
|
||||
|
||||
if rs.StatusCode == 0 {
|
||||
rs.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
t.RespInfo = rs
|
||||
t.CallStats = trace.CallStats{
|
||||
Latency: rs.Time.Sub(rw.StartTime),
|
||||
InputBytes: int(r.ContentLength),
|
||||
OutputBytes: rw.Size(),
|
||||
TimeToFirstByte: rw.TimeToFirstByte,
|
||||
}
|
||||
}
|
||||
|
||||
t.ReqInfo = rq
|
||||
return t
|
||||
}
|
||||
|
||||
// Trace gets trace of http request
|
||||
func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Request) trace.Info {
|
||||
name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -56,6 +57,24 @@ import (
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
)
|
||||
|
||||
func extractBucketObject(args reflect.Value) (bucketName, objectName string) {
|
||||
switch args.Kind() {
|
||||
case reflect.Ptr:
|
||||
a := args.Elem()
|
||||
for i := 0; i < a.NumField(); i++ {
|
||||
switch a.Type().Field(i).Name {
|
||||
case "BucketName":
|
||||
bucketName = a.Field(i).String()
|
||||
case "Prefix":
|
||||
objectName = a.Field(i).String()
|
||||
case "ObjectName":
|
||||
objectName = a.Field(i).String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return bucketName, objectName
|
||||
}
|
||||
|
||||
// WebGenericArgs - empty struct for calls that don't accept arguments
|
||||
// for ex. ServerInfo, GenerateAuth
|
||||
type WebGenericArgs struct{}
|
||||
|
@ -189,7 +189,7 @@ func testStorageInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
|
||||
storageInfoRequest := &WebGenericArgs{}
|
||||
storageInfoReply := &StorageInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.StorageInfo", authorization, storageInfoRequest)
|
||||
req, err := newTestWebRPCRequest("web.StorageInfo", authorization, storageInfoRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -222,7 +222,7 @@ func testServerInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
|
||||
|
||||
serverInfoRequest := &WebGenericArgs{}
|
||||
serverInfoReply := &ServerInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.ServerInfo", authorization, serverInfoRequest)
|
||||
req, err := newTestWebRPCRequest("web.ServerInfo", authorization, serverInfoRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -279,7 +279,7 @@ func testMakeBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
|
||||
for i, testCase := range testCases {
|
||||
makeBucketRequest := MakeBucketArgs{BucketName: testCase.bucketName}
|
||||
makeBucketReply := &WebGenericRep{}
|
||||
req, err := newTestWebRPCRequest("Web.MakeBucket", authorization, makeBucketRequest)
|
||||
req, err := newTestWebRPCRequest("web.MakeBucket", authorization, makeBucketRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request: <ERROR> %v", i+1, err)
|
||||
}
|
||||
@ -366,7 +366,7 @@ func testDeleteBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
makeBucketRequest := MakeBucketArgs{BucketName: test.bucketName}
|
||||
makeBucketReply := &WebGenericRep{}
|
||||
|
||||
req, err := newTestWebRPCRequest("Web.DeleteBucket", test.token, makeBucketRequest)
|
||||
req, err := newTestWebRPCRequest("web.DeleteBucket", test.token, makeBucketRequest)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -439,7 +439,7 @@ func testListBucketsWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
|
||||
listBucketsRequest := WebGenericArgs{}
|
||||
listBucketsReply := &ListBucketsRep{}
|
||||
req, err := newTestWebRPCRequest("Web.ListBuckets", authorization, listBucketsRequest)
|
||||
req, err := newTestWebRPCRequest("web.ListBuckets", authorization, listBucketsRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -500,7 +500,7 @@ func testListObjectsWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
listObjectsRequest := ListObjectsArgs{BucketName: bucketName, Prefix: ""}
|
||||
listObjectsReply := &ListObjectsRep{}
|
||||
var req *http.Request
|
||||
req, err = newTestWebRPCRequest("Web.ListObjects", token, listObjectsRequest)
|
||||
req, err = newTestWebRPCRequest("web.ListObjects", token, listObjectsRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -584,7 +584,7 @@ func testRemoveObjectWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
removeRequest := RemoveObjectArgs{BucketName: bucketName, Objects: []string{"a/", "object"}}
|
||||
removeReply := &WebGenericRep{}
|
||||
req, err := newTestWebRPCRequest("Web.RemoveObject", authorization, removeRequest)
|
||||
req, err := newTestWebRPCRequest("web.RemoveObject", authorization, removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -599,7 +599,7 @@ func testRemoveObjectWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
removeRequest = RemoveObjectArgs{BucketName: bucketName, Objects: []string{"a/", "object"}}
|
||||
removeReply = &WebGenericRep{}
|
||||
req, err = newTestWebRPCRequest("Web.RemoveObject", authorization, removeRequest)
|
||||
req, err = newTestWebRPCRequest("web.RemoveObject", authorization, removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -614,7 +614,7 @@ func testRemoveObjectWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
removeRequest = RemoveObjectArgs{BucketName: bucketName}
|
||||
removeReply = &WebGenericRep{}
|
||||
req, err = newTestWebRPCRequest("Web.RemoveObject", authorization, removeRequest)
|
||||
req, err = newTestWebRPCRequest("web.RemoveObject", authorization, removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -650,7 +650,7 @@ func testGenerateAuthWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
|
||||
generateAuthRequest := WebGenericArgs{}
|
||||
generateAuthReply := &GenerateAuthReply{}
|
||||
req, err := newTestWebRPCRequest("Web.GenerateAuth", authorization, generateAuthRequest)
|
||||
req, err := newTestWebRPCRequest("web.GenerateAuth", authorization, generateAuthRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -692,7 +692,7 @@ func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler)
|
||||
args := WebGenericArgs{}
|
||||
tokenReply := &URLTokenReply{}
|
||||
|
||||
req, err := newTestWebRPCRequest("Web.CreateURLToken", authorization, args)
|
||||
req, err := newTestWebRPCRequest("web.CreateURLToken", authorization, args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1043,7 +1043,7 @@ func testWebPresignedGetHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
Expiry: 1000,
|
||||
}
|
||||
presignGetRep := &PresignedGetRep{}
|
||||
req, err := newTestWebRPCRequest("Web.PresignedGet", authorization, presignGetReq)
|
||||
req, err := newTestWebRPCRequest("web.PresignedGet", authorization, presignGetReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -1088,7 +1088,7 @@ func testWebPresignedGetHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
ObjectName: "",
|
||||
}
|
||||
presignGetRep = &PresignedGetRep{}
|
||||
req, err = newTestWebRPCRequest("Web.PresignedGet", authorization, presignGetReq)
|
||||
req, err = newTestWebRPCRequest("web.PresignedGet", authorization, presignGetReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -1140,7 +1140,7 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
}
|
||||
for _, rpcCall := range webRPCs {
|
||||
reply := &WebGenericRep{}
|
||||
req, nerr := newTestWebRPCRequest("Web."+rpcCall, "Bearer fooauthorization", &WebGenericArgs{})
|
||||
req, nerr := newTestWebRPCRequest("web."+rpcCall, "Bearer fooauthorization", &WebGenericArgs{})
|
||||
if nerr != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request: <ERROR> %v", rpcCall, nerr)
|
||||
}
|
||||
@ -1159,7 +1159,7 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
// Test authorization of Web.Download
|
||||
// Test authorization of web.Download
|
||||
req, err := http.NewRequest(http.MethodGet, "/minio/download/bucket/object?token=wrongauth", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create upload request, %v", err)
|
||||
@ -1175,7 +1175,7 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
// Test authorization of Web.Upload
|
||||
// Test authorization of web.Upload
|
||||
content := []byte("temporary file's content")
|
||||
req, err = http.NewRequest(http.MethodPut, "/minio/upload/bucket/object", nil)
|
||||
req.Header.Set("Authorization", "Bearer foo-authorization")
|
||||
@ -1264,7 +1264,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
for _, rpcCall := range webRPCs {
|
||||
args := &rpcCall.ReqArgs
|
||||
reply := &rpcCall.RepArgs
|
||||
req, nerr := newTestWebRPCRequest("Web."+rpcCall.webRPCName, authorization, args)
|
||||
req, nerr := newTestWebRPCRequest("web."+rpcCall.webRPCName, authorization, args)
|
||||
if nerr != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request: <ERROR> %v", rpcCall, nerr)
|
||||
}
|
||||
@ -1278,10 +1278,10 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test Web.StorageInfo
|
||||
// Test web.StorageInfo
|
||||
storageInfoRequest := &WebGenericArgs{}
|
||||
storageInfoReply := &StorageInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.StorageInfo", authorization, storageInfoRequest)
|
||||
req, err := newTestWebRPCRequest("web.StorageInfo", authorization, storageInfoRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request: <ERROR> %v", err)
|
||||
}
|
||||
@ -1294,7 +1294,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
t.Fatalf("Failed %v", err)
|
||||
}
|
||||
|
||||
// Test authorization of Web.Download
|
||||
// Test authorization of web.Download
|
||||
req, err = http.NewRequest(http.MethodGet, "/minio/download/bucket/object?token="+authorization, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create upload request, %v", err)
|
||||
@ -1304,7 +1304,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code)
|
||||
}
|
||||
|
||||
// Test authorization of Web.Upload
|
||||
// Test authorization of web.Upload
|
||||
content := []byte("temporary file's content")
|
||||
req, err = http.NewRequest(http.MethodPut, "/minio/upload/bucket/object", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+authorization)
|
||||
|
@ -23,9 +23,10 @@ import (
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
jsonrpc "github.com/gorilla/rpc/v2"
|
||||
"github.com/gorilla/rpc/v2/json2"
|
||||
"github.com/minio/minio/browser"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
jsonrpc "github.com/minio/minio/pkg/rpc"
|
||||
"github.com/minio/minio/pkg/rpc/json2"
|
||||
)
|
||||
|
||||
// webAPI container for Web API.
|
||||
@ -76,9 +77,23 @@ func registerWebRouter(router *mux.Router) error {
|
||||
webRPC := jsonrpc.NewServer()
|
||||
webRPC.RegisterCodec(codec, "application/json")
|
||||
webRPC.RegisterCodec(codec, "application/json; charset=UTF-8")
|
||||
webRPC.RegisterAfterFunc(func(ri *jsonrpc.RequestInfo) {
|
||||
if ri != nil {
|
||||
claims, _, _ := webRequestAuthenticate(ri.Request)
|
||||
bucketName, objectName := extractBucketObject(ri.Args)
|
||||
ri.Request = mux.SetURLVars(ri.Request, map[string]string{
|
||||
"bucket": bucketName,
|
||||
"object": objectName,
|
||||
})
|
||||
if globalHTTPTrace.HasSubscribers() {
|
||||
globalHTTPTrace.Publish(WebTrace(ri))
|
||||
}
|
||||
logger.AuditLog(ri.ResponseWriter, ri.Request, ri.Method, claims.Map())
|
||||
}
|
||||
})
|
||||
|
||||
// Register RPC handlers with server
|
||||
if err := webRPC.RegisterService(web, "Web"); err != nil {
|
||||
if err := webRPC.RegisterService(web, "web"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
202
pkg/rpc/LICENSE
Normal file
202
pkg/rpc/LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
27
pkg/rpc/LICENSE.ORIG
Normal file
27
pkg/rpc/LICENSE.ORIG
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
9
pkg/rpc/README.md
Normal file
9
pkg/rpc/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
rpc
|
||||
===
|
||||
|
||||
|
||||
rpc/v2 support for JSON-RPC 2.0 Specification.
|
||||
|
||||
gorilla/rpc is a foundation for RPC over HTTP services, providing access to the exported methods of an object through HTTP requests.
|
||||
|
||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/rpc
|
84
pkg/rpc/compression_selector.go
Normal file
84
pkg/rpc/compression_selector.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.ORIG file.
|
||||
//
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// gzipWriter writes and closes the gzip writer.
|
||||
type gzipWriter struct {
|
||||
w *gzip.Writer
|
||||
}
|
||||
|
||||
func (gw *gzipWriter) Write(p []byte) (n int, err error) {
|
||||
defer gw.w.Close()
|
||||
return gw.w.Write(p)
|
||||
}
|
||||
|
||||
// gzipEncoder implements the gzip compressed http encoder.
|
||||
type gzipEncoder struct {
|
||||
}
|
||||
|
||||
func (enc *gzipEncoder) Encode(w http.ResponseWriter) io.Writer {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
return &gzipWriter{gzip.NewWriter(w)}
|
||||
}
|
||||
|
||||
// flateWriter writes and closes the flate writer.
|
||||
type flateWriter struct {
|
||||
w *flate.Writer
|
||||
}
|
||||
|
||||
func (fw *flateWriter) Write(p []byte) (n int, err error) {
|
||||
defer fw.w.Close()
|
||||
return fw.w.Write(p)
|
||||
}
|
||||
|
||||
// flateEncoder implements the flate compressed http encoder.
|
||||
type flateEncoder struct {
|
||||
}
|
||||
|
||||
func (enc *flateEncoder) Encode(w http.ResponseWriter) io.Writer {
|
||||
fw, err := flate.NewWriter(w, flate.DefaultCompression)
|
||||
if err != nil {
|
||||
return w
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "deflate")
|
||||
return &flateWriter{fw}
|
||||
}
|
||||
|
||||
// CompressionSelector generates the compressed http encoder.
|
||||
type CompressionSelector struct {
|
||||
}
|
||||
|
||||
// Select method selects the correct compression encoder based on http HEADER.
|
||||
func (*CompressionSelector) Select(r *http.Request) Encoder {
|
||||
encHeader := r.Header.Get("Accept-Encoding")
|
||||
encTypes := strings.FieldsFunc(encHeader, func(r rune) bool {
|
||||
return unicode.IsSpace(r) || r == ','
|
||||
})
|
||||
|
||||
for _, enc := range encTypes {
|
||||
switch enc {
|
||||
case "gzip":
|
||||
return &gzipEncoder{}
|
||||
case "deflate":
|
||||
return &flateEncoder{}
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultEncoder
|
||||
}
|
82
pkg/rpc/doc.go
Normal file
82
pkg/rpc/doc.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package gorilla/rpc is a foundation for RPC over HTTP services, providing
|
||||
access to the exported methods of an object through HTTP requests.
|
||||
|
||||
This package derives from the standard net/rpc package but uses a single HTTP
|
||||
request per call instead of persistent connections. Other differences
|
||||
compared to net/rpc:
|
||||
|
||||
- Multiple codecs can be registered in the same server.
|
||||
- A codec is chosen based on the "Content-Type" header from the request.
|
||||
- Service methods also receive http.Request as parameter.
|
||||
- This package can be used on Google App Engine.
|
||||
|
||||
Let's setup a server and register a codec and service:
|
||||
|
||||
import (
|
||||
"http"
|
||||
"github.com/minio/minio/pkg/rpc/"
|
||||
"github.com/minio/minio/pkg/rpc/json2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
s := rpc.NewServer()
|
||||
s.RegisterCodec(json2.NewCodec(), "application/json")
|
||||
s.RegisterService(new(HelloService), "")
|
||||
http.Handle("/rpc", s)
|
||||
}
|
||||
|
||||
This server handles requests to the "/rpc" path using a JSON codec.
|
||||
A codec is tied to a content type. In the example above, the JSON codec is
|
||||
registered to serve requests with "application/json" as the value for the
|
||||
"Content-Type" header. If the header includes a charset definition, it is
|
||||
ignored; only the media-type part is taken into account.
|
||||
|
||||
A service can be registered using a name. If the name is empty, like in the
|
||||
example above, it will be inferred from the service type.
|
||||
|
||||
That's all about the server setup. Now let's define a simple service:
|
||||
|
||||
type HelloArgs struct {
|
||||
Who string
|
||||
}
|
||||
|
||||
type HelloReply struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type HelloService struct {}
|
||||
|
||||
func (h *HelloService) Say(r *http.Request, args *HelloArgs, reply *HelloReply) error {
|
||||
reply.Message = "Hello, " + args.Who + "!"
|
||||
return nil
|
||||
}
|
||||
|
||||
The example above defines a service with a method "HelloService.Say" and
|
||||
the arguments and reply related to that method.
|
||||
|
||||
The service must be exported (begin with an upper case letter) or local
|
||||
(defined in the package registering the service).
|
||||
|
||||
When a service is registered, the server inspects the service methods
|
||||
and make available the ones that follow these rules:
|
||||
|
||||
- The method name is exported.
|
||||
- The method has three arguments: *http.Request, *args, *reply.
|
||||
- All three arguments are pointers.
|
||||
- The second and third arguments are exported or local.
|
||||
- The method has return type error.
|
||||
|
||||
All other methods are ignored.
|
||||
*/
|
||||
package rpc
|
48
pkg/rpc/encoder_selector.go
Normal file
48
pkg/rpc/encoder_selector.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Encoder interface contains the encoder for http response.
|
||||
// Eg. gzip, flate compressions.
|
||||
type Encoder interface {
|
||||
Encode(w http.ResponseWriter) io.Writer
|
||||
}
|
||||
|
||||
type encoder struct {
|
||||
}
|
||||
|
||||
func (_ *encoder) Encode(w http.ResponseWriter) io.Writer {
|
||||
return w
|
||||
}
|
||||
|
||||
var DefaultEncoder = &encoder{}
|
||||
|
||||
// EncoderSelector interface provides a way to select encoder using the http
|
||||
// request. Typically people can use this to check HEADER of the request and
|
||||
// figure out client capabilities.
|
||||
// Eg. "Accept-Encoding" tells about supported compressions.
|
||||
type EncoderSelector interface {
|
||||
Select(r *http.Request) Encoder
|
||||
}
|
||||
|
||||
type encoderSelector struct {
|
||||
}
|
||||
|
||||
func (_ *encoderSelector) Select(_ *http.Request) Encoder {
|
||||
return DefaultEncoder
|
||||
}
|
||||
|
||||
var DefaultEncoderSelector = &encoderSelector{}
|
83
pkg/rpc/json2/client.go
Normal file
83
pkg/rpc/json2/client.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package json2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Request and Response
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// clientRequest represents a JSON-RPC request sent by a client.
|
||||
type clientRequest struct {
|
||||
// JSON-RPC protocol.
|
||||
Version string `json:"jsonrpc"`
|
||||
|
||||
// A String containing the name of the method to be invoked.
|
||||
Method string `json:"method"`
|
||||
|
||||
// Object to pass as request parameter to the method.
|
||||
Params interface{} `json:"params"`
|
||||
|
||||
// The request id. This can be of any type. It is used to match the
|
||||
// response with the request that it is replying to.
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
// clientResponse represents a JSON-RPC response returned to a client.
|
||||
type clientResponse struct {
|
||||
Version string `json:"jsonrpc"`
|
||||
Result *jsoniter.RawMessage `json:"result"`
|
||||
Error *jsoniter.RawMessage `json:"error"`
|
||||
}
|
||||
|
||||
// EncodeClientRequest encodes parameters for a JSON-RPC client request.
|
||||
func EncodeClientRequest(method string, args interface{}) ([]byte, error) {
|
||||
c := &clientRequest{
|
||||
Version: "2.0",
|
||||
Method: method,
|
||||
Params: args,
|
||||
Id: uint64(rand.Int63()),
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// DecodeClientResponse decodes the response body of a client request into
|
||||
// the interface reply.
|
||||
func DecodeClientResponse(r io.Reader, reply interface{}) error {
|
||||
var c clientResponse
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err := json.NewDecoder(r).Decode(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Error != nil {
|
||||
jsonErr := &Error{}
|
||||
if err := json.Unmarshal(*c.Error, jsonErr); err != nil {
|
||||
return &Error{
|
||||
Code: E_SERVER,
|
||||
Message: string(*c.Error),
|
||||
}
|
||||
}
|
||||
return jsonErr
|
||||
}
|
||||
|
||||
if c.Result == nil {
|
||||
return ErrNullResult
|
||||
}
|
||||
|
||||
return json.Unmarshal(*c.Result, reply)
|
||||
}
|
44
pkg/rpc/json2/error.go
Normal file
44
pkg/rpc/json2/error.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package json2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type ErrorCode int
|
||||
|
||||
const (
|
||||
E_PARSE ErrorCode = -32700
|
||||
E_INVALID_REQ ErrorCode = -32600
|
||||
E_NO_METHOD ErrorCode = -32601
|
||||
E_BAD_PARAMS ErrorCode = -32602
|
||||
E_INTERNAL ErrorCode = -32603
|
||||
E_SERVER ErrorCode = -32000
|
||||
)
|
||||
|
||||
var ErrNullResult = errors.New("result is null")
|
||||
|
||||
type Error struct {
|
||||
// A Number that indicates the error type that occurred.
|
||||
Code ErrorCode `json:"code"` /* required */
|
||||
|
||||
// A String providing a short description of the error.
|
||||
// The message SHOULD be limited to a concise single sentence.
|
||||
Message string `json:"message"` /* required */
|
||||
|
||||
// A Primitive or Structured value that contains additional information about the error.
|
||||
Data interface{} `json:"data"` /* optional */
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Message
|
||||
}
|
292
pkg/rpc/json2/json_test.go
Normal file
292
pkg/rpc/json2/json_test.go
Normal file
@ -0,0 +1,292 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package json2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/rpc"
|
||||
)
|
||||
|
||||
// ResponseRecorder is an implementation of http.ResponseWriter that
|
||||
// records its mutations for later inspection in tests.
|
||||
type ResponseRecorder struct {
|
||||
Code int // the HTTP response code from WriteHeader
|
||||
HeaderMap http.Header // the HTTP response headers
|
||||
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
|
||||
Flushed bool
|
||||
}
|
||||
|
||||
// NewRecorder returns an initialized ResponseRecorder.
|
||||
func NewRecorder() *ResponseRecorder {
|
||||
return &ResponseRecorder{
|
||||
HeaderMap: make(http.Header),
|
||||
Body: new(bytes.Buffer),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
|
||||
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
|
||||
const DefaultRemoteAddr = "1.2.3.4"
|
||||
|
||||
// Header returns the response headers.
|
||||
func (rw *ResponseRecorder) Header() http.Header {
|
||||
return rw.HeaderMap
|
||||
}
|
||||
|
||||
// Write always succeeds and writes to rw.Body, if not nil.
|
||||
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
|
||||
if rw.Body != nil {
|
||||
rw.Body.Write(buf)
|
||||
}
|
||||
if rw.Code == 0 {
|
||||
rw.Code = http.StatusOK
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
// WriteHeader sets rw.Code.
|
||||
func (rw *ResponseRecorder) WriteHeader(code int) {
|
||||
rw.Code = code
|
||||
}
|
||||
|
||||
// Flush sets rw.Flushed to true.
|
||||
func (rw *ResponseRecorder) Flush() {
|
||||
rw.Flushed = true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var ErrResponseError = errors.New("response error")
|
||||
var ErrMappedResponseError = errors.New("mapped response error")
|
||||
|
||||
type Service1Request struct {
|
||||
A int
|
||||
B int
|
||||
}
|
||||
|
||||
type Service1NoParamsRequest struct {
|
||||
V string `json:"jsonrpc"`
|
||||
M string `json:"method"`
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
|
||||
type Service1ParamsArrayRequest struct {
|
||||
V string `json:"jsonrpc"`
|
||||
P []struct {
|
||||
T string
|
||||
} `json:"params"`
|
||||
M string `json:"method"`
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
|
||||
type Service1Response struct {
|
||||
Result int
|
||||
}
|
||||
|
||||
type Service1 struct {
|
||||
}
|
||||
|
||||
const Service1DefaultResponse = 9999
|
||||
|
||||
func (t *Service1) Multiply(r *http.Request, req *Service1Request, res *Service1Response) error {
|
||||
if req.A == 0 && req.B == 0 {
|
||||
// Sentinel value for test with no params.
|
||||
res.Result = Service1DefaultResponse
|
||||
} else {
|
||||
res.Result = req.A * req.B
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Service1) ResponseError(r *http.Request, req *Service1Request, res *Service1Response) error {
|
||||
return ErrResponseError
|
||||
}
|
||||
|
||||
func (t *Service1) MappedResponseError(r *http.Request, req *Service1Request, res *Service1Response) error {
|
||||
return ErrMappedResponseError
|
||||
}
|
||||
|
||||
func execute(t *testing.T, s *rpc.Server, method string, req, res interface{}) error {
|
||||
if !s.HasMethod(method) {
|
||||
t.Fatal("Expected to be registered:", method)
|
||||
}
|
||||
|
||||
buf, _ := EncodeClientRequest(method, req)
|
||||
body := bytes.NewBuffer(buf)
|
||||
r, _ := http.NewRequest("POST", "http://localhost:8080/", body)
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := NewRecorder()
|
||||
s.ServeHTTP(w, r)
|
||||
|
||||
return DecodeClientResponse(w.Body, res)
|
||||
}
|
||||
|
||||
func executeRaw(t *testing.T, s *rpc.Server, req interface{}, res interface{}) error {
|
||||
j, _ := json.Marshal(req)
|
||||
r, _ := http.NewRequest("POST", "http://localhost:8080/", bytes.NewBuffer(j))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := NewRecorder()
|
||||
s.ServeHTTP(w, r)
|
||||
|
||||
return DecodeClientResponse(w.Body, res)
|
||||
}
|
||||
|
||||
func executeInvalidJSON(t *testing.T, s *rpc.Server, res interface{}) error {
|
||||
r, _ := http.NewRequest("POST", "http://localhost:8080/", strings.NewReader(`not even a json`))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := NewRecorder()
|
||||
s.ServeHTTP(w, r)
|
||||
|
||||
return DecodeClientResponse(w.Body, res)
|
||||
}
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
s := rpc.NewServer()
|
||||
s.RegisterCodec(NewCodec(), "application/json")
|
||||
s.RegisterService(new(Service1), "")
|
||||
|
||||
var res Service1Response
|
||||
if err := execute(t, s, "Service1.Multiply", &Service1Request{4, 2}, &res); err != nil {
|
||||
t.Error("Expected err to be nil, but got:", err)
|
||||
}
|
||||
if res.Result != 8 {
|
||||
t.Errorf("Wrong response: %v.", res.Result)
|
||||
}
|
||||
|
||||
if err := execute(t, s, "Service1.ResponseError", &Service1Request{4, 2}, &res); err == nil {
|
||||
t.Errorf("Expected to get %q, but got nil", ErrResponseError)
|
||||
} else if err.Error() != ErrResponseError.Error() {
|
||||
t.Errorf("Expected to get %q, but got %q", ErrResponseError, err)
|
||||
}
|
||||
|
||||
// No parameters.
|
||||
res = Service1Response{}
|
||||
if err := executeRaw(t, s, &Service1NoParamsRequest{"2.0", "Service1.Multiply", 1}, &res); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if res.Result != Service1DefaultResponse {
|
||||
t.Errorf("Wrong response: got %v, want %v", res.Result, Service1DefaultResponse)
|
||||
}
|
||||
|
||||
// Parameters as by-position.
|
||||
res = Service1Response{}
|
||||
req := Service1ParamsArrayRequest{
|
||||
V: "2.0",
|
||||
P: []struct {
|
||||
T string
|
||||
}{{
|
||||
T: "test",
|
||||
}},
|
||||
M: "Service1.Multiply",
|
||||
ID: 1,
|
||||
}
|
||||
if err := executeRaw(t, s, &req, &res); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if res.Result != Service1DefaultResponse {
|
||||
t.Errorf("Wrong response: got %v, want %v", res.Result, Service1DefaultResponse)
|
||||
}
|
||||
|
||||
res = Service1Response{}
|
||||
if err := executeInvalidJSON(t, s, &res); err == nil {
|
||||
t.Error("Expected to receive an E_PARSE error, but got nil")
|
||||
} else if jsonRpcErr, ok := err.(*Error); !ok {
|
||||
t.Errorf("Expected to receive an Error, but got %T: %s", err, err)
|
||||
} else if jsonRpcErr.Code != E_PARSE {
|
||||
t.Errorf("Expected to receive an E_PARSE JSON-RPC error (%d) but got %d", E_PARSE, jsonRpcErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceWithErrorMapper(t *testing.T) {
|
||||
const mappedErrorCode = 100
|
||||
|
||||
// errorMapper maps ErrMappedResponseError to an Error with mappedErrorCode Code, everything else is returned as-is
|
||||
errorMapper := func(err error) error {
|
||||
if err == ErrMappedResponseError {
|
||||
return &Error{
|
||||
Code: mappedErrorCode,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
// Map everything else to E_SERVER
|
||||
return &Error{
|
||||
Code: E_SERVER,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
s := rpc.NewServer()
|
||||
s.RegisterCodec(NewCustomCodecWithErrorMapper(rpc.DefaultEncoderSelector, errorMapper), "application/json")
|
||||
s.RegisterService(new(Service1), "")
|
||||
|
||||
var res Service1Response
|
||||
if err := execute(t, s, "Service1.MappedResponseError", &Service1Request{4, 2}, &res); err == nil {
|
||||
t.Errorf("Expected to get a JSON-RPC error, but got nil")
|
||||
} else if jsonRpcErr, ok := err.(*Error); !ok {
|
||||
t.Errorf("Expected to get an *Error, but got %T: %s", err, err)
|
||||
} else if jsonRpcErr.Code != mappedErrorCode {
|
||||
t.Errorf("Expected to get Code %d, but got %d", mappedErrorCode, jsonRpcErr.Code)
|
||||
} else if jsonRpcErr.Message != ErrMappedResponseError.Error() {
|
||||
t.Errorf("Expected to get Message %q, but got %q", ErrMappedResponseError.Error(), jsonRpcErr.Message)
|
||||
}
|
||||
|
||||
// Unmapped error behaves as usual
|
||||
if err := execute(t, s, "Service1.ResponseError", &Service1Request{4, 2}, &res); err == nil {
|
||||
t.Errorf("Expected to get a JSON-RPC error, but got nil")
|
||||
} else if jsonRpcErr, ok := err.(*Error); !ok {
|
||||
t.Errorf("Expected to get an *Error, but got %T: %s", err, err)
|
||||
} else if jsonRpcErr.Code != E_SERVER {
|
||||
t.Errorf("Expected to get Code %d, but got %d", E_SERVER, jsonRpcErr.Code)
|
||||
} else if jsonRpcErr.Message != ErrResponseError.Error() {
|
||||
t.Errorf("Expected to get Message %q, but got %q", ErrResponseError.Error(), jsonRpcErr.Message)
|
||||
}
|
||||
|
||||
// Malformed request without method: our framework tries to return an error: we shouldn't map that one
|
||||
malformedRequest := struct {
|
||||
V string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
}{
|
||||
V: "3.0",
|
||||
ID: "any",
|
||||
}
|
||||
if err := executeRaw(t, s, &malformedRequest, &res); err == nil {
|
||||
t.Errorf("Expected to get a JSON-RPC error, but got nil")
|
||||
} else if jsonRpcErr, ok := err.(*Error); !ok {
|
||||
t.Errorf("Expected to get an *Error, but got %T: %s", err, err)
|
||||
} else if jsonRpcErr.Code != E_INVALID_REQ {
|
||||
t.Errorf("Expected to get an E_INVALID_REQ error (%d), but got %d", E_INVALID_REQ, jsonRpcErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeNullResult(t *testing.T) {
|
||||
data := `{"jsonrpc": "2.0", "id": 12345, "result": null}`
|
||||
reader := bytes.NewReader([]byte(data))
|
||||
var result interface{}
|
||||
|
||||
err := DecodeClientResponse(reader, &result)
|
||||
|
||||
if err != ErrNullResult {
|
||||
t.Error("Expected err no be ErrNullResult, but got:", err)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Error("Expected result to be nil, but got:", result)
|
||||
}
|
||||
}
|
239
pkg/rpc/json2/server.go
Normal file
239
pkg/rpc/json2/server.go
Normal file
@ -0,0 +1,239 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package json2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/pkg/rpc"
|
||||
)
|
||||
|
||||
var null = jsoniter.RawMessage([]byte("null"))
|
||||
var Version = "2.0"
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Request and Response
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// serverRequest represents a JSON-RPC request received by the server.
|
||||
type serverRequest struct {
|
||||
// JSON-RPC protocol.
|
||||
Version string `json:"jsonrpc"`
|
||||
|
||||
// A String containing the name of the method to be invoked.
|
||||
Method string `json:"method"`
|
||||
|
||||
// A Structured value to pass as arguments to the method.
|
||||
Params *jsoniter.RawMessage `json:"params"`
|
||||
|
||||
// The request id. MUST be a string, number or null.
|
||||
// Our implementation will not do type checking for id.
|
||||
// It will be copied as it is.
|
||||
ID *jsoniter.RawMessage `json:"id"`
|
||||
}
|
||||
|
||||
// serverResponse represents a JSON-RPC response returned by the server.
|
||||
type serverResponse struct {
|
||||
// JSON-RPC protocol.
|
||||
Version string `json:"jsonrpc"`
|
||||
|
||||
// The Object that was returned by the invoked method. This must be null
|
||||
// in case there was an error invoking the method.
|
||||
// As per spec the member will be omitted if there was an error.
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
|
||||
// An Error object if there was an error invoking the method. It must be
|
||||
// null if there was no error.
|
||||
// As per spec the member will be omitted if there was no error.
|
||||
Error *Error `json:"error,omitempty"`
|
||||
|
||||
// This must be the same id as the request it is responding to.
|
||||
ID *jsoniter.RawMessage `json:"id"`
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Codec
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewCustomCodec returns a new JSON Codec based on passed encoder selector.
|
||||
func NewCustomCodec(encSel rpc.EncoderSelector) *Codec {
|
||||
return &Codec{encSel: encSel}
|
||||
}
|
||||
|
||||
// NewCustomCodecWithErrorMapper returns a new JSON Codec based on the passed encoder selector
|
||||
// and also accepts an errorMapper function.
|
||||
// The errorMapper function will be called if the Service implementation returns an error, with that
|
||||
// error as a param, replacing it by the value returned by this function. This function is intended
|
||||
// to decouple your service implementation from the codec itself, making possible to return abstract
|
||||
// errors in your service, and then mapping them here to the JSON-RPC error codes.
|
||||
func NewCustomCodecWithErrorMapper(encSel rpc.EncoderSelector, errorMapper func(error) error) *Codec {
|
||||
return &Codec{
|
||||
encSel: encSel,
|
||||
errorMapper: errorMapper,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCodec returns a new JSON Codec.
|
||||
func NewCodec() *Codec {
|
||||
return NewCustomCodec(rpc.DefaultEncoderSelector)
|
||||
}
|
||||
|
||||
// Codec creates a CodecRequest to process each request.
|
||||
type Codec struct {
|
||||
encSel rpc.EncoderSelector
|
||||
errorMapper func(error) error
|
||||
}
|
||||
|
||||
// NewRequest returns a CodecRequest.
|
||||
func (c *Codec) NewRequest(r *http.Request) rpc.CodecRequest {
|
||||
return newCodecRequest(r, c.encSel.Select(r), c.errorMapper)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CodecRequest
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// newCodecRequest returns a new CodecRequest.
|
||||
func newCodecRequest(r *http.Request, encoder rpc.Encoder, errorMapper func(error) error) rpc.CodecRequest {
|
||||
// Decode the request body and check if RPC method is valid.
|
||||
req := new(serverRequest)
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
|
||||
if err != nil {
|
||||
err = &Error{
|
||||
Code: E_PARSE,
|
||||
Message: err.Error(),
|
||||
Data: req,
|
||||
}
|
||||
} else if req.Version != Version {
|
||||
err = &Error{
|
||||
Code: E_INVALID_REQ,
|
||||
Message: "jsonrpc must be " + Version,
|
||||
Data: req,
|
||||
}
|
||||
}
|
||||
|
||||
r.Body.Close()
|
||||
return &CodecRequest{request: req, err: err, encoder: encoder, errorMapper: errorMapper}
|
||||
}
|
||||
|
||||
// CodecRequest decodes and encodes a single request.
|
||||
type CodecRequest struct {
|
||||
request *serverRequest
|
||||
err error
|
||||
encoder rpc.Encoder
|
||||
errorMapper func(error) error
|
||||
}
|
||||
|
||||
// Method returns the RPC method for the current request.
|
||||
//
|
||||
// The method uses a dotted notation as in "Service.Method".
|
||||
func (c *CodecRequest) Method() (string, error) {
|
||||
if c.err == nil {
|
||||
return c.request.Method, nil
|
||||
}
|
||||
return "", c.err
|
||||
}
|
||||
|
||||
// ReadRequest fills the request object for the RPC method.
|
||||
//
|
||||
// ReadRequest parses request parameters in two supported forms in
|
||||
// accordance with http://www.jsonrpc.org/specification#parameter_structures
|
||||
//
|
||||
// by-position: params MUST be an Array, containing the
|
||||
// values in the Server expected order.
|
||||
//
|
||||
// by-name: params MUST be an Object, with member names
|
||||
// that match the Server expected parameter names. The
|
||||
// absence of expected names MAY result in an error being
|
||||
// generated. The names MUST match exactly, including
|
||||
// case, to the method's expected parameters.
|
||||
func (c *CodecRequest) ReadRequest(args interface{}) error {
|
||||
if c.err == nil && c.request.Params != nil {
|
||||
// Note: if c.request.Params is nil it's not an error, it's an optional member.
|
||||
// JSON params structured object. Unmarshal to the args object.
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err := json.Unmarshal(*c.request.Params, args); err != nil {
|
||||
// Clearly JSON params is not a structured object,
|
||||
// fallback and attempt an unmarshal with JSON params as
|
||||
// array value and RPC params is struct. Unmarshal into
|
||||
// array containing the request struct.
|
||||
params := [1]interface{}{args}
|
||||
if err = json.Unmarshal(*c.request.Params, ¶ms); err != nil {
|
||||
c.err = &Error{
|
||||
Code: E_INVALID_REQ,
|
||||
Message: err.Error(),
|
||||
Data: c.request.Params,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.err
|
||||
}
|
||||
|
||||
// WriteResponse encodes the response and writes it to the ResponseWriter.
|
||||
func (c *CodecRequest) WriteResponse(w http.ResponseWriter, reply interface{}) {
|
||||
res := &serverResponse{
|
||||
Version: Version,
|
||||
Result: reply,
|
||||
ID: c.request.ID,
|
||||
}
|
||||
c.writeServerResponse(w, res)
|
||||
}
|
||||
|
||||
func (c *CodecRequest) WriteError(w http.ResponseWriter, status int, err error) {
|
||||
err = c.tryToMapIfNotAnErrorAlready(err)
|
||||
jsonErr, ok := err.(*Error)
|
||||
if !ok {
|
||||
jsonErr = &Error{
|
||||
Code: E_SERVER,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
res := &serverResponse{
|
||||
Version: Version,
|
||||
Error: jsonErr,
|
||||
ID: c.request.ID,
|
||||
}
|
||||
c.writeServerResponse(w, res)
|
||||
}
|
||||
|
||||
func (c CodecRequest) tryToMapIfNotAnErrorAlready(err error) error {
|
||||
if _, ok := err.(*Error); ok || c.errorMapper == nil {
|
||||
return err
|
||||
}
|
||||
return c.errorMapper(err)
|
||||
}
|
||||
|
||||
func (c *CodecRequest) writeServerResponse(w http.ResponseWriter, res *serverResponse) {
|
||||
// ID is null for notifications and they don't have a response, unless we couldn't even parse the JSON, in that
|
||||
// case we can't know whether it was intended to be a notification
|
||||
if c.request.ID != nil || isParseErrorResponse(res) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
encoder := json.NewEncoder(c.encoder.Encode(w))
|
||||
err := encoder.Encode(res)
|
||||
|
||||
// Not sure in which case will this happen. But seems harmless.
|
||||
if err != nil {
|
||||
rpc.WriteError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isParseErrorResponse(res *serverResponse) bool {
|
||||
return res != nil && res.Error != nil && res.Error.Code == E_PARSE
|
||||
}
|
||||
|
||||
type EmptyResponse struct {
|
||||
}
|
169
pkg/rpc/map.go
Normal file
169
pkg/rpc/map.go
Normal file
@ -0,0 +1,169 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
// Precompute the reflect.Type of error and http.Request
|
||||
typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
typeOfRequest = reflect.TypeOf((*http.Request)(nil)).Elem()
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// service
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type service struct {
|
||||
name string // name of service
|
||||
rcvr reflect.Value // receiver of methods for the service
|
||||
rcvrType reflect.Type // type of the receiver
|
||||
methods map[string]*serviceMethod // registered methods
|
||||
}
|
||||
|
||||
type serviceMethod struct {
|
||||
method reflect.Method // receiver method
|
||||
argsType reflect.Type // type of the request argument
|
||||
replyType reflect.Type // type of the response argument
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// serviceMap
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// serviceMap is a registry for services.
|
||||
type serviceMap struct {
|
||||
mutex sync.Mutex
|
||||
services map[string]*service
|
||||
}
|
||||
|
||||
// register adds a new service using reflection to extract its methods.
|
||||
func (m *serviceMap) register(rcvr interface{}, name string) error {
|
||||
// Setup service.
|
||||
s := &service{
|
||||
name: name,
|
||||
rcvr: reflect.ValueOf(rcvr),
|
||||
rcvrType: reflect.TypeOf(rcvr),
|
||||
methods: make(map[string]*serviceMethod),
|
||||
}
|
||||
if name == "" {
|
||||
s.name = reflect.Indirect(s.rcvr).Type().Name()
|
||||
if !isExported(s.name) {
|
||||
return fmt.Errorf("rpc: type %q is not exported", s.name)
|
||||
}
|
||||
}
|
||||
if s.name == "" {
|
||||
return fmt.Errorf("rpc: no service name for type %q",
|
||||
s.rcvrType.String())
|
||||
}
|
||||
// Setup methods.
|
||||
for i := 0; i < s.rcvrType.NumMethod(); i++ {
|
||||
method := s.rcvrType.Method(i)
|
||||
mtype := method.Type
|
||||
// Method must be exported.
|
||||
if method.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
// Method needs four ins: receiver, *http.Request, *args, *reply.
|
||||
if mtype.NumIn() != 4 {
|
||||
continue
|
||||
}
|
||||
// First argument must be a pointer and must be http.Request.
|
||||
reqType := mtype.In(1)
|
||||
if reqType.Kind() != reflect.Ptr || reqType.Elem() != typeOfRequest {
|
||||
continue
|
||||
}
|
||||
// Second argument must be a pointer and must be exported.
|
||||
args := mtype.In(2)
|
||||
if args.Kind() != reflect.Ptr || !isExportedOrBuiltin(args) {
|
||||
continue
|
||||
}
|
||||
// Third argument must be a pointer and must be exported.
|
||||
reply := mtype.In(3)
|
||||
if reply.Kind() != reflect.Ptr || !isExportedOrBuiltin(reply) {
|
||||
continue
|
||||
}
|
||||
// Method needs one out: error.
|
||||
if mtype.NumOut() != 1 {
|
||||
continue
|
||||
}
|
||||
if returnType := mtype.Out(0); returnType != typeOfError {
|
||||
continue
|
||||
}
|
||||
s.methods[method.Name] = &serviceMethod{
|
||||
method: method,
|
||||
argsType: args.Elem(),
|
||||
replyType: reply.Elem(),
|
||||
}
|
||||
}
|
||||
if len(s.methods) == 0 {
|
||||
return fmt.Errorf("rpc: %q has no exported methods of suitable type",
|
||||
s.name)
|
||||
}
|
||||
// Add to the map.
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
if m.services == nil {
|
||||
m.services = make(map[string]*service)
|
||||
} else if _, ok := m.services[s.name]; ok {
|
||||
return fmt.Errorf("rpc: service already defined: %q", s.name)
|
||||
}
|
||||
m.services[s.name] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
// get returns a registered service given a method name.
|
||||
//
|
||||
// The method name uses a dotted notation as in "Service.Method".
|
||||
func (m *serviceMap) get(method string) (*service, *serviceMethod, error) {
|
||||
parts := strings.Split(method, ".")
|
||||
if len(parts) != 2 {
|
||||
err := fmt.Errorf("rpc: service/method request ill-formed: %q", method)
|
||||
return nil, nil, err
|
||||
}
|
||||
m.mutex.Lock()
|
||||
service := m.services[parts[0]]
|
||||
m.mutex.Unlock()
|
||||
if service == nil {
|
||||
err := fmt.Errorf("rpc: can't find service %q", method)
|
||||
return nil, nil, err
|
||||
}
|
||||
serviceMethod := service.methods[parts[1]]
|
||||
if serviceMethod == nil {
|
||||
err := fmt.Errorf("rpc: can't find method %q", method)
|
||||
return nil, nil, err
|
||||
}
|
||||
return service, serviceMethod, nil
|
||||
}
|
||||
|
||||
// isExported returns true of a string is an exported (upper case) name.
|
||||
func isExported(name string) bool {
|
||||
rune, _ := utf8.DecodeRuneInString(name)
|
||||
return unicode.IsUpper(rune)
|
||||
}
|
||||
|
||||
// isExportedOrBuiltin returns true if a type is exported or a builtin.
|
||||
func isExportedOrBuiltin(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
// PkgPath will be non-empty even for an exported type,
|
||||
// so we need to check the type name as well.
|
||||
return isExported(t.Name()) || t.PkgPath() == ""
|
||||
}
|
320
pkg/rpc/server.go
Normal file
320
pkg/rpc/server.go
Normal file
@ -0,0 +1,320 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var nilErrorValue = reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Codec
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Codec creates a CodecRequest to process each request.
|
||||
type Codec interface {
|
||||
NewRequest(*http.Request) CodecRequest
|
||||
}
|
||||
|
||||
// CodecRequest decodes a request and encodes a response using a specific
|
||||
// serialization scheme.
|
||||
type CodecRequest interface {
|
||||
// Reads the request and returns the RPC method name.
|
||||
Method() (string, error)
|
||||
// Reads the request filling the RPC method args.
|
||||
ReadRequest(interface{}) error
|
||||
// Writes the response using the RPC method reply.
|
||||
WriteResponse(http.ResponseWriter, interface{})
|
||||
// Writes an error produced by the server.
|
||||
WriteError(w http.ResponseWriter, status int, err error)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Server
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewServer returns a new RPC server.
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
codecs: make(map[string]Codec),
|
||||
services: new(serviceMap),
|
||||
}
|
||||
}
|
||||
|
||||
// RequestInfo contains all the information we pass to before/after functions
|
||||
type RequestInfo struct {
|
||||
Args reflect.Value
|
||||
Method string
|
||||
Error error
|
||||
ResponseWriter http.ResponseWriter
|
||||
Request *http.Request
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
// Server serves registered RPC services using registered codecs.
|
||||
type Server struct {
|
||||
codecs map[string]Codec
|
||||
services *serviceMap
|
||||
interceptFunc func(i *RequestInfo) *http.Request
|
||||
beforeFunc func(i *RequestInfo)
|
||||
afterFunc func(i *RequestInfo)
|
||||
validateFunc reflect.Value
|
||||
}
|
||||
|
||||
// RegisterCodec adds a new codec to the server.
|
||||
//
|
||||
// Codecs are defined to process a given serialization scheme, e.g., JSON or
|
||||
// XML. A codec is chosen based on the "Content-Type" header from the request,
|
||||
// excluding the charset definition.
|
||||
func (s *Server) RegisterCodec(codec Codec, contentType string) {
|
||||
s.codecs[strings.ToLower(contentType)] = codec
|
||||
}
|
||||
|
||||
// RegisterInterceptFunc registers the specified function as the function
|
||||
// that will be called before every request. The function is allowed to intercept
|
||||
// the request e.g. add values to the context.
|
||||
//
|
||||
// Note: Only one function can be registered, subsequent calls to this
|
||||
// method will overwrite all the previous functions.
|
||||
func (s *Server) RegisterInterceptFunc(f func(i *RequestInfo) *http.Request) {
|
||||
s.interceptFunc = f
|
||||
}
|
||||
|
||||
// RegisterBeforeFunc registers the specified function as the function
|
||||
// that will be called before every request.
|
||||
//
|
||||
// Note: Only one function can be registered, subsequent calls to this
|
||||
// method will overwrite all the previous functions.
|
||||
func (s *Server) RegisterBeforeFunc(f func(i *RequestInfo)) {
|
||||
s.beforeFunc = f
|
||||
}
|
||||
|
||||
// RegisterValidateRequestFunc registers the specified function as the function
|
||||
// that will be called after the BeforeFunc (if registered) and before invoking
|
||||
// the actual Service method. If this function returns a non-nil error, the method
|
||||
// won't be invoked and this error will be considered as the method result.
|
||||
// The first argument is information about the request, useful for accessing to http.Request.Context()
|
||||
// The second argument of this function is the already-unmarshalled *args parameter of the method.
|
||||
func (s *Server) RegisterValidateRequestFunc(f func(r *RequestInfo, i interface{}) error) {
|
||||
s.validateFunc = reflect.ValueOf(f)
|
||||
}
|
||||
|
||||
// RegisterAfterFunc registers the specified function as the function
|
||||
// that will be called after every request
|
||||
//
|
||||
// Note: Only one function can be registered, subsequent calls to this
|
||||
// method will overwrite all the previous functions.
|
||||
func (s *Server) RegisterAfterFunc(f func(i *RequestInfo)) {
|
||||
s.afterFunc = f
|
||||
}
|
||||
|
||||
// RegisterService adds a new service to the server.
|
||||
//
|
||||
// The name parameter is optional: if empty it will be inferred from
|
||||
// the receiver type name.
|
||||
//
|
||||
// Methods from the receiver will be extracted if these rules are satisfied:
|
||||
//
|
||||
// - The receiver is exported (begins with an upper case letter) or local
|
||||
// (defined in the package registering the service).
|
||||
// - The method name is exported.
|
||||
// - The method has three arguments: *http.Request, *args, *reply.
|
||||
// - All three arguments are pointers.
|
||||
// - The second and third arguments are exported or local.
|
||||
// - The method has return type error.
|
||||
//
|
||||
// All other methods are ignored.
|
||||
func (s *Server) RegisterService(receiver interface{}, name string) error {
|
||||
return s.services.register(receiver, name)
|
||||
}
|
||||
|
||||
// HasMethod returns true if the given method is registered.
|
||||
//
|
||||
// The method uses a dotted notation as in "Service.Method".
|
||||
func (s *Server) HasMethod(method string) bool {
|
||||
if _, _, err := s.services.get(method); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ServeHTTP
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
err := fmt.Errorf("rpc: POST method required, received %s", r.Method)
|
||||
WriteError(w, http.StatusMethodNotAllowed, err.Error())
|
||||
// Call the registered After Function
|
||||
if s.afterFunc != nil {
|
||||
s.afterFunc(&RequestInfo{
|
||||
ResponseWriter: w,
|
||||
Request: r,
|
||||
Method: "Unknown." + r.Method,
|
||||
StatusCode: http.StatusMethodNotAllowed,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
idx := strings.Index(contentType, ";")
|
||||
if idx != -1 {
|
||||
contentType = contentType[:idx]
|
||||
}
|
||||
var codec Codec
|
||||
if contentType == "" && len(s.codecs) == 1 {
|
||||
// If Content-Type is not set and only one codec has been registered,
|
||||
// then default to that codec.
|
||||
for _, c := range s.codecs {
|
||||
codec = c
|
||||
}
|
||||
} else if codec = s.codecs[strings.ToLower(contentType)]; codec == nil {
|
||||
err := fmt.Errorf("rpc: unrecognized Content-Type: %s", contentType)
|
||||
WriteError(w, http.StatusUnsupportedMediaType, err.Error())
|
||||
// Call the registered After Function
|
||||
if s.afterFunc != nil {
|
||||
s.afterFunc(&RequestInfo{
|
||||
ResponseWriter: w,
|
||||
Request: r,
|
||||
Method: "Unknown." + r.Method,
|
||||
Error: err,
|
||||
StatusCode: http.StatusUnsupportedMediaType,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
// Create a new codec request.
|
||||
codecReq := codec.NewRequest(r)
|
||||
// Get service method to be called.
|
||||
method, errMethod := codecReq.Method()
|
||||
if errMethod != nil {
|
||||
codecReq.WriteError(w, http.StatusBadRequest, errMethod)
|
||||
if s.afterFunc != nil {
|
||||
s.afterFunc(&RequestInfo{
|
||||
ResponseWriter: w,
|
||||
Request: r,
|
||||
Method: "Unknown." + r.Method,
|
||||
Error: errMethod,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
serviceSpec, methodSpec, errGet := s.services.get(method)
|
||||
if errGet != nil {
|
||||
codecReq.WriteError(w, http.StatusBadRequest, errGet)
|
||||
if s.afterFunc != nil {
|
||||
s.afterFunc(&RequestInfo{
|
||||
ResponseWriter: w,
|
||||
Request: r,
|
||||
Method: method,
|
||||
Error: errGet,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
// Decode the args.
|
||||
args := reflect.New(methodSpec.argsType)
|
||||
if errRead := codecReq.ReadRequest(args.Interface()); errRead != nil {
|
||||
codecReq.WriteError(w, http.StatusBadRequest, errRead)
|
||||
if s.afterFunc != nil {
|
||||
s.afterFunc(&RequestInfo{
|
||||
ResponseWriter: w,
|
||||
Request: r,
|
||||
Method: method,
|
||||
Error: errRead,
|
||||
StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Call the registered Intercept Function
|
||||
if s.interceptFunc != nil {
|
||||
req := s.interceptFunc(&RequestInfo{
|
||||
Request: r,
|
||||
Method: method,
|
||||
})
|
||||
if req != nil {
|
||||
r = req
|
||||
}
|
||||
}
|
||||
|
||||
requestInfo := &RequestInfo{
|
||||
Request: r,
|
||||
Method: method,
|
||||
}
|
||||
|
||||
// Call the registered Before Function
|
||||
if s.beforeFunc != nil {
|
||||
s.beforeFunc(requestInfo)
|
||||
}
|
||||
|
||||
// Prepare the reply, we need it even if validation fails
|
||||
reply := reflect.New(methodSpec.replyType)
|
||||
errValue := []reflect.Value{nilErrorValue}
|
||||
|
||||
// Call the registered Validator Function
|
||||
if s.validateFunc.IsValid() {
|
||||
errValue = s.validateFunc.Call([]reflect.Value{reflect.ValueOf(requestInfo), args})
|
||||
}
|
||||
|
||||
// If still no errors after validation, call the method
|
||||
if errValue[0].IsNil() {
|
||||
errValue = methodSpec.method.Func.Call([]reflect.Value{
|
||||
serviceSpec.rcvr,
|
||||
reflect.ValueOf(r),
|
||||
args,
|
||||
reply,
|
||||
})
|
||||
}
|
||||
|
||||
// Extract the result to error if needed.
|
||||
var errResult error
|
||||
statusCode := http.StatusOK
|
||||
errInter := errValue[0].Interface()
|
||||
if errInter != nil {
|
||||
statusCode = http.StatusBadRequest
|
||||
errResult = errInter.(error)
|
||||
}
|
||||
|
||||
// Prevents Internet Explorer from MIME-sniffing a response away
|
||||
// from the declared content-type
|
||||
w.Header().Set("x-content-type-options", "nosniff")
|
||||
|
||||
// Encode the response.
|
||||
if errResult == nil {
|
||||
codecReq.WriteResponse(w, reply.Interface())
|
||||
} else {
|
||||
codecReq.WriteError(w, statusCode, errResult)
|
||||
}
|
||||
|
||||
// Call the registered After Function
|
||||
if s.afterFunc != nil {
|
||||
s.afterFunc(&RequestInfo{
|
||||
Args: args,
|
||||
ResponseWriter: w,
|
||||
Request: r,
|
||||
Method: method,
|
||||
Error: errResult,
|
||||
StatusCode: statusCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, status int, msg string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprint(w, msg)
|
||||
}
|
268
pkg/rpc/server_test.go
Normal file
268
pkg/rpc/server_test.go
Normal file
@ -0,0 +1,268 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copyright 2020 MinIO, Inc. All rights reserved.
|
||||
// forked from https://github.com/gorilla/rpc/v2
|
||||
// modified to be used with MinIO under Apache
|
||||
// 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Service1Request struct {
|
||||
A int
|
||||
B int
|
||||
}
|
||||
|
||||
type Service1Response struct {
|
||||
Result int
|
||||
}
|
||||
|
||||
type Service1 struct {
|
||||
}
|
||||
|
||||
func (t *Service1) Multiply(r *http.Request, req *Service1Request, res *Service1Response) error {
|
||||
res.Result = req.A * req.B
|
||||
return nil
|
||||
}
|
||||
|
||||
type Service2 struct {
|
||||
}
|
||||
|
||||
func TestRegisterService(t *testing.T) {
|
||||
var err error
|
||||
s := NewServer()
|
||||
service1 := new(Service1)
|
||||
service2 := new(Service2)
|
||||
|
||||
// Inferred name.
|
||||
err = s.RegisterService(service1, "")
|
||||
if err != nil || !s.HasMethod("Service1.Multiply") {
|
||||
t.Errorf("Expected to be registered: Service1.Multiply")
|
||||
}
|
||||
// Provided name.
|
||||
err = s.RegisterService(service1, "Foo")
|
||||
if err != nil || !s.HasMethod("Foo.Multiply") {
|
||||
t.Errorf("Expected to be registered: Foo.Multiply")
|
||||
}
|
||||
// No methods.
|
||||
err = s.RegisterService(service2, "")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error on service2")
|
||||
}
|
||||
}
|
||||
|
||||
// MockCodec decodes to Service1.Multiply.
|
||||
type MockCodec struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
func (c MockCodec) NewRequest(*http.Request) CodecRequest {
|
||||
return MockCodecRequest{c.A, c.B}
|
||||
}
|
||||
|
||||
type MockCodecRequest struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
func (r MockCodecRequest) Method() (string, error) {
|
||||
return "Service1.Multiply", nil
|
||||
}
|
||||
|
||||
func (r MockCodecRequest) ReadRequest(args interface{}) error {
|
||||
req := args.(*Service1Request)
|
||||
req.A, req.B = r.A, r.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r MockCodecRequest) WriteResponse(w http.ResponseWriter, reply interface{}) {
|
||||
res := reply.(*Service1Response)
|
||||
w.Write([]byte(strconv.Itoa(res.Result)))
|
||||
}
|
||||
|
||||
func (r MockCodecRequest) WriteError(w http.ResponseWriter, status int, err error) {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
type MockResponseWriter struct {
|
||||
header http.Header
|
||||
Status int
|
||||
Body string
|
||||
}
|
||||
|
||||
func NewMockResponseWriter() *MockResponseWriter {
|
||||
header := make(http.Header)
|
||||
return &MockResponseWriter{header: header}
|
||||
}
|
||||
|
||||
func (w *MockResponseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *MockResponseWriter) Write(p []byte) (int, error) {
|
||||
w.Body = string(p)
|
||||
if w.Status == 0 {
|
||||
w.Status = 200
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *MockResponseWriter) WriteHeader(status int) {
|
||||
w.Status = status
|
||||
}
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
const (
|
||||
A = 2
|
||||
B = 3
|
||||
)
|
||||
expected := A * B
|
||||
|
||||
s := NewServer()
|
||||
s.RegisterService(new(Service1), "")
|
||||
s.RegisterCodec(MockCodec{A, B}, "mock")
|
||||
r, err := http.NewRequest("POST", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set("Content-Type", "mock; dummy")
|
||||
w := NewMockResponseWriter()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Status != 200 {
|
||||
t.Errorf("Status was %d, should be 200.", w.Status)
|
||||
}
|
||||
if w.Body != strconv.Itoa(expected) {
|
||||
t.Errorf("Response body was %s, should be %s.", w.Body, strconv.Itoa(expected))
|
||||
}
|
||||
|
||||
// Test wrong Content-Type
|
||||
r.Header.Set("Content-Type", "invalid")
|
||||
w = NewMockResponseWriter()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Status != 415 {
|
||||
t.Errorf("Status was %d, should be 415.", w.Status)
|
||||
}
|
||||
if w.Body != "rpc: unrecognized Content-Type: invalid" {
|
||||
t.Errorf("Wrong response body.")
|
||||
}
|
||||
|
||||
// Test omitted Content-Type; codec should default to the sole registered one.
|
||||
r.Header.Del("Content-Type")
|
||||
w = NewMockResponseWriter()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Status != 200 {
|
||||
t.Errorf("Status was %d, should be 200.", w.Status)
|
||||
}
|
||||
if w.Body != strconv.Itoa(expected) {
|
||||
t.Errorf("Response body was %s, should be %s.", w.Body, strconv.Itoa(expected))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterception(t *testing.T) {
|
||||
const (
|
||||
A = 2
|
||||
B = 3
|
||||
)
|
||||
expected := A * B
|
||||
|
||||
r2, err := http.NewRequest("POST", "mocked/request", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := NewServer()
|
||||
s.RegisterService(new(Service1), "")
|
||||
s.RegisterCodec(MockCodec{A, B}, "mock")
|
||||
s.RegisterInterceptFunc(func(i *RequestInfo) *http.Request {
|
||||
return r2
|
||||
})
|
||||
s.RegisterValidateRequestFunc(func(info *RequestInfo, v interface{}) error { return nil })
|
||||
s.RegisterAfterFunc(func(i *RequestInfo) {
|
||||
if i.Request != r2 {
|
||||
t.Errorf("Request was %v, should be %v.", i.Request, r2)
|
||||
}
|
||||
})
|
||||
|
||||
r, err := http.NewRequest("POST", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set("Content-Type", "mock; dummy")
|
||||
w := NewMockResponseWriter()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Status != 200 {
|
||||
t.Errorf("Status was %d, should be 200.", w.Status)
|
||||
}
|
||||
if w.Body != strconv.Itoa(expected) {
|
||||
t.Errorf("Response body was %s, should be %s.", w.Body, strconv.Itoa(expected))
|
||||
}
|
||||
}
|
||||
func TestValidationSuccessful(t *testing.T) {
|
||||
const (
|
||||
A = 2
|
||||
B = 3
|
||||
|
||||
expected = A * B
|
||||
)
|
||||
|
||||
validate := func(info *RequestInfo, v interface{}) error { return nil }
|
||||
|
||||
s := NewServer()
|
||||
s.RegisterService(new(Service1), "")
|
||||
s.RegisterCodec(MockCodec{A, B}, "mock")
|
||||
s.RegisterValidateRequestFunc(validate)
|
||||
|
||||
r, err := http.NewRequest("POST", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set("Content-Type", "mock; dummy")
|
||||
w := NewMockResponseWriter()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Status != 200 {
|
||||
t.Errorf("Status was %d, should be 200.", w.Status)
|
||||
}
|
||||
if w.Body != strconv.Itoa(expected) {
|
||||
t.Errorf("Response body was %s, should be %s.", w.Body, strconv.Itoa(expected))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationFails(t *testing.T) {
|
||||
const expected = "this instance only supports zero values"
|
||||
|
||||
validate := func(r *RequestInfo, v interface{}) error {
|
||||
req := v.(*Service1Request)
|
||||
if req.A != 0 || req.B != 0 {
|
||||
return errors.New(expected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
s := NewServer()
|
||||
s.RegisterService(new(Service1), "")
|
||||
s.RegisterCodec(MockCodec{1, 2}, "mock")
|
||||
s.RegisterValidateRequestFunc(validate)
|
||||
|
||||
r, err := http.NewRequest("POST", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set("Content-Type", "mock; dummy")
|
||||
w := NewMockResponseWriter()
|
||||
s.ServeHTTP(w, r)
|
||||
if w.Status != 400 {
|
||||
t.Errorf("Status was %d, should be 200.", w.Status)
|
||||
}
|
||||
if w.Body != expected {
|
||||
t.Errorf("Response body was %s, should be %s.", w.Body, expected)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user