From e1ed7f681c652bef8263175d4b0c20d47047d741 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 12 Dec 2019 00:11:27 +0100 Subject: [PATCH] Added bluetooth OBEX file browser service (see #89) --- platypush/backend/bluetooth/__init__.py | 99 +++++++++++++++++++++++ platypush/backend/bluetooth/fileserver.py | 92 +++++++++++++++++++++ platypush/backend/bluetooth/pushserver.py | 96 ++-------------------- platypush/message/event/bluetooth.py | 8 ++ 4 files changed, 205 insertions(+), 90 deletions(-) create mode 100644 platypush/backend/bluetooth/fileserver.py diff --git a/platypush/backend/bluetooth/__init__.py b/platypush/backend/bluetooth/__init__.py index e69de29bb..4e67b955d 100644 --- a/platypush/backend/bluetooth/__init__.py +++ b/platypush/backend/bluetooth/__init__.py @@ -0,0 +1,99 @@ +import os +import time + +# noinspection PyPackageRequirements +from PyOBEX import headers, requests, responses +# noinspection PyPackageRequirements +from PyOBEX.server import Server + +from platypush.backend import Backend +from platypush.message.event.bluetooth import BluetoothDeviceConnectedEvent, BluetoothFileReceivedEvent, \ + BluetoothDeviceDisconnectedEvent, BluetoothFilePutRequestEvent + + +class BluetoothBackend(Backend, Server): + _sleep_on_error = 10.0 + + def __init__(self, address: str = '', port: int = None, directory: str = None, whitelisted_addresses=None, + **kwargs): + Backend.__init__(self, **kwargs) + Server.__init__(self, address=address) + self.port = port + self.directory = os.path.join(os.path.expanduser(directory)) + self.whitelisted_addresses = whitelisted_addresses or [] + self._sock = None + + def run(self): + self.logger.info('Starting bluetooth service [address={}] [port={}]'.format( + self.address, self.port)) + + while not self.should_stop(): + try: + # noinspection PyArgumentList + self._sock = self.start_service(self.port) + self.serve(self._sock) + except Exception as e: + self.logger.error('Error on bluetooth connection [address={}] [port={}]: {}'.format( + self.address, self.port, str(e))) + time.sleep(self._sleep_on_error) + finally: + self.stop() + + def stop(self): + if self._sock: + self.stop_service(self._sock) + self._sock = None + + def put(self, socket, request): + name = "" + body = "" + + while True: + for header in request.header_data: + if isinstance(header, headers.Name): + name = header.decode() + self.logger.info("Receiving {}".format(name)) + elif isinstance(header, headers.Length): + length = header.decode() + self.logger.info("Content length: {} bytes".format(length)) + elif isinstance(header, headers.Body): + body += header.decode() + elif isinstance(header, headers.End_Of_Body): + body += header.decode() + + if request.is_final(): + break + + # Ask for more data. + Server.send_response(self, socket, responses.Continue()) + + # Get the next part of the data. + request = self.request_handler.decode(socket) + + Server.send_response(self, socket, responses.Success()) + name = os.path.basename(name.strip("\x00")) + path = os.path.join(self.directory, name) + + self.logger.info("Writing file {}" .format(path)) + open(path, "wb").write(body.encode()) + self.bus.post(BluetoothFileReceivedEvent(path=path)) + + def process_request(self, connection, request, *address): + if isinstance(request, requests.Connect): + self.connect(connection, request) + self.bus.post(BluetoothDeviceConnectedEvent(address=address[0], port=address[1])) + elif isinstance(request, requests.Disconnect): + self.disconnect(connection) + self.bus.post(BluetoothDeviceDisconnectedEvent(address=address[0], port=address[1])) + elif isinstance(request, requests.Put): + self.bus.post(BluetoothFilePutRequestEvent(address=address[0], port=address[1])) + self.put(connection, request) + else: + self._reject(connection) + self.bus.post(BluetoothFilePutRequestEvent(address=address[0], port=address[1])) + + def accept_connection(self, address, port): + return address in self.whitelisted_addresses + + +# vim:sw=4:ts=4:et: diff --git a/platypush/backend/bluetooth/fileserver.py b/platypush/backend/bluetooth/fileserver.py new file mode 100644 index 000000000..c2d212285 --- /dev/null +++ b/platypush/backend/bluetooth/fileserver.py @@ -0,0 +1,92 @@ +import os +import stat + +# noinspection PyPackageRequirements +from PyOBEX import requests, responses, headers +# noinspection PyPackageRequirements +from PyOBEX.server import BrowserServer + +from platypush.backend.bluetooth import BluetoothBackend +from platypush.message.event.bluetooth import BluetoothFileGetRequestEvent + + +class BluetoothFileserverBackend(BluetoothBackend, BrowserServer): + """ + Bluetooth OBEX file server. + Enable it to allow bluetooth devices to browse files on this machine. + + If you run platypush as a non-root user (and you should) then you to change the group owner of the + service discovery protocol file (/var/run/sdp) and add your user to that group. See + `here `_ + for details. + + Requires: + + * **pybluez** (``pip install pybluez``) + * **pyobex** (``pip install git+https://github.com/BlackLight/PyOBEX``) + + """ + + def __init__(self, port: int, address: str = '', directory: str = os.path.expanduser('~'), + whitelisted_addresses: list = None, **kwargs): + """ + :param port: Bluetooth listen port + :param address: Bluetooth address to bind the server to (default: any) + :param directory: Directory to share (default: HOME directory) + :param whitelisted_addresses: If set then only accept connections from the listed device addresses + """ + BluetoothBackend.__init__(self, address=address, port=port, directory=directory, + whitelisted_addresses=whitelisted_addresses, **kwargs) + + if not os.path.isdir(self.directory): + raise FileNotFoundError(self.directory) + + def process_request(self, socket, request, *address): + if isinstance(request, requests.Get): + self.bus.post(BluetoothFileGetRequestEvent(address=address[0], port=address[1])) + self.get(socket, request) + else: + super().process_request(socket, request, *address) + + def get(self, socket, request): + name = "" + req_type = "" + + for header in request.header_data: + if isinstance(header, headers.Name): + name = header.decode().strip("\x00") + self.logger.info("Receiving request for {}".format(name)) + elif isinstance(header, headers.Type): + req_type = header.decode().strip("\x00") + self.logger.info("Request type: {}".format(req_type)) + + path = os.path.abspath(os.path.join(self.directory, name)) + + if os.path.isdir(path) or req_type == "x-obex/folder-listing": + if path.startswith(self.directory): + filelist = os.listdir(path) + s = '\n\n' + + for i in filelist: + objpath = os.path.join(path, i) + if os.path.isdir(objpath): + s += ' '.format(i, os.stat(objpath)[stat.ST_CTIME]) + else: + s += ' '.format( + i, os.stat(objpath)[stat.ST_CTIME], os.stat(objpath)[stat.ST_SIZE]) + + s += "\n" + self.logger.debug('Bluetooth get XML output:\n' + s) + + response = responses.Success() + response_headers = [headers.Name(name.encode("utf8")), + headers.Length(len(s)), + headers.Body(s.encode("utf8"))] + BrowserServer.send_response(self, socket, response, response_headers) + else: + self._reject(socket) + else: + self._reject(socket) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/backend/bluetooth/pushserver.py b/platypush/backend/bluetooth/pushserver.py index 6ff5c7be6..e3ec450ab 100644 --- a/platypush/backend/bluetooth/pushserver.py +++ b/platypush/backend/bluetooth/pushserver.py @@ -1,15 +1,12 @@ import os -import time # noinspection PyPackageRequirements -from PyOBEX import headers, requests, responses, server +from PyOBEX.server import PushServer -from platypush.backend import Backend -from platypush.message.event.bluetooth import BluetoothDeviceConnectedEvent, BluetoothFileReceivedEvent, \ - BluetoothDeviceDisconnectedEvent, BluetoothFilePutRequestEvent +from platypush.backend.bluetooth import BluetoothBackend -class BluetoothPushserverBackend(Backend, server.PushServer): +class BluetoothPushserverBackend(BluetoothBackend, PushServer): """ Bluetooth OBEX push server. Enable it to allow bluetooth file transfers from other devices. @@ -37,95 +34,14 @@ class BluetoothPushserverBackend(Backend, server.PushServer): :param directory: Destination directory where files will be downloaded (default: ~/bluetooth) :param whitelisted_addresses: If set then only accept connections from the listed device addresses """ - Backend.__init__(self, **kwargs) - server.PushServer.__init__(self, address=address) - - self.port = port - self.directory = os.path.join(os.path.expanduser(directory)) - self.whitelisted_addresses = whitelisted_addresses or [] - self._sock = None + BluetoothBackend.__init__(self, address=address, port=port, directory=directory, + whitelisted_addresses=whitelisted_addresses, **kwargs) def run(self): - super().run() - if not os.path.isdir(self.directory): os.makedirs(self.directory, exist_ok=True) - self.logger.info('Started bluetooth push service [address={}] [port={}]'.format( - self.address, self.port)) - - while not self.should_stop(): - try: - self._sock = self.start_service(self.port) - self.serve(self._sock) - except Exception as e: - self.logger.error('Error on bluetooth connection [address={}] [port={}]: {}'.format( - self.address, self.port, str(e))) - time.sleep(self._sleep_on_error) - finally: - self.stop() - - def stop(self): - if self._sock: - self.stop_service(self._sock) - self._sock = None - - def put(self, socket, request): - name = "" - body = "" - - while True: - for header in request.header_data: - if isinstance(header, headers.Name): - name = header.decode() - self.logger.info("Receiving {}".format(name)) - elif isinstance(header, headers.Length): - length = header.decode() - self.logger.info("Content length: {} bytes".format(length)) - elif isinstance(header, headers.Body): - body += header.decode() - elif isinstance(header, headers.End_Of_Body): - body += header.decode() - - if request.is_final(): - break - - # Ask for more data. - self.send_response(socket, responses.Continue()) - - # Get the next part of the data. - request = self.request_handler.decode(socket) - - self.send_response(socket, responses.Success()) - name = os.path.basename(name.strip("\x00")) - path = os.path.join(self.directory, name) - - self.logger.info("Writing file {}" .format(path)) - open(path, "wb").write(body.encode()) - self.bus.post(BluetoothFileReceivedEvent(path=path)) - - def process_request(self, connection, request, *address): - """Processes the request from the connection. - - This method should be reimplemented in subclasses to add support for - more request types. - """ - - if isinstance(request, requests.Connect): - self.connect(connection, request) - self.bus.post(BluetoothDeviceConnectedEvent(address=address[0], port=address[1])) - elif isinstance(request, requests.Disconnect): - self.disconnect(connection) - self.bus.post(BluetoothDeviceDisconnectedEvent(address=address[0], port=address[1])) - elif isinstance(request, requests.Put): - self.bus.post(BluetoothFilePutRequestEvent(address=address[0], port=address[1])) - self.put(connection, request) - else: - self._reject(connection) - self.bus.post(BluetoothFilePutRequestEvent(address=address[0], port=address[1])) - - def accept_connection(self, address, port): - return address in self.whitelisted_addresses + super().run() # vim:sw=4:ts=4:et: diff --git a/platypush/message/event/bluetooth.py b/platypush/message/event/bluetooth.py index f5ed03b45..dd5940782 100644 --- a/platypush/message/event/bluetooth.py +++ b/platypush/message/event/bluetooth.py @@ -37,6 +37,14 @@ class BluetoothFilePutRequestEvent(Event): super().__init__(*args, address=address, port=port, **kwargs) +class BluetoothFileGetRequestEvent(Event): + """ + Event triggered on bluetooth device file transfer get request + """ + def __init__(self, address: str = None, port: str = None, *args, **kwargs): + super().__init__(*args, address=address, port=port, **kwargs) + + class BluetoothFileReceivedEvent(Event): """ Event triggered on bluetooth device file transfer put request