rpc_users.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2015-2022 The Limenka 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 LimenkaTestFramework
   8  from test_framework.util import (
   9      assert_equal,
  10      str_to_b64str,
  11  )
  12  
  13  import http.client
  14  import os
  15  from pathlib import Path
  16  import platform
  17  import urllib.parse
  18  import subprocess
  19  from random import SystemRandom
  20  import string
  21  import configparser
  22  import sys
  23  from typing import Optional
  24  
  25  
  26  def call_with_auth(node, user, password, *, uripath='/', method='getbestblockhash'):
  27      url = urllib.parse.urlparse(node.url)
  28      headers = {"Authorization": "Basic " + str_to_b64str('{}:{}'.format(user, password))}
  29  
  30      conn = http.client.HTTPConnection(url.hostname, url.port)
  31      conn.connect()
  32      conn.request('POST', uripath, f'{{"method": "{method}"}}', headers)
  33      resp = conn.getresponse()
  34      resp.data = resp.read()
  35      conn.close()
  36      return resp
  37  
  38  
  39  class HTTPBasicsTest(LimenkaTestFramework):
  40      def add_options(self, parser):
  41          self.add_wallet_options(parser)
  42  
  43      def set_test_params(self):
  44          self.num_nodes = 2
  45          self.supports_cli = False
  46  
  47      def conf_setup(self):
  48          self.authinfo = []
  49  
  50          #Append rpcauth to limenka.conf before initialization
  51          self.rtpassword = "cA773lm788buwYe4g4WT+05pKyNruVKjQ25x3n0DQcM="
  52          rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144"
  53  
  54          self.rpcuser = "rpcuser💻"
  55          self.rpcpassword = "rpcpassword🔑"
  56  
  57          config = configparser.ConfigParser()
  58          config.read_file(open(self.options.configfile))
  59          gen_rpcauth = config['environment']['RPCAUTH']
  60  
  61          # Generate RPCAUTH with specified password
  62          self.rt2password = "8/F3uMDw4KSEbw96U3CA1C4X05dkHDN2BPFjTgZW4KI="
  63          p = subprocess.Popen([sys.executable, gen_rpcauth, 'rt2', self.rt2password], stdout=subprocess.PIPE, text=True)
  64          lines = p.stdout.read().splitlines()
  65          rpcauth2 = lines[1]
  66  
  67          # Generate RPCAUTH without specifying password
  68          self.user = ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
  69          p = subprocess.Popen([sys.executable, gen_rpcauth, self.user], stdout=subprocess.PIPE, text=True)
  70          lines = p.stdout.read().splitlines()
  71          rpcauth3 = lines[1]
  72          self.password = lines[3]
  73  
  74          # Generate rpcauthfile with one entry
  75          username = 'rpcauth_single_' + ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
  76          p = subprocess.Popen([sys.executable, gen_rpcauth, "--output", Path(self.options.tmpdir) / 'rpcauth_single', username], stdout=subprocess.PIPE, universal_newlines=True)
  77          lines = p.stdout.read().splitlines()
  78          self.authinfo.append( (username, lines[1]) )
  79  
  80          # Generate rpcauthfile with two entries
  81          username = 'rpcauth_multi1_' + ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
  82          p = subprocess.Popen([sys.executable, gen_rpcauth, "--output", Path(self.options.tmpdir) / 'rpcauth_multi', username], stdout=subprocess.PIPE, universal_newlines=True)
  83          lines = p.stdout.read().splitlines()
  84          self.authinfo.append( (username, lines[1]) )
  85          # Blank lines in between should get ignored
  86          with open(Path(self.options.tmpdir) / 'rpcauth_multi', "a", encoding='utf8') as f:
  87              f.write("\n\n")
  88          username = 'rpcauth_multi2_' + ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
  89          p = subprocess.Popen([sys.executable, gen_rpcauth, "--output", Path(self.options.tmpdir) / 'rpcauth_multi', username], stdout=subprocess.PIPE, universal_newlines=True)
  90          lines = p.stdout.read().splitlines()
  91          self.authinfo.append( (username, lines[1]) )
  92  
  93          def gen_userpass(username_prefix, wallet_restrictions=None):
  94              username = username_prefix + '_' + ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
  95              p = subprocess.Popen([sys.executable, gen_rpcauth, username], stdout=subprocess.PIPE, universal_newlines=True)
  96              lines = p.stdout.read().splitlines()
  97              assert "\n" not in lines[1]
  98              assert lines[1][:8] == 'rpcauth='
  99              config_line = lines[1]
 100              self.authinfo.append( (username, lines[3], wallet_restrictions) )
 101              if not (wallet_restrictions is None):
 102                  config_line += ":" + wallet_restrictions
 103              return config_line + "\n"
 104  
 105          # Hand-generated rpcauthfile with one entry and no newline
 106          with open(Path(self.options.tmpdir) / 'rpcauth_nonewline', "a", encoding='utf8') as f:
 107              f.write(gen_userpass('rpcauth_nonewline')[8:-1])
 108  
 109          if self.is_wallet_compiled():
 110              # Hand-generated rpcauthfile with wallet restrictions
 111              with open(Path(self.options.tmpdir) / 'rpcauth_walletrestricted', "a", encoding='utf8') as f:
 112                  f.write(gen_userpass('rpcauth_walletrestricted_allow_all', '')[8:])
 113                  f.write(gen_userpass('rpcauth_walletrestricted_allow_none', '-')[8:])
 114                  f.write(gen_userpass('rpcauth_walletrestricted_allow_one', 'limitedwallet1')[8:])
 115                  # Uses the same username as a privileged one, but with different passwords:
 116                  f.write(gen_userpass('rpcauth_walletrestricted_allow_all', 'limitedwallet1')[8:])
 117                  f.write(gen_userpass('rpcauth_walletrestricted_allow_all', 'limitedwallet2')[8:])
 118                  f.write(gen_userpass('rpcauth_walletrestricted_allow_all', '-')[8:])
 119                  f.write(gen_userpass('rpcauth_walletrestricted_allow_one', 'limitedwallet2')[8:])
 120                  f.write(gen_userpass('rpcauth_walletrestricted_allow_one', '-')[8:])
 121  
 122          with open(self.nodes[0].datadir_path / "limenka.conf", "a", encoding="utf8") as f:
 123              f.write(rpcauth + "\n")
 124              f.write(rpcauth2 + "\n")
 125              f.write(rpcauth3 + "\n")
 126              f.write("rpcauthfile=rpcauth_single\n")
 127              f.write("rpcauthfile=rpcauth_multi\n")
 128              f.write("rpcauthfile=rpcauth_nonewline\n")
 129              if self.is_wallet_compiled():
 130                  f.write("rpcauthfile=rpcauth_walletrestricted\n")
 131                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_all', '') + "\n")
 132                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_none', '-') + "\n")
 133                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_one', 'limitedwallet1') + "\n")
 134                  # Uses the same username as a privileged one, but with different passwords:
 135                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_all', 'limitedwallet1') + "\n")
 136                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_all', 'limitedwallet2') + "\n")
 137                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_all', '-') + "\n")
 138                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_one', 'limitedwallet2') + "\n")
 139                  f.write(gen_userpass('rpcauth_walletrestricted2_allow_one', '-') + "\n")
 140          with open(self.nodes[1].datadir_path / "limenka.conf", "a", encoding="utf8") as f:
 141              f.write("rpcuser={}\n".format(self.rpcuser))
 142              f.write("rpcpassword={}\n".format(self.rpcpassword))
 143          self.restart_node(0)
 144          self.restart_node(1)
 145  
 146      def test_auth(self, node, user, password, wallet_restrictions=None):
 147          self.log.info('Correct... %s (wallet_restrictions=%s)' % (user, wallet_restrictions))
 148          assert_equal(200, call_with_auth(node, user, password).status)
 149  
 150          self.log.info('Wrong...')
 151          assert_equal(401, call_with_auth(node, user, password + 'wrong').status)
 152  
 153          self.log.info('Wrong...')
 154          assert_equal(401, call_with_auth(node, user + 'wrong', password).status)
 155  
 156          self.log.info('Wrong...')
 157          assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
 158  
 159          if not (wallet_restrictions is None):
 160              for n in range(1, 3):
 161                  wallet_name = f'limitedwallet{n}'
 162                  self.log.info(f'{wallet_name}...')
 163                  resp = call_with_auth(node, user, password, uripath=f'/wallet/{wallet_name}', method='getwalletinfo')
 164                  if wallet_restrictions in ('', f'{wallet_name}'):
 165                      assert_equal(200, resp.status)
 166                  else:
 167                      assert_equal(500, resp.status)
 168                      assert b'"Requested wallet does not exist or is not loaded"' in resp.data
 169  
 170      def test_rpccookieperms(self):
 171          p = {
 172              "owner": 0o600,
 173              "group": 0o640,
 174              "all": 0o644,
 175              "440": 0o440,
 176              "0640": 0o640,
 177              "444": 0o444,
 178              "1660": 0o1660,
 179          }
 180  
 181          if platform.system() == 'Windows':
 182              self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
 183              return
 184  
 185          self.log.info('Check cookie file permissions can be set using -rpccookieperms')
 186  
 187          cookie_file_path = self.nodes[1].chain_path / '.cookie'
 188          PERM_BITS_UMASK = 0o7777
 189  
 190          def test_perm(perm: Optional[str]):
 191              if not perm:
 192                  perm = 'owner'
 193                  self.restart_node(1)
 194              else:
 195                  self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
 196  
 197              file_stat = os.stat(cookie_file_path)
 198              actual_perms = file_stat.st_mode & PERM_BITS_UMASK
 199              expected_perms = p[perm]
 200              assert_equal(expected_perms, actual_perms)
 201              return actual_perms
 202  
 203          # Remove any leftover rpc{user|password} config options from previous tests
 204          self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
 205  
 206          self.log.info('Check default cookie permission')
 207          default_perms = test_perm(None)
 208  
 209          self.log.info('Check custom cookie permissions')
 210          for perm in p.keys():
 211              test_perm(perm)
 212  
 213          self.log.info('Check leaving cookie permissions alone')
 214          unassigned_perms = os.stat(self.nodes[1].chain_path / 'debug.log').st_mode & PERM_BITS_UMASK
 215          self.restart_node(1, extra_args=["-rpccookieperms=0"])
 216          actual_perms = os.stat(cookie_file_path).st_mode & PERM_BITS_UMASK
 217          assert_equal(unassigned_perms, actual_perms)
 218          self.restart_node(1, extra_args=["-norpccookieperms"])
 219          actual_perms = os.stat(cookie_file_path).st_mode & PERM_BITS_UMASK
 220          assert_equal(unassigned_perms, actual_perms)
 221  
 222          self.log.info('Check -norpccookieperms -rpccookieperms')
 223          self.restart_node(1, extra_args=["-rpccookieperms=0", "-rpccookieperms=1"])
 224          actual_perms = os.stat(cookie_file_path).st_mode & PERM_BITS_UMASK
 225          assert_equal(default_perms, actual_perms)
 226          self.restart_node(1, extra_args=["-norpccookieperms", "-rpccookieperms"])
 227          actual_perms = os.stat(cookie_file_path).st_mode & PERM_BITS_UMASK
 228          assert_equal(default_perms, actual_perms)
 229          self.restart_node(1, extra_args=["-rpccookieperms=1660", "-norpccookieperms", "-rpccookieperms"])
 230          actual_perms = os.stat(cookie_file_path).st_mode & PERM_BITS_UMASK
 231          assert_equal(default_perms, actual_perms)
 232  
 233      def test_norpccookiefile(self, node0_cookie_path):
 234          assert self.nodes[0].is_node_stopped(), "We expect previous test to stopped the node"
 235          assert not node0_cookie_path.exists()
 236  
 237          self.log.info('Starting with -norpccookiefile')
 238          # Start, but don't wait for RPC connection as TestNode.wait_for_rpc_connection() requires the cookie.
 239          with self.nodes[0].busy_wait_for_debug_log([b'init message: Done loading']):
 240              self.nodes[0].start(extra_args=["-norpccookiefile"])
 241          assert not node0_cookie_path.exists()
 242  
 243          self.log.info('Testing user/password authentication still works without cookie file')
 244          assert_equal(200, call_with_auth(self.nodes[0], "rt", self.rtpassword).status)
 245          # After confirming that we could log in, check that cookie file does not exist.
 246          assert not node0_cookie_path.exists()
 247  
 248          # Need to shut down in slightly unorthodox way since cookie auth can't be used
 249          assert_equal(200, call_with_auth(self.nodes[0], "rt", self.rtpassword, method="stop").status)
 250          self.nodes[0].wait_until_stopped()
 251  
 252      def run_test(self):
 253          self.conf_setup()
 254          self.log.info('Check correctness of the rpcauth config option')
 255          url = urllib.parse.urlparse(self.nodes[0].url)
 256  
 257          if self.is_wallet_compiled():
 258              self.nodes[0].createwallet('limitedwallet1')
 259              self.nodes[0].createwallet('limitedwallet2')
 260  
 261          self.test_auth(self.nodes[0], url.username, url.password)
 262          self.test_auth(self.nodes[0], 'rt', self.rtpassword)
 263          self.test_auth(self.nodes[0], 'rt2', self.rt2password)
 264          self.test_auth(self.nodes[0], self.user, self.password)
 265          for info in self.authinfo:
 266              self.test_auth(self.nodes[0], *info)
 267  
 268          self.log.info('Check correctness of the rpcuser/rpcpassword config options')
 269          url = urllib.parse.urlparse(self.nodes[1].url)
 270  
 271          self.test_auth(self.nodes[1], self.rpcuser, self.rpcpassword)
 272  
 273          init_error = 'Error: Unable to start HTTP server. See debug log for details.'
 274  
 275          self.log.info('Check blank -rpcauth is ignored')
 276          rpcauth_abc = '-rpcauth=abc:$2e32c2f20c67e29c328dd64a4214180f18da9e667d67c458070fd856f1e9e5e7'
 277          rpcauth_def = '-rpcauth=def:$fd7adb152c05ef80dccf50a1fa4c05d5a3ec6da95575fc312ae7c5d091836351'
 278          self.restart_node(0, extra_args=['-rpcauth'])
 279          self.restart_node(0, extra_args=['-rpcauth=', rpcauth_abc])
 280          self.restart_node(0, extra_args=[rpcauth_def, '-rpcauth='])
 281          # ...without disrupting usage of other -rpcauth tokens
 282          assert_equal(200, call_with_auth(self.nodes[0], 'def', 'abc').status)
 283          assert_equal(200, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 284          for info in self.authinfo:
 285              assert_equal(200, call_with_auth(self.nodes[0], *info[:2]).status)
 286  
 287          self.log.info('Check -norpcauth disables all previous -rpcauth* params')
 288          self.restart_node(0, extra_args=[rpcauth_def, '-norpcauth'])
 289          assert_equal(401, call_with_auth(self.nodes[0], 'def', 'abc').status)
 290          assert_equal(401, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 291          for info in self.authinfo:
 292              assert_equal(401, call_with_auth(self.nodes[0], *info[:2]).status)
 293  
 294          self.log.info('Check -norpcauth can be reversed with -rpcauth')
 295          self.restart_node(0, extra_args=[rpcauth_def, '-norpcauth', '-rpcauth'])
 296          # FIXME: assert_equal(200, call_with_auth(self.nodes[0], 'def', 'abc').status)
 297          assert_equal(200, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 298          for info in self.authinfo:
 299              assert_equal(200, call_with_auth(self.nodes[0], *info[:2]).status)
 300  
 301          self.log.info('Check -norpcauth followed by a specific -rpcauth=* restores config file -rpcauth=* values too')
 302          self.restart_node(0, extra_args=[rpcauth_def, '-norpcauth', rpcauth_abc])
 303          assert_equal(401, call_with_auth(self.nodes[0], 'def', 'abc').status)
 304          assert_equal(200, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 305          for info in self.authinfo:
 306              assert_equal(200, call_with_auth(self.nodes[0], *info[:2]).status)
 307          self.restart_node(0, extra_args=[rpcauth_def, '-norpcauth', '-rpcauth='])
 308          assert_equal(401, call_with_auth(self.nodes[0], 'def', 'abc').status)
 309          assert_equal(200, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 310          for info in self.authinfo:
 311              assert_equal(200, call_with_auth(self.nodes[0], *info[:2]).status)
 312  
 313          self.log.info('Check -rpcauth are validated')
 314          self.stop_node(0)
 315          self.log.info('Check malformed -rpcauth')
 316          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo'])
 317          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar'])
 318          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar:baz'])
 319          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar:baz'])
 320          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar$baz'])
 321  
 322          # pw = limenka
 323          rpcauth_user1 = '-rpcauth=user1:6dd184e5e69271fdd69103464630014f$eb3d7ce67c4d1ff3564270519b03b636c0291012692a5fa3dd1d2075daedd07b'
 324          rpcauth_user2 = '-rpcauth=user2:57b2f77c919eece63cfa46c2f06e46ae$266b63902f99f97eeaab882d4a87f8667ab84435c3799f2ce042ef5a994d620b'
 325  
 326          self.log.info('Check -norpcauth disables previous -rpcauth params')
 327          self.restart_node(0, extra_args=[rpcauth_user1, rpcauth_user2, '-norpcauth'])
 328          assert_equal(401, call_with_auth(self.nodes[0], 'user1', 'limenka').status)
 329          assert_equal(401, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
 330          self.stop_node(0)
 331  
 332          self.log.info('Check that failure to write cookie file will abort the node gracefully')
 333          cookie_path =     self.nodes[0].chain_path / ".cookie"
 334          cookie_path_tmp = self.nodes[0].chain_path / ".cookie.tmp"
 335          cookie_path_tmp.mkdir()
 336          cookie_path_tmp_subdir = cookie_path_tmp / "subdir"
 337          cookie_path_tmp_subdir.mkdir()
 338          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
 339          cookie_path_tmp_subdir.rmdir()
 340          cookie_path_tmp.rmdir()
 341          assert not cookie_path.exists()
 342          self.restart_node(0)
 343          assert cookie_path.exists()
 344          self.stop_node(0)
 345  
 346          cookie_path.mkdir()
 347          cookie_path_subdir = cookie_path / "subdir"
 348          cookie_path_subdir.mkdir()
 349          self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
 350          cookie_path_subdir.rmdir()
 351          cookie_path.rmdir()
 352  
 353          self.log.info('Check that a non-writable cookie file will get replaced gracefully')
 354          cookie_path.mkdir(mode=1)
 355          self.restart_node(0)
 356          self.stop_node(0)
 357  
 358          self.test_rpccookieperms()
 359  
 360          self.test_norpccookiefile(cookie_path)
 361  
 362  if __name__ == '__main__':
 363      HTTPBasicsTest(__file__).main()
 364