diff --git a/cmd/clef/pythonsigner.py b/cmd/clef/pythonsigner.py index 315aabd73f..b9ea1e406a 100644 --- a/cmd/clef/pythonsigner.py +++ b/cmd/clef/pythonsigner.py @@ -1,177 +1,315 @@ -import os,sys, subprocess +import sys +import subprocess + from tinyrpc.transports import ServerTransport from tinyrpc.protocols.jsonrpc import JSONRPCProtocol -from tinyrpc.dispatch import public,RPCDispatcher +from tinyrpc.dispatch import public, RPCDispatcher from tinyrpc.server import RPCServer -""" This is a POC example of how to write a custom UI for Clef. The UI starts the -clef process with the '--stdio-ui' option, and communicates with clef using standard input / output. +""" +This is a POC example of how to write a custom UI for Clef. +The UI starts the clef process with the '--stdio-ui' option +and communicates with clef using standard input / output. -The standard input/output is a relatively secure way to communicate, as it does not require opening any ports -or IPC files. Needless to say, it does not protect against memory inspection mechanisms where an attacker -can access process memory.""" +The standard input/output is a relatively secure way to communicate, +as it does not require opening any ports or IPC files. Needless to say, +it does not protect against memory inspection mechanisms +where an attacker can access process memory. + +To make this work install all the requirements: + + pip install -r requirements.txt +""" try: import urllib.parse as urlparse except ImportError: import urllib as urlparse + class StdIOTransport(ServerTransport): - """ Uses std input/output for RPC """ + """Uses std input/output for RPC""" + def receive_message(self): return None, urlparse.unquote(sys.stdin.readline()) def send_reply(self, context, reply): print(reply) -class PipeTransport(ServerTransport): - """ Uses std a pipe for RPC """ - def __init__(self,input, output): +class PipeTransport(ServerTransport): + """Uses std a pipe for RPC""" + + def __init__(self, input, output): self.input = input self.output = output def receive_message(self): data = self.input.readline() - print(">> {}".format( data)) + print(">> {}".format(data)) return None, urlparse.unquote(data) def send_reply(self, context, reply): - print("<< {}".format( reply)) - self.output.write(reply) - self.output.write("\n") + reply = str(reply, "utf-8") + print("<< {}".format(reply)) + self.output.write("{}\n".format(reply)) -class StdIOHandler(): + +def sanitize(txt, limit=100): + return txt[:limit].encode("unicode_escape").decode("utf-8") + + +def metaString(meta): + """ + "meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""} + """ # noqa: E501 + message = ( + "\tRequest context:\n" + "\t\t{remote} -> {scheme} -> {local}\n" + "\tAdditional HTTP header data, provided by the external caller:\n" + "\t\tUser-Agent: {user_agent}\n" + "\t\tOrigin: {origin}\n" + ) + return message.format( + remote=meta.get("remote", ""), + scheme=meta.get("scheme", ""), + local=meta.get("local", ""), + user_agent=sanitize(meta.get("User-Agent"), 200), + origin=sanitize(meta.get("Origin"), 100), + ) + + +class StdIOHandler: def __init__(self): pass @public - def ApproveTx(self,req): + def approveTx(self, req): """ Example request: - { - "jsonrpc": "2.0", - "method": "ApproveTx", - "params": [{ - "transaction": { - "to": "0xae967917c465db8578ca9024c205720b1a3651A9", - "gas": "0x333", - "gasPrice": "0x123", - "value": "0x10", - "data": "0xd7a5865800000000000000000000000000000000000000000000000000000000000000ff", - "nonce": "0x0" - }, - "from": "0xAe967917c465db8578ca9024c205720b1a3651A9", - "call_info": "Warning! Could not validate ABI-data against calldata\nSupplied ABI spec does not contain method signature in data: 0xd7a58658", - "meta": { - "remote": "127.0.0.1:34572", - "local": "localhost:8550", - "scheme": "HTTP/1.1" - } - }], - "id": 1 - } + + {"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":""}}]} :param transaction: transaction info :param call_info: info abou the call, e.g. if ABI info could not be :param meta: metadata about the request, e.g. where the call comes from :return: - """ - transaction = req.get('transaction') - _from = req.get('from') - call_info = req.get('call_info') - meta = req.get('meta') - + """ # noqa: E501 + message = ( + "Sign transaction request:\n" + "\t{meta_string}\n" + "\n" + "\tFrom: {from_}\n" + "\tTo: {to}\n" + "\n" + "\tAuto-rejecting request" + ) + meta = req.get("meta", {}) + transaction = req.get("transaction") + sys.stdout.write( + message.format( + meta_string=metaString(meta), + from_=transaction.get("from", ""), + to=transaction.get("to", ""), + ) + ) return { - "approved" : False, - #"transaction" : transaction, - # "from" : _from, -# "password" : None, + "approved": False, } @public - def ApproveSignData(self, req): - """ Example request - - """ - return {"approved": False, "password" : None} - - @public - def ApproveExport(self, req): - """ Example request - - """ - return {"approved" : False} - - @public - def ApproveImport(self, req): - """ Example request - - """ - return { "approved" : False, "old_password": "", "new_password": ""} - - @public - def ApproveListing(self, req): - """ Example request - - """ - return {'accounts': []} - - @public - def ApproveNewAccount(self, req): - """ - Example request - - :return: - """ - return {"approved": False, - #"password": "" - } - - @public - def ShowError(self,message = {}): + def approveSignData(self, req): """ Example request: - {"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowError'"},"id":1} + {"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":""}}]} + """ # noqa: E501 + message = ( + "Sign data request:\n" + "\t{meta_string}\n" + "\n" + "\tContent-type: {content_type}\n" + "\tAddress: {address}\n" + "\tHash: {hash_}\n" + "\n" + "\tAuto-rejecting request\n" + ) + meta = req.get("meta", {}) + sys.stdout.write( + message.format( + meta_string=metaString(meta), + content_type=req.get("content_type"), + address=req.get("address"), + hash_=req.get("hash"), + ) + ) - :param message: to show - :return: nothing - """ - if 'text' in message.keys(): - sys.stderr.write("Error: {}\n".format( message['text'])) - return + return { + "approved": False, + "password": None, + } @public - def ShowInfo(self,message = {}): + def approveNewAccount(self, req): """ - Example request - {"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0} + Example request: + + {"jsonrpc":"2.0","id":25,"method":"ui_approveNewAccount","params":[{"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} + """ # noqa: E501 + message = ( + "Create new account request:\n" + "\t{meta_string}\n" + "\n" + "\tAuto-rejecting request\n" + ) + meta = req.get("meta", {}) + sys.stdout.write(message.format(meta_string=metaString(meta))) + return { + "approved": False, + } + + @public + def showError(self, req): + """ + Example request: + + {"jsonrpc":"2.0","method":"ui_showError","params":[{"text":"If you see this message, enter 'yes' to the next question"}]} :param message: to display :return:nothing - """ - - if 'text' in message.keys(): - sys.stdout.write("Error: {}\n".format( message['text'])) + """ # noqa: E501 + message = ( + "## Error\n{text}\n" + "Press enter to continue\n" + ) + text = req.get("text") + sys.stdout.write(message.format(text=text)) + input() return + @public + def showInfo(self, req): + """ + Example request: + + {"jsonrpc":"2.0","method":"ui_showInfo","params":[{"text":"If you see this message, enter 'yes' to next question"}]} + + :param message: to display + :return:nothing + """ # noqa: E501 + message = ( + "## Info\n{text}\n" + "Press enter to continue\n" + ) + text = req.get("text") + sys.stdout.write(message.format(text=text)) + input() + return + + @public + def onSignerStartup(self, req): + """ + Example request: + + {"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"}}]} + """ # noqa: E501 + message = ( + "\n" + "\t\tExt api url: {extapi_http}\n" + "\t\tInt api ipc: {extapi_ipc}\n" + "\t\tExt api ver: {extapi_version}\n" + "\t\tInt api ver: {intapi_version}\n" + ) + info = req.get("info") + sys.stdout.write( + message.format( + extapi_http=info.get("extapi_http"), + extapi_ipc=info.get("extapi_ipc"), + extapi_version=info.get("extapi_version"), + intapi_version=info.get("intapi_version"), + ) + ) + + @public + def approveListing(self, req): + """ + Example request: + + {"jsonrpc":"2.0","id":23,"method":"ui_approveListing","params":[{"accounts":[{"address":... + """ # noqa: E501 + message = ( + "\n" + "## Account listing request\n" + "\t{meta_string}\n" + "\tDo you want to allow listing the following accounts?\n" + "\t-{addrs}\n" + "\n" + "->Auto-answering No\n" + ) + meta = req.get("meta", {}) + accounts = req.get("accounts", []) + addrs = [x.get("address") for x in accounts] + sys.stdout.write( + message.format( + addrs="\n\t-".join(addrs), + meta_string=metaString(meta) + ) + ) + return {} + + @public + def onInputRequired(self, req): + """ + Example request: + + {"jsonrpc":"2.0","id":1,"method":"ui_onInputRequired","params":[{"title":"Master Password","prompt":"Please enter the password to decrypt the master seed","isPassword":true}]} + + :param message: to display + :return:nothing + """ # noqa: E501 + message = ( + "\n" + "## {title}\n" + "\t{prompt}\n" + "\n" + "> " + ) + sys.stdout.write( + message.format( + title=req.get("title"), + prompt=req.get("prompt") + ) + ) + isPassword = req.get("isPassword") + if not isPassword: + return {"text": input()} + + return "" + + def main(args): cmd = ["clef", "--stdio-ui"] if len(args) > 0 and args[0] == "test": cmd.extend(["--stdio-ui-test"]) print("cmd: {}".format(" ".join(cmd))) + dispatcher = RPCDispatcher() - dispatcher.register_instance(StdIOHandler(), '') + dispatcher.register_instance(StdIOHandler(), "ui_") + # line buffered - p = subprocess.Popen(cmd, bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + p = subprocess.Popen( + cmd, + bufsize=1, + universal_newlines=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) rpc_server = RPCServer( - PipeTransport(p.stdout, p.stdin), - JSONRPCProtocol(), - dispatcher + PipeTransport(p.stdout, p.stdin), JSONRPCProtocol(), dispatcher ) rpc_server.serve_forever() -if __name__ == '__main__': + +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/cmd/clef/requirements.txt b/cmd/clef/requirements.txt new file mode 100644 index 0000000000..5381862e30 --- /dev/null +++ b/cmd/clef/requirements.txt @@ -0,0 +1 @@ +tinyrpc==1.1.4