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