Passed
Push — main ( 281afb...25807a )
by Jochen
01:30
created

weitersager.httpreceiver   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 113
Duplicated Lines 0 %

Test Coverage

Coverage 85.94%

Importance

Changes 0
Metric Value
eloc 70
dl 0
loc 113
ccs 55
cts 64
cp 0.8594
rs 10
c 0
b 0
f 0
wmc 13

2 Functions

Rating   Name   Duplication   Size   Complexity  
A parse_json_message() 0 8 1
A start_receive_server() 0 15 2

4 Methods

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