Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to upload dnsmasq.conf and Docker #1

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__
*.pyc
Dockerfile
.*
tmp
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,7 @@ venv.bak/

# macOS
.DS_Store

curls.txt
*.bak
tmp/*
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,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

Expand All @@ -26,15 +26,31 @@ 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 <path>`
- Downloads hosts file if `-d` was supplied (you can specify a custom url with the `-u <url>` 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.
- Turns off telnet on the debug Orbi GUI if `-t` flag was supplied.

## 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 <parameters>
```
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 <full path to dnsmasq.conf>:/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:

Expand Down Expand Up @@ -70,27 +86,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 <password>` 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
Expand Down
32 changes: 28 additions & 4 deletions orbi_dnsmasq/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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():
Expand Down
148 changes: 60 additions & 88 deletions orbi_dnsmasq/orbi_dnsmasq.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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: ")
Expand All @@ -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 <<EOF > /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)
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down