config.py 12.8 KB
Newer Older
1
from __future__ import annotations
2

3
4
import importlib
import importlib.util
Phil Jones's avatar
Phil Jones committed
5
import logging
6
import os
7
8
import socket
import stat
9
import types
Phil Jones's avatar
Phil Jones committed
10
import warnings
11
from dataclasses import dataclass
Phil Jones's avatar
Phil Jones committed
12
13
14
15
16
from ssl import (
    create_default_context,
    OP_NO_COMPRESSION,
    Purpose,
    SSLContext,
17
    TLSVersion,
Phil Jones's avatar
Phil Jones committed
18
19
20
    VerifyFlags,
    VerifyMode,
)
21
22
23
from time import time
from typing import Any, AnyStr, Dict, List, Mapping, Optional, Tuple, Type, Union
from wsgiref.handlers import format_date_time
24

Phil Jones's avatar
Phil Jones committed
25
import toml
Phil Jones's avatar
Phil Jones committed
26

27
from .logging import Logger
28

Phil Jones's avatar
Phil Jones committed
29
BYTES = 1
30
OCTETS = 1
Phil Jones's avatar
Phil Jones committed
31
32
SECONDS = 1.0

33
FilePath = Union[AnyStr, os.PathLike]
Peter Smith's avatar
Peter Smith committed
34
SocketKind = Union[int, socket.SocketKind]
35

Phil Jones's avatar
Phil Jones committed
36

37
38
39
40
@dataclass
class Sockets:
    secure_sockets: List[socket.socket]
    insecure_sockets: List[socket.socket]
Phil Jones's avatar
Phil Jones committed
41
    quic_sockets: List[socket.socket]
42
43


Peter Smith's avatar
Peter Smith committed
44
45
46
47
48
49
50
51
class SocketTypeError(Exception):
    def __init__(self, expected: SocketKind, actual: SocketKind) -> None:
        super().__init__(
            f'Unexpected socket type, wanted "{socket.SocketKind(expected)}" got '
            f'"{socket.SocketKind(actual)}"'
        )


Phil Jones's avatar
Phil Jones committed
52
class Config:
53
    _bind = ["127.0.0.1:8000"]
54
    _insecure_bind: List[str] = []
Phil Jones's avatar
Phil Jones committed
55
    _quic_bind: List[str] = []
56
    _quic_addresses: List[Tuple] = []
57
    _log: Optional[Logger] = None
58
    _root_path: str = ""
Phil Jones's avatar
Phil Jones committed
59

60
    access_log_format = '%(h)s %(l)s %(l)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
61
    accesslog: Union[logging.Logger, str, None] = None
62
    alpn_protocols = ["h2", "http/1.1"]
63
    alt_svc_headers: List[str] = []
64
    application_path: str
65
    backlog = 100
66
67
    ca_certs: Optional[str] = None
    certfile: Optional[str] = None
68
    ciphers: str = "ECDHE+AESGCM"
Phil Jones's avatar
Phil Jones committed
69
    debug = False
70
    dogstatsd_tags = ""
71
    errorlog: Union[logging.Logger, str, None] = "-"
72
    graceful_timeout: float = 3 * SECONDS
73
    read_timeout: Optional[int] = None
74
    group: Optional[int] = None
Phil Jones's avatar
Phil Jones committed
75
    h11_max_incomplete_size = 16 * 1024 * BYTES
76
    h2_max_concurrent_streams = 100
77
78
    h2_max_header_list_size = 2**16
    h2_max_inbound_frame_size = 2**14 * OCTETS
79
    include_date_header = True
80
    include_server_header = True
Phil Jones's avatar
Phil Jones committed
81
    keep_alive_timeout = 5 * SECONDS
82
    keyfile: Optional[str] = None
83
    keyfile_password: Optional[str] = None
84
85
    logconfig: Optional[str] = None
    logconfig_dict: Optional[dict] = None
86
    logger_class = Logger
Phil Jones's avatar
Phil Jones committed
87
    loglevel: str = "INFO"
