github.com/jimmyx0x/go-ethereum@v1.10.28/cmd/clef/pythonsigner.py (about) 1 import sys 2 import subprocess 3 4 from tinyrpc.transports import ServerTransport 5 from tinyrpc.protocols.jsonrpc import JSONRPCProtocol 6 from tinyrpc.dispatch import public, RPCDispatcher 7 from tinyrpc.server import RPCServer 8 9 """ 10 This is a POC example of how to write a custom UI for Clef. 11 The UI starts the clef process with the '--stdio-ui' option 12 and communicates with clef using standard input / output. 13 14 The standard input/output is a relatively secure way to communicate, 15 as it does not require opening any ports or IPC files. Needless to say, 16 it does not protect against memory inspection mechanisms 17 where an attacker can access process memory. 18 19 To make this work install all the requirements: 20 21 pip install -r requirements.txt 22 """ 23 24 try: 25 import urllib.parse as urlparse 26 except ImportError: 27 import urllib as urlparse 28 29 30 class StdIOTransport(ServerTransport): 31 """Uses std input/output for RPC""" 32 33 def receive_message(self): 34 return None, urlparse.unquote(sys.stdin.readline()) 35 36 def send_reply(self, context, reply): 37 print(reply) 38 39 40 class PipeTransport(ServerTransport): 41 """Uses std a pipe for RPC""" 42 43 def __init__(self, input, output): 44 self.input = input 45 self.output = output 46 47 def receive_message(self): 48 data = self.input.readline() 49 print(">> {}".format(data)) 50 return None, urlparse.unquote(data) 51 52 def send_reply(self, context, reply): 53 reply = str(reply, "utf-8") 54 print("<< {}".format(reply)) 55 self.output.write("{}\n".format(reply)) 56 57 58 def sanitize(txt, limit=100): 59 return txt[:limit].encode("unicode_escape").decode("utf-8") 60 61 62 def metaString(meta): 63 """ 64 "meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""} 65 """ # noqa: E501 66 message = ( 67 "\tRequest context:\n" 68 "\t\t{remote} -> {scheme} -> {local}\n" 69 "\tAdditional HTTP header data, provided by the external caller:\n" 70 "\t\tUser-Agent: {user_agent}\n" 71 "\t\tOrigin: {origin}\n" 72 ) 73 return message.format( 74 remote=meta.get("remote", "<missing>"), 75 scheme=meta.get("scheme", "<missing>"), 76 local=meta.get("local", "<missing>"), 77 user_agent=sanitize(meta.get("User-Agent"), 200), 78 origin=sanitize(meta.get("Origin"), 100), 79 ) 80 81 82 class StdIOHandler: 83 def __init__(self): 84 pass 85 86 @public 87 def approveTx(self, req): 88 """ 89 Example request: 90 91 {"jsonrpc":"2.0","id":20,"method":"ui_approveTx","params":[{"transaction":{"from":"0xDEADbEeF000000000000000000000000DeaDbeEf","to":"0xDEADbEeF000000000000000000000000DeaDbeEf","gas":"0x3e8","gasPrice":"0x5","maxFeePerGas":null,"maxPriorityFeePerGas":null,"value":"0x6","nonce":"0x1","data":"0x"},"call_info":null,"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} 92 93 :param transaction: transaction info 94 :param call_info: info abou the call, e.g. if ABI info could not be 95 :param meta: metadata about the request, e.g. where the call comes from 96 :return: 97 """ # noqa: E501 98 message = ( 99 "Sign transaction request:\n" 100 "\t{meta_string}\n" 101 "\n" 102 "\tFrom: {from_}\n" 103 "\tTo: {to}\n" 104 "\n" 105 "\tAuto-rejecting request" 106 ) 107 meta = req.get("meta", {}) 108 transaction = req.get("transaction") 109 sys.stdout.write( 110 message.format( 111 meta_string=metaString(meta), 112 from_=transaction.get("from", "<missing>"), 113 to=transaction.get("to", "<missing>"), 114 ) 115 ) 116 return { 117 "approved": False, 118 } 119 120 @public 121 def approveSignData(self, req): 122 """ 123 Example request: 124 125 {"jsonrpc":"2.0","id":8,"method":"ui_approveSignData","params":[{"content_type":"application/x-clique-header","address":"0x0011223344556677889900112233445566778899","raw_data":"+QIRoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIFOYIFOYIFOoIFOoIFOppFeHRyYSBkYXRhIEV4dHJhIGRhdGEgRXh0cqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAA==","messages":[{"name":"Clique header","value":"clique header 1337 [0x44381ab449d77774874aca34634cb53bc21bd22aef2d3d4cf40e51176cb585ec]","type":"clique"}],"call_info":null,"hash":"0xa47ab61438a12a06c81420e308c2b7aae44e9cd837a5df70dd021421c0f58643","meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} 126 """ # noqa: E501 127 message = ( 128 "Sign data request:\n" 129 "\t{meta_string}\n" 130 "\n" 131 "\tContent-type: {content_type}\n" 132 "\tAddress: {address}\n" 133 "\tHash: {hash_}\n" 134 "\n" 135 "\tAuto-rejecting request\n" 136 ) 137 meta = req.get("meta", {}) 138 sys.stdout.write( 139 message.format( 140 meta_string=metaString(meta), 141 content_type=req.get("content_type"), 142 address=req.get("address"), 143 hash_=req.get("hash"), 144 ) 145 ) 146 147 return { 148 "approved": False, 149 "password": None, 150 } 151 152 @public 153 def approveNewAccount(self, req): 154 """ 155 Example request: 156 157 {"jsonrpc":"2.0","id":25,"method":"ui_approveNewAccount","params":[{"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} 158 """ # noqa: E501 159 message = ( 160 "Create new account request:\n" 161 "\t{meta_string}\n" 162 "\n" 163 "\tAuto-rejecting request\n" 164 ) 165 meta = req.get("meta", {}) 166 sys.stdout.write(message.format(meta_string=metaString(meta))) 167 return { 168 "approved": False, 169 } 170 171 @public 172 def showError(self, req): 173 """ 174 Example request: 175 176 {"jsonrpc":"2.0","method":"ui_showError","params":[{"text":"If you see this message, enter 'yes' to the next question"}]} 177 178 :param message: to display 179 :return:nothing 180 """ # noqa: E501 181 message = ( 182 "## Error\n{text}\n" 183 "Press enter to continue\n" 184 ) 185 text = req.get("text") 186 sys.stdout.write(message.format(text=text)) 187 input() 188 return 189 190 @public 191 def showInfo(self, req): 192 """ 193 Example request: 194 195 {"jsonrpc":"2.0","method":"ui_showInfo","params":[{"text":"If you see this message, enter 'yes' to next question"}]} 196 197 :param message: to display 198 :return:nothing 199 """ # noqa: E501 200 message = ( 201 "## Info\n{text}\n" 202 "Press enter to continue\n" 203 ) 204 text = req.get("text") 205 sys.stdout.write(message.format(text=text)) 206 input() 207 return 208 209 @public 210 def onSignerStartup(self, req): 211 """ 212 Example request: 213 214 {"jsonrpc":"2.0", "method":"ui_onSignerStartup", "params":[{"info":{"extapi_http":"n/a","extapi_ipc":"/home/user/.clef/clef.ipc","extapi_version":"6.1.0","intapi_version":"7.0.1"}}]} 215 """ # noqa: E501 216 message = ( 217 "\n" 218 "\t\tExt api url: {extapi_http}\n" 219 "\t\tInt api ipc: {extapi_ipc}\n" 220 "\t\tExt api ver: {extapi_version}\n" 221 "\t\tInt api ver: {intapi_version}\n" 222 ) 223 info = req.get("info") 224 sys.stdout.write( 225 message.format( 226 extapi_http=info.get("extapi_http"), 227 extapi_ipc=info.get("extapi_ipc"), 228 extapi_version=info.get("extapi_version"), 229 intapi_version=info.get("intapi_version"), 230 ) 231 ) 232 233 @public 234 def approveListing(self, req): 235 """ 236 Example request: 237 238 {"jsonrpc":"2.0","id":23,"method":"ui_approveListing","params":[{"accounts":[{"address":... 239 """ # noqa: E501 240 message = ( 241 "\n" 242 "## Account listing request\n" 243 "\t{meta_string}\n" 244 "\tDo you want to allow listing the following accounts?\n" 245 "\t-{addrs}\n" 246 "\n" 247 "->Auto-answering No\n" 248 ) 249 meta = req.get("meta", {}) 250 accounts = req.get("accounts", []) 251 addrs = [x.get("address") for x in accounts] 252 sys.stdout.write( 253 message.format( 254 addrs="\n\t-".join(addrs), 255 meta_string=metaString(meta) 256 ) 257 ) 258 return {} 259 260 @public 261 def onInputRequired(self, req): 262 """ 263 Example request: 264 265 {"jsonrpc":"2.0","id":1,"method":"ui_onInputRequired","params":[{"title":"Master Password","prompt":"Please enter the password to decrypt the master seed","isPassword":true}]} 266 267 :param message: to display 268 :return:nothing 269 """ # noqa: E501 270 message = ( 271 "\n" 272 "## {title}\n" 273 "\t{prompt}\n" 274 "\n" 275 "> " 276 ) 277 sys.stdout.write( 278 message.format( 279 title=req.get("title"), 280 prompt=req.get("prompt") 281 ) 282 ) 283 isPassword = req.get("isPassword") 284 if not isPassword: 285 return {"text": input()} 286 287 return "" 288 289 290 def main(args): 291 cmd = ["clef", "--stdio-ui"] 292 if len(args) > 0 and args[0] == "test": 293 cmd.extend(["--stdio-ui-test"]) 294 print("cmd: {}".format(" ".join(cmd))) 295 296 dispatcher = RPCDispatcher() 297 dispatcher.register_instance(StdIOHandler(), "ui_") 298 299 # line buffered 300 p = subprocess.Popen( 301 cmd, 302 bufsize=1, 303 universal_newlines=True, 304 stdin=subprocess.PIPE, 305 stdout=subprocess.PIPE, 306 ) 307 308 rpc_server = RPCServer( 309 PipeTransport(p.stdout, p.stdin), JSONRPCProtocol(), dispatcher 310 ) 311 rpc_server.serve_forever() 312 313 314 if __name__ == "__main__": 315 main(sys.argv[1:])