decred.org/dcrdex@v1.0.5/docs/examples/rpcclient/main.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"path/filepath"
    18  
    19  	"decred.org/dcrdex/client/rpcserver"
    20  	"decred.org/dcrdex/dex/msgjson"
    21  	"github.com/decred/dcrd/dcrutil/v4"
    22  	"github.com/gorilla/websocket"
    23  )
    24  
    25  const (
    26  	user    = "user"
    27  	pass    = "pass"
    28  	addr    = "127.0.0.1:5757"
    29  	dexAddr = "127.0.0.1:17273"
    30  )
    31  
    32  func main() {
    33  	if err := run(); err != nil {
    34  		fmt.Fprintf(os.Stderr, "%v", err)
    35  	}
    36  }
    37  
    38  func run() error {
    39  	home := dcrutil.AppDataDir("dexc", false)
    40  	certFile := filepath.Join(home, "rpc.cert")
    41  	cfg := &config{
    42  		RPCUser: user,
    43  		RPCPass: pass,
    44  		RPCAddr: addr,
    45  		RPCCert: certFile,
    46  	}
    47  	urlStr := "https://" + cfg.RPCAddr
    48  
    49  	// Create a new HTTP client.
    50  	client, err := newHTTPClient(cfg, urlStr)
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	rawParams := rpcserver.RawParams{Args: []string{"trade"}}
    56  	msg, err := msgjson.NewRequest(1, "help", rawParams)
    57  	b, err := json.Marshal(msg)
    58  	fmt.Println("http request:", string(b))
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	// Send a one shot request over https.
    64  	res, err := sendPostRequest(b, urlStr, cfg, client)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	fmt.Println("response: ", res)
    69  
    70  	// Create a new ws client.
    71  	wsc, err := newWSClient(cfg)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	defer wsc.Close()
    76  
    77  	// Send a request to the websocket server.
    78  	type marketLoad struct {
    79  		Host  string `json:"host"`
    80  		Base  uint32 `json:"base"`
    81  		Quote uint32 `json:"quote"`
    82  	}
    83  	ml := marketLoad{
    84  		Host:  dexAddr,
    85  		Base:  42,
    86  		Quote: 0,
    87  	}
    88  	msg, err = msgjson.NewRequest(1, "loadmarket", ml)
    89  	b, err = json.Marshal(msg)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	fmt.Println("ws request:", string(b))
    94  	err = wsc.WriteMessage(1, b)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	// Wait for a notification to come.
   100  	_, r, err := wsc.NextReader()
   101  	if err != nil {
   102  		return err
   103  	}
   104  	notification, err := io.ReadAll(r)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	fmt.Println("ws notification:", string(notification))
   109  	return nil
   110  }
   111  
   112  type config struct {
   113  	RPCUser string
   114  	RPCPass string
   115  	RPCAddr string
   116  	RPCCert string
   117  }
   118  
   119  // newHTTPClient returns a new HTTP client
   120  func newHTTPClient(cfg *config, urlStr string) (*http.Client, error) {
   121  	// Configure TLS.
   122  	pem, err := os.ReadFile(cfg.RPCCert)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	uri, err := url.Parse(urlStr)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("error parsing URL: %v", err)
   130  	}
   131  
   132  	pool := x509.NewCertPool()
   133  	if ok := pool.AppendCertsFromPEM(pem); !ok {
   134  		return nil, fmt.Errorf("invalid certificate file: %v",
   135  			cfg.RPCCert)
   136  	}
   137  	tlsConfig := &tls.Config{
   138  		RootCAs:    pool,
   139  		ServerName: uri.Hostname(),
   140  	}
   141  
   142  	// Create and return the new HTTP client potentially configured with a
   143  	// proxy and TLS.
   144  	client := http.Client{
   145  		Transport: &http.Transport{
   146  			TLSClientConfig: tlsConfig,
   147  		},
   148  	}
   149  	return &client, nil
   150  }
   151  
   152  // newWSClient obtains a websocket connection.
   153  func newWSClient(cfg *config) (*websocket.Conn, error) {
   154  	urlStr := "wss://" + cfg.RPCAddr + "/ws"
   155  	// Configure TLS.
   156  	pem, err := os.ReadFile(cfg.RPCCert)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	uri, err := url.Parse(urlStr)
   162  	if err != nil {
   163  		return nil, fmt.Errorf("error parsing URL: %v", err)
   164  	}
   165  
   166  	pool := x509.NewCertPool()
   167  	if ok := pool.AppendCertsFromPEM(pem); !ok {
   168  		return nil, fmt.Errorf("invalid certificate file: %v",
   169  			cfg.RPCCert)
   170  	}
   171  	tlsConfig := &tls.Config{
   172  		RootCAs:    pool,
   173  		ServerName: uri.Hostname(),
   174  	}
   175  
   176  	// Configure dialer with tls.
   177  	dialer := websocket.Dialer{
   178  		TLSClientConfig: tlsConfig,
   179  	}
   180  
   181  	// Configure basic access authorization.
   182  	authStr := base64.StdEncoding.EncodeToString([]byte(cfg.RPCUser + ":" + cfg.RPCPass))
   183  	header := http.Header{}
   184  	header.Set("Authorization", "Basic "+authStr)
   185  	conn, _, err := dialer.Dial(urlStr, header)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	return conn, nil
   190  }
   191  
   192  // sendPostRequest sends the marshalled JSON-RPC command using HTTP-POST mode
   193  // to the server described in the passed config struct.  It also attempts to
   194  // unmarshal the response as a msgjson.Message response and returns either the
   195  // response or error.
   196  func sendPostRequest(marshalledJSON []byte, urlStr string, cfg *config, httpClient *http.Client) (*msgjson.Message, error) {
   197  	bodyReader := bytes.NewReader(marshalledJSON)
   198  	httpRequest, err := http.NewRequest("POST", urlStr, bodyReader)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	httpRequest.Close = true
   203  	httpRequest.Header.Set("Content-Type", "application/json")
   204  
   205  	// Configure basic access authorization.
   206  	httpRequest.SetBasicAuth(cfg.RPCUser, cfg.RPCPass)
   207  
   208  	httpResponse, err := httpClient.Do(httpRequest)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	// Read the raw bytes and close the response.
   214  	respBytes, err := io.ReadAll(httpResponse.Body)
   215  	httpResponse.Body.Close()
   216  	if err != nil {
   217  		err = fmt.Errorf("error reading json reply: %v", err)
   218  		return nil, err
   219  	}
   220  
   221  	// Handle unsuccessful HTTP responses
   222  	if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
   223  		// Generate a standard error to return if the server body is
   224  		// empty.  This should not happen very often, but it's better
   225  		// than showing nothing in case the target server has a poor
   226  		// implementation.
   227  		if len(respBytes) == 0 {
   228  			return nil, fmt.Errorf("%d %s", httpResponse.StatusCode,
   229  				http.StatusText(httpResponse.StatusCode))
   230  		}
   231  		return nil, fmt.Errorf("%s", respBytes)
   232  	}
   233  
   234  	// Unmarshal the response.
   235  	var resp *msgjson.Message
   236  	if err := json.Unmarshal(respBytes, &resp); err != nil {
   237  		return nil, err
   238  	}
   239  	return resp, nil
   240  }