├── MANIFEST.in ├── requirements.txt ├── tests ├── __init__.py ├── models │ ├── __init__.py │ ├── test_base.py │ └── test_router.py ├── test_extractor.py ├── test_runner.py └── test_cli.py ├── croutera ├── __init__.py ├── models │ ├── dlink │ │ ├── __init__.py │ │ └── dir610.py │ ├── __init__.py │ ├── cisco │ │ ├── __init__.py │ │ └── dpc3928s.py │ ├── tplink │ │ ├── __init__.py │ │ ├── wr720n.py │ │ ├── wr340g.py │ │ └── tlwrbased.py │ ├── base.py │ └── routers.py ├── version.py ├── exceptions.py ├── extractor.py ├── runner.py ├── commands.py └── cli.py ├── bin └── croutera ├── .travis.yml ├── Makefile ├── .gitignore ├── LICENSE ├── setup.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | requests==2.20.0 3 | beautifulsoup4==4.4.1 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /croutera/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from croutera.version import version 5 | -------------------------------------------------------------------------------- /bin/croutera: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from croutera import runner 5 | 6 | runner.run() 7 | -------------------------------------------------------------------------------- /croutera/models/dlink/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .dir610 import DLinkDir610 5 | -------------------------------------------------------------------------------- /croutera/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | def version(): 6 | return '0.0.6' 7 | -------------------------------------------------------------------------------- /croutera/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from croutera.models.routers import Routers 5 | -------------------------------------------------------------------------------- /croutera/models/cisco/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from croutera.models.cisco.dpc3928s import CiscoDPC3928S 5 | -------------------------------------------------------------------------------- /croutera/models/tplink/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .wr340g import TplinkWR340 5 | from .wr720n import TplinkWR720N 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | # command to install dependencies 6 | install: "pip install -r requirements.txt" 7 | # command to run tests 8 | script: make test 9 | -------------------------------------------------------------------------------- /croutera/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Custom Croutera Exceptions 6 | """ 7 | 8 | class ModelNotFoundError(Exception): 9 | pass 10 | 11 | class InvalidCommandArgs(Exception): 12 | pass 13 | -------------------------------------------------------------------------------- /croutera/models/tplink/wr720n.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import base64 5 | import requests 6 | 7 | from croutera.models.tplink.tlwrbased import TplinkTLWRBased 8 | 9 | 10 | class TplinkWR720N(TplinkTLWRBased): 11 | """ Implementation for TpLink WR720N 12 | see: http://www.tp-link.com.br/products/details/?model=TL-WR720N 13 | """ 14 | 15 | model = 'wr720n' 16 | 17 | def __init__(self): 18 | self.config['ip'] = '192.168.0.1' 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | setup: 2 | pip install -r requirements.txt 3 | 4 | install: 5 | python setup.py install 6 | 7 | test: 8 | python -m unittest discover 9 | 10 | test3: 11 | python3 -m unittest discover 12 | 13 | clean: 14 | rm -rf croutera.egg-info && rm -rf $(find . -name '*.pyc') 15 | rm -rf build && rm -rf dist 16 | 17 | publish: 18 | python setup.py register -r pypi 19 | python setup.py sdist upload -r pypi 20 | 21 | publish-test: 22 | python setup.py register -r pypitest 23 | python setup.py sdist upload -r pypitest 24 | -------------------------------------------------------------------------------- /croutera/models/tplink/wr340g.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import base64 5 | import requests 6 | import re 7 | 8 | from croutera.models.tplink.tlwrbased import TplinkTLWRBased 9 | from bs4 import BeautifulSoup 10 | 11 | 12 | class TplinkWR340(TplinkTLWRBased): 13 | """ Implementation for TpLink WR340G and WR340GD 14 | see: http://www.tp-link.com.br/products/details/?model=TL-WR340G 15 | """ 16 | 17 | model = "wr340" 18 | 19 | def __init__(self): 20 | self.config['ip'] = '192.168.1.1' 21 | -------------------------------------------------------------------------------- /croutera/extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | def extract_manufacturer(model_param): 6 | 'Extract manufacturer from a given model param [model]' 7 | return model_param.split('-',1)[0] 8 | 9 | def extract_model(model_param): 10 | 'Extract model from a given model param [model]' 11 | return model_param.split('-',1)[1:][0] 12 | 13 | def extract_subclasses(clazz): 14 | 'Extract all subclasses (inclusive inherited) from a given class' 15 | subclasses = [] 16 | for cls in clazz.__subclasses__(): 17 | subclasses.extend([cls] + extract_subclasses(cls)) 18 | return subclasses 19 | -------------------------------------------------------------------------------- /croutera/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | from croutera.cli import ArgsParserBuilder, Cli 7 | 8 | 9 | def run(): 10 | print("Croutera...") 11 | 12 | args = ArgsParserBuilder.build(sys.argv[1:]) 13 | if not Cli.validate(args): 14 | show_help() 15 | return 16 | 17 | cmd = Cli.command(args) 18 | if cmd.valid() and cmd.execute(): 19 | print('Command executed.') 20 | else: 21 | print('Command was not executed.') 22 | 23 | def show_help(): 24 | print('--------HELP-------') 25 | ArgsParserBuilder.build_help() 26 | 27 | if __name__ == '__main__': 28 | run() 29 | -------------------------------------------------------------------------------- /tests/models/test_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import argparse 6 | import mock 7 | 8 | from croutera.models.base import Router 9 | 10 | class TestBase(unittest.TestCase): 11 | 12 | def test_it_should_compose_endpoint(self): 13 | router = StubRouter() 14 | router.config['ip'] = '1.1.1.1' 15 | router.config['uris']['login'] = 'login.html' 16 | self.assertEqual('http://1.1.1.1/login.html', 17 | router.endpoint('login')) 18 | 19 | 20 | class StubRouter(Router): 21 | 22 | manufacturer = 'stub' 23 | model = 'stub' 24 | 25 | def login(self, user, password): 26 | return True 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | .pypirc 59 | -------------------------------------------------------------------------------- /tests/test_extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from unittest import TestCase 5 | from croutera import extractor 6 | 7 | class A(object): 8 | 'Stub classes for test pourposes' 9 | pass 10 | 11 | 12 | class B(A): 13 | 'Stub classes for test pourposes' 14 | pass 15 | 16 | 17 | class C(B): 18 | 'Stub classes for test pourposes' 19 | pass 20 | 21 | 22 | class TestExtract(TestCase): 23 | 24 | def setUp(self): 25 | self.args_model = 'manufacturer-model' 26 | 27 | def test_it_extract_manufacturer(self): 28 | self.assertEqual('manufacturer', 29 | extractor.extract_manufacturer(self.args_model)) 30 | 31 | def test_it_extract_model(self): 32 | self.assertEqual('model', 33 | extractor.extract_model(self.args_model)) 34 | 35 | def test_it_extract_all_subclasses_inclusive_derived(self): 36 | subclasses = extractor.extract_subclasses(A) 37 | self.assertTrue(B in subclasses) 38 | self.assertTrue(C in subclasses) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cristian Oliveira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/test_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for Runner module 6 | """ 7 | 8 | import unittest 9 | import sys 10 | 11 | from mock import Mock, patch 12 | from croutera import runner 13 | 14 | 15 | class RunnerTest(unittest.TestCase): 16 | 17 | @patch('croutera.runner.show_help') 18 | def test_it_runs_with_no_argument(self, show_help): 19 | sys.args = [] 20 | 21 | runner.run() 22 | 23 | show_help.assert_called_with() 24 | 25 | @patch('croutera.runner.Cli') 26 | @patch('croutera.runner.ArgsParserBuilder') 27 | def test_it_does_not_execute_invalid_commands(self, ArgsParserBuilder, Cli): 28 | command = Mock() 29 | command.valid.return_value = False 30 | Cli.command.return_value = command 31 | 32 | runner.run() 33 | 34 | assert not command.execute.called 35 | 36 | @patch('croutera.runner.Cli') 37 | @patch('croutera.runner.ArgsParserBuilder') 38 | def test_it_does_execute_valid_commands(self, ArgsParserBuilder, Cli): 39 | command = Mock() 40 | command.valid.return_value = True 41 | Cli.command.return_value = command 42 | 43 | runner.run() 44 | 45 | assert command.execute.called 46 | -------------------------------------------------------------------------------- /croutera/models/base.py: -------------------------------------------------------------------------------- 1 | 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class Router(object): 5 | __metaclass__ = ABCMeta 6 | 7 | manufacturer = "None" 8 | model = "None" 9 | 10 | config = { 11 | 'ip': '192.168.0.1', 12 | 'uris': { 13 | 'login': '', 14 | 'reboot': '', 15 | 'wifi_settings': '' 16 | } 17 | } 18 | 19 | @abstractmethod 20 | def login(self, user, password): 21 | """ Provide logic to auth into router admin page """ 22 | pass 23 | 24 | def restart(self): 25 | """ Provide logic to restart the router (logged action) """ 26 | self._command_not_implemented() 27 | return False 28 | 29 | def wifi_pass(self): 30 | """ Provide logic to show the wifi password """ 31 | self._command_not_implemented() 32 | return "" 33 | 34 | def _command_not_implemented(self): 35 | print('This command was not implemented for this model.') 36 | 37 | def endpoint(self, uri): 38 | ' provide a url to reach a given endpoint ' 39 | return "http://%s/%s" % (self.config['ip'], self.config['uris'][uri]) 40 | 41 | @classmethod 42 | def as_str(cls): 43 | return "%s-%s" % (cls.manufacturer, cls.model) 44 | -------------------------------------------------------------------------------- /croutera/models/routers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Models routers implementations 6 | """ 7 | 8 | import base64 9 | 10 | from croutera.models.base import Router 11 | from croutera.models.cisco import CiscoDPC3928S 12 | from croutera.models.tplink import TplinkWR340, TplinkWR720N, tlwrbased 13 | from croutera.models.dlink import DLinkDir610 14 | from croutera.exceptions import ModelNotFoundError 15 | from croutera.extractor import extract_subclasses 16 | 17 | class Routers(object): 18 | """ Provide Router.class instances """ 19 | 20 | @staticmethod 21 | def get(manufacturer, model): 22 | models = Routers.from_manufacturer(manufacturer) 23 | router = [r for r in models if r.model == model] 24 | if not router: 25 | raise ModelNotFoundError('Model not found for this manufacturer.') 26 | 27 | return router[0]() 28 | 29 | @staticmethod 30 | def from_manufacturer(manufacturer): 31 | models = [r for r in extract_subclasses(Router) 32 | if r.manufacturer == manufacturer] 33 | 34 | if not models: 35 | raise ModelNotFoundError('Manufacturer not found.') 36 | 37 | return models 38 | 39 | @staticmethod 40 | def list(): 41 | available = [r.as_str() for r in extract_subclasses(Router)] 42 | return sorted(available) 43 | -------------------------------------------------------------------------------- /tests/models/test_router.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for Cli module 6 | """ 7 | 8 | import unittest 9 | import argparse 10 | 11 | import mock 12 | from croutera.models.base import Router 13 | from croutera.models.routers import Routers 14 | from croutera.exceptions import ModelNotFoundError 15 | 16 | class RoutersTest(unittest.TestCase): 17 | 18 | def test_it_returns_all_models_from_manufacturer(self): 19 | manufacturer = AStubRouter.manufacturer 20 | routers = Routers.from_manufacturer(manufacturer) 21 | self.assertTrue(AStubRouter in routers) 22 | self.assertFalse(ZStubRouter in routers) 23 | 24 | def test_it_returns_model_from_manufacturer(self): 25 | manufacturer = AStubRouter.manufacturer 26 | model = AStubRouter.model 27 | router = Routers.get(manufacturer, model) 28 | self.assertIsInstance(router, AStubRouter) 29 | 30 | def test_it_raise_model_not_found_error(self): 31 | self.assertRaises(ModelNotFoundError, Routers.get, 'manufacturer', 'model_invalid') 32 | 33 | def test_it_returns_sorted_models_available(self): 34 | self.assertEqual(AStubRouter.as_str(), Routers.list()[0]) 35 | self.assertEqual(ZStubRouter.as_str(), Routers.list()[-1]) 36 | 37 | class AStubRouter(Router): 38 | manufacturer = 'aaaaa' 39 | model = 'aaaaa' 40 | 41 | def login(self, user, passw): 42 | return True 43 | 44 | class ZStubRouter(Router): 45 | manufacturer = 'zzzzz' 46 | model = 'zzzzz' 47 | pass 48 | -------------------------------------------------------------------------------- /croutera/models/dlink/dir610.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import base64 5 | 6 | from croutera.models.base import Router 7 | import requests 8 | from bs4 import BeautifulSoup 9 | 10 | 11 | class DLinkDir610(Router): 12 | """ Implementation for D-link DR610 13 | see: http://www.dlink.com.br/produto/dir-610-a1 14 | """ 15 | 16 | manufacturer = 'dlink' 17 | model = 'dir610' 18 | 19 | config = { 20 | 'ip': '192.168.100.1', 21 | 'uris': { 22 | 'login': 'login.cgi', 23 | 'reboot': 'form2Reboot.cgi', 24 | 'wifi_settings': 'wlan_basic.htm' 25 | } 26 | } 27 | 28 | def login(self, username, password): 29 | data = { 30 | 'username': username, 31 | 'password': password, 32 | 'submit.htm%3Flogin.htm': 'Send' 33 | } 34 | 35 | self.session = requests.Session() 36 | response = self.session.post(self.endpoint('login'), data = data) 37 | 38 | return response.ok 39 | 40 | def restart(self): 41 | data = { 42 | 'reboot': 'Reboot', 43 | 'submit.htm%3Freboot.htm': 'Send' 44 | } 45 | 46 | response = self.session.post(self.endpoint('reboot'), data = data) 47 | return response.ok 48 | 49 | def wifi_pass(self): 50 | response = self.session.get(self.endpoint('wifi_settings')) 51 | soup = BeautifulSoup(response.content, 'html.parser') 52 | 53 | return soup.find('input', {'name': 'pskValue'}).get('value') 54 | 55 | 56 | -------------------------------------------------------------------------------- /croutera/models/cisco/dpc3928s.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from croutera.models.base import Router 5 | import requests 6 | from bs4 import BeautifulSoup 7 | 8 | 9 | class CiscoDPC3928S(Router): 10 | 11 | manufacturer = 'cisco' 12 | model = 'dpc3928s' 13 | 14 | config = { 15 | 'ip': '192.168.0.1', 16 | 'uris': { 17 | 'login': 'goform/Docsis_system', 18 | 'reboot': 'goform/Devicerestart', 19 | 'wifi_settings': 'Quick_setup.asp' 20 | } 21 | } 22 | 23 | def login(self, username, password): 24 | data = { 25 | 'username_login': username, 26 | 'password_login': password, 27 | 'LanguageSelect': 'en', 28 | 'Language_Submit' :'0', 29 | 'login' :'Log In' 30 | } 31 | 32 | self.password = password 33 | 34 | self.session = requests.Session() 35 | self.session.post(self.endpoint('login'), data = self.login_data) 36 | 37 | def restart(self): 38 | data = { 39 | 'devicerestrat_Password_check': self.password, 40 | 'mtenRestore': 'Device Restart', 41 | 'devicerestart':1, 42 | 'devicerestrat_getUsercheck': '' 43 | } 44 | 45 | res = self.session.post(self.endpoint('reboot'), data = data) 46 | return res.ok 47 | 48 | def wifi_pass(self): 49 | response = self.session.get(self.endpoint('wifi_settings')) 50 | soup = BeautifulSoup(response.content, 'html.parser') 51 | return soup.find('input', {'id':'wl5g_wpa_psk_key'}).get('value') 52 | 53 | 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | from croutera import version 13 | 14 | import os 15 | 16 | def is_package(path): 17 | return ( 18 | os.path.isdir(path) and 19 | os.path.isfile(os.path.join(path, '__init__.py')) 20 | ) 21 | 22 | def find_packages(path, base=""): 23 | """ Find all packages in path """ 24 | packages = {} 25 | for item in os.listdir(path): 26 | dir = os.path.join(path, item) 27 | if is_package( dir ): 28 | if base: 29 | module_name = "%(base)s.%(item)s" % vars() 30 | else: 31 | module_name = item 32 | packages[module_name] = dir 33 | packages.update(find_packages(dir, module_name)) 34 | return packages 35 | 36 | def publish(): 37 | """Publish to PyPi""" 38 | os.system("python setup.py sdist upload") 39 | 40 | if sys.argv[-1] == "publish": 41 | publish() 42 | sys.exit() 43 | 44 | setup(name='croutera', 45 | version= version(), 46 | description='Simple Cli Router Admin', 47 | long_description=open('README.md').read(), 48 | url='https://github.com/cristianoliveira/croutera', 49 | author='Cristian Oliveira', 50 | author_email='contato@cristianoliveira.com.br', 51 | license='MIT', 52 | packages=find_packages('.'), 53 | install_requires=[ 54 | "requests", "beautifulsoup4" 55 | ], 56 | scripts=['bin/croutera'], 57 | zip_safe=False) 58 | -------------------------------------------------------------------------------- /croutera/models/tplink/tlwrbased.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import base64 5 | import requests 6 | import re 7 | 8 | from croutera.models.base import Router 9 | from bs4 import BeautifulSoup 10 | 11 | 12 | class TplinkTLWRBased(Router): 13 | """Implementation for TpLink WR based models. 14 | 15 | For more details see all them: 16 | http://www.tp-link.com.br/search/?q=wr """ 17 | 18 | manufacturer = "tplink" 19 | model = "wrbased" 20 | 21 | config = { 22 | 'ip': '192.168.1.1', 23 | 'uris': { 24 | 'login': '', 25 | 'reboot': 'userRpm/SysRebootRpm.htm?Reboot=Reboot', 26 | 'wifi_settings': 'userRpm/WlanNetworkRpm.htm' 27 | } 28 | } 29 | 30 | def login(self, username, password): 31 | self.username = username 32 | self.password = password 33 | 34 | self.session = requests.Session() 35 | response = self.session.get(self.endpoint('login'), 36 | auth = (username, password)) 37 | 38 | return response.ok 39 | 40 | def restart(self): 41 | response = self.session.get(self.endpoint('reboot'), 42 | auth = (self.username, self.password)) 43 | 44 | return response.ok 45 | 46 | def wifi_pass(self): 47 | response = self.session.get(self.endpoint('wifi_settings'), 48 | auth = (self.username, self.password)) 49 | soup = BeautifulSoup(response.content, 'html.parser') 50 | wifi_data = re.findall("(.*?),", soup.find('script').text) 51 | return wifi_data[26] 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Croutera [![PyPI version](https://badge.fury.io/py/croutera.svg)](https://badge.fury.io/py/croutera) [![Build Status](https://travis-ci.org/cristianoliveira/croutera.svg)](https://travis-ci.org/cristianoliveira/croutera) 2 | Simple Cli Router Admin (*CR*outer*A*). 3 | 4 | The missing CLI for common Routers actions like login, restart , list clientes, log, etc. 5 | 6 | ## Motivation 7 | Almost all of tools I am used to use have a CLI to handle their functionality 8 | but my WiFi router haven't. Croutera comes to supply this missing CLI for this 9 | kind of WiFi routers/modems. 10 | 11 | ## Installing 12 | ```bash 13 | pip install croutera 14 | ``` 15 | 16 | ### From Source 17 | Clone this repo and inside this folder do: 18 | ```bash 19 | make setup && make install 20 | ``` 21 | 22 | ## Using 23 | ```bash 24 | croutera -h 25 | ``` 26 | 27 | ## Commands 28 | The current commands 29 | 30 | #### Show models available 31 | ```bash 32 | croutera -list-models 33 | ``` 34 | 35 | #### Restart 36 | ```bash 37 | croutera -restart [model] [username] [password] 38 | ``` 39 | 40 | #### Show Wifi Password 41 | ```bash 42 | croutera -wifi-pass [model] [username] [password] 43 | ``` 44 | 45 | You can set this params in Environment Variables: 46 | ``` 47 | ROUTER_MODEL=dlink-dir610 48 | ROUTER_IP=10.0.0.1 49 | ROUTER_USERNAME=admin 50 | ROUTER_PASSWORD=admin 51 | ``` 52 | # Contributing 53 | 54 | - Suggesting new routers to be added. 55 | - Adding new routers. See [Routers](https://github.com/cristianoliveira/croutera/blob/master/croutera/models/base.py) to get the router interface you should implement. 56 | - Creating issues/requests/bug fixes 57 | - Adding Unit Tests 58 | - Using! And sending feedback. 59 | 60 | ## Code Contribute 61 | - Fork it! 62 | - Create your feature branch: `git checkout -b my-new-feature` 63 | - Commit your changes: `git commit -am 'Add some feature'` 64 | - Push to the branch: `git push origin my-new-feature` 65 | - Submit a pull request 66 | 67 | **Pull Request should have unit tests** 68 | 69 | ## How to test my Implementation? 70 | See the online simulators available. [Simulators](https://github.com/cristianoliveira/croutera/issues/11) 71 | 72 | ### Routers available: 73 | - Cisco: 74 | - DPC3928S / EPC3928: http://www.cisco.com/web/consumer/support/modem_dpc3928.html 75 | - Dlink: 76 | - DR610: http://www.dlink.com.br/produto/dir-610-a1 77 | - TpLink: 78 | - WR340G: http://www.tp-link.com.br/products/details/?model=TL-WR340G 79 | - WR720N: http://www.tp-link.com.br/products/details/?model=TL-WR720N 80 | - And all TL-WR* based router using 'tplink-tl-wrbased' 81 | 82 | **MIT License** 83 | -------------------------------------------------------------------------------- /croutera/commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from croutera.version import version 5 | from croutera.models import Routers 6 | from abc import ABCMeta, abstractmethod 7 | 8 | 9 | class Command(object): 10 | __metaclass__ = ABCMeta 11 | 12 | def valid(self): 13 | """ Implement logic for command validation """ 14 | return True 15 | 16 | @abstractmethod 17 | def execute(self): 18 | """ Command process """ 19 | raise NotImplementedError('Command not implemented') 20 | 21 | 22 | class ModelListCommand(Command): 23 | """ List all router models available """ 24 | 25 | def execute(self): 26 | print("Models list: \n") 27 | for model in Routers.list(): 28 | print(model+"\n") 29 | 30 | print("For more models open an issue on: \n ") 31 | print("https://github.com/CristianOliveira/croutera") 32 | 33 | return True 34 | 35 | 36 | class VersionCommand(Command): 37 | """ Show current version installed """ 38 | 39 | def execute(self): 40 | print('Croutera Version: ' + version()) 41 | return True 42 | 43 | 44 | class AuthorizeCommand(Command): 45 | 46 | def __init__(self, router, user, password): 47 | self.router = router 48 | self.user = user 49 | self.password = password 50 | 51 | def execute(self): 52 | print('Authorizing...') 53 | try: 54 | return self.router.login(self.user, self.password) 55 | except: 56 | print("Error while authorizing.") 57 | return False 58 | 59 | 60 | class RestartCommand(Command): 61 | 62 | def __init__(self, router): 63 | self.router = router 64 | 65 | def execute(self): 66 | print('Router restarting...') 67 | try: 68 | return self.router.restart() 69 | except: 70 | print("Error while restarting.") 71 | return False 72 | 73 | 74 | class ShowWifiPassCommand(Command): 75 | 76 | def __init__(self, router): 77 | self.router = router 78 | 79 | def execute(self): 80 | print("Current Wifi Pass: " + self.router.wifi_pass()) 81 | return True 82 | 83 | 84 | class ChainCommand(Command): 85 | """ Compose a chain of commands 86 | to be executed in a roll. 87 | """ 88 | def __init__(self): 89 | self.commands = [] 90 | 91 | def add(self, command): 92 | self.commands.append(command) 93 | 94 | def execute(self): 95 | for command in self.commands: 96 | if not command.execute(): 97 | return False 98 | return True 99 | 100 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for Cli module 6 | """ 7 | 8 | import mock 9 | import argparse 10 | 11 | from unittest import TestCase 12 | from croutera.cli import Cli, ArgsParserBuilder 13 | from croutera.commands import * 14 | from croutera.exceptions import ModelNotFoundError 15 | 16 | 17 | class CliTest(TestCase): 18 | 19 | def test_it_returns_model_list_command(self): 20 | args = ArgsParserBuilder.build(self.cmd('-list-models')) 21 | command = Cli.command(args) 22 | self.assertTrue(command, ModelListCommand) 23 | 24 | def test_it_returns_version_command(self): 25 | args = ArgsParserBuilder.build(self.cmd('-v')) 26 | self.assertIsInstance( 27 | Cli.command(ArgsParserBuilder.build(self.cmd('-v'))), 28 | VersionCommand 29 | ) 30 | self.assertIsInstance( 31 | Cli.command(ArgsParserBuilder.build(self.cmd('--version'))), 32 | VersionCommand 33 | ) 34 | 35 | @mock.patch('croutera.models.routers.Routers') 36 | def test_it_returns_chain_command(self, Routers): 37 | setup_mocked_routers(Routers) 38 | args = ArgsParserBuilder.build(self.cmd('-restart manufacturer-model1 usr pas')) 39 | 40 | command = Cli.command(args) 41 | self.assertIsInstance(command, ChainCommand) 42 | 43 | @mock.patch('croutera.models.routers.Routers') 44 | def test_it_returns_at_first_authorize_command(self, Routers): 45 | setup_mocked_routers(Routers) 46 | 47 | args = ArgsParserBuilder.build(self.cmd('-restart manufacturer-model1 usr pas')) 48 | 49 | command = Cli.command(args) 50 | self.assertTrue(command.commands[0], AuthorizeCommand) 51 | 52 | @mock.patch('croutera.models.routers.Routers') 53 | def test_it_returns_at_end_restart_command(self, Routers): 54 | setup_mocked_routers(Routers) 55 | args = ArgsParserBuilder.build(self.cmd('-restart manufacturer-model1 usr pas')) 56 | 57 | command = Cli.command(args) 58 | self.assertTrue(command.commands[1], RestartCommand) 59 | 60 | @mock.patch('croutera.models.routers.Routers') 61 | def test_it_returns_at_end_show_wifi_command(self, Routers): 62 | setup_mocked_routers(Routers) 63 | args = ArgsParserBuilder.build(self.cmd('-wifi-pass manufacturer-model1 usr pas')) 64 | 65 | command = Cli.command(args) 66 | self.assertIsInstance(command.commands[1], ShowWifiPassCommand) 67 | 68 | def cmd(self, terminal_args): 69 | return terminal_args.split() 70 | 71 | def setup_mocked_routers(routers): 72 | router = mock.Mock() 73 | router.model = 'model1' 74 | router.login.return_value = True 75 | routers.from_manufacturer.return_value = [router] 76 | 77 | -------------------------------------------------------------------------------- /croutera/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Cli terminal module 6 | """ 7 | 8 | import os 9 | import argparse 10 | 11 | from croutera import extractor 12 | from croutera.commands import VersionCommand, RestartCommand, \ 13 | ModelListCommand, ShowWifiPassCommand, ChainCommand, \ 14 | AuthorizeCommand 15 | from croutera.exceptions import InvalidCommandArgs 16 | from croutera.models import Routers 17 | 18 | 19 | class Cli(object): 20 | 21 | @staticmethod 22 | def command(args): 23 | """ Retrive command by args """ 24 | 25 | if args.version: 26 | return VersionCommand() 27 | 28 | if args.list_models: 29 | return ModelListCommand() 30 | 31 | manufacturer = extractor.extract_manufacturer(args.model) 32 | model = extractor.extract_model(args.model) 33 | router = Routers.get(manufacturer, model) 34 | 35 | chain = ChainCommand() 36 | chain.add(AuthorizeCommand(router, args.username, args.password)) 37 | 38 | if args.wifi_pass: 39 | chain.add(ShowWifiPassCommand(router)) 40 | return chain 41 | 42 | if args.restart: 43 | chain.add(RestartCommand(router)) 44 | return chain 45 | 46 | @staticmethod 47 | def validate(args): 48 | if args.version or args.list_models: 49 | return True 50 | 51 | if not args.model: 52 | print('Model was not informed.') 53 | return False 54 | 55 | if not '-' in args.model or len(args.model) < 3: 56 | print('Invalid model format.') 57 | return False 58 | 59 | return True 60 | 61 | class ArgsParserBuilder(object): 62 | """ 63 | Implement logic to parse args. 64 | """ 65 | 66 | @staticmethod 67 | def build(args): 68 | ' Compose a parsers ' 69 | 70 | description = """Simple terminal cli to manage modem 71 | / routers admin actions""" 72 | 73 | parser = argparse.ArgumentParser(description=description, 74 | argument_default=argparse.SUPPRESS) 75 | 76 | parser.add_argument( 77 | 'model', 78 | nargs='?', default=os.getenv('ROUTER_USERNAME'), 79 | help='Router model. format: manufacturer-model (see --list-model)' 80 | ) 81 | 82 | parser.add_argument( 83 | 'username', default=os.getenv('ROUTER_USERNAME'), 84 | nargs='?', help='User name to access admin page.') 85 | parser.add_argument( 86 | 'password', default=os.getenv('ROUTER_PASSWORD'), 87 | nargs='?', help='Password to access admin page.') 88 | 89 | # Commands 90 | parser.add_argument( 91 | '-restart', dest='restart', 92 | action='store_true', default=False, 93 | help='Reset router by model.' 94 | ) 95 | 96 | parser.add_argument( 97 | '-wifi-pass', dest='wifi_pass', 98 | action='store_true', default=False, 99 | help='Reset router by model.' 100 | ) 101 | 102 | parser.add_argument( 103 | '-list-models', dest='list_models', 104 | action='store_true', default=False, 105 | help='Shows models available.' 106 | ) 107 | 108 | # Aux 109 | parser.add_argument( 110 | '-ip', dest='ip', default=os.getenv('ROUTER_IP'), 111 | help='Provide router ip.' 112 | ) 113 | 114 | parser.add_argument( 115 | '-v', '--version', dest='version', 116 | action='store_true', default=False, 117 | help='Shows current version.' 118 | ) 119 | 120 | return parser.parse_args(args) 121 | 122 | @staticmethod 123 | def build_help(): 124 | return ArgsParserBuilder.build(['-h']) 125 | 126 | 127 | 128 | --------------------------------------------------------------------------------