code.vegaprotocol.io/vega@v0.79.0/wallet/service/v2/api.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package v2
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"net/http"
    22  	"net/url"
    23  	"strings"
    24  
    25  	"code.vegaprotocol.io/vega/libs/jsonrpc"
    26  	"code.vegaprotocol.io/vega/wallet/api"
    27  	"code.vegaprotocol.io/vega/wallet/service/v2/connections"
    28  	"code.vegaprotocol.io/vega/wallet/wallet"
    29  
    30  	"go.uber.org/zap"
    31  )
    32  
    33  // Generates mocks
    34  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/wallet/service/v2 ClientAPI
    35  
    36  type ClientAPI interface {
    37  	ConnectWallet(ctx context.Context, hostname string) (wallet.Wallet, *jsonrpc.ErrorDetails)
    38  	GetChainID(ctx context.Context) (jsonrpc.Result, *jsonrpc.ErrorDetails)
    39  	ListKeys(ctx context.Context, connectedWallet api.ConnectedWallet) (jsonrpc.Result, *jsonrpc.ErrorDetails)
    40  	CheckTransaction(ctx context.Context, params jsonrpc.Params, connectedWallet api.ConnectedWallet) (jsonrpc.Result, *jsonrpc.ErrorDetails)
    41  	SignTransaction(ctx context.Context, rawParams jsonrpc.Params, connectedWallet api.ConnectedWallet) (jsonrpc.Result, *jsonrpc.ErrorDetails)
    42  	SendTransaction(ctx context.Context, rawParams jsonrpc.Params, connectedWallet api.ConnectedWallet) (jsonrpc.Result, *jsonrpc.ErrorDetails)
    43  }
    44  
    45  type Command func(ctx context.Context, lw *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails)
    46  
    47  type API struct {
    48  	log *zap.Logger
    49  
    50  	commands map[string]Command
    51  }
    52  
    53  // NewAPI builds the wallet JSON-RPC API with specific methods that are
    54  // intended to be publicly exposed to third-party applications in a
    55  // non-trustable environment.
    56  // Because of the nature of the environment from where these methods are called,
    57  // (the "wild, wild web"), no administration methods are exposed. We don't want
    58  // malicious third-party applications to leverage administration capabilities
    59  // that could expose the user and/or compromise his wallets.
    60  func NewAPI(log *zap.Logger, clientAPI ClientAPI, connectionsManager *connections.Manager) *API {
    61  	commands := map[string]Command{}
    62  
    63  	commands["client.connect_wallet"] = func(ctx context.Context, lw *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
    64  		hostname, err := resolveHostname(httpRequest)
    65  		if err != nil {
    66  			return nil, jsonrpc.NewServerError(api.ErrorCodeHostnameResolutionFailure, err)
    67  		}
    68  
    69  		selectedWallet, errDetails := clientAPI.ConnectWallet(ctx, hostname)
    70  		if errDetails != nil {
    71  			return nil, errDetails
    72  		}
    73  
    74  		token, err := connectionsManager.StartSession(hostname, selectedWallet)
    75  		if err != nil {
    76  			return nil, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
    77  		}
    78  
    79  		lw.SetAuthorization(AsVWT(token))
    80  
    81  		return nil, nil
    82  	}
    83  
    84  	commands["client.disconnect_wallet"] = func(ctx context.Context, _ *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
    85  		vwt, err := ExtractVWT(httpRequest)
    86  		if err != nil {
    87  			return nil, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
    88  		}
    89  
    90  		connectionsManager.EndSessionConnectionWithToken(vwt.Token())
    91  
    92  		return nil, nil
    93  	}
    94  
    95  	commands["client.get_chain_id"] = func(ctx context.Context, _ *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
    96  		return clientAPI.GetChainID(ctx)
    97  	}
    98  
    99  	commands["client.list_keys"] = func(ctx context.Context, _ *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
   100  		hostname, err := resolveHostname(httpRequest)
   101  		if err != nil {
   102  			return nil, jsonrpc.NewServerError(api.ErrorCodeHostnameResolutionFailure, err)
   103  		}
   104  
   105  		vwt, err := ExtractVWT(httpRequest)
   106  		if err != nil {
   107  			return nil, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
   108  		}
   109  
   110  		connectedWallet, errDetails := connectionsManager.ConnectedWallet(ctx, hostname, vwt.Token())
   111  		if errDetails != nil {
   112  			return nil, errDetails
   113  		}
   114  
   115  		return clientAPI.ListKeys(ctx, connectedWallet)
   116  	}
   117  
   118  	commands["client.sign_transaction"] = func(ctx context.Context, _ *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
   119  		hostname, err := resolveHostname(httpRequest)
   120  		if err != nil {
   121  			return nil, jsonrpc.NewServerError(api.ErrorCodeHostnameResolutionFailure, err)
   122  		}
   123  
   124  		vwt, err := ExtractVWT(httpRequest)
   125  		if err != nil {
   126  			return nil, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
   127  		}
   128  
   129  		connectedWallet, errDetails := connectionsManager.ConnectedWallet(ctx, hostname, vwt.Token())
   130  		if errDetails != nil {
   131  			return nil, errDetails
   132  		}
   133  
   134  		return clientAPI.SignTransaction(ctx, rpcRequest.Params, connectedWallet)
   135  	}
   136  
   137  	commands["client.send_transaction"] = func(ctx context.Context, _ *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
   138  		hostname, err := resolveHostname(httpRequest)
   139  		if err != nil {
   140  			return nil, jsonrpc.NewServerError(api.ErrorCodeHostnameResolutionFailure, err)
   141  		}
   142  
   143  		vwt, err := ExtractVWT(httpRequest)
   144  		if err != nil {
   145  			return nil, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
   146  		}
   147  
   148  		connectedWallet, errDetails := connectionsManager.ConnectedWallet(ctx, hostname, vwt.Token())
   149  		if errDetails != nil {
   150  			return nil, errDetails
   151  		}
   152  
   153  		return clientAPI.SendTransaction(ctx, rpcRequest.Params, connectedWallet)
   154  	}
   155  
   156  	commands["client.check_transaction"] = func(ctx context.Context, _ *responseWriter, httpRequest *http.Request, rpcRequest jsonrpc.Request) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
   157  		hostname, err := resolveHostname(httpRequest)
   158  		if err != nil {
   159  			return nil, jsonrpc.NewServerError(api.ErrorCodeHostnameResolutionFailure, err)
   160  		}
   161  
   162  		vwt, err := ExtractVWT(httpRequest)
   163  		if err != nil {
   164  			return nil, jsonrpc.NewServerError(api.ErrorCodeAuthenticationFailure, err)
   165  		}
   166  
   167  		connectedWallet, errDetails := connectionsManager.ConnectedWallet(ctx, hostname, vwt.Token())
   168  		if errDetails != nil {
   169  			return nil, errDetails
   170  		}
   171  
   172  		return clientAPI.CheckTransaction(ctx, rpcRequest.Params, connectedWallet)
   173  	}
   174  
   175  	return &API{
   176  		log:      log,
   177  		commands: commands,
   178  	}
   179  }
   180  
   181  // resolveHostname attempts to resolve the source of the request by parsing the
   182  // Origin (and if absent, the Referer) header.
   183  // If it fails to resolve the hostname, it returns an error.
   184  func resolveHostname(r *http.Request) (string, error) {
   185  	origin := r.Header.Get("Origin")
   186  	if origin != "" {
   187  		parsedOrigin, err := url.Parse(origin)
   188  		if err != nil {
   189  			return origin, nil //nolint:nilerr
   190  		}
   191  		if parsedOrigin.Host != "" {
   192  			return parsedOrigin.Host, nil
   193  		}
   194  		return normalizeHostname(origin)
   195  	}
   196  
   197  	// In some scenario, the Origin can be set to null by the browser for privacy
   198  	// reasons. Since we are not trying to fingerprint or spoof anyone, we
   199  	// attempt to parse the Referer.
   200  	referer := r.Header.Get("Referer")
   201  	if referer != "" {
   202  		parsedReferer, err := url.Parse(referer)
   203  		if err != nil {
   204  			return "", fmt.Errorf("could not parse the Referer header: %w", err)
   205  		}
   206  		return normalizeHostname(parsedReferer.Host)
   207  	}
   208  
   209  	// If none of the Origin and Referer headers are present, we just report that
   210  	// the missing Origin header as we should, ideally, only rely on this header.
   211  	// The Referer is just a "desperate" attempt to get information about the
   212  	// origin of the request and minimize the friction with future privacy
   213  	// settings.
   214  	return "", ErrOriginHeaderIsRequired
   215  }
   216  
   217  func normalizeHostname(host string) (string, error) {
   218  	host = trimBlankCharacters(host)
   219  
   220  	if host == "" {
   221  		return "", ErrOriginHeaderIsRequired
   222  	}
   223  
   224  	return host, nil
   225  }
   226  
   227  func trimBlankCharacters(host string) string {
   228  	return strings.Trim(host, " \r\n\t")
   229  }