rpc_users.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2015-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test multiple RPC users."""
   6  
   7  from test_framework.test_framework import BitcoinTestFramework
   8  from test_framework.util import (
   9      assert_equal,
  10      str_to_b64str,
  11  )
  12  
  13  import http.client
  14  import os
  15  import platform
  16  import urllib.parse
  17  import subprocess
  18  from random import SystemRandom
  19  import string
  20  import sys
  21  from typing import Optional
  22  
  23  
  24  def call_with_auth(node, user, password, method="getbestblockhash"):
  25      url = urllib.parse.urlparse(node.url)
  26      headers = {"Authorization": "Basic " + str_to_b64str('{}:{}'.format(user, password))}
  27  
  28      conn = http.client.HTTPConnection(url.hostname, url.port)
  29      conn.connect()
  30      conn.request('POST', '/', f'{{"method": "{method}"}}', headers)
  31      resp = conn.getresponse()
  32      conn.close()
  33      return resp
  34  
  35  
  36  class HTTPBasicsTest(BitcoinTestFramework):
  37      def set_test_params(self):
  38          self.num_nodes = 2
  39  
  40      def conf_setup(self):
  41          #Append rpcauth to bitcoin.conf before initialization
  42          self.rtpassword = "cA773lm788buwYe4g4WT+05pKyNruVKjQ25x3n0DQcM="
  43          rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144"
  44  
  45          self.rpcuser = "rpcuser💻"
  46          self.rpcpassword = "rpcpassword🔑"
  47  
  48          gen_rpcauth = self.config["environment"]["RPCAUTH"]
  49  
  50          # Generate RPCAUTH with specified password
  51          self.rt2password = "8/F3uMDw4KSEbw96U3CA1C4X05dkHDN2BPFjTgZW4KI="
  52          p = subprocess.Popen([sys.executable, gen_rpcauth, 'rt2', self.rt2password], stdout=subprocess.PIPE, text=True)
  53          lines = p.stdout.read().splitlines()
  54          rpcauth2 = lines[1]
  55  
  56          # Generate RPCAUTH without specifying password
  57          self.user = ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
  58          p = subprocess.Popen([sys.executable, gen_rpcauth, self.user], stdout=subprocess.PIPE, text=True)
  59          lines = p.stdout.read().splitlines()
  60          rpcauth3 = lines[1]
  61          self.password = lines[3]
  62  
  63          self.stop_nodes()
  64          with open(self.nodes[0].datadir_path / "bitcoin.conf", "a") as f:
  65              f.write(rpcauth + "\n")
  66              f.write(rpcauth2 + "\n")
  67              f.write(rpcauth3 + "\n")
  68          with open(self.nodes[1].datadir_path / "bitcoin.conf", "a") as f:
  69              f.write("rpcuser={}\n".format(self.rpcuser))
  70              f.write("rpcpassword={}\n".format(self.rpcpassword))
  71          self.start_nodes()
  72  
  73      def test_auth(self, node, user, password):
  74          self.log.info('Correct...')
  75          assert_equal(200, call_with_auth(node, user, password).status)
  76  
  77          self.log.info('Wrong...')
  78          assert_equal(401, call_with_auth(node, user, password + 'wrong').status)
  79  
  80          self.log.info('Wrong...')
  81          assert_equal(401, call_with_auth(node, user + 'wrong', password).status)
  82  
  83          self.log.info('Wrong...')
  84          assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
  85  
  86      def test_rpccookieperms(self):
  87          p = {"owner": 0o600, "group": 0o640, "all": 0o644}
  88  
  89          if platform.system() == 'Windows':
  90              self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
  91              return
  92  
  93          self.log.info('Check cookie file permissions can be set using -rpccookieperms')
  94  
  95          cookie_file_path = self.nodes[1].chain_path / '.cookie'
  96          PERM_BITS_UMASK = 0o777
  97  
  98          def test_perm(perm: Optional[str]):
  99              if not perm:
 100                  perm = 'owner'
 101                  self.restart_node(1)
 102              else:
 103                  self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
 104  
 105              file_stat = os.stat(cookie_file_path)
 106              actual_perms = file_stat.st_mode & PERM_BITS_UMASK
 107              expected_perms = p[perm]
 108              assert_equal(expected_perms, actual_perms)
 109  
 110          # Remove any leftover rpc{user|password} config options from previous tests
 111          self.stop_node(1)
 112          self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
 113  
 114          self.log.info('Check default cookie permission')
 115          test_perm(None)
 116  
 117          self.log.info('Check custom cookie permissions')
 118          for perm in ["owner", "group", "all"]:
 119              test_perm(perm)
 120  
 121      def test_norpccookiefile(self, node0_cookie_path):
 122          assert self.nodes[0].is_node_stopped(), "We expect previous test to stopped the node"
 123          assert not node0_cookie_path.exists()
 124  
 125          self.log.info('Starting with -norpccookiefile')
 126          # Start, but don't wait for RPC connection as TestNode.wait_for_rpc_connection() requires the cookie.
 127          with self.nodes[0].busy_wait_for_debug_log([b'init message: Done loading']):
 128              self.nodes[0].start(extra_args=["-norpccookiefile"])
 129          assert not node0_cookie_path.exists()
 130  
 131          self.log.info('Testing user/password authentication still works without cookie file')
 132          assert_equal(200, call_with_auth(self.nodes[0], "rt", self.rtpassword).status)
 133          # After confirming that we could log in, check that cookie file does not exist.
 134          assert not node0_cookie_path.exists()
 135  
 136          # Need to shut down in slightly unorthodox way since cookie auth can't be used
 137          assert_equal(200, call_with_auth(self.nodes[0], "rt", self.rtpassword, method="stop").status)
 138          self.nodes[0].wait_until_stopped()
 139  
 140      def run_test(self):
 141          self.conf_setup()
 142          self.log.info('Check correctness of the rpcauth config option')
 143          url = urllib.parse.urlparse(self.nodes[0].url)
 144  
 145          self.test_auth(self.nodes[0], url.username, url.password)
 146          self.test_auth(self.nodes[0], 'rt', self.rtpassword)
 147          self.test_auth(self.nodes[0], 'rt2', self.rt2password)
 148          self.test_auth(self.nodes[0], self.user, self.password)
 149  
 150          self.log.info('Check correctness of the rpcuser/rpcpassword config options')
 151          url = urllib.parse.urlparse(self.nodes[1].url)
 152  
 153          self.test_auth(self.nodes[1], self.rpcuser, self.rpcpassword)
 154  
 155          init_error = 'Error: Unable to start HTTP server. See debug log for details.'
 156  
 157          self.log.info('Check -rpcauth are validated')
 158          self.log.info('Empty -rpcauth are treated as error')
 159          self.stop_node(0)
 160          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth'])
 161          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth='])
 162          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=""'])
 163          self.log.info('Check malformed -rpcauth')
 164          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo'])
 165          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar'])
 166          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar:baz'])
 167          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar:baz'])
 168          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar$baz'])
 169  
 170          self.log.info('Check interactions between blank and non-blank rpcauth')
 171          # pw = bitcoin
 172          rpcauth_user1 = '-rpcauth=user1:6dd184e5e69271fdd69103464630014f$eb3d7ce67c4d1ff3564270519b03b636c0291012692a5fa3dd1d2075daedd07b'
 173          rpcauth_user2 = '-rpcauth=user2:57b2f77c919eece63cfa46c2f06e46ae$266b63902f99f97eeaab882d4a87f8667ab84435c3799f2ce042ef5a994d620b'
 174          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=[rpcauth_user1, rpcauth_user2, '-rpcauth='])
 175          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=[rpcauth_user1, '-rpcauth=', rpcauth_user2])
 176          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=', rpcauth_user1, rpcauth_user2])
 177  
 178          self.log.info('Check -norpcauth disables previous -rpcauth params')
 179          self.restart_node(0, extra_args=[rpcauth_user1, rpcauth_user2, '-norpcauth'])
 180          assert_equal(401, call_with_auth(self.nodes[0], 'user1', 'bitcoin').status)
 181          assert_equal(401, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 182          self.stop_node(0)
 183  
 184          self.log.info('Check that failure to write cookie file will abort the node gracefully')
 185          cookie_path =     self.nodes[0].chain_path / ".cookie"
 186          cookie_path_tmp = self.nodes[0].chain_path / ".cookie.tmp"
 187          cookie_path_tmp.mkdir()
 188          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
 189          cookie_path_tmp.rmdir()
 190          assert not cookie_path.exists()
 191          self.restart_node(0)
 192          assert cookie_path.exists()
 193          self.stop_node(0)
 194  
 195          self.test_rpccookieperms()
 196  
 197          self.test_norpccookiefile(cookie_path)
 198  
 199  if __name__ == '__main__':
 200      HTTPBasicsTest(__file__).main()
 201