def test_normal_path_windows(self, mock_readlink): # Python <3.8 mock_readlink.return_value = "C:\\short\\path" self.assertEqual(filesystem.readlink("dummy"), "C:\\short\\path") # Python >=3.8 (os.readlink always returns the extended form) mock_readlink.return_value = "\\\\?\\C:\\short\\path" self.assertEqual(filesystem.readlink("dummy"), "C:\\short\\path")
def _update_link_to(self, kind, version): """Make the specified item point at the specified version. (Note that this method doesn't verify that the specified version exists.) :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :param int version: the desired version """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = getattr(self, kind) filename = "{0}{1}.pem".format(kind, version) # Relative rather than absolute target directory target_directory = os.path.dirname(filesystem.readlink(link)) # TODO: it could be safer to make the link first under a temporary # filename, then unlink the old link, then rename the new link # to the old link; this ensures that this process is able to # create symlinks. # TODO: we might also want to check consistency of related links # for the other corresponding items os.unlink(link) os.symlink(os.path.join(target_directory, filename), link)
def test_update_live_symlinks(self): """Test update_live_symlinks""" # create files with incorrect symlinks from certbot._internal import cert_manager archive_paths = {} for domain in self.domains: custom_archive = self.domains[domain] if custom_archive is not None: archive_dir_path = custom_archive else: archive_dir_path = os.path.join(self.config.default_archive_dir, domain) archive_paths[domain] = {kind: os.path.join(archive_dir_path, kind + "1.pem") for kind in ALL_FOUR} for kind in ALL_FOUR: live_path = self.config_files[domain][kind] archive_path = archive_paths[domain][kind] open(archive_path, 'a').close() # path is incorrect but base must be correct os.symlink(os.path.join(self.config.config_dir, kind + "1.pem"), live_path) # run update symlinks cert_manager.update_live_symlinks(self.config) # check that symlinks go where they should prev_dir = os.getcwd() try: for domain in self.domains: for kind in ALL_FOUR: os.chdir(os.path.dirname(self.config_files[domain][kind])) self.assertEqual( filesystem.realpath(filesystem.readlink(self.config_files[domain][kind])), filesystem.realpath(archive_paths[domain][kind])) finally: os.chdir(prev_dir)
def _delete_links_and_find_target_dir( self, server_path: str, link_func: Callable[[str], str]) -> str: """Delete symlinks and return the nonsymlinked directory path. :param str server_path: file path based on server :param callable link_func: callable that returns possible links given a server_path :returns: the final, non-symlinked target :rtype: str """ dir_path = link_func(server_path) # does an appropriate directory link to me? if so, make sure that's gone reused_servers = {} for k, v in constants.LE_REUSE_SERVERS.items(): reused_servers[v] = k # is there a next one up? possible_next_link = True while possible_next_link: possible_next_link = False if server_path in reused_servers: next_server_path = reused_servers[server_path] next_dir_path = link_func(next_server_path) if os.path.islink(next_dir_path) and filesystem.readlink( next_dir_path) == dir_path: possible_next_link = True server_path = next_server_path dir_path = next_dir_path # if there's not a next one up to delete, then delete me # and whatever I link to while os.path.islink(dir_path): target = filesystem.readlink(dir_path) os.unlink(dir_path) dir_path = target return dir_path
def _fix_symlinks(self): """Fixes symlinks in the event of an incomplete version update. If there is no problem with the current symlinks, this function has no effect. """ previous_symlinks = self._previous_symlinks() if all(os.path.exists(link[1]) for link in previous_symlinks): for kind, previous_link in previous_symlinks: current_link = getattr(self, kind) if os.path.lexists(current_link): os.unlink(current_link) os.symlink(filesystem.readlink(previous_link), current_link) for _, link in previous_symlinks: if os.path.exists(link): os.unlink(link)
def test_update_link_to(self): for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) self.test_rc._update_link_to("privkey", 2) self.assertEqual(3, self.test_rc.current_version("cert")) self.assertEqual(2, self.test_rc.current_version("privkey")) self.assertEqual(5, self.test_rc.current_version("chain")) self.assertEqual(5, self.test_rc.current_version("fullchain")) # Currently we are allowed to update to a version that doesn't exist self.test_rc._update_link_to("chain", 3000) # However, current_version doesn't allow querying the resulting # version (because it's a broken link). self.assertEqual(os.path.basename(filesystem.readlink(self.test_rc.chain)), "chain3000.pem")
def get_link_target(link): """Get an absolute path to the target of link. :param str link: Path to a symbolic link :returns: Absolute path to the target of link :rtype: str :raises .CertStorageError: If link does not exists. """ try: target = filesystem.readlink(link) except OSError: raise errors.CertStorageError( "Expected {0} to be a symlink".format(link)) if not os.path.isabs(target): target = os.path.join(os.path.dirname(link), target) return os.path.abspath(target)
def test_save_successor(self, mock_rv): # Mock relevant_values() to claim that all values are relevant here # (to avoid instantiating parser) mock_rv.side_effect = lambda x: x for ver in range(1, 6): for kind in ALL_FOUR: self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, b'new cert', None, b'new chain', self.config)) with open(self.test_rc.version("cert", 6)) as f: self.assertEqual(f.read(), "new cert") with open(self.test_rc.version("chain", 6)) as f: self.assertEqual(f.read(), "new chain") with open(self.test_rc.version("fullchain", 6)) as f: self.assertEqual(f.read(), "new cert" + "new chain") # version 6 of the key should be a link back to version 3 self.assertFalse(os.path.islink(self.test_rc.version("privkey", 3))) self.assertTrue(os.path.islink(self.test_rc.version("privkey", 6))) # Let's try two more updates self.assertEqual( 7, self.test_rc.save_successor(6, b'again', None, b'newer chain', self.config)) self.assertEqual( 8, self.test_rc.save_successor(7, b'hello', None, b'other chain', self.config)) # All of the subsequent versions should link directly to the original # privkey. for i in (6, 7, 8): self.assertTrue(os.path.islink(self.test_rc.version("privkey", i))) self.assertEqual( "privkey3.pem", os.path.basename( filesystem.readlink(self.test_rc.version("privkey", i)))) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), list(range(1, 9))) self.assertEqual(self.test_rc.current_version(kind), 3) # Test updating from latest version rather than old version self.test_rc.update_all_links_to(8) self.assertEqual( 9, self.test_rc.save_successor(8, b'last', None, b'attempt', self.config)) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), list(range(1, 10))) self.assertEqual(self.test_rc.current_version(kind), 8) with open(self.test_rc.version("fullchain", 9)) as f: self.assertEqual(f.read(), "last" + "attempt") temp_config_file = os.path.join(self.config.renewal_configs_dir, self.test_rc.lineagename) + ".conf.new" with open(temp_config_file, "w") as f: f.write("We previously crashed while writing me :(") # Test updating when providing a new privkey. The key should # be saved in a new file rather than creating a new symlink. self.assertEqual( 10, self.test_rc.save_successor(9, b'with', b'a', b'key', self.config)) self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file))
def save_successor(self, prior_version, new_cert, new_privkey, new_chain, cli_config): """Save new cert and chain as a successor of a prior version. Returns the new version number that was created. .. note:: this function does NOT update links to deploy this version :param int prior_version: the old version to which this version is regarded as a successor (used to choose a privkey, if the key has not changed, but otherwise this information is not permanently recorded anywhere) :param bytes new_cert: the new certificate, in PEM format :param bytes new_privkey: the new private key, in PEM format, or ``None``, if the private key has not changed :param bytes new_chain: the new chain, in PEM format :param .NamespaceConfig cli_config: parsed command line arguments :returns: the new version number that was created :rtype: int """ # XXX: assumes official archive location rather than examining links # XXX: consider using os.open for availability of os.O_EXCL # XXX: ensure file permissions are correct; also create directories # if needed (ensuring their permissions are correct) # Figure out what the new version is and hence where to save things self.cli_config = cli_config target_version = self.next_free_version() target = { kind: os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version)) for kind in ALL_FOUR } old_privkey = os.path.join(self.archive_dir, "privkey{0}.pem".format(prior_version)) # Distinguish the cases where the privkey has changed and where it # has not changed (in the latter case, making an appropriate symlink # to an earlier privkey version) if new_privkey is None: # The behavior below keeps the prior key by creating a new # symlink to the old key or the target of the old key symlink. if os.path.islink(old_privkey): old_privkey = filesystem.readlink(old_privkey) else: old_privkey = "privkey{0}.pem".format(prior_version) logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: with util.safe_open(target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f: logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) # Preserve gid and (mode & MASK_FOR_PRIVATE_KEY_PERMISSIONS) # from previous privkey in this lineage. mode = filesystem.compute_private_key_mode(old_privkey, BASE_PRIVKEY_MODE) filesystem.copy_ownership_and_apply_mode(old_privkey, target["privkey"], mode, copy_user=False, copy_group=True) # Save everything else with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(new_cert) with open(target["chain"], "wb") as f: logger.debug("Writing chain to %s.", target["chain"]) f.write(new_chain) with open(target["fullchain"], "wb") as f: logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(new_cert + new_chain) symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR} # Update renewal config file self.configfile = update_configuration(self.lineagename, self.archive_dir, symlinks, cli_config) self.configuration = config_with_defaults(self.configfile) return target_version
def test_extended_path_windows(self, mock_readlink): # Following path is largely over the 260 characters permitted in the normal form. mock_readlink.return_value = "\\\\?\\C:\\long" + 1000 * "\\path" with self.assertRaises(ValueError): filesystem.readlink("dummy")
def test_path_posix(self, mock_readlink): mock_readlink.return_value = "/normal/path" self.assertEqual(filesystem.readlink("dummy"), "/normal/path")