1"""Certbot user-supplied configuration."""
2import argparse
3import copy
4from typing import Any
5from typing import List
6from typing import Optional
7from urllib import parse
8
9from certbot import errors
10from certbot import util
11from certbot._internal import constants
12from certbot.compat import misc
13from certbot.compat import os
14
15
16class NamespaceConfig:
17    """Configuration wrapper around :class:`argparse.Namespace`.
18
19    Please note that the following attributes are dynamically resolved using
20    :attr:`~certbot.configuration.NamespaceConfig.work_dir` and relative
21    paths defined in :py:mod:`certbot._internal.constants`:
22
23      - `accounts_dir`
24      - `csr_dir`
25      - `in_progress_dir`
26      - `key_dir`
27      - `temp_checkpoint_dir`
28
29    And the following paths are dynamically resolved using
30    :attr:`~certbot.configuration.NamespaceConfig.config_dir` and relative
31    paths defined in :py:mod:`certbot._internal.constants`:
32
33      - `default_archive_dir`
34      - `live_dir`
35      - `renewal_configs_dir`
36
37    :ivar namespace: Namespace typically produced by
38        :meth:`argparse.ArgumentParser.parse_args`.
39    :type namespace: :class:`argparse.Namespace`
40
41    """
42
43    def __init__(self, namespace: argparse.Namespace) -> None:
44        self.namespace: argparse.Namespace
45        # Avoid recursion loop because of the delegation defined in __setattr__
46        object.__setattr__(self, 'namespace', namespace)
47
48        self.namespace.config_dir = os.path.abspath(self.namespace.config_dir)
49        self.namespace.work_dir = os.path.abspath(self.namespace.work_dir)
50        self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir)
51
52        # Check command line parameters sanity, and error out in case of problem.
53        _check_config_sanity(self)
54
55    # Delegate any attribute not explicitly defined to the underlying namespace object.
56
57    def __getattr__(self, name: str) -> Any:
58        return getattr(self.namespace, name)
59
60    def __setattr__(self, name: str, value: Any) -> None:
61        setattr(self.namespace, name, value)
62
63    @property
64    def server(self) -> str:
65        """ACME Directory Resource URI."""
66        return self.namespace.server
67
68    @server.setter
69    def server(self, server_: str) -> None:
70        self.namespace.server = server_
71
72    @property
73    def email(self) -> Optional[str]:
74        """Email used for registration and recovery contact.
75
76        Use comma to register multiple emails,
77        ex: u1@example.com,u2@example.com. (default: Ask).
78        """
79        return self.namespace.email
80
81    @email.setter
82    def email(self, mail: str) -> None:
83        self.namespace.email = mail
84
85    @property
86    def rsa_key_size(self) -> int:
87        """Size of the RSA key."""
88        return self.namespace.rsa_key_size
89
90    @rsa_key_size.setter
91    def rsa_key_size(self, ksize: int) -> None:
92        """Set the rsa_key_size property"""
93        self.namespace.rsa_key_size = ksize
94
95    @property
96    def elliptic_curve(self) -> str:
97        """The SECG elliptic curve name to use.
98
99        Please see RFC 8446 for supported values.
100        """
101        return self.namespace.elliptic_curve
102
103    @elliptic_curve.setter
104    def elliptic_curve(self, ecurve: str) -> None:
105        """Set the elliptic_curve property"""
106        self.namespace.elliptic_curve = ecurve
107
108    @property
109    def key_type(self) -> str:
110        """Type of generated private key.
111
112        Only *ONE* per invocation can be provided at this time.
113        """
114        return self.namespace.key_type
115
116    @key_type.setter
117    def key_type(self, ktype: str) -> None:
118        """Set the key_type property"""
119        self.namespace.key_type = ktype
120
121    @property
122    def must_staple(self) -> bool:
123        """Adds the OCSP Must Staple extension to the certificate.
124
125        Autoconfigures OCSP Stapling for supported setups
126        (Apache version >= 2.3.3 ).
127        """
128        return self.namespace.must_staple
129
130    @property
131    def config_dir(self) -> str:
132        """Configuration directory."""
133        return self.namespace.config_dir
134
135    @property
136    def work_dir(self) -> str:
137        """Working directory."""
138        return self.namespace.work_dir
139
140    @property
141    def accounts_dir(self) -> str:
142        """Directory where all account information is stored."""
143        return self.accounts_dir_for_server_path(self.server_path)
144
145    @property
146    def backup_dir(self) -> str:
147        """Configuration backups directory."""
148        return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
149
150    @property
151    def csr_dir(self) -> str:
152        """Directory where new Certificate Signing Requests (CSRs) are saved."""
153        return os.path.join(self.namespace.config_dir, constants.CSR_DIR)
154
155    @property
156    def in_progress_dir(self) -> str:
157        """Directory used before a permanent checkpoint is finalized."""
158        return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
159
160    @property
161    def key_dir(self) -> str:
162        """Keys storage."""
163        return os.path.join(self.namespace.config_dir, constants.KEY_DIR)
164
165    @property
166    def temp_checkpoint_dir(self) -> str:
167        """Temporary checkpoint directory."""
168        return os.path.join(
169            self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
170
171    @property
172    def no_verify_ssl(self) -> bool:
173        """Disable verification of the ACME server's certificate."""
174        return self.namespace.no_verify_ssl
175
176    @property
177    def http01_port(self) -> int:
178        """Port used in the http-01 challenge.
179
180        This only affects the port Certbot listens on.
181        A conforming ACME server will still attempt to connect on port 80.
182        """
183        return self.namespace.http01_port
184
185    @property
186    def http01_address(self) -> str:
187        """The address the server listens to during http-01 challenge."""
188        return self.namespace.http01_address
189
190    @property
191    def https_port(self) -> int:
192        """Port used to serve HTTPS.
193
194        This affects which port Nginx will listen on after a LE certificate
195        is installed.
196        """
197        return self.namespace.https_port
198
199    @property
200    def pref_challs(self) -> List[str]:
201        """List of user specified preferred challenges.
202
203        Sorted with the most preferred challenge listed first.
204        """
205        return self.namespace.pref_challs
206
207    @property
208    def allow_subset_of_names(self) -> bool:
209        """Allow only a subset of names to be authorized to perform validations.
210
211        When performing domain validation, do not consider it a failure
212        if authorizations can not be obtained for a strict subset of
213        the requested domains. This may be useful for allowing renewals for
214        multiple domains to succeed even if some domains no longer point
215        at this system.
216        """
217        return self.namespace.allow_subset_of_names
218
219    @property
220    def strict_permissions(self) -> bool:
221        """Enable strict permissions checks.
222
223        Require that all configuration files are owned by the current
224        user; only needed if your config is somewhere unsafe like /tmp/.
225        """
226        return self.namespace.strict_permissions
227
228    @property
229    def disable_renew_updates(self) -> bool:
230        """Disable renewal updates.
231
232        If updates provided by installer enhancements when Certbot is being run
233        with \"renew\" verb should be disabled.
234        """
235        return self.namespace.disable_renew_updates
236
237    @property
238    def preferred_chain(self) -> Optional[str]:
239        """Set the preferred certificate chain.
240
241        If the CA offers multiple certificate chains, prefer the chain whose
242        topmost certificate was issued from this Subject Common Name.
243        If no match, the default offered chain will be used.
244        """
245        return self.namespace.preferred_chain
246
247    @property
248    def server_path(self) -> str:
249        """File path based on ``server``."""
250        parsed = parse.urlparse(self.namespace.server)
251        return (parsed.netloc + parsed.path).replace('/', os.path.sep)
252
253    def accounts_dir_for_server_path(self, server_path: str) -> str:
254        """Path to accounts directory based on server_path"""
255        server_path = misc.underscores_for_unsupported_characters_in_path(server_path)
256        return os.path.join(
257            self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path)
258
259    @property
260    def default_archive_dir(self) -> str:  # pylint: disable=missing-function-docstring
261        return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
262
263    @property
264    def live_dir(self) -> str:  # pylint: disable=missing-function-docstring
265        return os.path.join(self.namespace.config_dir, constants.LIVE_DIR)
266
267    @property
268    def renewal_configs_dir(self) -> str:  # pylint: disable=missing-function-docstring
269        return os.path.join(
270            self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)
271
272    @property
273    def renewal_hooks_dir(self) -> str:
274        """Path to directory with hooks to run with the renew subcommand."""
275        return os.path.join(self.namespace.config_dir,
276                            constants.RENEWAL_HOOKS_DIR)
277
278    @property
279    def renewal_pre_hooks_dir(self) -> str:
280        """Path to the pre-hook directory for the renew subcommand."""
281        return os.path.join(self.renewal_hooks_dir,
282                            constants.RENEWAL_PRE_HOOKS_DIR)
283
284    @property
285    def renewal_deploy_hooks_dir(self) -> str:
286        """Path to the deploy-hook directory for the renew subcommand."""
287        return os.path.join(self.renewal_hooks_dir,
288                            constants.RENEWAL_DEPLOY_HOOKS_DIR)
289
290    @property
291    def renewal_post_hooks_dir(self) -> str:
292        """Path to the post-hook directory for the renew subcommand."""
293        return os.path.join(self.renewal_hooks_dir,
294                            constants.RENEWAL_POST_HOOKS_DIR)
295
296    @property
297    def issuance_timeout(self) -> int:
298        """This option specifies how long (in seconds) Certbot will wait
299        for the server to issue a certificate.
300        """
301        return self.namespace.issuance_timeout
302
303    # Magic methods
304
305    def __deepcopy__(self, _memo: Any) -> 'NamespaceConfig':
306        # Work around https://bugs.python.org/issue1515 for py26 tests :( :(
307        # https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276
308        new_ns = copy.deepcopy(self.namespace)
309        return type(self)(new_ns)
310
311
312def _check_config_sanity(config: NamespaceConfig) -> None:
313    """Validate command line options and display error message if
314    requirements are not met.
315
316    :param config: NamespaceConfig instance holding user configuration
317    :type args: :class:`certbot.configuration.NamespaceConfig`
318
319    """
320    # Port check
321    if config.http01_port == config.https_port:
322        raise errors.ConfigurationError(
323            "Trying to run http-01 and https-port "
324            "on the same port ({0})".format(config.https_port))
325
326    # Domain checks
327    if config.namespace.domains is not None:
328        for domain in config.namespace.domains:
329            # This may be redundant, but let's be paranoid
330            util.enforce_domain_sanity(domain)
331