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:])