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