cmd/clef: fixups to the python clef poc (#24440)
This PR fixes up the example python clef wrapper. The poc is intended to demonstrate how to wite a UI for clef, and had severely bitrotted. With these changes, it "works" in the sense that all the built-in tests triggers the intended python callbacks (no errors about method not found). It does not "work" in the sense that the wrapper can be used as an actual UI. It will auto-reject any signing requests, for example.
This commit is contained in:
parent
f94e23ca66
commit
559a174899
@ -1,29 +1,42 @@
|
|||||||
import os,sys, subprocess
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from tinyrpc.transports import ServerTransport
|
from tinyrpc.transports import ServerTransport
|
||||||
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
|
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
|
||||||
from tinyrpc.dispatch import public, RPCDispatcher
|
from tinyrpc.dispatch import public, RPCDispatcher
|
||||||
from tinyrpc.server import RPCServer
|
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
|
The standard input/output is a relatively secure way to communicate,
|
||||||
or IPC files. Needless to say, it does not protect against memory inspection mechanisms where an attacker
|
as it does not require opening any ports or IPC files. Needless to say,
|
||||||
can access process memory."""
|
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:
|
try:
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import urllib as urlparse
|
import urllib as urlparse
|
||||||
|
|
||||||
|
|
||||||
class StdIOTransport(ServerTransport):
|
class StdIOTransport(ServerTransport):
|
||||||
"""Uses std input/output for RPC"""
|
"""Uses std input/output for RPC"""
|
||||||
|
|
||||||
def receive_message(self):
|
def receive_message(self):
|
||||||
return None, urlparse.unquote(sys.stdin.readline())
|
return None, urlparse.unquote(sys.stdin.readline())
|
||||||
|
|
||||||
def send_reply(self, context, reply):
|
def send_reply(self, context, reply):
|
||||||
print(reply)
|
print(reply)
|
||||||
|
|
||||||
|
|
||||||
class PipeTransport(ServerTransport):
|
class PipeTransport(ServerTransport):
|
||||||
"""Uses std a pipe for RPC"""
|
"""Uses std a pipe for RPC"""
|
||||||
|
|
||||||
@ -37,141 +50,266 @@ class PipeTransport(ServerTransport):
|
|||||||
return None, urlparse.unquote(data)
|
return None, urlparse.unquote(data)
|
||||||
|
|
||||||
def send_reply(self, context, reply):
|
def send_reply(self, context, reply):
|
||||||
|
reply = str(reply, "utf-8")
|
||||||
print("<< {}".format(reply))
|
print("<< {}".format(reply))
|
||||||
self.output.write(reply)
|
self.output.write("{}\n".format(reply))
|
||||||
self.output.write("\n")
|
|
||||||
|
|
||||||
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", "<missing>"),
|
||||||
|
scheme=meta.get("scheme", "<missing>"),
|
||||||
|
local=meta.get("local", "<missing>"),
|
||||||
|
user_agent=sanitize(meta.get("User-Agent"), 200),
|
||||||
|
origin=sanitize(meta.get("Origin"), 100),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StdIOHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@public
|
@public
|
||||||
def ApproveTx(self,req):
|
def approveTx(self, req):
|
||||||
"""
|
"""
|
||||||
Example request:
|
Example request:
|
||||||
{
|
|
||||||
"jsonrpc": "2.0",
|
{"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":""}}]}
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
:param transaction: transaction info
|
:param transaction: transaction info
|
||||||
:param call_info: info abou the call, e.g. if ABI info could not be
|
: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
|
:param meta: metadata about the request, e.g. where the call comes from
|
||||||
:return:
|
:return:
|
||||||
"""
|
""" # noqa: E501
|
||||||
transaction = req.get('transaction')
|
message = (
|
||||||
_from = req.get('from')
|
"Sign transaction request:\n"
|
||||||
call_info = req.get('call_info')
|
"\t{meta_string}\n"
|
||||||
meta = req.get('meta')
|
"\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", "<missing>"),
|
||||||
|
to=transaction.get("to", "<missing>"),
|
||||||
|
)
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"approved": False,
|
"approved": False,
|
||||||
#"transaction" : transaction,
|
|
||||||
# "from" : _from,
|
|
||||||
# "password" : None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@public
|
@public
|
||||||
def ApproveSignData(self, req):
|
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 = {}):
|
|
||||||
"""
|
"""
|
||||||
Example request:
|
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 {
|
||||||
:return: nothing
|
"approved": False,
|
||||||
"""
|
"password": None,
|
||||||
if 'text' in message.keys():
|
}
|
||||||
sys.stderr.write("Error: {}\n".format( message['text']))
|
|
||||||
return
|
|
||||||
|
|
||||||
@public
|
@public
|
||||||
def ShowInfo(self,message = {}):
|
def approveNewAccount(self, req):
|
||||||
"""
|
"""
|
||||||
Example request
|
Example request:
|
||||||
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0}
|
|
||||||
|
{"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
|
:param message: to display
|
||||||
:return:nothing
|
:return:nothing
|
||||||
"""
|
""" # noqa: E501
|
||||||
|
message = (
|
||||||
if 'text' in message.keys():
|
"## Error\n{text}\n"
|
||||||
sys.stdout.write("Error: {}\n".format( message['text']))
|
"Press enter to continue\n"
|
||||||
|
)
|
||||||
|
text = req.get("text")
|
||||||
|
sys.stdout.write(message.format(text=text))
|
||||||
|
input()
|
||||||
return
|
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):
|
def main(args):
|
||||||
cmd = ["clef", "--stdio-ui"]
|
cmd = ["clef", "--stdio-ui"]
|
||||||
if len(args) > 0 and args[0] == "test":
|
if len(args) > 0 and args[0] == "test":
|
||||||
cmd.extend(["--stdio-ui-test"])
|
cmd.extend(["--stdio-ui-test"])
|
||||||
print("cmd: {}".format(" ".join(cmd)))
|
print("cmd: {}".format(" ".join(cmd)))
|
||||||
|
|
||||||
dispatcher = RPCDispatcher()
|
dispatcher = RPCDispatcher()
|
||||||
dispatcher.register_instance(StdIOHandler(), '')
|
dispatcher.register_instance(StdIOHandler(), "ui_")
|
||||||
|
|
||||||
# line buffered
|
# 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(
|
rpc_server = RPCServer(
|
||||||
PipeTransport(p.stdout, p.stdin),
|
PipeTransport(p.stdout, p.stdin), JSONRPCProtocol(), dispatcher
|
||||||
JSONRPCProtocol(),
|
|
||||||
dispatcher
|
|
||||||
)
|
)
|
||||||
rpc_server.serve_forever()
|
rpc_server.serve_forever()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
|
1
cmd/clef/requirements.txt
Normal file
1
cmd/clef/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
tinyrpc==1.1.4
|
Loading…
Reference in New Issue
Block a user