diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb7c163 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ + +# Created by https://www.gitignore.io/api/python,virtualenv,visualstudiocode +# Edit at https://www.gitignore.io/?templates=python,virtualenv,visualstudiocode + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Python Patch ### +.venv/ + +### VirtualEnv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + +### VisualStudioCode ### +.vscode/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/python,virtualenv,visualstudiocode \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..32d7dc7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.md +include pysip/templates/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc8a5e5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# PySIP + +CLI para realização de testes na area de VoIP. \ No newline at end of file diff --git a/pysip/__init__.py b/pysip/__init__.py new file mode 100644 index 0000000..a676f05 --- /dev/null +++ b/pysip/__init__.py @@ -0,0 +1,4 @@ +from pysip.cli import cli + + +__all__ = ['cli'] \ No newline at end of file diff --git a/pysip/cli.py b/pysip/cli.py new file mode 100644 index 0000000..56e3b68 --- /dev/null +++ b/pysip/cli.py @@ -0,0 +1,93 @@ +import click +import socket +import logging +from time import time +from random import random +from pprint import pprint + +from pysip.log import logger +from pysip.messages import Message, parse_header + + +@click.group() +def cli(): + ''' + Função que representa a CLI em si. + ''' + ... + + +@cli.group() +@click.option('--debug', type=bool, help='Enable debug mode.') +def server(debug): + ''' + Command that starts the application in SERVER mode. + ''' + logger.debug('DEBUG mode enabled.') + logger.setLevel(logging.DEBUG if debug else logging.INFO) + logger.info('Application started in SERVER mode.') + +@cli.group() +@click.option('--debug', type=bool, help='Enable debug mode.') +def client(debug): + ''' + Command that starts the application in CLIENT mode. + ''' + logger.setLevel(logging.DEBUG if debug else logging.INFO) + logger.debug('DEBUG mode enabled.') + logger.info('Application started in CLIENT mode.') + +@client.command('alg', help='Performs the ALG test with the host informed.') +@click.option('--host', type=str, required=True, help='Host of the test.') +@click.option('--username', type=str, required=True, help='Username of the test.') +@click.option('--domain', type=str, required=True, help='Domain of the test.') +@click.option('--port', type=int, default=5060, help='Port of the test.') +def aug(host, port, username, domain): + logger.info('Performing ALG test.') + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as interface: + interface.settimeout(5) + destiny_address = (host, port) + logger.info(f'Destiny IP: {host}') + logger.info(f'Destiny Port: {port}') + interface.connect(destiny_address) + in_host, in_port = interface.getsockname() + logger.info(f'Internal IP used: {in_host}') + logger.info(f'Internal selected port: {in_port}') + callid = Message.make_hash(str(time() * random())) + to_tag = Message.make_hash(str(time() * random())) + branch = Message.make_hash(str(time() * random())) + logger.debug(f'Call-ID generated for the package: {callid}') + logger.debug(f'To-Tag generated for the package: {to_tag}') + logger.debug(f'Branch-Tag generated for the package: {branch}') + logger.debug('Generating INVITE that will be forwarded to the Host.') + message = Message('invite', **{ + 'address': {'ip': in_host, 'port': in_port }, + 'branch': branch, + 'from': { + 'user': username, + 'domain': domain, + 'tag': to_tag + }, + 'callid': callid + }) + try: + interface.send(message.render) + response = interface.recv(4096).decode('ASCII') + resp_list = [v for v in response.split('\r\n') if v] + result = dict(parse_header(value) for value in resp_list) + logger.info(f'Response title: {result["title"]}') + logger.debug(f'Call-ID response: {result["Call-ID"]}') + logger.debug(f'Response source server: {result["Server"]}') + _, title = result["title"].split('200') + + if title.strip() == 'OK': + logger.info('ALG test completed successfully.') + + else: + logger.warn('Router with ALG detected.') + + except KeyError: + logger.error('Could not parse the response.') + + except socket.timeout: + logger.error('Connection timeout with SipProxy.') diff --git a/pysip/log.py b/pysip/log.py new file mode 100644 index 0000000..4191d78 --- /dev/null +++ b/pysip/log.py @@ -0,0 +1,15 @@ +import logging + + +logger = logging.getLogger(__name__) + +formatter = logging.Formatter( + '[%(asctime)s] - %(levelname)s - %(message)s', + datefmt='%d/%m/%Y %H:%M:%S' +) + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) + +logger.addHandler(ch) diff --git a/pysip/messages.py b/pysip/messages.py new file mode 100644 index 0000000..9c7458e --- /dev/null +++ b/pysip/messages.py @@ -0,0 +1,42 @@ +from os import path +from time import time +from hashlib import md5 + +from jinja2 import Environment, FileSystemLoader + + +here = path.abspath(path.dirname(__file__)) + +class Message: + def __init__(self, name, **kwargs): + self.loader = FileSystemLoader(path.join(here, 'templates')) + self.env = Environment(loader=self.loader) + self.name = name + self.params = kwargs + + @staticmethod + def make_hash(text): + hasher = md5() + hasher.update(text.encode('utf-8')) + return hasher.hexdigest() + + @property + def render(self): + if path.exists(path.join(here, 'templates', self.name + '.txt')): + file = self.name + '.txt' + template = self.env.get_template(file) + message = template.render(self.params) + return message.encode().replace(b'\n', b'\r\n') + + else: + raise FileNotFoundError('Message not Found') + + @render.setter + def render(self): + raise AttributeError('This message is not editable.') + +def parse_header(text): + length = len(text.split(':')) - 1 + result = text.split(':', 1) if length else ['title', text] + chave, valor = result + return (chave.strip(), valor.strip()) diff --git a/pysip/templates/invite.txt b/pysip/templates/invite.txt new file mode 100644 index 0000000..d6f2a2f --- /dev/null +++ b/pysip/templates/invite.txt @@ -0,0 +1,30 @@ +INVITE sip:{{ from['user'] }}@{{ address['ip'] }}:{{ address['port'] }} SIP/2.0 +Via: SIP/2.0/UDP {{ address['ip'] }}:{{ address['port'] }};rport;branch={{ branch }} +Max-Forwards: 100 +From: ;tag={{ from['tag'] }} +To: +Call-ID: {{ callid }} +Alg-test: 0 +CSeq: 1 INVITE +Contact: +User-Agent: PySIP +Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE +Supported: path, replaces +Allow-Events: talk, hold, conference, presence, as-feature-event, dialog, line-seize, call-info, sla, include-session-description, presence.winfo, message-summary, refer +Content-Type: application/sdp +Content-Disposition: session +Content-Length: 281 + +v=0 +o=SIPPulse 1546586511 1546586512 IN IP4 {{ address['ip'] }} +s=SIPPulse +c=IN IP4 {{ address['ip'] }} +t=0 0 +m=audio 17828 RTP/AVP 18 0 8 101 +a=rtpmap:18 G729/8000 +a=fmtp:18 annexb=no +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:101 telephone-event/8000 +a=fmtp:101 0-16 +a=ptime:20 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b98f660 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +click \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..859a3b3 --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +"""PySIP + +CLI para reealização de testes ligados a area de VoIP. +""" +from setuptools import setup, find_packages +from os import path + + +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='PySIP', + version='0.1.3', + description='CLI para testes em VoIP.', + include_package_data=True, + long_description=long_description, + long_description_content_type='text/markdown', + url='https://gitlab.sippulse.com/vitor/pysip/', + author='Vitor Hugo de Oliveira Vargas', + author_email='vitor.hugo@sippulse.com', + classifiers=[ + 'Development Status :: 1 - Planning', + 'Environment :: Console', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Telecommunications Industry', + 'Natural Language :: Portuguese (Brazilian)', + 'Operating System :: Unix', + 'Programming Language :: Python :: 3.7', + 'Topic :: Scientific/Engineering :: Information Analysis', + 'Topic :: System :: Networking' + ], + keywords='VoIP, SIP, Telecomunicações', + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + install_requires=[ + 'click', + 'jinja2' + ], + extras_require={ + 'dev': [ + 'ipython' + ], + }, + entry_points={ + 'console_scripts': [ + 'pysipctl=pysip:cli', + ], + }, + #project_urls={ + # 'Bug Reports': 'https://github.com/pypa/sampleproject/issues', + # 'Source': 'https://github.com/pypa/sampleproject/', + #}, +) \ No newline at end of file