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 }