add audit/admin trace support for browser requests (#10947)
To support this functionality we had to fork the gorilla/rpc package with relevant changesmaster
parent
7bc47a14cc
commit
86409fa93d
File diff suppressed because one or more lines are too long
@ -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. |
@ -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. |
@ -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 |
@ -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 |
||||
} |
@ -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 |
@ -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{} |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 { |
||||
} |
@ -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() == "" |
||||
} |
@ -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) |
||||
} |
@ -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