Completed
Push — main ( 25807a...2ed45a )
by Jochen
01:37
created

weitersager.http   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 116
Duplicated Lines 0 %

Test Coverage

Coverage 86.36%

Importance

Changes 0
Metric Value
eloc 72
dl 0
loc 116
ccs 57
cts 66
cp 0.8636
rs 10
c 0
b 0
f 0
wmc 13

4 Methods

Rating   Name   Duplication   Size   Complexity  
A RequestHandler.version_string() 0 3 1
A ReceiveServer.__init__() 0 6 1
A RequestHandler._get_api_token() 0 10 3
B RequestHandler.do_POST() 0 30 5

2 Functions

Rating   Name   Duplication   Size   Complexity  
A parse_json_message() 0 8 1
A start_receive_server() 0 15 2
1
"""
2
weitersager.http
3
~~~~~~~~~~~~~~~~
4
5
HTTP server to receive messages
6
7
:Copyright: 2007-2020 Jochen Kupperschmidt
8
:License: MIT, see LICENSE for details.
9
"""
10
11 1
from dataclasses import dataclass
12 1
from http import HTTPStatus
13 1
from http.server import BaseHTTPRequestHandler, HTTPServer
14 1
import json
15 1
import sys
16 1
from typing import Optional
17
18 1
from .config import HttpConfig
19 1
from .signals import message_received
20 1
from .util import log, start_thread
21
22
23 1
@dataclass(frozen=True)
24
class Message:
25 1
    channel: str
26 1
    text: str
27
28
29 1
def parse_json_message(json_data: str) -> Message:
30
    """Extract message from JSON."""
31 1
    data = json.loads(json_data)
32
33 1
    channel = data['channel']
34 1
    text = data['text']
35
36 1
    return Message(channel=channel, text=text)
37
38
39 1
class RequestHandler(BaseHTTPRequestHandler):
40
    """Handler for messages submitted via HTTP."""
41
42 1
    def do_POST(self) -> None:
43 1
        valid_api_tokens = self.server.api_tokens
44 1
        if valid_api_tokens:
45 1
            api_token = self._get_api_token()
46 1
            if not api_token:
47 1
                self.send_response(HTTPStatus.UNAUTHORIZED)
48 1
                self.end_headers()
49 1
                return
50
51 1
            if api_token not in valid_api_tokens:
52 1
                self.send_response(HTTPStatus.FORBIDDEN)
53 1
                self.end_headers()
54 1
                return
55
56 1
        try:
57 1
            content_length = int(self.headers.get('Content-Length', 0))
58 1
            data = self.rfile.read(content_length).decode('utf-8')
59 1
            message = parse_json_message(data)
60 1
        except (KeyError, ValueError):
61 1
            log(f'Invalid message received from {self.address_string()}.')
62 1
            self.send_error(HTTPStatus.BAD_REQUEST)
63 1
            return
64
65 1
        self.send_response(HTTPStatus.ACCEPTED)
66 1
        self.end_headers()
67
68 1
        message_received.send(
69
            channel_name=message.channel,
70
            text=message.text,
71
            source_address=self.client_address,
72
        )
73
74 1
    def _get_api_token(self) -> Optional[str]:
75 1
        authorization_value = self.headers.get('authorization')
76 1
        if not authorization_value:
77 1
            return None
78
79 1
        prefix = 'WTRSGR '
80 1
        if not authorization_value.startswith(prefix):
81
            return None
82
83 1
        return authorization_value[len(prefix) :]
84
85 1
    def version_string(self) -> str:
86
        """Return custom server version string."""
87 1
        return 'Weitersager'
88
89
90 1
class ReceiveServer(HTTPServer):
91
    """HTTP server that waits for messages."""
92
93 1
    def __init__(self, config: HttpConfig) -> None:
94 1
        address = (config.host, config.port)
95 1
        HTTPServer.__init__(self, address, RequestHandler)
96 1
        log('Listening for HTTP requests on {}:{:d}.', *address)
97
98 1
        self.api_tokens = config.api_tokens
99
100
101 1
def start_receive_server(config: HttpConfig) -> None:
102
    """Start in a separate thread."""
103
    try:
104
        receiver = ReceiveServer(config)
105
    except OSError as e:
106
        sys.stderr.write(f'Error {e.errno:d}: {e.strerror}\n')
107
        sys.stderr.write(
108
            f'Probably no permission to open port {config.port}. '
109
            'Try to specify a port number above 1,024 (or even '
110
            '4,096) and up to 65,535.\n'
111
        )
112
        sys.exit(1)
113
114
    thread_name = receiver.__class__.__name__
115
    start_thread(receiver.serve_forever, thread_name)
116