diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e1f44ed --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +Dockerfile +.* +tmp diff --git a/.gitignore b/.gitignore index cef0c88..8537fab 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,7 @@ venv.bak/ # macOS .DS_Store + +curls.txt +*.bak +tmp/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8974549 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.8-alpine3.12 + +RUN pip install requests + +ADD . /orbi_dnsmasq +WORKDIR /orbi_dnsmasq +RUN python setup.py install + +RUN addgroup -S orbi && adduser -S orbi -G orbi +USER orbi + +ENTRYPOINT ["orbi-dnsmasq"] diff --git a/README.md b/README.md index cf0d6e2..c396bdd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PyPI version](https://badge.fury.io/py/orbi-dnsmasq.svg)](https://badge.fury.io/py/orbi-dnsmasq) -Python command to set a hosted hosts file as dnsmasq on your orbi router. +Python command to update dnsmasq configuration file and / or set hosted hosts file on your Netgear Orbi router. ## Description @@ -29,7 +29,8 @@ And that's the reason I created `orbi-dnsmasq`, and why you are reading this now - Asks for router password (note: the password is not stored in any way, you'll need to re-enter it every time). - Turns on telnet on the debug Orbi GUI if `-t` flag was supplied. - Telnet into Orbi. -- Downloads hosts file (you can specify a custom url with the `-d` flag, or check the default one I'm using +- Replace `/etc/dnsmasq.conf` with a custom configuration file specified by `-c ` +- Downloads hosts file if `-d` was supplied (you can specify a custom url with the `-u ` flag, or check the default one I'm using on [someonewhocares.org](https://someonewhocares.org/hosts/)). - Deletes the `no-hosts` line to active dnsmasq. - Reboots Orbi's dns. @@ -37,7 +38,22 @@ on [someonewhocares.org](https://someonewhocares.org/hosts/)). ## Getting Started -### Prerequisites +### Running in Docker + +This is the easiest way to run, all you need is Docker installed an run: +``` +docker build -t orbi-dnsmasq . && \ +docker run -it --rm orbi-dnsmasq +``` +You only need the first line on the first run or if you change any Python files. + +If you want to upload a custom configuration file, you'll have to expose it as Docker volume, ie: +``` +docker run -it --rm -v :/tmp/dnsmasq.conf orbi-dnsmasq -p $orbipw -a 192.168.21.1 -t -c /tmp/dnsmasq.conf +``` + + +### Prerequisites (without Docker) First, make sure python is already installed on your system by opening the interactive environment by running on your terminal: @@ -73,27 +89,24 @@ Also, **don't worry if you end up running this command twice**. I made sure it d #### Auto turn on telnet before command, and turn it off after -I don't like leaving my telnet port open when I'm not using it, so I built in feature with selenium to toggle this option -on before running configs on the Orbi, and turning it off afterwards. If you want to use this feature, you'll first need to -[download one of the selenium webdrivers](https://selenium-python.readthedocs.io/installation.html#introduction). -Any will do, just make sure you also have that browser installed on your system. +If you don't like leaving telnet port open when not using it, you can use the `-t` parameter to automatically toggle Telnet on and off after work is done. After that, run: ``` -orbi-dnsmasq -t -w path/to/downloaded/webdriver +orbi-dnsmasq -t ``` -You can avoid the `-w` flag if you put the downloaded webdriver in your PATH. +## Known issues +Sometimes authentication will fail and you'll see a `ts not found, retry in a minute or two` error. Normally retrying works. ## Possible things to add: -- Read password from ENV var, don't ask it if already found. +- ~~Read password from ENV var, don't ask it if already found.~~ You can provide the `admin` password with `-p ` parameter (warning: your password may be recorded on your terminal's history, watch out for that). - Flag option to set username to connect with, use admin as default (is this even necessary?). - Create Telnet object with telnet_write methods, cleaning up the code. -- Flag option to indicate how much to wait for web elements to appear, and to indicate polling rate. -- Use polling rate and selenium with expected condition wait. -- Remove selenium's send keys TODO and `keyboard` dependency when [this issue is fixed](https://github.com/w3c/webdriver/issues/385). +- Understand why authentication fails every now and then and fix it. +- Improve `README.md` - Unit testing? (does it even makes sense? All telnet communications would need to be mocked) ## Acknowledgments diff --git a/orbi_dnsmasq/__main__.py b/orbi_dnsmasq/__main__.py index 911955b..d87c9d4 100644 --- a/orbi_dnsmasq/__main__.py +++ b/orbi_dnsmasq/__main__.py @@ -7,6 +7,14 @@ parser.add_option( "-d", "--dns-hosts", + action="store_true", + default=False, + dest="download_hosts", + help="Download dns-hosts file" +) +parser.add_option( + "-u", + "--dns-url", action="store", default=DEFAULT_DNS_HOSTS_FILE, dest="dns_hosts_url", @@ -21,14 +29,30 @@ help="Toggle telnet service on the orbi debug site with selenium" ) parser.add_option( - "-w", - "--webdriver", + "-a", + "--address", + action="store", + default="orbilogin.com", + dest="address", + help="IP or hostname of your router", + type="string" +) +parser.add_option( + "-p", + "--password", action="store", default=None, - dest="webdriver_path", - help="Path to the webdriver for selenium", + dest="password", + help="The password for the admin user (warning: insecure, watch out for your terminal's history)", type="string" ) +parser.add_option( + "-c", + "--config-file", + action="store", + dest="config_file", + help="Path to config file to override default Orbi file" +) def command_line_main(): diff --git a/orbi_dnsmasq/orbi_dnsmasq.py b/orbi_dnsmasq/orbi_dnsmasq.py index 1c48e40..d9024b8 100644 --- a/orbi_dnsmasq/orbi_dnsmasq.py +++ b/orbi_dnsmasq/orbi_dnsmasq.py @@ -1,55 +1,13 @@ -import keyboard import re import telnetlib from getpass import getpass -from selenium import webdriver -from selenium.common.exceptions import WebDriverException from time import sleep +import requests -DEBUG_HOST = "https://orbilogin.com/debug.htm" -TELNET_HOST = "orbilogin.com" +DEBUG_ADDRESS = "http://{}/debug.htm" USERNAME = "admin" -def ask_for_credentials(): - _pass = getpass('Password: ') - return USERNAME, _pass - - -def enter_orbi_debug_credentials(_user, _pass, driver): - user_pass_alert = driver.switch_to.alert - - for word in [_user, _pass]: - keyboard.write(word) # TODO: Use selenium's send keys when fixed for alerts - keyboard.press_and_release("tab") - user_pass_alert.accept() - - -def get_telnet_checkbox(driver): - form_frame_array = [] - while len(form_frame_array) == 0: - sleep(1) - form_frame_array = driver.find_elements_by_id("formframe") - driver.switch_to.frame(form_frame_array.pop()) - - telnet_array = [] - while len(telnet_array) == 0: - sleep(1) - telnet_array = driver.find_elements_by_name("enable_telnet") - - return telnet_array.pop() - - -def change_telnet_status(status, checkbox): - if status == (checkbox.get_attribute('checked') is None): - checkbox.click() - - -def set_orbi_telnet(status, driver): - telnet_checkbox = get_telnet_checkbox(driver) - change_telnet_status(status, telnet_checkbox) - - def telnet_write(tn, _str, wait_until): tn.write((_str + "\n").encode('utf-8')) telnet_read_until(tn, wait_until) @@ -59,11 +17,11 @@ def telnet_read_until(tn, str_regex): regex_list = [str_regex] if isinstance(str_regex, list): regex_list = str_regex - tn.expect([re.compile(s.encode('utf-8')) for s in regex_list]) + tn.expect([re.compile(s.encode('utf-8')) for s in regex_list], timeout=5) -def telnet_into_router(_user, _pass): - tn = telnetlib.Telnet(TELNET_HOST) +def telnet_into_router(_host, _user, _pass): + tn = telnetlib.Telnet(_host) telnet_read_until(tn, "login: ") telnet_write(tn, _user, "Password: ") @@ -72,65 +30,79 @@ def telnet_into_router(_user, _pass): return tn -def go_into_etc(tn): - telnet_write(tn, "cd etc", "#") - - def download_hosts_file_into_router(tn, dns_hosts_url): - telnet_write(tn, "curl %s -k -o hosts" % dns_hosts_url, "#") + print("downloading hosts file") + telnet_write(tn, "curl %s -k -o /etc/hosts" % dns_hosts_url, "#") def activate_hosts_file(tn): - telnet_write(tn, "vim dnsmasq.conf", "- dnsmasq.conf") - telnet_write(tn, ":%s/no-hosts//e", ["- dnsmasq.conf", "Put"]) # Edge case: line not present - telnet_write(tn, ":wq", "#") + print("removing `no-hosts` from `/etc/dnsmasq.conf`") + telnet_write(tn, "sed -i 's/no-hosts//g' /etc/dnsmasq.conf", "#") # Edge case: line not present def reboot_dns(tn): + print("killing dnsmasq") telnet_write(tn, "kill $(pidof dnsmasq)", "#") + print("starting dnsmasq") telnet_write( tn, "/usr/sbin/dnsmasq --except-interface=lo -r /tmp/resolv.conf --addn-hosts=/tmp/dhcpd_hostlist", "#" ) +def upload_config_file(tn, file_path): + with open(file_path, "r") as f: + content = f.read() + print("Applying the following configuration: \n{}".format(content)) + telnet_write(tn, "cat < /etc/dnsmasq.conf\n{}\nEOF\n".format(content), "#") + print("\n new config uploaded") -def find_selenium_driver(webdriver_path): - possible_drivers = [ - webdriver.Chrome, - webdriver.Edge, - webdriver.Firefox, - webdriver.Safari - ] - for driver in possible_drivers: - try: - if webdriver_path: - return driver(webdriver_path) - else: - return driver() - except WebDriverException: - continue - raise IOError('No selenium web driver was found') +def get_web_ts(host, username, password): + r = requests.get(f'http://{host}/debug_detail.htm', auth=(username, password)) + results = re.search('ts="(\d+)', r.text) + if results: + print(f"ts={results.group(1)}") + return results.group(1) + else: + raise Exception("ts not found, retry in a minute or two") -def script_main(options, _): - _user, _pass = ask_for_credentials() - driver = None +def enable_telnet(host, username, password): + ts = get_web_ts(host, username, password) + r = requests.post(f'http://{host}/apply.cgi?/debug_detail.htm%20timestamp={ts}', + auth=(username, password), + data={'submit_flag':'debug_info', 'hid_telnet':'1', 'enable_telnet':'on'}) + print(f"telnet enable {r.status_code}") + return r.status_code == 200 + + +def disable_telnet(host, username, password): + ts = get_web_ts(host, username, password) + r = requests.post(f'http://{host}/apply.cgi?/debug_detail.htm%20timestamp={ts}', + auth=(username, password), + data={'submit_flag':'debug_info', 'hid_telnet':'0'}) + print(f"telnet disable {r.status_code}") + return r.status_code == 200 + + +def script_main(options, _): + _pass = options.password + if _pass is None: + _pass = getpass('Password: ') + if options.toggle_telnet: - driver = find_selenium_driver(options.webdriver_path) - driver.get(DEBUG_HOST) - enter_orbi_debug_credentials(_user, _pass, driver) - set_orbi_telnet(True, driver) - - tn = telnet_into_router(_user, _pass) - go_into_etc(tn) - download_hosts_file_into_router(tn, options.dns_hosts_url) - activate_hosts_file(tn) - reboot_dns(tn) - tn.close() + enable_telnet(options.address, USERNAME, _pass) + + if options.config_file or options.download_hosts: + tn = telnet_into_router(options.address, USERNAME, _pass) + if options.config_file: + upload_config_file(tn, options.config_file) + if options.download_hosts: + download_hosts_file_into_router(tn, options.dns_hosts_url) + activate_hosts_file(tn) + reboot_dns(tn) + tn.close() if options.toggle_telnet: - driver.get(DEBUG_HOST) - set_orbi_telnet(False, driver) - driver.quit() + disable_telnet(options.address, USERNAME, _pass) diff --git a/setup.py b/setup.py index 4586e97..4068a91 100644 --- a/setup.py +++ b/setup.py @@ -19,8 +19,7 @@ download_url="https://github.com/Diego-Zulu/orbi-dnsmasq/archive/v1.1.0.tar.gz", entry_points={"console_scripts": ["orbi-dnsmasq = orbi_dnsmasq:command_line_main"]}, install_requires=[ - "keyboard", - "selenium", + "requests", ], keywords=["orbi", "dnsmasq"], license="MIT",