├── requirements.txt ├── pydiscover ├── example.cfg ├── example.jpg ├── utils.py ├── __init__.py ├── client.py └── server.py ├── .github ├── FUNDING.yml └── .github │ └── FUNDING.yml ├── CHANGELOG ├── MANIFEST.in ├── .gitignore ├── __init__.py ├── LICENSE ├── setup.py └── README.rst /requirements.txt: -------------------------------------------------------------------------------- 1 | pyaes -------------------------------------------------------------------------------- /pydiscover/example.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | services = 10.0.0.1 3 | net_password = asfi0j9ask123 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cr0hn] 4 | -------------------------------------------------------------------------------- /pydiscover/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/PyDiscover/HEAD/pydiscover/example.jpg -------------------------------------------------------------------------------- /.github/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cr0hn] 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 1.0.0 2 | ============= 3 | 4 | Improvements and fixes 5 | ---------------------- 6 | 7 | - First release. 8 | 9 | New features 10 | ------------ 11 | 12 | - First release. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE CHANGELOG README.rst requirements.txt 2 | 3 | recursive-exclude * __pycache__ 4 | recursive-exclude * *.pyc 5 | recursive-exclude * *.pyo 6 | recursive-exclude * *.orig 7 | recursive-exclude * .DS_Store 8 | global-exclude __pycache__/* 9 | global-exclude .deps/* 10 | global-exclude *.so 11 | global-exclude *.pyd 12 | global-exclude *.pyc 13 | global-exclude .git* 14 | global-exclude .DS_Store 15 | global-exclude .mailmap -------------------------------------------------------------------------------- /pydiscover/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pyaes 4 | 5 | 6 | def prepare_text(password): 7 | if len(password) < 32: 8 | return "%s%s" % (password, "".join("0" for _ in range(32 - len(password)))) 9 | else: 10 | return password[:32] 11 | 12 | 13 | def _get_crypter(password): 14 | try: 15 | dc = pyaes.AESModeOfOperationCTR(password.encode(errors="ignore")) 16 | except TypeError: 17 | dc = pyaes.AESModeOfOperationCTR(password) 18 | 19 | return dc 20 | 21 | 22 | def crypt(text, password): 23 | if not password: 24 | return text.encode(errors="ignore") 25 | 26 | try: 27 | return _get_crypter(password).encrypt(text) 28 | except (TypeError, AttributeError): 29 | return _get_crypter(password).encrypt(text.encode(errors="ignore")) 30 | 31 | 32 | def decrypt(text, password): 33 | if not password: 34 | return text.decode(errors="ignore") 35 | 36 | try: 37 | return _get_crypter(password).decrypt(text).decode(errors="ignore") 38 | except (TypeError, AttributeError): 39 | return _get_crypter(password).decrypt(text.decode()) 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | .idea 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | doc/en/build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | ### VirtualEnv template 60 | # Virtualenv 61 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 62 | .Python 63 | [Bb]in 64 | [Ii]nclude 65 | [Ss]cripts 66 | pyvenv.cfg 67 | pip-selfcheck.json 68 | 69 | # Created by .ignore support plugin (hsz.mobi) 70 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyDiscover - https://github.com/cr0hn/pydiscover 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the 9 | # following disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # -------------------------------------------------------------------------------- /pydiscover/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyDiscover - https://github.com/cr0hn/pydiscover 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the 9 | # following disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) - cr0hn[at]cr0hn.com 2 | 3 | Project home: https://github.com/cr0hn/pydiscover 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of nor the names of its contributors may be used 16 | to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyDiscover - https://github.com/cr0hn/pydiscover 4 | # 5 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | # following conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the 9 | # following disclaimer. 10 | # 11 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 12 | # following disclaimer in the documentation and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 15 | # products derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | 26 | import sys 27 | 28 | from os.path import dirname, join 29 | from setuptools import setup, find_packages 30 | 31 | # Import requirements 32 | with open(join(dirname(__file__), 'requirements.txt')) as f: 33 | required = f.read().splitlines() 34 | 35 | setup( 36 | name='pydiscover', 37 | version="1.0.1", 38 | install_requires=required, 39 | url='https://github.com/cr0hn/pydiscover', 40 | license='BSD', 41 | author='Daniel Garcia (cr0hn) - @ggdaniel', 42 | author_email='cr0hn@cr0hn.com', 43 | packages=find_packages(), 44 | include_package_data=True, 45 | entry_points={'console_scripts': [ 46 | 'pydiscover-server = pydiscover.server:main', 47 | 'pydiscover-client = pydiscover.client:main' 48 | ]}, 49 | description='Simple Secure and Lightweight Python Service Discovery ', 50 | long_description=open('README.rst', "r").read(), 51 | classifiers=[ 52 | 'Environment :: Console', 53 | 'Intended Audience :: System Administrators', 54 | 'Intended Audience :: Other Audience', 55 | 'License :: OSI Approved :: BSD License', 56 | 'Operating System :: MacOS', 57 | 'Operating System :: Microsoft :: Windows', 58 | 'Operating System :: POSIX', 59 | 'Programming Language :: Python :: 3', 60 | 'Topic :: Security', 61 | ] 62 | ) 63 | -------------------------------------------------------------------------------- /pydiscover/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | import logging 4 | import argparse 5 | import datetime 6 | 7 | from pydiscover.utils import prepare_text, crypt, decrypt 8 | 9 | logging.basicConfig(level=logging.INFO, format='[%(levelname)s - Client Discovery ] %(asctime)s - %(message)s') 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | # 14 | # This code is based in: http://stackoverflow.com/a/21090815 15 | # 16 | 17 | class TimeStampException(Exception): 18 | pass 19 | 20 | 21 | class PasswordMagicException(Exception): 22 | pass 23 | 24 | 25 | class TimeOutException(Exception): 26 | pass 27 | 28 | 29 | def discover(magic="fna349fn", port=50000, password=None, timeout=5): 30 | log.info("Looking for a server discovery") 31 | 32 | # Prepare password 33 | if password: 34 | password = prepare_text(password) 35 | 36 | # Build message 37 | msg = "%s%s" % (magic, datetime.datetime.now().timestamp()) 38 | 39 | try: 40 | # Send discover 41 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # create UDP socket 42 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # this is a broadcast socket 43 | s.sendto(crypt(msg, password), ('', port)) 44 | s.settimeout(timeout) 45 | 46 | data, addr = s.recvfrom(1024) # wait for a packet 47 | except socket.timeout: 48 | log.info("No servers found") 49 | 50 | raise TimeOutException("No servers found") 51 | 52 | msg = decrypt(data, password) 53 | 54 | # Get a correlates response 55 | if msg.startswith(magic): 56 | msg_details = msg[len(magic):] 57 | 58 | log.debug("Got service announcement from '%s' with response: %s" % ("%s:%s" % addr, msg_details)) 59 | 60 | if msg_details.startswith("#ERROR#"): 61 | error_details = msg_details[len("#ERROR#"):] 62 | 63 | log.debug("Response from server: %s" % error_details) 64 | 65 | if "timestamp" in error_details: 66 | raise TimeStampException(error_details) 67 | elif "password" in error_details: 68 | raise PasswordMagicException(error_details) 69 | else: 70 | undecoded_msg = msg_details[len("#OK#"):] 71 | 72 | # Decode the json 73 | ok_details = json.loads(undecoded_msg) 74 | 75 | return ok_details, "%s:%s" % addr 76 | 77 | 78 | def main(): 79 | parser = argparse.ArgumentParser(description='PyDiscover Client', 80 | formatter_class=argparse.RawTextHelpFormatter) 81 | 82 | # Main options 83 | parser.add_argument('-m', '--magic', dest="MAGIC", help="preamble for streams.", default="fna349fn") 84 | parser.add_argument('-p', '--port', dest="PORT", type=int, help="listen port. Default 50000", default=50000) 85 | parser.add_argument("-v", "--verbosity", dest="VERBOSE", action="count", help="verbosity level: -v, -vv, -vvv.", 86 | default=2) 87 | 88 | gr_options = parser.add_argument_group("more options") 89 | 90 | gr_options.add_argument('-t', '--timeout', dest="TIMEOUT", type=int, 91 | help="timeout to wait for a server. Default 5s", 92 | default=5) 93 | gr_options.add_argument('--password', dest="PASSWORD", help="server access password. Default None", default=None) 94 | 95 | parsed_args = parser.parse_args() 96 | 97 | # Setting 98 | log.setLevel(abs(50 - (parsed_args.VERBOSE * 10))) 99 | 100 | # Call server 101 | try: 102 | response, server = discover(magic=parsed_args.MAGIC, 103 | port=parsed_args.PORT, 104 | password=parsed_args.PASSWORD, 105 | timeout=parsed_args.TIMEOUT) 106 | 107 | log.info("Discovered server: '%s - Response: \"%s\"" % (server, str(response))) 108 | except Exception as e: 109 | log.info(e) 110 | 111 | if __name__ == '__main__': 112 | main() 113 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyDiscover 2 | ========== 3 | 4 | *PyDiscover: Simple Secure and Lightweight Python Service Discovery* 5 | 6 | :Codename: ZaRZaner0 7 | :Version: 1.0 8 | :Code: https://github.com/cr0hn/pydiscover 9 | :Issues: https://github.com/cr0hn/pydiscover/issues/ 10 | :Python version: Python 3.4 and above 11 | :Author: Daniel Garcia (cr0hn) - @ggdaniel 12 | 13 | Support this project 14 | -------------------- 15 | 16 | Support this project (to solve issues, new features...) by applying the Github "Sponsor" button. 17 | 18 | What's PyDiscover 19 | ----------------- 20 | 21 | PyDiscover is a simple service discovery client and server, designed with simplicity, performance and security in mind. Instead of implement SSDP protocol or any else, use a very simple mechanism: send to the clients information as a JSON format. 22 | 23 | PyDiscover is very flexible and lightweight, and incorporates a cypher mechanism to secure (password based) the communication between server and clients. 24 | 25 | Features 26 | -------- 27 | 28 | - Simple usage. 29 | - Configurable multicast service discovery. 30 | - Password protected access to server info (optional). 31 | - AES encryption (if you defines a password). 32 | - Custom channel definition. 33 | - High server performance, based in the new Python asyncio module. 34 | - Server can spread any information to clients. This information are sent/received as a JSON format. 35 | - Simple configuration file 36 | 37 | Install 38 | ------- 39 | 40 | Install is so easy: 41 | 42 | .. code-block:: bash 43 | 44 | # python3.4 -m pip install pydiscover 45 | 46 | How it works? 47 | ------------- 48 | 49 | **Architecture**: 50 | 51 | PyDiscover is composed by client and server: 52 | 53 | - Server listen for multicast clients request in the port 50000 (by default). 54 | - Clients send requests using a multicast address to the port 50000. 55 | 56 | **Virtual Channels (or magic)**: 57 | 58 | Client and server must transmit information in the same *virtual channel (or magic)*. The magic is a pre-shared word that server/client known. Only messages with this word will be attended, performing the "virtual channel". 59 | 60 | **Hidden mode**: 61 | 62 | By default (per security reasons) server runs as **hidden mode**. This is: if server receives a messages without the correct magic or with wrong password, doesn't answer nothing to the client request. If we want that server answer with error message, we'll activate explicitly. 63 | 64 | **Securing communication** 65 | 66 | We can set a password for the server. When it's set, the information will be sent cyphered using AES to the clients. Only in the clients known the password could be understand the messages. 67 | 68 | **Sent/received information** 69 | 70 | Server must be started with *-d* param. This param referees to a *.cfg* file. This file must have the format: 71 | 72 | .. code-block:: ini 73 | 74 | [DEFAULT] 75 | services = 10.0.0.1 76 | net_password = asfi0j9ask123 77 | 78 | - A *[DEFAULT]* section. 79 | - Any information as: *key = value*. 80 | 81 | The DEFAULT section content will be sent as a JSON format to the clients. 82 | 83 | Usage 84 | ----- 85 | 86 | **Server** 87 | 88 | Starting server in port 40000, with a password an the virtual channel is build by word: "askskAls828": 89 | 90 | .. code-block:: bash 91 | 92 | # pydiscover-server -p 40000 --password 1238d8KKls_jj -m askskAls828 -d example.cfg 93 | 94 | Disablind hidden mode: 95 | 96 | .. code-block:: bash 97 | 98 | # pydiscover-server -p 40000 --password 1238d8KKls_jj -m askskAls828 -d example.cfg --disable-hidden 99 | 100 | You can see more examples typing: 101 | 102 | .. code-block:: bash 103 | 104 | # pydiscover-server -h 105 | 106 | **Client** 107 | 108 | Connecting to the server with the above configuration: 109 | 110 | .. code-block:: bash 111 | 112 | # pydiscover-client -p 40000 --password 1238d8KKls_jj -m askskAls828 -v 113 | 114 | Real example 115 | ------------ 116 | 117 | .. image:: https://raw.githubusercontent.com/cr0hn/pydiscover/master/pydiscover/example.jpg 118 | 119 | What's new? 120 | ----------- 121 | 122 | Version 1.0.0 123 | +++++++++++++ 124 | 125 | - First version released 126 | 127 | License 128 | ------- 129 | 130 | PyDiscover is released under BSD licence. 131 | -------------------------------------------------------------------------------- /pydiscover/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import asyncio 4 | import logging 5 | import argparse 6 | import datetime 7 | import configparser 8 | 9 | from pydiscover.utils import prepare_text, crypt, decrypt 10 | from socket import gethostbyname, gethostname 11 | 12 | logging.basicConfig(level=logging.INFO, format='[ %(levelname)-5s - Service Discovery ] %(asctime)s - %(message)s') 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class DiscoverServerProtocol: 17 | 18 | magic = None 19 | my_ip = None 20 | password = None 21 | answer = None 22 | disable_hidden = False 23 | 24 | def connection_made(self, transport): 25 | self.transport = transport 26 | 27 | def datagram_received(self, data, addr): 28 | message = decrypt(data, self.password) 29 | 30 | # Get a correlates query 31 | if message.startswith(self.magic): 32 | 33 | # Check timestamp and if there's more than 20 second between send -> not response 34 | try: 35 | timestamp = float(message[len(self.magic):]) 36 | except TypeError: 37 | log.debug('Received invalid timestamp in package from %s' % ("%s:%s" % addr)) 38 | if self.disable_hidden: 39 | self.transport.sendto(crypt("%s#ERROR#%s" % (self.magic, "Invalid timestamp"), self.password), addr) 40 | return 41 | 42 | # Check if packet was generated before 20 seconds 43 | if datetime.datetime.fromtimestamp(timestamp) < (datetime.datetime.now() - datetime.timedelta(seconds=20)): 44 | if self.disable_hidden: 45 | self.transport.sendto(crypt("%s#ERROR#%s" % (self.magic, "Timestamp is too old"), self.password), addr) 46 | log.debug('Received outdated package from %s' % ("%s:%s" % addr)) 47 | return 48 | 49 | # Timestamp is correct -> continue 50 | log.debug('Received %r from %s' % (message, "%s:%s" % addr)) 51 | 52 | self.transport.sendto(crypt("%s#OK#%s" % (self.magic, self.answer), self.password), addr) 53 | else: 54 | if self.disable_hidden: 55 | self.transport.sendto(("%s#ERROR#%s" % (self.magic, "Invalid MAGIC or Password")).encode(), addr) 56 | log.debug('Received bad magic or password from %s:%s' % addr) 57 | 58 | 59 | def server_discover(answer, magic="fna349fn", listen_ip="0.0.0.0", port=50000, password=None, disable_hidden=False): 60 | 61 | my_ip = gethostbyname(gethostname()) 62 | 63 | log.info("Starting discover server") 64 | 65 | # Prepare password 66 | if password: 67 | password = prepare_text(password) 68 | 69 | # Load the answer as JSON 70 | config = configparser.ConfigParser() 71 | config.read_file(open(answer)) 72 | _answer = json.dumps(dict(config["DEFAULT"])) 73 | 74 | # Setup Protocol 75 | DiscoverServerProtocol.magic = magic 76 | DiscoverServerProtocol.my_ip = my_ip 77 | DiscoverServerProtocol.password = password 78 | DiscoverServerProtocol.disable_hidden = disable_hidden 79 | DiscoverServerProtocol.answer = _answer 80 | 81 | # Start running 82 | loop = asyncio.get_event_loop() 83 | 84 | listen = loop.create_datagram_endpoint(DiscoverServerProtocol, 85 | local_addr=(listen_ip, port), 86 | allow_broadcast=True) 87 | transport, protocol = loop.run_until_complete(listen) 88 | 89 | try: 90 | loop.run_forever() 91 | except KeyboardInterrupt: 92 | pass 93 | finally: 94 | log.info("Shutdown server") 95 | transport.close() 96 | loop.close() 97 | 98 | 99 | def main(): 100 | 101 | example = """ 102 | Examples: 103 | 104 | Put server to listen and return the Service Register Server IP: 105 | %(name)s -d 10.0.0.1 106 | 107 | Put server listen and return various Services Registers Servers: 108 | %(name)s -d 10.0.0.1,192.168.1.1 109 | 110 | Increase verbosity: 111 | %(name)s -vvv -d 10.0.0.1 112 | 113 | Securing channel communication setting a password: 114 | %(name)s -vvv -d 10.0.0.1 --password "lasi)8sn;k18s7hfalsk" 115 | 116 | Changing channel port: 117 | %(name)s -vvv -d 10.0.0.1 -p 91882 --password "lasi)8sn;k18s7hfalsk" 118 | 119 | Changing the MAGIC: 120 | %(name)s -vvv -m Iksjj19k2j -d 10.0.0.1 -p 91882 --password "lasi)8sn;k18s7hfalsk" 121 | 122 | Disabling hidden mode: 123 | %(name)s -vvv --disable-hidden -m Iksjj19k2j -d 10.0.0.1 -p 91882 --password "lasi)8sn;k18s7hfalsk" 124 | 125 | * MAGIC: 126 | 127 | The MAGIC is a string that defines an internal "channel". Client and server must known the channel. The server only 128 | responds to the clients with known this MAGIC. 129 | 130 | * Hidden mode: 131 | 132 | By default, the server doesn't response to the clients with ack with and invalid MAGIC, PASSWORD or messages that was 133 | sent more than 20 seconds before the server receive it. If we disable the hidden mode, server will respond this the 134 | appropriate error to the client. 135 | """ % dict(name="discovery-server") 136 | 137 | parser = argparse.ArgumentParser(description='PyDiscover Server', 138 | formatter_class=argparse.RawTextHelpFormatter, epilog=example) 139 | 140 | # Main options 141 | parser.add_argument('-d', '--discover-info', dest="INFO", help="file with info to send to the clients", 142 | required=True) 143 | parser.add_argument("-v", "--verbosity", dest="VERBOSE", action="count", help="verbosity level: -v, -vv, -vvv.", 144 | default=3) 145 | 146 | gr_options = parser.add_argument_group("more options") 147 | parser.add_argument('-m', '--magic', dest="MAGIC", help="preamble for streams.", default="fna349fn") 148 | gr_options.add_argument('--password', dest="PASSWORD", help="server access password. Default None", default=None) 149 | parser.add_argument('-p', '--port', dest="PORT", type=int, help="listen port. Default 50000", default=50000) 150 | parser.add_argument('-l', '--listen', dest="IP", help="listen IP. Default 0.0.0.0", default="0.0.0.0") 151 | parser.add_argument('--disable-hidden', dest="NO_HIDDEN", action="store_true", help="disable hidden mode", default=False) 152 | 153 | parsed_args = parser.parse_args() 154 | 155 | # Setting 156 | log.setLevel(abs(50 - (parsed_args.VERBOSE * 10))) 157 | 158 | # Call server 159 | server_discover(answer=parsed_args.INFO, 160 | magic=parsed_args.MAGIC, 161 | listen_ip=parsed_args.IP, 162 | port=parsed_args.PORT, 163 | password=parsed_args.PASSWORD, 164 | disable_hidden=parsed_args.NO_HIDDEN) 165 | 166 | if __name__ == '__main__': 167 | main() 168 | --------------------------------------------------------------------------------