good morning!!!!

Skip to content
Snippets Groups Projects
client.go 3.9 KiB
Newer Older
a's avatar
a committed
package http
a's avatar
rpc
a committed

import (
	"context"
a's avatar
ok  
a committed
	"crypto/tls"
a's avatar
rpc
a committed
	"encoding/json"
	"errors"
a's avatar
a committed
	"fmt"
	"io"
a's avatar
ok  
a committed
	"net"
a's avatar
ok  
a committed
	"net/http"
a's avatar
a committed
	"sync"
a's avatar
rpc
a committed
	"sync/atomic"

a's avatar
a committed
	"gfx.cafe/open/jrpc/pkg/jjson"
a's avatar
a committed
	"gfx.cafe/open/jrpc/pkg/jsonrpc"
a's avatar
ok  
a committed
	"golang.org/x/net/http2"
a's avatar
rpc
a committed
)

var (
	ErrClientQuit                = errors.New("client is closed")
	ErrNoResult                  = errors.New("no result in JSON-RPC response")
	ErrSubscriptionQueueOverflow = errors.New("subscription queue overflow")
	errClientReconnected         = errors.New("client reconnected")
	errDead                      = errors.New("connection lost")
)

a's avatar
ok  
a committed
var DefaultH2CClient = &http.Client{
	Transport: &http2.Transport{
		// So http2.Transport doesn't complain the URL scheme isn't 'https'
		AllowHTTP: true,
		// Pretend we are dialing a TLS endpoint. (Note, we ignore the passed tls.Config)
		DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
			var d net.Dialer
			return d.DialContext(ctx, network, addr)
		},
	},
}

a's avatar
a committed
var _ jsonrpc.Conn = (*Client)(nil)
a's avatar
a committed

a's avatar
rpc
a committed
// Client represents a connection to an RPC server.
type Client struct {
a's avatar
ok  
a committed
	remote string
	c      *http.Client
a's avatar
rpc
a committed

a's avatar
ok  
a committed
	id atomic.Int64
a's avatar
a committed

	headers http.Header
a's avatar
a committed

a's avatar
a committed
	m       jsonrpc.Middlewares
	handler jsonrpc.Handler
a's avatar
a committed
	mu      sync.RWMutex
a's avatar
a committed
}

a's avatar
a committed
func (c *Client) Mount(h jsonrpc.Middleware) {
a's avatar
a committed
	c.mu.Lock()
	defer c.mu.Unlock()
	c.m = append(c.m, h)
a's avatar
a committed
	c.handler = c.m.HandlerFunc(func(w jsonrpc.ResponseWriter, r *jsonrpc.Request) {
a's avatar
a committed
		// do nothing on no handler
	})
a's avatar
a committed
}

func DialHTTP(target string) (*Client, error) {
	return Dial(nil, http.DefaultClient, target)
a's avatar
rpc
a committed
}
a's avatar
ok  
a committed
func DialH2C(target string) (*Client, error) {
	return Dial(nil, DefaultH2CClient, target)
}
a's avatar
ok  
a committed
func Dial(ctx context.Context, client *http.Client, target string) (*Client, error) {
a's avatar
a committed
	if client == nil {
		client = http.DefaultClient
	}
a's avatar
a committed
	return &Client{remote: target, c: client, headers: http.Header{
		"Content-Type": []string{"application/json"},
	}}, nil
a's avatar
a committed
}

func (c *Client) SetHeader(key string, value string) {
a's avatar
a committed
	c.mu.Lock()
	defer c.mu.Unlock()
a's avatar
a committed
	c.headers.Set(key, value)
a's avatar
rpc
a committed
}

a's avatar
ok  
a committed
func (c *Client) Do(ctx context.Context, result any, method string, params any) error {
a's avatar
a committed
	req, err := jsonrpc.NewRequest(ctx, jsonrpc.NewId(c.id.Add(1)), method, params)
a's avatar
a committed
	if err != nil {
		return err
	}
a's avatar
a committed
	resp, err := c.post(req)
a's avatar
a committed
	if err != nil {
		return err
	}
a's avatar
ok  
a committed
	defer resp.Body.Close()
a's avatar
a committed
	if resp.StatusCode != 200 {
		b, _ := io.ReadAll(resp.Body)
a's avatar
a committed
		return &jsonrpc.HTTPError{
a's avatar
a committed
			StatusCode: resp.StatusCode,
			Status:     resp.Status,
			Body:       b,
		}
	}
a's avatar
a committed
	msg := &jsonrpc.Message{}
a's avatar
ok  
a committed

a's avatar
ok  
a committed
	err = json.NewDecoder(resp.Body).Decode(&msg)
	if err != nil {
a's avatar
a committed
		return fmt.Errorf("decode json: %w", err)
a's avatar
ok  
a committed
	}
	if msg.Error != nil {
a's avatar
a committed
		return msg.Error
a's avatar
ok  
a committed
	}
a's avatar
a committed
	if result != nil && msg.Result != nil {
		err = json.NewDecoder(msg.Result).Decode(result)
a's avatar
ok  
a committed
		if err != nil {
			return err
		}
a's avatar
a committed
	}
a's avatar
ok  
a committed
	return nil
a's avatar
a committed
}

a's avatar
a committed
func (c *Client) Notify(ctx context.Context, method string, params any) error {
a's avatar
a committed
	req, err := jsonrpc.NewRequest(ctx, nil, method, params)
a's avatar
a committed
	if err != nil {
		return err
	}
a's avatar
a committed
	resp, err := c.post(req)
a's avatar
rpc
a committed
	if err != nil {
		return err
	}
a's avatar
a committed
	resp.Body.Close()
a's avatar
ok  
a committed
	return err
a's avatar
a committed
}

a's avatar
ok  
a committed
func (c *Client) Close() error {
	return nil
a's avatar
rpc
a committed
}
a's avatar
a committed

func (c *Client) Closed() <-chan struct{} {
	return make(chan struct{})
}

a's avatar
a committed
func (c *Client) post(req *jsonrpc.Request) (*http.Response, error) {
a's avatar
a committed
	// TODO: use buffer for this
a's avatar
a committed
	buf := jjson.GetBuf()
	defer jjson.PutBuf(buf)
a's avatar
a committed
	buf.Reset()
	err := json.NewEncoder(buf).Encode(req)
	if err != nil {
		return nil, err
	}
a's avatar
a committed
	resp, err := c.postBuf(req.Context(), buf)
	if err != nil {
		return nil, err
	}
	return resp, nil
a's avatar
a committed
}

func (c *Client) postBuf(ctx context.Context, rd io.Reader) (*http.Response, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.remote, rd)
	if err != nil {
		return nil, err
	}
	func() {
		c.mu.RLock()
		defer c.mu.RUnlock()
		for k, v := range c.headers {
			for _, vv := range v {
				hreq.Header.Add(k, vv)
			}
		}
	}()
	return c.c.Do(hreq)
}