Phil Jones's avatar
Phil Jones committed
88
    max_app_queue_size: int = 10
89
    pid_path: Optional[str] = None
Phil Jones's avatar
Phil Jones committed
90
    server_names: List[str] = []
91
    shutdown_timeout = 60 * SECONDS
Phil Jones's avatar
Phil Jones committed
92
93
    ssl_handshake_timeout = 60 * SECONDS
    startup_timeout = 60 * SECONDS
94
95
    statsd_host: Optional[str] = None
    statsd_prefix = ""
96
    umask: Optional[int] = None
Phil Jones's avatar
Phil Jones committed
97
    use_reloader = False
98
    user: Optional[int] = None
99
    verify_flags: Optional[VerifyFlags] = None
Phil Jones's avatar
Phil Jones committed
100
    verify_mode: Optional[VerifyMode] = None
Phil Jones's avatar
Phil Jones committed
101
    websocket_max_message_size = 16 * 1024 * 1024 * BYTES
Phil Jones's avatar
Phil Jones committed
102
    websocket_ping_interval: Optional[float] = None
103
    worker_class = "asyncio"
104
    workers = 1
Phil Jones's avatar
Phil Jones committed
105

Phil Jones's avatar
Phil Jones committed
106
107
108
109
110
111
    def set_cert_reqs(self, value: int) -> None:
        warnings.warn("Please use verify_mode instead", Warning)
        self.verify_mode = VerifyMode(value)

    cert_reqs = property(None, set_cert_reqs)

Phil Jones's avatar
Phil Jones committed
112
    @property
113
114
    def log(self) -> Logger:
        if self._log is None:
115
            self._log = self.logger_class(self)
116
        return self._log
117

118
119
120
121
122
123
124
125
126
127
128
    @property
    def bind(self) -> List[str]:
        return self._bind

    @bind.setter
    def bind(self, value: Union[List[str], str]) -> None:
        if isinstance(value, str):
            self._bind = [value]
        else:
            self._bind = value

129
130
131
132
133
134
135
136
137
138
139
    @property
    def insecure_bind(self) -> List[str]:
        return self._insecure_bind

    @insecure_bind.setter
    def insecure_bind(self, value: Union[List[str], str]) -> None:
        if isinstance(value, str):
            self._insecure_bind = [value]
        else:
            self._insecure_bind = value

Phil Jones's avatar
Phil Jones committed
140
141
142
143
144
145
146
147
148
149
150
    @property
    def quic_bind(self) -> List[str]:
        return self._quic_bind

    @quic_bind.setter
    def quic_bind(self, value: Union[List[str], str]) -> None:
        if isinstance(value, str):
            self._quic_bind = [value]
        else:
            self._quic_bind = value

151
152
153
154
155
156
157
158
    @property
    def root_path(self) -> str:
        return self._root_path

    @root_path.setter
    def root_path(self, value: str) -> None:
        self._root_path = value.rstrip("/")

159
160
161
    def create_ssl_context(self) -> Optional[SSLContext]:
        if not self.ssl_enabled:
            return None
162

Phil Jones's avatar
Phil Jones committed
163
        context = create_default_context(Purpose.CLIENT_AUTH)
164
        context.set_ciphers(self.ciphers)
165
166
        context.minimum_version = TLSVersion.TLSv1_2  # RFC 7540 Section 9.2: MUST be TLS >=1.2
        context.options = OP_NO_COMPRESSION  # RFC 7540 Section 9.2.1: MUST disable compression
167
        context.set_alpn_protocols(self.alpn_protocols)
Daniel Holth's avatar
Daniel Holth committed
168

169
        if self.certfile is not None and self.keyfile is not None:
170
171
172
173
174
            context.load_cert_chain(
                certfile=self.certfile,
                keyfile=self.keyfile,
                password=self.keyfile_password,
            )
175

176
177
        if self.ca_certs is not None:
            context.load_verify_locations(self.ca_certs)
jarek's avatar
jarek committed
178
179
        if self.verify_mode is not None:
            context.verify_mode = self.verify_mode
180
181
        if self.verify_flags is not None:
            context.verify_flags = self.verify_flags
