Completed
Push — main ( 681ed3...7f4e70 )
by Jochen
01:45
created

weitersager.httpreceiver   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 122
Duplicated Lines 0 %

Test Coverage

Coverage 86.96%

Importance

Changes 0
Metric Value
eloc 76
dl 0
loc 122
ccs 60
cts 69
cp 0.8696
rs 10
c 0
b 0
f 0
wmc 13

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

2 Functions

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