+++ /dev/null
-"""Provide …
-
-TODO: documentation, doctests, resilient conf-parsing
-"""
-import os
-import json
-from websockets import (WebSocketServerProtocol, ConnectionClosedOK,
- ConnectionClosedError, serve)
-from websockets.http import Headers
-from http import HTTPStatus
-from typing import Optional, Tuple
-
-from controlpi import BasePlugin, MessageBus, Message, PluginConfiguration
-
-
-class Connection:
- def __init__(self, bus: MessageBus, websocket: WebSocketServerProtocol,
- path: str) -> None:
- self._bus = bus
- self._websocket = websocket
- address = self._websocket.remote_address
- self._address = address[0]
- self._port = address[1]
- self._name = f"{self._address}:{self._port}"
- if path != '/':
- self._name = path[1:]
- self._bus.register(self._name, 'WSServer-Connection',
- [{}], [{}], self._receive)
-
- async def _receive(self, message: Message) -> None:
- json_message = json.dumps(message)
- try:
- await self._websocket.send(json_message)
- except (ConnectionClosedOK, ConnectionClosedError):
- pass
-
- async def run(self):
- await self._bus.send({'sender': self._name,
- 'event': 'connection opened',
- 'address': self._address,
- 'port': self._port})
- try:
- async for json_message in self._websocket:
- original_message = json.loads(json_message)
- message = {'sender': self._name}
- message.update(original_message)
- await self._bus.send(message)
- except (ConnectionClosedOK, ConnectionClosedError):
- pass
- await self._bus.send({'sender': self._name,
- 'event': 'connection closed'})
- self._bus.unregister(self._name)
-
-
-Response = Optional[Tuple[HTTPStatus, Headers, bytes]]
-
-
-class WSServer(BasePlugin):
- async def _handler(self, websocket: WebSocketServerProtocol,
- path: str) -> None:
- connection = Connection(self._bus, websocket, path)
- await connection.run()
-
- async def _process_request(self, path: str,
- request_headers: Headers) -> Response:
- if 'Upgrade' in request_headers:
- return None
- if path == '/':
- path = '/index.html'
- response_headers = Headers()
- response_headers['Server'] = 'controlpi-wsserver websocket server'
- response_headers['Connection'] = 'close'
- file_path = os.path.realpath(os.path.join(self._web_root, path[1:]))
- if os.path.commonpath((self._web_root, file_path)) != self._web_root \
- or not os.path.exists(file_path) \
- or not os.path.isfile(file_path):
- return (HTTPStatus.NOT_FOUND, response_headers,
- f"File '{path}' not found!".encode())
- mime_type = 'application/octet-stream'
- extension = file_path.split('.')[-1]
- if extension == 'html':
- mime_type = 'text/html'
- elif extension == 'js':
- mime_type = 'text/javascript'
- elif extension == 'css':
- mime_type = 'text/css'
- response_headers['Content-Type'] = mime_type
- body = open(file_path, 'rb').read()
- response_headers['Content-Length'] = str(len(body))
- return HTTPStatus.OK, response_headers, body
-
- def _process_conf(self, conf: PluginConfiguration) -> None:
- self._port = 80
- if 'port' in conf:
- self._port = conf['port']
- else:
- print(f"'port' not configured for WSServer '{self._name}'."
- " Using 80.")
- web_root = 'web'
- if 'web root' in conf:
- web_root = conf['web root']
- else:
- print(f"'web root' not configured for WSServer '{self._name}'."
- " Using 'web'.")
- self._web_root = os.path.realpath(os.path.join(os.getcwd(),
- web_root))
- super()._process_conf(conf)
-
- async def run(self) -> None:
- await super().run()
- await serve(self._handler, port=self._port,
- process_request=self._process_request)
- print(f"WSServer '{self._name}' serving on port {self._port}.")
--- /dev/null
+"""Provide websocket server plugin.
+
+…
+
+TODO: documentation, doctests
+TODO: Mount multiple web apps from packages and file paths
+TODO: Let Debug web app collapse/expand nested structures
+TODO: Make Debug web app work with nested structures in commands
+"""
+import os
+import json
+from websockets import (WebSocketServerProtocol, ConnectionClosedOK,
+ ConnectionClosedError, serve)
+from websockets.http import Headers
+from http import HTTPStatus
+from typing import Optional, Tuple
+
+from controlpi import BasePlugin, MessageBus, Message, MessageTemplate
+
+
+class Connection:
+ """Connection to websocket.
+
+ Instances are created on external connections to websocket.
+ """
+
+ def __init__(self, bus: MessageBus,
+ websocket: WebSocketServerProtocol) -> None:
+ """Initialise conncection.
+
+ Message bus and websocket are set by websocket server when creating
+ the connection.
+ """
+ self._bus = bus
+ self._websocket = websocket
+ address = self._websocket.remote_address
+ self._address = address[0]
+ self._port = address[1]
+ self._name = f"{self._address}:{self._port}"
+ self._bus.register(self._name, 'WSServer',
+ [MessageTemplate()], [MessageTemplate()],
+ self._receive)
+
+ async def _receive(self, message: Message) -> None:
+ """Receive messages from bus and relay to websocket."""
+ json_message = json.dumps(message)
+ try:
+ await self._websocket.send(json_message)
+ except (ConnectionClosedOK, ConnectionClosedError):
+ pass
+
+ async def run(self):
+ """Listen on websocket and relay messages to bus."""
+ await self._bus.send({'sender': self._name,
+ 'event': 'connection opened',
+ 'address': self._address,
+ 'port': self._port})
+ try:
+ async for json_message in self._websocket:
+ original_message = json.loads(json_message)
+ message = {'sender': self._name}
+ message.update(original_message)
+ await self._bus.send(message)
+ except (ConnectionClosedOK, ConnectionClosedError):
+ pass
+ await self._bus.send({'sender': self._name,
+ 'event': 'connection closed'})
+ self._bus.unregister(self._name)
+
+
+Response = Optional[Tuple[HTTPStatus, Headers, bytes]]
+
+
+class WSServer(BasePlugin):
+ """Websocket server as ControlPi plugin.
+
+ Run websocket server on host and port given in configuration, serving
+ the contents in given web root.
+ """
+
+ CONF_SCHEMA = {'properties': {'host': {'type': 'string'},
+ 'port': {'type': 'integer'},
+ 'web root': {'type': 'string'}}}
+ """Schema for WServer plugin configuration.
+
+ Optional configuration keys:
+
+ - 'host': network interfaces to listen on (default: None, meaning all
+ interfaces)
+ - 'port': port to connect to (default: 80)
+ - 'web root': root of files to serve (default: 'web')
+ """
+
+ async def _handler(self, websocket: WebSocketServerProtocol,
+ path: str) -> None:
+ connection = Connection(self.bus, websocket)
+ await connection.run()
+
+ async def _process_request(self, path: str,
+ request_headers: Headers) -> Response:
+ if 'Upgrade' in request_headers:
+ return None
+ if path == '/':
+ path = '/index.html'
+ response_headers = Headers()
+ response_headers['Server'] = 'controlpi-wsserver websocket server'
+ response_headers['Connection'] = 'close'
+ file_path = os.path.realpath(os.path.join(self._web_root, path[1:]))
+ if os.path.commonpath((self._web_root, file_path)) != self._web_root \
+ or not os.path.exists(file_path) \
+ or not os.path.isfile(file_path):
+ return (HTTPStatus.NOT_FOUND, response_headers,
+ f"File '{path}' not found!".encode())
+ mime_type = 'application/octet-stream'
+ extension = file_path.split('.')[-1]
+ if extension == 'html':
+ mime_type = 'text/html'
+ elif extension == 'js':
+ mime_type = 'text/javascript'
+ elif extension == 'css':
+ mime_type = 'text/css'
+ response_headers['Content-Type'] = mime_type
+ body = open(file_path, 'rb').read()
+ response_headers['Content-Length'] = str(len(body))
+ return HTTPStatus.OK, response_headers, body
+
+ def process_conf(self) -> None:
+ """Get host, port and path settings from configuration."""
+ self._port = 80
+ if 'port' in self.conf:
+ self._port = self.conf['port']
+ else:
+ print(f"'port' not configured for WSServer '{self.name}'."
+ " Using 80.")
+ web_root = 'web'
+ if 'web root' in self.conf:
+ web_root = self.conf['web root']
+ else:
+ print(f"'web root' not configured for WSServer '{self.name}'."
+ " Using 'web'.")
+ self._web_root = os.path.realpath(os.path.join(os.getcwd(),
+ web_root))
+
+ async def run(self) -> None:
+ """Set up websocket server."""
+ await serve(self._handler, port=self._port,
+ process_request=self._process_request)
+ print(f"WSServer '{self.name}' serving on port {self._port}.")