1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# ==============================================================================
# Copyright (C) 2019 - Philip Paquette, Steven Bocco
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
# ==============================================================================
""" Tornado connection handler class, used internally to manage data received by server application. """
import logging
from urllib.parse import urlparse
from tornado import gen
from tornado.websocket import WebSocketHandler, WebSocketClosedError
import ujson as json
from diplomacy.communication import responses, requests
from diplomacy.server import request_managers
from diplomacy.utils import exceptions, strings
LOGGER = logging.getLogger(__name__)
class ConnectionHandler(WebSocketHandler):
""" ConnectionHandler class. Properties:
- server: server object representing running server.
"""
# pylint: disable=abstract-method
def __init__(self, *args, **kwargs):
self.server = None
super(ConnectionHandler, self).__init__(*args, **kwargs)
def initialize(self, server=None):
""" Initialize the connection handler.
:param server: a Server object.
:type server: diplomacy.Server
"""
# pylint: disable=arguments-differ
if self.server is None:
self.server = server
def get_compression_options(self):
""" Return compression options for the connection (see parent method).
Non-None enables compression with default options.
"""
return {}
def check_origin(self, origin):
""" Return True if we should accept connexion from given origin (str). """
# It seems origin may be 'null', e.g. if client is a web page loaded from disk (`file:///my_test_file.html`).
# Accept it.
if origin == 'null':
return True
# Try to check if origin matches host (without regarding port).
# Adapted from parent method code (tornado 4.5.3).
parsed_origin = urlparse(origin)
origin = parsed_origin.netloc.split(':')[0]
origin = origin.lower()
# Split host with ':' and keep only first piece to ignore eventual port.
host = self.request.headers.get("Host").split(':')[0]
return origin == host
def on_close(self):
""" Invoked when the socket is closed (see parent method).
Detach this connection handler from server users.
"""
self.server.users.remove_connection(self, remove_tokens=False)
LOGGER.info("Removed connection. Remaining %d connection(s).", self.server.users.count_connections())
@gen.coroutine
def on_message(self, message):
""" Parse given message and manage parsed data (expected a string representation of a request). """
try:
json_request = json.loads(message)
if not isinstance(json_request, dict):
raise ValueError("Unable to convert a JSON string to a dictionary.")
except ValueError as exc:
# Error occurred because either message is not a JSON string or parsed JSON object is not a dict.
response = responses.Error(message='%s/%s' % (type(exc).__name__, str(exc)))
else:
try:
request = requests.parse_dict(json_request)
if request.level is not None:
# Link request token to this connection handler.
self.server.users.attach_connection_handler(request.token, self)
response = yield request_managers.handle_request(self.server, request, self)
if response is None:
response = responses.Ok(request_id=request.request_id)
except exceptions.ResponseException as exc:
response = responses.Error(message='%s/%s' % (type(exc).__name__, exc.message),
request_id=json_request.get(strings.REQUEST_ID, None))
try:
yield self.write_message(response.json())
except WebSocketClosedError:
LOGGER.error('Websocket is closed.')
|