182

183
184
185
186
187
        return context

    @property
    def ssl_enabled(self) -> bool:
        return self.certfile is not None and self.keyfile is not None
188

189
190
191
192
    def create_sockets(self) -> Sockets:
        if self.ssl_enabled:
            secure_sockets = self._create_sockets(self.bind)
            insecure_sockets = self._create_sockets(self.insecure_bind)
Phil Jones's avatar
Phil Jones committed
193
            quic_sockets = self._create_sockets(self.quic_bind, socket.SOCK_DGRAM)
194
            self._set_quic_addresses(quic_sockets)
195
196
197
        else:
            secure_sockets = []
            insecure_sockets = self._create_sockets(self.bind)
Phil Jones's avatar
Phil Jones committed
198
199
            quic_sockets = []
        return Sockets(secure_sockets, insecure_sockets, quic_sockets)
200

201
202
203
204
205
206
    def _set_quic_addresses(self, sockets: List[socket.socket]) -> None:
        self._quic_addresses = []
        for sock in sockets:
            name = sock.getsockname()
            if type(name) is not str and len(name) >= 2:
                self._quic_addresses.append(name)
207
208
209
210
211
            else:
                warnings.warn(
                    f'Cannot create a alt-svc header for the QUIC socket with address "{name}"',
                    Warning,
                )
212

Phil Jones's avatar
Phil Jones committed
213
214
215
    def _create_sockets(
        self, binds: List[str], type_: int = socket.SOCK_STREAM
    ) -> List[socket.socket]:
216
        sockets: List[socket.socket] = []
217
        for bind in binds:
218
219
            binding: Any = None
            if bind.startswith("unix:"):
Phil Jones's avatar
Phil Jones committed
220
                sock = socket.socket(socket.AF_UNIX, type_)
221
222
223
224
225
226
227
                binding = bind[5:]
                try:
                    if stat.S_ISSOCK(os.stat(binding).st_mode):
                        os.remove(binding)
                except FileNotFoundError:
                    pass
            elif bind.startswith("fd://"):
228
                sock = socket.socket(fileno=int(bind[5:]))
Peter Smith's avatar
Peter Smith committed
229
230
231
                actual_type = sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)
                if actual_type != type_:
                    raise SocketTypeError(type_, actual_type)
232
            else:
233
                bind = bind.replace("[", "").replace("]", "")
234
235
236
237
238
                try:
                    value = bind.rsplit(":", 1)
                    host, port = value[0], int(value[1])
                except (ValueError, IndexError):
                    host, port = bind, 8000
Phil Jones's avatar
Phil Jones committed
239
                sock = socket.socket(socket.AF_INET6 if ":" in host else socket.AF_INET, type_)
240
241
                if self.workers > 1:
                    try:
242
                        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
243
244
245
246
247
                    except AttributeError:
                        pass
                binding = (host, port)

            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
248
249

            if bind.startswith("unix:"):
250
251
                if self.umask is not None:
                    current_umask = os.umask(self.umask)
252
                sock.bind(binding)
253
254
255
256
                if self.user is not None and self.group is not None:
                    os.chown(binding, self.user, self.group)
                if self.umask is not None:
                    os.umask(current_umask)
257
258
259
            elif bind.startswith("fd://"):
                pass
            else:
260
                sock.bind(binding)
261

262
            sock.setblocking(False)
263
            try:
Phil Jones's avatar
Phil Jones committed
264
                sock.set_inheritable(True)
265
266
267
268
            except AttributeError:
                pass
            sockets.append(sock)
        return sockets
269

270
    def response_headers(self, protocol: str) -> List[Tuple[bytes, bytes]]:
271
272
273
        headers = []
        if self.include_date_header:
            headers.append((b"date", format_date_time(time()).encode("ascii")))
274
275
        if self.include_server_header:
            headers.append((b"server", f"hypercorn-{protocol}".encode("ascii")))
276
277
278

        for alt_svc_header in self.alt_svc_headers:
            headers.append((b"alt-svc", alt_svc_header.encode()))
