Passed
Push — main ( ecffc8...3be6b0 )
by Jochen
02:08
created

weitersager.http   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 127
Duplicated Lines 0 %

Test Coverage

Coverage 82.86%

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 127
ccs 58
cts 70
cp 0.8286
rs 10
c 0
b 0
f 0
wmc 18

4 Methods

Rating   Name   Duplication   Size   Complexity  
B Application.on_root() 0 29 7
A Application.__init__() 0 6 1
A Application.__call__() 0 2 1
A Application.wsgi_app() 0 9 2

4 Functions

Rating   Name   Duplication   Size   Complexity  
A create_app() 0 2 1
A _get_api_token() 0 10 3
A create_server() 0 5 1
A start_receive_server() 0 16 2
1
"""
2
weitersager.http
3
~~~~~~~~~~~~~~~~
4
5
HTTP server to receive messages
6
7
:Copyright: 2007-2022 Jochen Kupperschmidt
8
:License: MIT, see LICENSE for details.
9
"""
10
11 1
from __future__ import annotations
12 1
from http import HTTPStatus
13 1
import logging
14 1
import sys
15 1
from typing import Optional
16 1
from wsgiref.simple_server import make_server, ServerHandler, WSGIServer
17
18 1
from werkzeug.exceptions import abort, HTTPException
19 1
from werkzeug.routing import Map, Rule
20 1
from werkzeug.wrappers import Request, Response
21
22 1
from .config import HttpConfig
23 1
from .signals import message_received
24 1
from .util import start_thread
25
26
27 1
logger = logging.getLogger(__name__)
28
29
30 1
def create_app(api_tokens: set[str]):
31 1
    return Application(api_tokens)
32
33
34 1
class Application:
35 1
    def __init__(self, api_tokens: set[str]):
36 1
        self._api_tokens = api_tokens
37
38 1
        self._url_map = Map(
39
            [
40
                Rule('/', endpoint='root'),
41
            ]
42
        )
43
44 1
    def __call__(self, environ, start_response):
45 1
        return self.wsgi_app(environ, start_response)
46
47 1
    @Request.application
48
    def wsgi_app(self, request):
49 1
        adapter = self._url_map.bind_to_environ(request.environ)
50 1
        try:
51 1
            endpoint, values = adapter.match()
52 1
            handler = getattr(self, f'on_{endpoint}')
53 1
            return handler(request, **values)
54 1
        except HTTPException as exc:
55 1
            return exc
56
57 1
    def on_root(self, request):
58 1
        if self._api_tokens:
59 1
            api_token = _get_api_token(request.headers)
60 1
            if not api_token:
61 1
                abort(HTTPStatus.UNAUTHORIZED)
62
63 1
            if api_token not in self._api_tokens:
64 1
                abort(HTTPStatus.FORBIDDEN)
65
66 1
        if not request.is_json:
67
            abort(HTTPStatus.UNSUPPORTED_MEDIA_TYPE)
68
69 1
        payload = request.json
70 1
        if payload is None:
71
            abort(HTTPStatus.BAD_REQUEST)
72
73 1
        try:
74 1
            channel = payload['channel']
75 1
            text = payload['text']
76 1
        except KeyError:
77 1
            abort(HTTPStatus.BAD_REQUEST)
78
79 1
        message_received.send(
80
            channel_name=channel,
81
            text=text,
82
            source_ip_address=request.remote_addr,
83
        )
84
85 1
        return Response('', status=HTTPStatus.ACCEPTED)
86
87
88 1
def _get_api_token(headers) -> Optional[str]:
89 1
    authorization_value = headers.get('Authorization')
90 1
    if not authorization_value:
91 1
        return None
92
93 1
    prefix = 'Token '
94 1
    if not authorization_value.startswith(prefix):
95
        return None
96
97 1
    return authorization_value[len(prefix) :]
98
99
100
# Override value of `Server:` header sent by wsgiref.
101 1
ServerHandler.server_software = 'Weitersager'
102
103
104 1
def create_server(config: HttpConfig) -> WSGIServer:
105
    """Create the HTTP server."""
106 1
    app = create_app(config.api_tokens)
107
108 1
    return make_server(config.host, config.port, app)
109
110
111 1
def start_receive_server(config: HttpConfig) -> None:
112
    """Start in a separate thread."""
113
    try:
114
        server = create_server(config)
115
    except OSError as e:
116
        sys.stderr.write(f'Error {e.errno:d}: {e.strerror}\n')
117
        sys.stderr.write(
118
            f'Probably no permission to open port {config.port}. '
119
            'Try to specify a port number above 1,024 (or even '
120
            '4,096) and up to 65,535.\n'
121
        )
122
        sys.exit(1)
123
124
    thread_name = server.__class__.__name__
125
    start_thread(server.serve_forever, thread_name)
126
    logger.info('Listening for HTTP requests on %s:%d.', *server.server_address)
127