1""" 2This compat module handles various platform specific calls that do not fall into one 3particular category. 4""" 5from __future__ import absolute_import 6 7import logging 8import select 9import subprocess 10import sys 11from typing import Optional 12from typing import Tuple 13import warnings 14 15from certbot import errors 16from certbot.compat import os 17 18try: 19 from pywintypes import error as pywinerror 20 from win32com.shell import shell as shellwin32 21 from win32console import GetStdHandle 22 from win32console import STD_OUTPUT_HANDLE 23 POSIX_MODE = False 24except ImportError: # pragma: no cover 25 POSIX_MODE = True 26 27 28logger = logging.getLogger(__name__) 29 30# For Linux: define OS specific standard binary directories 31STANDARD_BINARY_DIRS = ["/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] if POSIX_MODE else [] 32 33 34def raise_for_non_administrative_windows_rights() -> None: 35 """ 36 On Windows, raise if current shell does not have the administrative rights. 37 Do nothing on Linux. 38 39 :raises .errors.Error: If the current shell does not have administrative rights on Windows. 40 """ 41 if not POSIX_MODE and shellwin32.IsUserAnAdmin() == 0: # pragma: no cover 42 raise errors.Error('Error, certbot must be run on a shell with administrative rights.') 43 44 45def prepare_virtual_console() -> None: 46 """ 47 On Windows, ensure that Console Virtual Terminal Sequences are enabled. 48 49 """ 50 if POSIX_MODE: 51 return 52 53 # https://docs.microsoft.com/en-us/windows/console/setconsolemode 54 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 55 56 # stdout/stderr will be the same console screen buffer, but this could return None or raise 57 try: 58 h = GetStdHandle(STD_OUTPUT_HANDLE) 59 if h: 60 h.SetConsoleMode(h.GetConsoleMode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING) 61 except pywinerror: 62 logger.debug("Failed to set console mode", exc_info=True) 63 64 65def readline_with_timeout(timeout: float, prompt: Optional[str]) -> str: 66 """ 67 Read user input to return the first line entered, or raise after specified timeout. 68 69 :param float timeout: The timeout in seconds given to the user. 70 :param str prompt: The prompt message to display to the user. 71 72 :returns: The first line entered by the user. 73 :rtype: str 74 75 """ 76 try: 77 # Linux specific 78 # 79 # Call to select can only be done like this on UNIX 80 rlist, _, _ = select.select([sys.stdin], [], [], timeout) 81 if not rlist: 82 raise errors.Error( 83 "Timed out waiting for answer to prompt '{0}'".format(prompt if prompt else "")) 84 return rlist[0].readline() 85 except OSError: 86 # Windows specific 87 # 88 # No way with select to make a timeout to the user input on Windows, 89 # as select only supports socket in this case. 90 # So no timeout on Windows for now. 91 return sys.stdin.readline() 92 93 94WINDOWS_DEFAULT_FOLDERS = { 95 'config': 'C:\\Certbot', 96 'work': 'C:\\Certbot\\lib', 97 'logs': 'C:\\Certbot\\log', 98} 99LINUX_DEFAULT_FOLDERS = { 100 'config': '/etc/letsencrypt', 101 'work': '/var/lib/letsencrypt', 102 'logs': '/var/log/letsencrypt', 103} 104FREEBSD_DEFAULT_FOLDERS = { 105 'config': '/usr/local/etc/letsencrypt', 106 'work': '/var/db/letsencrypt', 107 'logs': '/var/log/letsencrypt', 108} 109 110 111def get_default_folder(folder_type: str) -> str: 112 """ 113 Return the relevant default folder for the current OS 114 115 :param str folder_type: The type of folder to retrieve (config, work or logs) 116 117 :returns: The relevant default folder. 118 :rtype: str 119 120 """ 121 if os.name != 'nt': 122 # Unix-like 123 if sys.platform.startswith('freebsd') or sys.platform.startswith('dragonfly'): 124 # FreeBSD specific 125 return FREEBSD_DEFAULT_FOLDERS[folder_type] 126 else: 127 # Linux specific 128 return LINUX_DEFAULT_FOLDERS[folder_type] 129 # Windows specific 130 return WINDOWS_DEFAULT_FOLDERS[folder_type] 131 132 133def underscores_for_unsupported_characters_in_path(path: str) -> str: 134 """ 135 Replace unsupported characters in path for current OS by underscores. 136 :param str path: the path to normalize 137 :return: the normalized path 138 :rtype: str 139 """ 140 if os.name != 'nt': 141 # Linux specific 142 return path 143 144 # Windows specific 145 drive, tail = os.path.splitdrive(path) 146 return drive + tail.replace(':', '_') 147 148 149def execute_command_status(cmd_name: str, shell_cmd: str, 150 env: Optional[dict] = None) -> Tuple[int, str, str]: 151 """ 152 Run a command: 153 - on Linux command will be run by the standard shell selected with 154 subprocess.run(shell=True) 155 - on Windows command will be run in a Powershell shell 156 157 This differs from execute_command: it returns the exit code, and does not log the result 158 and output of the command. 159 160 :param str cmd_name: the user facing name of the hook being run 161 :param str shell_cmd: shell command to execute 162 :param dict env: environ to pass into subprocess.run 163 164 :returns: `tuple` (`int` returncode, `str` stderr, `str` stdout) 165 """ 166 logger.info("Running %s command: %s", cmd_name, shell_cmd) 167 168 if POSIX_MODE: 169 proc = subprocess.run(shell_cmd, shell=True, stdout=subprocess.PIPE, 170 stderr=subprocess.PIPE, universal_newlines=True, 171 check=False, env=env) 172 else: 173 line = ['powershell.exe', '-Command', shell_cmd] 174 proc = subprocess.run(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 175 universal_newlines=True, check=False, env=env) 176 177 # universal_newlines causes stdout and stderr to be str objects instead of 178 # bytes in Python 3 179 out, err = proc.stdout, proc.stderr 180 return proc.returncode, err, out 181 182 183def execute_command(cmd_name: str, shell_cmd: str, env: Optional[dict] = None) -> Tuple[str, str]: 184 """ 185 Run a command: 186 - on Linux command will be run by the standard shell selected with 187 subprocess.run(shell=True) 188 - on Windows command will be run in a Powershell shell 189 190 This differs from execute_command: it returns the exit code, and does not log the result 191 and output of the command. 192 193 :param str cmd_name: the user facing name of the hook being run 194 :param str shell_cmd: shell command to execute 195 :param dict env: environ to pass into subprocess.run 196 197 :returns: `tuple` (`str` stderr, `str` stdout) 198 """ 199 # Deprecation per https://github.com/certbot/certbot/issues/8854 200 warnings.warn( 201 "execute_command will be deprecated in the future, use execute_command_status instead", 202 PendingDeprecationWarning 203 ) 204 returncode, err, out = execute_command_status(cmd_name, shell_cmd, env) 205 base_cmd = os.path.basename(shell_cmd.split(None, 1)[0]) 206 if out: 207 logger.info('Output from %s command %s:\n%s', cmd_name, base_cmd, out) 208 if returncode != 0: 209 logger.error('%s command "%s" returned error code %d', 210 cmd_name, shell_cmd, returncode) 211 if err: 212 logger.error('Error output from %s command %s:\n%s', cmd_name, base_cmd, err) 213 return err, out 214