279
        if len(self.alt_svc_headers) == 0 and self._quic_addresses:
Phil Jones's avatar
Phil Jones committed
280
281
            from aioquic.h3.connection import H3_ALPN

Phil Jones's avatar
Phil Jones committed
282
            for version in H3_ALPN:
283
284
                for addr in self._quic_addresses:
                    port = addr[1]
Phil Jones's avatar
Phil Jones committed
285
                    headers.append((b"alt-svc", b'%s=":%d"; ma=3600' % (version.encode(), port)))
Phil Jones's avatar
Phil Jones committed
286

287
288
        return headers

289
290
291
292
    def set_statsd_logger_class(self, statsd_logger: Type[Logger]) -> None:
        if self.logger_class == Logger and self.statsd_host is not None:
            self.logger_class = statsd_logger

293
294
    @classmethod
    def from_mapping(
295
296
        cls: Type["Config"], mapping: Optional[Mapping[str, Any]] = None, **kwargs: Any
    ) -> "Config":
297
298
299
300
301
302
303
304
305
        """Create a configuration from a mapping.

        This allows either a mapping to be directly passed or as
        keyword arguments, for example,

        .. code-block:: python

            config = {'keep_alive_timeout': 10}
            Config.from_mapping(config)
306
            Config.from_mapping(keep_alive_timeout=10)
307
308
309
310
311
312
313
314
315
316
317
318

        Arguments:
            mapping: Optionally a mapping object.
            kwargs: Optionally a collection of keyword arguments to
                form a mapping.
        """
        mappings: Dict[str, Any] = {}
        if mapping is not None:
            mappings.update(mapping)
        mappings.update(kwargs)
        config = cls()
        for key, value in mappings.items():
319
            try:
320
                setattr(config, key, value)
321
322
            except AttributeError:
                pass
323

324
325
326
        return config

    @classmethod
327
    def from_pyfile(cls: Type["Config"], filename: FilePath) -> "Config":
328
329
330
331
332
333
334
335
336
337
        """Create a configuration from a Python file.

        .. code-block:: python

            Config.from_pyfile('hypercorn_config.py')

        Arguments:
            filename: The filename which gives the path to the file.
        """
        file_path = os.fspath(filename)
338
        spec = importlib.util.spec_from_file_location("module.name", file_path)
339
        module = importlib.util.module_from_spec(spec)
Phil Jones's avatar
Phil Jones committed
340
        spec.loader.exec_module(module)
341
342
343
        return cls.from_object(module)

    @classmethod
344
    def from_toml(cls: Type["Config"], filename: FilePath) -> "Config":
345
346
347
348
349
350
351
352
353
354
355
356
357
        """Load the configuration values from a TOML formatted file.

        This allows configuration to be loaded as so

        .. code-block:: python

            Config.from_toml('config.toml')

        Arguments:
            filename: The filename which gives the path to the file.
        """
        file_path = os.fspath(filename)
        with open(file_path) as file_:
Phil Jones's avatar
Phil Jones committed
358
            data = toml.load(file_)
359
360
361
        return cls.from_mapping(data)

    @classmethod
362
    def from_object(cls: Type["Config"], instance: Union[object, str]) -> "Config":
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
        """Create a configuration from a Python object.

        This can be used to reference modules or objects within
        modules for example,

        .. code-block:: python

            Config.from_object('module')
            Config.from_object('module.instance')
            from module import instance
            Config.from_object(instance)

        are valid.

        Arguments:
            instance: Either a str referencing a python object or the
                object itself.

        """
        if isinstance(instance, str):
            try:
                instance = importlib.import_module(instance)
385
            except ImportError:
Phil Jones's avatar
Phil Jones committed
386
                path, config = instance.rsplit(".", 1)
387
388
389
                module = importlib.import_module(path)
                instance = getattr(module, config)

390
        mapping = {
391
392
            key: getattr(instance, key)
            for key in dir(instance)
393
394
            if not isinstance(getattr(instance, key), types.ModuleType)
        }
395
        return cls.from_mapping(mapping)