├── test ├── __init__.py ├── assets │ ├── landing.html │ ├── login_page.html │ └── logged_in.html ├── test_utils.py ├── test_nauta_api.py └── test_protocol.py ├── requirements ├── test.txt └── base.txt ├── bin └── run-connected ├── setup.cfg ├── nautapy ├── __main__.py ├── __init__.py ├── exceptions.py ├── __about__.py ├── utils.py ├── nauta_api.py └── cli.py ├── screenshots └── console-screenshot.png ├── .gitignore ├── .bumpversion.cfg ├── LICENSE ├── PROXY_GUIDE.md ├── setup.py └── README.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | -r base.txt -------------------------------------------------------------------------------- /bin/run-connected: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | nauta run-connected $* -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /nautapy/__main__.py: -------------------------------------------------------------------------------- 1 | import nautapy.cli as cli 2 | 3 | 4 | if __name__ == '__main__': 5 | cli.main() 6 | -------------------------------------------------------------------------------- /screenshots/console-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atscub/nautapy/HEAD/screenshots/console-screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | .idea/ 5 | .vscode/ 6 | temp/ 7 | venv/ 8 | 9 | __pycache__ 10 | *.pyc 11 | -------------------------------------------------------------------------------- /nautapy/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | appdata_path = os.path.expanduser("~/.local/share/nautapy") 5 | os.makedirs(appdata_path, exist_ok=True) 6 | 7 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.8.1 2 | bs4==0.0.1 3 | certifi==2019.9.11 4 | chardet==3.0.4 5 | idna==2.8 6 | requests==2.27.1 7 | soupsieve==1.9.5 8 | urllib3==1.26.8 9 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 6 | serialize = {major}.{minor}.{patch} 7 | 8 | [bumpversion:file:./nautapy/__about__.py] 9 | 10 | [bumpversion:file:./README.md] 11 | 12 | -------------------------------------------------------------------------------- /nautapy/exceptions.py: -------------------------------------------------------------------------------- 1 | class NautaException(Exception): 2 | pass 3 | 4 | 5 | class NautaFormatException(NautaException): 6 | pass 7 | 8 | 9 | class NautaPreLoginException(NautaException): 10 | pass 11 | 12 | 13 | class NautaLoginException(NautaException): 14 | pass 15 | 16 | 17 | class NautaLogoutException(NautaException): 18 | pass 19 | 20 | -------------------------------------------------------------------------------- /test/assets/landing.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /nautapy/__about__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "__name__", 3 | "__cli__", 4 | "__author__", 5 | "__email__", 6 | "__version__", 7 | "__url__", 8 | "__description__", 9 | ] 10 | 11 | __name__ = "nautapy" 12 | __cli__ = "nauta" 13 | __version__ = "0.2.0" 14 | 15 | __author__ = "Abraham Toledo Sanchez" 16 | __email__ = "abrahamtoledo90@gmail.com" 17 | __url__ = "https://github.com/abrahamtoledo/nautapy" 18 | 19 | __description__ = "Python API para el portal cautivo Nauta de Cuba + CLI" -------------------------------------------------------------------------------- /nautapy/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from nautapy.exceptions import NautaFormatException 4 | 5 | _re_time = re.compile(r'^\s*(?P\d+?)\s*:\s*(?P\d+?)\s*:\s*(?P\d+?)\s*$') 6 | 7 | 8 | def strtime2seconds(str_time): 9 | res = _re_time.match(str_time) 10 | if not res: 11 | raise NautaFormatException("El formato del intervalo de tiempo es incorrecto: {}".format(str_time)) 12 | 13 | return \ 14 | int(res["hours"]) * 3600 + \ 15 | int(res["minutes"]) * 60 + \ 16 | int(res["seconds"]) 17 | 18 | 19 | def seconds2strtime(seconds): 20 | return "{:02d}:{:02d}:{:02d}".format( 21 | seconds // 3600, # hours 22 | (seconds % 3600) // 60, # minutes 23 | seconds % 60 # seconds 24 | ) 25 | 26 | 27 | def val_or_error(callback): 28 | try: 29 | return callback() 30 | except Exception as ex: 31 | return ex.args[0] -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | from nautapy.exceptions import NautaFormatException 2 | from nautapy.utils import strtime2seconds, seconds2strtime 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("strtime, seconds", [ 7 | ("01:00:00", 3600), 8 | (" 01:00 :30 ", 3630), 9 | (" 01: 05 :00 ", 3900), 10 | (" 50:00 :00 ", 50 * 3600), 11 | ]) 12 | def test_strtime2seconds(strtime, seconds): 13 | assert strtime2seconds(strtime) == seconds 14 | 15 | 16 | @pytest.mark.parametrize("bad_formated", [ 17 | ("10:00",), 18 | ("10",), 19 | ("a:00:00",), 20 | ("10:00::00",), 21 | ]) 22 | def test_strtime2seconds_raises_format_exception(bad_formated): 23 | with pytest.raises(NautaFormatException): 24 | strtime2seconds("") 25 | 26 | 27 | @pytest.mark.parametrize("strtime, seconds", [ 28 | ("01:00:00", 3600), 29 | ("01:00:30", 3630), 30 | ("01:05:00", 3900), 31 | ("50:00:00", 50 * 3600), 32 | ]) 33 | def test_seconds2strtime(strtime, seconds): 34 | assert seconds2strtime(seconds) == strtime 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /PROXY_GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Proxy para Nautapy: 2 | Proxy que establece sesión nauta de manera transparente, cada vez que recibe una nueva petición de conexión. Se debe iniciar la sesión al recibir una petición en el proxy, siempre y cuando no haya ya una sesión activa. Se debe cerrar la sesión cuando no queden conexiones activas. El proxy solo debe cerrar una sesión nauta si esta fue abierta por él mismo. 3 | 4 | 5 | ### Features: 6 | - Conexiones recurrentes, usar `asyncio`. 7 | - Debe ser posible especificar la unidad mínima de división de tiempo, según tarificación de ETECSA, por ejemplo para Nauta Hogar sería 120s. 8 | - Filtrado por `domain_name` (_whitelist_ o _blacklist_). 9 | 10 | 11 | ### Linea de comandos 12 | 13 | ```bash 14 | nauta proxy [OPTIONS] 15 | 16 | OPTIONS puede ser: 17 | -p, --port Puerto especificado del servicio, default: 3128 18 | -t, --time-unit Unidad de division de tiempo. Este parametro es obligatorio. 19 | -u, --user Usuario que se usara para conectarse, default: default_nautapy_user 20 | -m, --max-conn Maximo número de conexiones simultaneas 21 | -l, --log Logs file, use "-" for stdout, defautl: "-" 22 | ``` 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | from io import open 4 | 5 | 6 | def get_about(): 7 | scope = {} 8 | with open("nautapy/__about__.py") as fp: 9 | exec(fp.read(), scope) 10 | return scope 11 | 12 | 13 | def get_requirements(env="base.txt"): 14 | with open("requirements/{}".format(env)) as fd: 15 | requirements = [] 16 | for line in fd.readlines(): 17 | if line.startswith("-r"): 18 | _, _env = line.split(" ", 2) 19 | requirements += get_requirements(_env.strip()) 20 | else: 21 | requirements.append(line.strip()) 22 | return requirements 23 | 24 | 25 | def get_readme(): 26 | """ 27 | Get the long description from the README file 28 | :return: 29 | """ 30 | with open(path.join(here, "README.md"), encoding="utf-8") as f: 31 | return f.read() 32 | 33 | 34 | here = path.abspath(path.dirname(__file__)) 35 | about = get_about() 36 | 37 | setup( 38 | name=about["__name__"], 39 | version=about["__version__"], 40 | description=about["__description__"], 41 | long_description=get_readme(), 42 | long_description_content_type="text/markdown", 43 | url=about["__url__"], 44 | author=about["__author__"], 45 | author_email=about["__email__"], 46 | classifiers=[ 47 | "Topic :: Internet", 48 | "License :: OSI Approved :: MIT License", 49 | "Programming Language :: Python" 50 | ], 51 | keywords="nauta portal cautivo", 52 | packages=find_packages(), 53 | install_requires=get_requirements(), 54 | entry_points={ 55 | "console_scripts": [about["__cli__"] + "=nautapy.cli:main"], 56 | }, 57 | scripts=[ 58 | 'bin/run-connected', 59 | ] 60 | ) 61 | -------------------------------------------------------------------------------- /test/test_nauta_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import requests 5 | from requests_mock import Mocker as RequestMocker, ANY 6 | 7 | from nautapy.exceptions import NautaLoginException, NautaPreLoginException 8 | from nautapy.nauta_api import CHECK_PAGE, NautaProtocol 9 | 10 | 11 | _assets_dir = os.path.join( 12 | os.path.dirname(__file__), 13 | "assets" 14 | ) 15 | 16 | 17 | def read_asset(asset_name): 18 | with open(os.path.join(_assets_dir, asset_name)) as fp: 19 | return fp.read() 20 | 21 | 22 | LANDING_HTML = read_asset("landing.html") 23 | LOGIN_HTML = read_asset("login_page.html") 24 | LOGGED_IN_HTML = read_asset("logged_in.html") 25 | 26 | 27 | def test_nauta_protocol_creates_valid_session(): 28 | with RequestMocker() as mock: 29 | # Setup 30 | mget = mock.get(ANY, status_code=200, text=LANDING_HTML) 31 | mpost = mock.post(ANY, status_code=200, text=LOGIN_HTML) 32 | 33 | # Test 34 | session, login_action, data = NautaProtocol.create_session() 35 | 36 | assert mget.called and mpost.called 37 | assert isinstance(session, requests.Session) 38 | assert login_action and data and data["CSRFHW"] and data["wlanuserip"] 39 | 40 | 41 | def test_nauta_protocol_create_session_raises_when_connected(): 42 | with RequestMocker() as mock: 43 | # Setup 44 | mget = mock.get(ANY, status_code=200, text="FOFOFOOFOFOFOFO") 45 | mpost = mock.post(ANY, status_code=200, text=LOGIN_HTML) 46 | 47 | with pytest.raises(NautaPreLoginException): 48 | NautaProtocol.create_session() 49 | 50 | assert mget.called and not mpost.called 51 | 52 | 53 | def test_nauta_protocol_login_ok(): 54 | with RequestMocker() as mock: 55 | mpost = mock.post(ANY, status_code=200, text=LOGGED_IN_HTML, url="http://secure.etecsa.net:8443/online.do?fooo") 56 | NautaProtocol.login(requests.Session(), "http://test.com/some_action", {}, "pepe@nauta.com.cu", "somepass") 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NautaPy 2 | 3 | __NautaPy__ Python API para el portal cautivo [Nauta](https://secure.etecsa.net:8443/) de Cuba + CLI. 4 | 5 | ![Screenshot](screenshots/console-screenshot.png?raw=true) 6 | 7 | ## Requisitos 8 | 9 | 1. Instale la última versión estable de [Python3](https://www.python.org/downloads/) 10 | 11 | ## Instalación 12 | 13 | Instalación: 14 | 15 | ```bash 16 | pip3 install --upgrade git+https://github.com/atscub/nautapy.git 17 | ``` 18 | 19 | ## Modo de uso 20 | 21 | #### Agrega un usuario 22 | 23 | ```bash 24 | nauta users add periquito@nauta.com.cu 25 | ``` 26 | 27 | Introducir la contraseña cuando se pida. Cambie `periquito@nauta.com.cu` por 28 | su usuario Nauta. 29 | 30 | #### Iniciar sesión: 31 | 32 | __Especificando el usuario__ 33 | 34 | ```bash 35 | nauta up periquito 36 | ``` 37 | 38 | Se muestra el tiempo en el terminal, para cerrar la sesión se debe pulsar `Ctrl+C`. 39 | 40 | * Opcionalmente puede especificar la duración máxima para la sesión, luego de la cual se desconecta automáticamente: 41 | 42 | ```bash 43 | nauta up --session-time 60 periquito 44 | ``` 45 | 46 | El ejemplo anterior mantiene abierta la sesión durante un minuto. 47 | 48 | __Sin especificar el usuario__ 49 | 50 | ```bash 51 | nauta up 52 | ``` 53 | Se utiza el usuario predeterminado o el primero que se encuentre en la base de datos. 54 | 55 | 56 | #### Ejecutar un comando con conexión 57 | 58 | ```bash 59 | run-connected 60 | ``` 61 | Ejecuta la tarea especificada con conexión, la conexión se cierra al finalizar la tarea. 62 | 63 | 64 | #### Consultar información del usuario 65 | 66 | ```bash 67 | nauta info periquito 68 | ``` 69 | 70 | __Salida__: 71 | 72 | ```text 73 | Usuario Nauta: periquito@nauta.com.cu 74 | Tiempo restante: 02:14:24 75 | Crédito: 1.12 CUC 76 | ``` 77 | 78 | #### Determinar si hay conexión a internet 79 | 80 | ```text 81 | nauta is-online 82 | ``` 83 | 84 | __Salida__: 85 | ```text 86 | Online: No 87 | ``` 88 | 89 | #### Determinar si hay una sesión abierta 90 | 91 | ```text 92 | nauta is-logged-in 93 | ``` 94 | 95 | __Salida__: 96 | ```text 97 | Sesión activa: No 98 | ``` 99 | 100 | # Más Información 101 | 102 | Lee la ayuda del módulo una vez instalado: 103 | 104 | ```bash 105 | nauta --help 106 | ``` 107 | 108 | ## Contribuir 109 | __IMPORTANTE__: Notifícame por Twitter (enviar DM) sobre cualquier actividad en el proyecto (Issue o PR). 110 | 111 | Todas las contribuciones son bienvenidas. Puedes ayudar trabajando en uno de los issues existentes. 112 | Clona el repo, crea una rama para el issue que estés trabajando y cuando estés listo crea un Pull Request. 113 | 114 | También puedes contribuir difundiendo esta herramienta entre tus amigos y en tus redes. Mientras 115 | más grande sea la comunidad más sólido será el proyecto. 116 | 117 | Si te gusta el proyecto dale una estrella para que otros lo encuentren más fácilmente. 118 | 119 | ### Contacto del autor 120 | 121 | - Twitter: [@atscub](https://twitter.com/atscub) 122 | 123 | 124 | ### Compartir 125 | - [Twitter](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fatscub%2Fnautapy%2F&text=Python%20API%20para%20el%20portal%20cautivo%20Nauta%20de%20Cuba%20%2B%20CLI) 126 | -------------------------------------------------------------------------------- /test/test_protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Blackbox testing for the :class:`NautaProtocol` class 3 | 4 | Usage: 5 | These test MUST be run locally with a private nauta account 6 | User credentials must be set in environment variables: 7 | $ export TEST_NAUTA_USERNAME="periquito@nauta.com.cu 8 | $ export TEST_NAUTA_PASSWORD="periquito_password" 9 | 10 | These test should be run independently with: 11 | pytest test/test_protocol.py 12 | 13 | Description: 14 | The goal of these tests are to ensure that comunication 15 | with Nauta Captive Portal is not broken, maybe after an update. 16 | 17 | These tests must must not be run with the rest of the tests. 18 | Methods from :class:`NautaProtocol` must be mocked in all the 19 | other tests 20 | """ 21 | 22 | import os 23 | import warnings 24 | 25 | import pytest 26 | from nautapy.nauta_api import NautaProtocol 27 | 28 | 29 | def save_csrfhw(csrfhw): 30 | temp_dir = "temp" 31 | os.makedirs(temp_dir) 32 | with open(os.path.join("temp", "CSRFHW"), "w") as fp: 33 | fp.write(csrfhw) 34 | 35 | 36 | def get_env_or_raise(env_var_name): 37 | env_var_val = os.getenv(env_var_name) 38 | if not env_var_val: 39 | raise Exception("{} is not defined in the environment".format(env_var_name)) 40 | return env_var_val 41 | 42 | 43 | @pytest.fixture() 44 | def username(): 45 | return get_env_or_raise("TEST_NAUTA_USERNAME") 46 | 47 | 48 | @pytest.fixture() 49 | def password(): 50 | return get_env_or_raise("TEST_NAUTA_PASSWORD") 51 | 52 | 53 | def test_nauta_protocol_logs_in(username, password): 54 | session, login_action, data = NautaProtocol.create_session() 55 | assert data and data["CSRFHW"] 56 | 57 | warnings.warn( 58 | "In case something went wrong, " 59 | "disconnect with this CSRFHW={}".format( 60 | data["CSRFHW"] 61 | ) 62 | ) 63 | 64 | attribute_uuid = None 65 | try: 66 | attribute_uuid = NautaProtocol.login( 67 | session=session, 68 | login_action=login_action, 69 | data=data, 70 | username=username, 71 | password="AnaD!az56" 72 | ) 73 | 74 | if not attribute_uuid: 75 | warnings.warn("attribute_uuid was not found after login") 76 | finally: 77 | NautaProtocol.logout( 78 | csrfhw=data["CSRFHW"], 79 | username=username, 80 | wlanuserip=data["wlanuserip"], 81 | attribute_uuid=attribute_uuid 82 | ) 83 | 84 | 85 | # _assets_dir = os.path.join( 86 | # os.path.dirname(__file__), 87 | # "assets" 88 | # ) 89 | # 90 | # 91 | # def read_asset(asset_name): 92 | # with open(os.path.join(_assets_dir, asset_name)) as fp: 93 | # return fp.read() 94 | # 95 | # 96 | # LANDING_HTML = read_asset("landing.html") 97 | # LOGIN_HTML = read_asset("login_page.html") 98 | # LOGGED_IN_HTML = read_asset("logged_in.html") 99 | # 100 | # 101 | # def test_nauta_protocol_creates_valiad_session(): 102 | # with RequestMocker() as mock: 103 | # # Setup 104 | # mget = mock.get(ANY, status_code=200, text=LANDING_HTML) 105 | # mpost = mock.post(ANY, status_code=200, text=LOGIN_HTML) 106 | # 107 | # # Test 108 | # session, login_action, data = NautaProtocol.create_session() 109 | # 110 | # assert mget.called and mpost.called 111 | # assert isinstance(session, requests.Session) 112 | # assert login_action and data and data["CSRFHW"] and data["wlanuserip"] 113 | # 114 | # 115 | # def test_nauta_protocol_create_session_raises_when_connected(): 116 | # with RequestMocker() as mock: 117 | # # Setup 118 | # mget = mock.get(ANY, status_code=200, text="FOFOFOOFOFOFOFO") 119 | # mpost = mock.post(ANY, status_code=200, text=LOGIN_HTML) 120 | # 121 | # with pytest.raises(NautaPreLoginException): 122 | # NautaProtocol.create_session() 123 | # 124 | # assert mget.called and not mpost.called 125 | # 126 | # 127 | # def test_nauta_protocol_login_ok(): 128 | # with RequestMocker() as mock: 129 | # mpost = mock.post(ANY, status_code=200, text=LOGGED_IN_HTML, url="http://secure.etecsa.net:8443/online.do?fooo") 130 | # NautaProtocol.login(requests.Session(), "http://test.com/some_action", {}, "pepe@nauta.com.cu", "somepass") 131 | # 132 | -------------------------------------------------------------------------------- /test/assets/login_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Bienvenido 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 |
54 | 55 | 56 | 60 |
61 | 62 | 63 |
64 |
65 |
66 |
67 |
Bienvenido
68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Descargue aquí Información básica sobre la Ley Helms Burton y su título III 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 | 92 |
93 | 94 | 95 | 96 | 97 | 98 |
99 | 100 | 105 |
106 |
107 |
108 |
109 |
110 | 111 |
168 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /nautapy/nauta_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | API for interacting with Nauta Captive Portal 3 | 4 | Example: 5 | nauta_client = NautaClient("pepe@nauta.com.cu", "pepepass") 6 | try: 7 | with nauta_client.login(): 8 | # We are connected 9 | 10 | # We are disconnected now 11 | except NautaLoginException as ex: 12 | # Handle exception here 13 | # Clean session (logout) 14 | except NautaLogoutException as ex: 15 | # Handle exception here 16 | # Clean session (logout) 17 | except NautaException as ex: 18 | # Handle exception here 19 | # Clean session (logout) 20 | 21 | """ 22 | 23 | import json 24 | import re 25 | import os 26 | import time 27 | import http.cookiejar as cookielib 28 | 29 | import bs4 30 | import requests 31 | from requests import RequestException 32 | 33 | from nautapy import appdata_path 34 | from nautapy.__about__ import __name__ as prog_name 35 | from nautapy.exceptions import NautaLoginException, NautaLogoutException, NautaException, NautaPreLoginException 36 | 37 | MAX_DISCONNECT_ATTEMPTS = 10 38 | 39 | CHECK_PAGE = "http://www.cubadebate.cu/" 40 | LOGIN_DOMAIN = b"secure.etecsa.net" 41 | LOGIN_URL = "https://secure.etecsa.net:8443" 42 | 43 | NAUTA_SESSION_FILE = os.path.join(appdata_path, "nauta-session") 44 | 45 | 46 | class SessionObject(object): 47 | def __init__(self, login_action=None, csrfhw=None, wlanuserip=None, attribute_uuid=None): 48 | self.requests_session = self.__class__._create_requests_session() 49 | 50 | self.login_action = login_action 51 | self.csrfhw = csrfhw 52 | self.wlanuserip = wlanuserip 53 | self.attribute_uuid = attribute_uuid 54 | 55 | @classmethod 56 | def _create_requests_session(cls): 57 | requests_session = requests.Session() 58 | requests_session.cookies = cookielib.MozillaCookieJar(NAUTA_SESSION_FILE) 59 | return requests_session 60 | 61 | def save(self, username=None): 62 | self.requests_session.cookies.save() 63 | 64 | data = {**self.__dict__} 65 | data.pop("requests_session") 66 | data["username"] = username 67 | 68 | with open(NAUTA_SESSION_FILE, "w") as fp: 69 | json.dump(data, fp) 70 | 71 | @classmethod 72 | def load(cls): 73 | inst = object.__new__(cls) 74 | inst.requests_session = cls._create_requests_session() 75 | 76 | with open(NAUTA_SESSION_FILE, 'r') as fp: 77 | inst.__dict__.update( 78 | json.load(fp) 79 | ) 80 | 81 | return inst 82 | 83 | def dispose(self): 84 | self.requests_session.cookies.clear() 85 | self.requests_session.cookies.save() 86 | try: 87 | os.remove(NAUTA_SESSION_FILE) 88 | except: 89 | pass 90 | 91 | @classmethod 92 | def is_logged_in(cls): 93 | return os.path.exists(NAUTA_SESSION_FILE) 94 | 95 | 96 | class NautaProtocol(object): 97 | """Protocol Layer (Interface) 98 | 99 | Abstracts the details of dealing with nauta server 100 | This is the lower layer of the application. API client must 101 | use this instead of directly talk with nauta server 102 | 103 | """ 104 | @classmethod 105 | def _get_inputs(cls, form_soup): 106 | return { 107 | _["name"]: _.get("value", default=None) 108 | for _ in form_soup.select("input[name]") 109 | } 110 | 111 | @classmethod 112 | def is_connected(cls, timeout=3): 113 | try: 114 | r = requests.get(CHECK_PAGE, timeout=timeout) 115 | return LOGIN_DOMAIN not in r.content; 116 | except (requests.ConnectionError, requests.Timeout) as exception: 117 | return False; 118 | #return LOGIN_DOMAIN not in r.content 119 | 120 | @classmethod 121 | def create_session(cls): 122 | if cls.is_connected(): 123 | if SessionObject.is_logged_in(): 124 | raise NautaPreLoginException("Hay una sessión abierta") 125 | else: 126 | raise NautaPreLoginException("Hay una conexión activa") 127 | 128 | session = SessionObject() 129 | resp = session.requests_session.get(LOGIN_URL) 130 | if not resp.ok: 131 | raise NautaPreLoginException("Failed to create session") 132 | 133 | soup = bs4.BeautifulSoup(resp.text, 'html.parser') 134 | action = LOGIN_URL 135 | data = cls._get_inputs(soup) 136 | 137 | # Now go to the login page 138 | resp = session.requests_session.post(action, data) 139 | soup = bs4.BeautifulSoup(resp.text, 'html.parser') 140 | form_soup = soup.find("form", id="formulario") 141 | 142 | session.login_action = form_soup["action"] 143 | data = cls._get_inputs(form_soup) 144 | 145 | session.csrfhw = data['CSRFHW'] 146 | session.wlanuserip = data['wlanuserip'] 147 | 148 | return session 149 | 150 | @classmethod 151 | def login(cls, session, username, password): 152 | r = session.requests_session.post( 153 | session.login_action, 154 | { 155 | "CSRFHW": session.csrfhw, 156 | "wlanuserip": session.wlanuserip, 157 | "username": username, 158 | "password": password 159 | } 160 | ) 161 | 162 | if not r.ok: 163 | raise NautaLoginException( 164 | "Falló el inicio de sesión: {} - {}".format( 165 | r.status_code, 166 | r.reason 167 | ) 168 | ) 169 | 170 | if not "online.do" in r.url: 171 | soup = bs4.BeautifulSoup(r.text, "html.parser") 172 | script_text = soup.find_all("script")[-1].get_text() 173 | match = re.search(r'alert\(\"(?P[^\"]*?)\"\)', script_text) 174 | raise NautaLoginException( 175 | "Falló el inicio de sesión: {}".format( 176 | match and match.groupdict().get("reason") 177 | ) 178 | ) 179 | 180 | m = re.search(r'ATTRIBUTE_UUID=(\w+)&CSRFHW=', r.text) 181 | 182 | return m.group(1) if m \ 183 | else None 184 | 185 | @classmethod 186 | def logout(cls, session, username): 187 | logout_url = \ 188 | ( 189 | "https://secure.etecsa.net:8443/LogoutServlet?" + 190 | "CSRFHW={}&" + 191 | "username={}&" + 192 | "ATTRIBUTE_UUID={}&" + 193 | "wlanuserip={}" 194 | ).format( 195 | session.csrfhw, 196 | username, 197 | session.attribute_uuid, 198 | session.wlanuserip 199 | ) 200 | 201 | response = session.requests_session.post(logout_url) 202 | if not response.ok: 203 | raise NautaLogoutException( 204 | "Fallo al cerrar la sesión: {} - {}".format( 205 | response.status_code, 206 | response.reason 207 | ) 208 | ) 209 | 210 | if "SUCCESS" not in response.text.upper(): 211 | raise NautaLogoutException( 212 | "Fallo al cerrar la sesión: {}".format( 213 | response.text[:100] 214 | ) 215 | ) 216 | 217 | @classmethod 218 | def get_user_time(cls, session, username): 219 | 220 | r = session.requests_session.post( 221 | "https://secure.etecsa.net:8443/EtecsaQueryServlet", 222 | { 223 | "op": "getLeftTime", 224 | "ATTRIBUTE_UUID": session.attribute_uuid, 225 | "CSRFHW": session.csrfhw, 226 | "wlanuserip": session.wlanuserip, 227 | "username": username, 228 | } 229 | ) 230 | 231 | return r.text 232 | 233 | @classmethod 234 | def get_user_credit(cls, session, username, password): 235 | 236 | r = session.requests_session.post( 237 | "https://secure.etecsa.net:8443/EtecsaQueryServlet", 238 | { 239 | "CSRFHW": session.csrfhw, 240 | "wlanuserip": session.wlanuserip, 241 | "username": username, 242 | "password": password 243 | } 244 | ) 245 | 246 | if not r.ok: 247 | raise NautaException( 248 | "Fallo al obtener la información del usuario: {} - {}".format( 249 | r.status_code, 250 | r.reason 251 | ) 252 | ) 253 | 254 | if "secure.etecsa.net" not in r.url: 255 | raise NautaException( 256 | "No se puede obtener el crédito del usuario mientras está online" 257 | ) 258 | 259 | soup = bs4.BeautifulSoup(r.text, "html.parser") 260 | credit_tag = soup.select_one("#sessioninfo > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2)") 261 | 262 | if not credit_tag: 263 | raise NautaException( 264 | "Fallo al obtener el crédito del usuario: no se encontró la información" 265 | ) 266 | 267 | return credit_tag.get_text().strip() 268 | 269 | 270 | class NautaClient(object): 271 | def __init__(self, user, password): 272 | self.user = user 273 | self.password = password 274 | self.session = None 275 | 276 | def init_session(self): 277 | self.session = NautaProtocol.create_session() 278 | self.session.save() 279 | 280 | @property 281 | def is_logged_in(self): 282 | return SessionObject.is_logged_in() 283 | 284 | def login(self): 285 | if not self.session: 286 | self.init_session() 287 | 288 | self.session.attribute_uuid = NautaProtocol.login( 289 | self.session, 290 | self.user, 291 | self.password 292 | ) 293 | 294 | self.session.save(self.user) 295 | 296 | return self 297 | 298 | @property 299 | def user_credit(self): 300 | dispose_session = False 301 | try: 302 | if not self.session: 303 | dispose_session = True 304 | self.init_session() 305 | 306 | return NautaProtocol.get_user_credit( 307 | session=self.session, 308 | username=self.user, 309 | password=self.password 310 | ) 311 | finally: 312 | if self.session and dispose_session: 313 | self.session.dispose() 314 | self.session = None 315 | 316 | @property 317 | def remaining_time(self): 318 | dispose_session = False 319 | try: 320 | if not self.session: 321 | dispose_session = True 322 | self.session = SessionObject() 323 | 324 | return NautaProtocol.get_user_time( 325 | session=self.session, 326 | username=self.user, 327 | ) 328 | finally: 329 | if self.session and dispose_session: 330 | self.session.dispose() 331 | self.session = None 332 | 333 | def logout(self): 334 | for i in range(0, MAX_DISCONNECT_ATTEMPTS): 335 | try: 336 | NautaProtocol.logout( 337 | session=self.session, 338 | username=self.user, 339 | ) 340 | self.session.dispose() 341 | self.session = None 342 | 343 | return 344 | except RequestException: 345 | time.sleep(1) 346 | 347 | raise NautaLogoutException( 348 | "Hay problemas en la red y no se puede cerrar la sessión.\n" 349 | "Es posible que ya esté desconectado. Intente con '{} down' " 350 | "dentro de unos minutos".format(prog_name) 351 | ) 352 | 353 | def load_last_session(self): 354 | self.session = SessionObject.load() 355 | 356 | def __enter__(self): 357 | pass 358 | 359 | def __exit__(self, exc_type, exc_val, exc_tb): 360 | if SessionObject.is_logged_in(): 361 | self.logout() 362 | -------------------------------------------------------------------------------- /nautapy/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | import time 5 | import sqlite3 6 | from getpass import getpass 7 | 8 | from requests import RequestException 9 | 10 | from nautapy.exceptions import NautaException 11 | from nautapy.nauta_api import NautaClient, NautaProtocol 12 | from nautapy import utils 13 | from nautapy.__about__ import __cli__ as prog_name, __version__ as version 14 | from nautapy import appdata_path 15 | 16 | from base64 import b85encode, b85decode 17 | 18 | 19 | USERS_DB = os.path.join(appdata_path, "users.db") 20 | 21 | 22 | def users_db_connect(): 23 | conn = sqlite3.connect(USERS_DB) 24 | cursor = conn.cursor() 25 | cursor.execute("CREATE TABLE IF NOT EXISTS users (user TEXT, password TEXT)") 26 | cursor.execute("CREATE TABLE IF NOT EXISTS default_user (user TEXT)") 27 | 28 | conn.commit() 29 | return cursor, conn 30 | 31 | 32 | def _get_default_user(): 33 | cursor, _ = users_db_connect() 34 | 35 | # Search for explicit default value 36 | cursor.execute("SELECT user FROM default_user LIMIT 1") 37 | rec = cursor.fetchone() 38 | if rec: 39 | return rec[0] 40 | 41 | # If no explicit value exists, find the first user 42 | cursor.execute("SELECT * FROM users LIMIT 1") 43 | 44 | rec = cursor.fetchone() 45 | if rec: 46 | return rec[0] 47 | 48 | 49 | def _find_credentials(user, default_password=None): 50 | cursor, _ = users_db_connect() 51 | cursor.execute("SELECT * FROM users WHERE user LIKE ?", (user + '%',)) 52 | 53 | rec = cursor.fetchone() 54 | if rec: 55 | return rec[0], b85decode(rec[1]).decode('utf-8') 56 | else: 57 | return user, default_password 58 | 59 | 60 | def add_user(args): 61 | password = args.password or getpass("Contraseña para {}: ".format(args.user)) 62 | 63 | cursor, connection = users_db_connect() 64 | cursor.execute("INSERT INTO users VALUES (?, ?)", (args.user, b85encode(password.encode('utf-8')))) 65 | connection.commit() 66 | 67 | print("Usuario guardado: {}".format(args.user)) 68 | 69 | 70 | def set_default_user(args): 71 | cursor, connection = users_db_connect() 72 | cursor.execute("SELECT count(user) FROM default_user") 73 | res = cursor.fetchone() 74 | 75 | if res[0]: 76 | cursor.execute("UPDATE default_user SET user=?", (args.user,)) 77 | else: 78 | cursor.execute("INSERT INTO default_user VALUES (?)", (args.user,)) 79 | 80 | connection.commit() 81 | 82 | print("Usuario predeterminado: {}".format(args.user)) 83 | 84 | 85 | def remove_user(args): 86 | cursor, connection = users_db_connect() 87 | cursor.execute("DELETE FROM users WHERE user=?", (args.user,)) 88 | connection.commit() 89 | 90 | print("Usuario eliminado: {}".format(args.user)) 91 | 92 | 93 | def set_password(args): 94 | password = args.password or getpass("Contraseña para {}: ".format(args.user)) 95 | 96 | cursor, connection = users_db_connect() 97 | cursor.execute("UPDATE users SET password=? WHERE user=?", (b85encode(password.encode('utf-8')), args.user)) 98 | 99 | connection.commit() 100 | 101 | print("Contraseña actualizada: {}".format(args.user)) 102 | 103 | 104 | def list_users(args): 105 | cursor, _ = users_db_connect() 106 | 107 | for rec in cursor.execute("SELECT user FROM users"): 108 | print(rec[0]) 109 | 110 | 111 | def _get_credentials(args): 112 | user = args.user or _get_default_user() 113 | password = args.password or None 114 | 115 | if not user: 116 | print( 117 | "No existe ningún usuario. Debe crear uno. " 118 | "Ejecute '{} --help' para más ayuda".format( 119 | prog_name 120 | ), 121 | file=sys.stderr 122 | ) 123 | sys.exit(1) 124 | 125 | return _find_credentials(user=user, default_password=password) 126 | 127 | 128 | def up(args): 129 | user, password = _get_credentials(args) 130 | client = NautaClient(user=user, password=password) 131 | 132 | print( 133 | "Conectando usuario: {}".format( 134 | client.user, 135 | ) 136 | ) 137 | 138 | if args.batch: 139 | client.login() 140 | print("[Sesión iniciada]") 141 | print("Tiempo restante: {}".format(utils.val_or_error(lambda: client.remaining_time))) 142 | else: 143 | with client.login(): 144 | login_time = int(time.time()) 145 | print("[Sesión iniciada]") 146 | print("Tiempo restante: {}".format(utils.val_or_error(lambda: client.remaining_time))) 147 | print( 148 | "Presione Ctrl+C para desconectarse, o ejecute '{} down' desde otro terminal".format( 149 | prog_name 150 | ) 151 | ) 152 | 153 | try: 154 | while True: 155 | if not client.is_logged_in: 156 | break 157 | 158 | elapsed = int(time.time()) - login_time 159 | 160 | print( 161 | "\rTiempo de conexión: {}".format( 162 | utils.seconds2strtime(elapsed) 163 | ), 164 | end="" 165 | ) 166 | 167 | if args.session_time: 168 | if args.session_time < elapsed: 169 | break 170 | 171 | print( 172 | " La sesión se cerrará en {}".format( 173 | utils.seconds2strtime(args.session_time - elapsed) 174 | ), 175 | end="" 176 | ) 177 | 178 | time.sleep(1) 179 | except KeyboardInterrupt: 180 | pass 181 | finally: 182 | print("\n\nCerrando sesión ...") 183 | print("Tiempo restante: {}".format(utils.val_or_error(lambda: client.remaining_time))) 184 | 185 | 186 | 187 | print("Sesión cerrada con éxito.") 188 | #print("Crédito: {}".format( 189 | # utils.val_or_error(lambda: client.user_credit) 190 | #)) 191 | 192 | 193 | def down(args): 194 | client = NautaClient(user=None, password=None) 195 | 196 | if client.is_logged_in: 197 | client.load_last_session() 198 | client.user = client.session.__dict__.get("username") 199 | client.logout() 200 | print("Sesión cerrada con éxito") 201 | else: 202 | print("No hay ninguna sesión activa") 203 | 204 | 205 | def is_logged_in(args): 206 | client = NautaClient(user=None, password=None) 207 | 208 | print("Sesión activa: {}".format( 209 | "Sí" if client.is_logged_in 210 | else "No" 211 | )) 212 | 213 | 214 | def is_online(args): 215 | print("Online: {}".format( 216 | "Sí" if NautaProtocol.is_connected() 217 | else "No" 218 | )) 219 | 220 | 221 | def info(args): 222 | user, password = _get_credentials(args) 223 | client = NautaClient(user, password) 224 | 225 | if client.is_logged_in: 226 | client.load_last_session() 227 | 228 | print("Usuario Nauta: {}".format(user)) 229 | print("Tiempo restante: {}".format( 230 | utils.val_or_error(lambda: client.remaining_time) 231 | )) 232 | #print("Crédito: {}".format( 233 | # utils.val_or_error(lambda: client.user_credit) 234 | #)) 235 | 236 | 237 | def run_connected(args): 238 | user, password = _get_credentials(args) 239 | client = NautaClient(user, password) 240 | 241 | if not NautaProtocol.is_connected(): 242 | with client.login(): 243 | os.system(" ".join(args.cmd)) 244 | elif args.reuse_connection: 245 | os.system(" ".join(args.cmd)) 246 | if not client.is_logged_in: 247 | print("No hay ninguna sesión activa") 248 | return 249 | client.load_last_session() 250 | client.user = client.session.__dict__.get("username") 251 | client.logout() 252 | print("Sesión cerrada con éxito") 253 | else: 254 | print("ya hay una conexión activa a internet, si aún así desea usar -run-connected agregue el flag --reuse-connection") 255 | 256 | 257 | def create_user_subparsers(subparsers): 258 | users_parser = subparsers.add_parser("users") 259 | user_subparsers = users_parser.add_subparsers() 260 | 261 | # Add user 262 | user_add_parser = user_subparsers.add_parser("add") 263 | user_add_parser.set_defaults(func=add_user) 264 | user_add_parser.add_argument("user", help="Usuario Nauta") 265 | user_add_parser.add_argument("password", nargs="?", help="Password del usuario Nauta") 266 | 267 | # Set default user 268 | user_set_default_parser = user_subparsers.add_parser("set-default") 269 | user_set_default_parser.set_defaults(func=set_default_user) 270 | user_set_default_parser.add_argument("user", help="Usuario Nauta") 271 | 272 | # Set user password 273 | user_set_password_parser = user_subparsers.add_parser("set-password") 274 | user_set_password_parser.set_defaults(func=set_password) 275 | user_set_password_parser.add_argument("user", help="Usuario Nauta") 276 | user_set_password_parser.add_argument("password", nargs="?", help="Password del usuario Nauta") 277 | 278 | # Remove user 279 | user_remove_parser = user_subparsers.add_parser("remove") 280 | user_remove_parser.set_defaults(func=remove_user) 281 | user_remove_parser.add_argument("user", help="Usuario Nauta") 282 | 283 | user_list_parser = user_subparsers.add_parser("list") 284 | user_list_parser.set_defaults(func=list_users) 285 | 286 | 287 | def main(): 288 | parser = argparse.ArgumentParser(prog=prog_name) 289 | parser.add_argument("--version", action="version", version="{} v{}".format(prog_name, version)) 290 | parser.add_argument("-d", "--debug", action="store_true", help="show debug info") 291 | 292 | subparsers = parser.add_subparsers() 293 | 294 | # Create user subparsers in another function 295 | create_user_subparsers(subparsers) 296 | 297 | # loggin parser 298 | up_parser = subparsers.add_parser("up") 299 | up_parser.set_defaults(func=up) 300 | up_parser.add_argument("-t", "--session-time", action="store", default=None, type=int, help="Tiempo de desconexión en segundos") 301 | up_parser.add_argument("-b", "--batch", action="store_true", default=False, help="Ejecutar en modo no interactivo") 302 | up_parser.add_argument("user", nargs="?", help="Usuario Nauta") 303 | up_parser.add_argument("password", nargs="?", help="Password del usuario Nauta") 304 | 305 | # Logout parser 306 | down_parser = subparsers.add_parser("down") 307 | down_parser.set_defaults(func=down) 308 | 309 | # Is logged in parser 310 | is_logged_in_parser = subparsers.add_parser("is-logged-in") 311 | is_logged_in_parser.set_defaults(func=is_logged_in) 312 | 313 | # Is online parser 314 | is_online_parser = subparsers.add_parser("is-online") 315 | is_online_parser.set_defaults(func=is_online) 316 | 317 | # User information parser 318 | info_parser = subparsers.add_parser("info") 319 | info_parser.set_defaults(func=info) 320 | info_parser.add_argument("user", nargs="?", help="Usuario Nauta") 321 | info_parser.add_argument("password", nargs="?", help="Password del usuario Nauta") 322 | 323 | # Run connected parser 324 | run_connected_parser = subparsers.add_parser("run-connected") 325 | run_connected_parser.set_defaults(func=run_connected) 326 | run_connected_parser.add_argument("-u", "--user", required=False, help="Usuario Nauta") 327 | run_connected_parser.add_argument("-p", "--password", required=False, help="Password del usuario Nauta") 328 | run_connected_parser.add_argument("-rc", "--reuse-connection", action="store_true", required=False, 329 | help="ejecuta el comando incluso si hay una conexión activa, luego cierra " 330 | "la sesión si es posible") 331 | run_connected_parser.add_argument("cmd", nargs=argparse.REMAINDER, help="The command line to run") 332 | 333 | args = parser.parse_args() 334 | if "func" not in args: 335 | parser.print_help() 336 | sys.exit(1) 337 | 338 | try: 339 | args.func(args) 340 | except NautaException as ex: 341 | print(ex.args[0], file=sys.stderr) 342 | except RequestException: 343 | print("Hubo un problema en la red, por favor revise su conexión", file=sys.stderr) 344 | 345 | -------------------------------------------------------------------------------- /test/assets/logged_in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Nauta 40 | 41 | 42 | 43 | 46 | 47 | 50 | 51 | 672 | 673 | 674 | 675 | 676 | 677 | 680 | 681 | 690 | 691 |
692 | 693 |
694 | 695 |
696 | 697 |
698 | 699 | Bienvenido 700 | 701 |
702 | 703 |
704 | 705 |
708 | 709 |
710 | 711 |
712 | 713 |
714 | 715 | 716 | 717 |
718 | 719 | Usted está conectado 722 | 723 |
724 | 725 |
726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 738 | 739 | 744 | 745 | 746 | 747 | 748 | 749 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 |
734 | 735 | Usuario 736 | 737 | 740 | 741 | rrtoledo@nauta.com.cu 742 | 743 |
750 | 751 | Tiempo consumido 752 | 753 |
762 | 763 | Tiempo disponible 764 | 765 |
774 | 775 |
776 | 777 | 782 | 783 | 788 | 789 |
790 | 791 | 794 | 795 |
796 | 797 |
798 | 799 |
800 | 801 |
802 | 803 |
804 | 805 | 806 | 807 | 808 | 809 | 822 | 823 | --------------------------------------------------------------------------------