274 lines
8.7 KiB
Go
274 lines
8.7 KiB
Go
// Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.
|
|
|
|
// Package jhttp implements a bridge from HTTP to JSON-RPC. This permits
|
|
// requests to be submitted to a JSON-RPC server using HTTP as a transport.
|
|
package jhttp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/creachadair/jrpc2"
|
|
"github.com/creachadair/jrpc2/server"
|
|
)
|
|
|
|
// A Bridge is a http.Handler that bridges requests to a JSON-RPC server.
|
|
//
|
|
// By default, the bridge accepts only HTTP POST requests with the complete
|
|
// JSON-RPC request message in the body, with Content-Type application/json.
|
|
// Either a single request object or a list of request objects is supported.
|
|
//
|
|
// If the HTTP request method is not "POST", the bridge reports 405 (Method Not
|
|
// Allowed). If the Content-Type is not application/json, the bridge reports
|
|
// 415 (Unsupported Media Type).
|
|
//
|
|
// If a ParseRequest hook is set, these requirements are disabled, and the hook
|
|
// is entirely responsible for checking request structure.
|
|
//
|
|
// If a ParseGETRequest hook is set, HTTP "GET" requests are handled by a
|
|
// Getter using that hook; otherwise "GET" requests are handled as above.
|
|
//
|
|
// If the request completes, whether or not there is an error, the HTTP
|
|
// response is 200 (OK) for ordinary requests or 204 (No Response) for
|
|
// notifications, and the response body contains the JSON-RPC response.
|
|
//
|
|
// The bridge attaches the inbound HTTP request to the context passed to the
|
|
// client, allowing an EncodeContext callback to retrieve state from the HTTP
|
|
// headers. Use jhttp.HTTPRequest to retrieve the request from the context.
|
|
type Bridge struct {
|
|
local server.Local
|
|
parseReq func(*http.Request) ([]*jrpc2.Request, error)
|
|
getter *Getter
|
|
}
|
|
|
|
// ServeHTTP implements the required method of http.Handler.
|
|
func (b Bridge) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
// If a GET hook is defined, allow GET requests.
|
|
if req.Method == "GET" && b.getter != nil {
|
|
b.getter.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
// If no parse hook is defined, insist that the method is POST and the
|
|
// content-type is application/json. Setting a hook disables these checks.
|
|
if b.parseReq == nil {
|
|
if req.Method != "POST" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if req.Header.Get("Content-Type") != "application/json" {
|
|
w.WriteHeader(http.StatusUnsupportedMediaType)
|
|
return
|
|
}
|
|
}
|
|
if err := b.serveInternal(w, req); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintln(w, err.Error())
|
|
}
|
|
}
|
|
|
|
func (b Bridge) serveInternal(w http.ResponseWriter, req *http.Request) error {
|
|
// The HTTP request requires a response, but the server will not reply if
|
|
// all the requests are notifications. Check whether we have any calls
|
|
// needing a response, and choose whether to wait for a reply based on that.
|
|
//
|
|
// Note that we are forgiving about a missing version marker in a request,
|
|
// since we can't tell at this point whether the server is willing to accept
|
|
// messages like that.
|
|
jreq, err := b.parseHTTPRequest(req)
|
|
if err != nil && err != jrpc2.ErrInvalidVersion {
|
|
return err
|
|
}
|
|
|
|
// Because the bridge shares the JSON-RPC client between potentially many
|
|
// HTTP clients, we must virtualize the ID space for requests to preserve
|
|
// the HTTP client's assignment of IDs.
|
|
//
|
|
// To do this, we keep track of the inbound ID for each request so that we
|
|
// can map the responses back. This takes advantage of the fact that the
|
|
// *jrpc2.Client detangles batch order so that responses come back in the
|
|
// same order (modulo notifications) even if the server response did not
|
|
// preserve order.
|
|
|
|
// Generate request specifications for the client.
|
|
var inboundID []string // for requests
|
|
spec := make([]jrpc2.Spec, len(jreq)) // requests & notifications
|
|
for i, req := range jreq {
|
|
spec[i] = jrpc2.Spec{
|
|
Method: req.Method(),
|
|
Notify: req.IsNotification(),
|
|
}
|
|
if req.HasParams() {
|
|
var p json.RawMessage
|
|
req.UnmarshalParams(&p)
|
|
spec[i].Params = p
|
|
}
|
|
if !spec[i].Notify {
|
|
inboundID = append(inboundID, req.ID())
|
|
}
|
|
}
|
|
|
|
// Attach the HTTP request to the client context, so the encoder can see it.
|
|
ctx := context.WithValue(req.Context(), httpReqKey{}, req)
|
|
rsps, err := b.local.Client.Batch(ctx, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If all the requests were notifications, report success without responses.
|
|
if len(rsps) == 0 {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, map the responses back to their original IDs, and marshal the
|
|
// response back into the body.
|
|
for i, rsp := range rsps {
|
|
rsp.SetID(inboundID[i])
|
|
}
|
|
|
|
return b.encodeResponses(rsps, w)
|
|
}
|
|
|
|
func (b Bridge) parseHTTPRequest(req *http.Request) ([]*jrpc2.Request, error) {
|
|
if b.parseReq != nil {
|
|
return b.parseReq(req)
|
|
}
|
|
body, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return jrpc2.ParseRequests(body)
|
|
}
|
|
|
|
func (b Bridge) encodeResponses(rsps []*jrpc2.Response, w http.ResponseWriter) error {
|
|
// If there is only a single reply, send it alone; otherwise encode a batch.
|
|
// Per the spec (https://www.jsonrpc.org/specification#batch), this is OK;
|
|
// we are not required to respond to a batch with an array:
|
|
//
|
|
// The Server SHOULD respond with an Array containing the corresponding
|
|
// Response objects
|
|
//
|
|
data, err := marshalResponses(rsps)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeJSON(w, http.StatusOK, json.RawMessage(data))
|
|
return nil
|
|
}
|
|
|
|
// Close closes the channel to the server, waits for the server to exit, and
|
|
// reports its exit status.
|
|
func (b Bridge) Close() error {
|
|
if b.getter != nil {
|
|
b.getter.Close()
|
|
}
|
|
return b.local.Close()
|
|
}
|
|
|
|
// NewBridge constructs a new Bridge that starts a server on mux and dispatches
|
|
// HTTP requests to it. The server will run until the bridge is closed.
|
|
//
|
|
// Note that a bridge is not able to push calls or notifications from the
|
|
// server back to the remote client. The bridge client is shared by multiple
|
|
// active HTTP requests, and has no way to know which of the callers the push
|
|
// should be forwarded to. You can enable push on the bridge server and set
|
|
// hooks on the bridge client as usual, but the remote client will not see push
|
|
// messages from the server.
|
|
func NewBridge(mux jrpc2.Assigner, opts *BridgeOptions) Bridge {
|
|
b := Bridge{
|
|
local: server.NewLocal(mux, &server.LocalOptions{
|
|
Client: opts.clientOptions(),
|
|
Server: opts.serverOptions(),
|
|
}),
|
|
parseReq: opts.parseRequest(),
|
|
}
|
|
if pget := opts.parseGETRequest(); pget != nil {
|
|
g := NewGetter(mux, &GetterOptions{
|
|
Client: opts.clientOptions(),
|
|
Server: opts.serverOptions(),
|
|
ParseRequest: pget,
|
|
})
|
|
b.getter = &g
|
|
}
|
|
return b
|
|
}
|
|
|
|
// BridgeOptions are optional settings for a Bridge. A nil pointer is ready for
|
|
// use and provides default values as described.
|
|
type BridgeOptions struct {
|
|
// Options for the bridge client (default nil).
|
|
Client *jrpc2.ClientOptions
|
|
|
|
// Options for the bridge server (default nil).
|
|
Server *jrpc2.ServerOptions
|
|
|
|
// If non-nil, this function is called to parse JSON-RPC requests from the
|
|
// HTTP request body. If this function reports an error, the request fails.
|
|
// By default, the bridge uses jrpc2.ParseRequests on the HTTP request body.
|
|
//
|
|
// Setting this hook disables the default requirement that the request
|
|
// method be POST and the content-type be application/json.
|
|
ParseRequest func(*http.Request) ([]*jrpc2.Request, error)
|
|
|
|
// If non-nil, this function is used to parse a JSON-RPC method name and
|
|
// parameters from the URL of an HTTP GET request. If this function reports
|
|
// an error, the request fails.
|
|
//
|
|
// If this hook is set, all GET requests are handled by a Getter using this
|
|
// parse function, and are not passed to a ParseRequest hook even if one is
|
|
// defined.
|
|
ParseGETRequest func(*http.Request) (string, interface{}, error)
|
|
}
|
|
|
|
func (o *BridgeOptions) clientOptions() *jrpc2.ClientOptions {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
return o.Client
|
|
}
|
|
|
|
func (o *BridgeOptions) serverOptions() *jrpc2.ServerOptions {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
return o.Server
|
|
}
|
|
|
|
func (o *BridgeOptions) parseRequest() func(*http.Request) ([]*jrpc2.Request, error) {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
return o.ParseRequest
|
|
}
|
|
|
|
func (o *BridgeOptions) parseGETRequest() func(*http.Request) (string, interface{}, error) {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
return o.ParseGETRequest
|
|
}
|
|
|
|
type httpReqKey struct{}
|
|
|
|
// HTTPRequest returns the HTTP request associated with ctx, or nil. The
|
|
// context passed to the JSON-RPC client by the Bridge will contain this value.
|
|
func HTTPRequest(ctx context.Context) *http.Request {
|
|
req, ok := ctx.Value(httpReqKey{}).(*http.Request)
|
|
if ok {
|
|
return req
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// marshalResponses encodes a batch of JSON-RPC responses into JSON.
|
|
func marshalResponses(rsps []*jrpc2.Response) ([]byte, error) {
|
|
if len(rsps) == 1 {
|
|
return json.Marshal(rsps[0])
|
|
}
|
|
return json.Marshal(rsps)
|
|
}
|