mirror of
https://github.com/Nighthawk42/eero-py.git
synced 2026-08-30 07:22:27 +00:00
Initial commit
This commit is contained in:
@@ -1,2 +1,59 @@
|
||||
# eero-py
|
||||
eero-py is a Python-based CLI tool designed to interact with the eero API. This tool allows users dump information about their eero networks through simple command-line.
|
||||
|
||||
## Features
|
||||
|
||||
- Log into your eero account, using either your e-mail or phone number.
|
||||
- 2FA support.
|
||||
- Securely stores your session information.
|
||||
- Dump and monitor your eero network
|
||||
|
||||
## Installation
|
||||
|
||||
To get started, clone the repository and install the required dependencies:
|
||||
|
||||
```
|
||||
git clone https://github.com/yourusername/eero-py.git
|
||||
cd eero-py
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- **Account Information**
|
||||
```
|
||||
python eero_cli.py account
|
||||
```
|
||||
Retrieves and displays your account information.
|
||||
|
||||
- **List Devices:**
|
||||
```
|
||||
python eero_cli.py devices
|
||||
```
|
||||
Lists all devices connected to your eero network.
|
||||
|
||||
- **Get Network Status:**
|
||||
```
|
||||
python eero_cli.py networks
|
||||
```
|
||||
Displays the current status of your eero network.
|
||||
|
||||
- **Display Session Token**
|
||||
```
|
||||
python eero_cli.py session
|
||||
```
|
||||
Displays the encrypted session token if it exists.
|
||||
|
||||
## Output
|
||||
- ```
|
||||
--output <filename>.json | <filename>.csv
|
||||
```
|
||||
You can save the output of a quary by appending the --output switch after the command. JSON and CSV are supported.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please fork the repository and submit a pull request for review.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import requests
|
||||
import os
|
||||
import click
|
||||
import base64
|
||||
from getpass import getpass
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding, hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
BASE_URL = "https://api-user.e2ro.com/2.2/"
|
||||
SESSION_FILE = "eero_session"
|
||||
|
||||
def derive_key(passphrase, salt):
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
)
|
||||
return kdf.derive(passphrase.encode())
|
||||
|
||||
def encrypt_data(data, passphrase):
|
||||
salt = os.urandom(16)
|
||||
key = derive_key(passphrase, salt)
|
||||
iv = os.urandom(16)
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
encryptor = cipher.encryptor()
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(data.encode()) + padder.finalize()
|
||||
ct = encryptor.update(padded_data) + encryptor.finalize()
|
||||
return base64.b64encode(salt + iv + ct).decode()
|
||||
|
||||
def decrypt_data(encrypted_data, passphrase):
|
||||
encrypted_data = base64.b64decode(encrypted_data.encode())
|
||||
salt = encrypted_data[:16]
|
||||
iv = encrypted_data[16:32]
|
||||
ct = encrypted_data[32:]
|
||||
key = derive_key(passphrase, salt)
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
decryptor = cipher.decryptor()
|
||||
padded_data = decryptor.update(ct) + decryptor.finalize()
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
data = unpadder.update(padded_data) + unpadder.finalize()
|
||||
return data.decode()
|
||||
|
||||
class EeroClient:
|
||||
def __init__(self, session_token=""):
|
||||
self.session_token = session_token
|
||||
self.headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
self.headers["Cookie"] = f"s={session_token}"
|
||||
|
||||
def login(self, login):
|
||||
url = f"{BASE_URL}login"
|
||||
payload = {"login": login}
|
||||
response = requests.post(url, json=payload, headers=self.headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()["data"]
|
||||
else:
|
||||
raise Exception(f"Login failed: {response.text}")
|
||||
|
||||
def login_verify(self, session_token, verification_code):
|
||||
url = f"{BASE_URL}login/verify"
|
||||
payload = {"code": verification_code}
|
||||
headers = self.headers.copy()
|
||||
headers["Cookie"] = f"s={session_token}"
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"Verification failed: {response.text}")
|
||||
|
||||
@click.command()
|
||||
@click.option('--session-file', default=SESSION_FILE, help='Session file to load/save session token')
|
||||
def auth(session_file):
|
||||
while True:
|
||||
passphrase = getpass("Enter passphrase for encrypting session token: ")
|
||||
client = EeroClient()
|
||||
user_id = input("Enter Eero login ID (phone or email address): ").strip().lower()
|
||||
|
||||
try:
|
||||
login_data = client.login(user_id)
|
||||
session_token = login_data["user_token"]
|
||||
verification_code = input("Enter OTP from Email or SMS: ").strip()
|
||||
client.login_verify(session_token, verification_code)
|
||||
encrypted_token = encrypt_data(session_token, passphrase)
|
||||
with open(session_file, 'w') as file:
|
||||
file.write(encrypted_token)
|
||||
print("Authentication successful.")
|
||||
break # Exit the loop upon successful authentication
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
retry = input("Login failed. Would you like to try again? (yes/no): ").strip().lower()
|
||||
if retry != 'yes':
|
||||
break # Exit the loop if the user does not want to retry
|
||||
|
||||
if __name__ == "__main__":
|
||||
auth()
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
import csv
|
||||
import click
|
||||
import base64
|
||||
from getpass import getpass
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding, hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
BASE_URL = "https://api-user.e2ro.com/2.2/"
|
||||
SESSION_FILE = "eero_session"
|
||||
|
||||
def derive_key(passphrase, salt):
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
)
|
||||
return kdf.derive(passphrase.encode())
|
||||
|
||||
def encrypt_data(data, passphrase):
|
||||
salt = os.urandom(16) # Generate a unique salt
|
||||
key = derive_key(passphrase, salt)
|
||||
iv = os.urandom(16) # Generate a unique IV
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
encryptor = cipher.encryptor()
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(data.encode()) + padder.finalize()
|
||||
ct = encryptor.update(padded_data) + encryptor.finalize()
|
||||
return base64.b64encode(salt + iv + ct).decode()
|
||||
|
||||
def decrypt_data(encrypted_data, passphrase):
|
||||
encrypted_data = base64.b64decode(encrypted_data.encode())
|
||||
salt = encrypted_data[:16] # Extract the salt
|
||||
iv = encrypted_data[16:32] # Extract the IV
|
||||
ct = encrypted_data[32:] # Extract the ciphertext
|
||||
key = derive_key(passphrase, salt)
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
decryptor = cipher.decryptor()
|
||||
padded_data = decryptor.update(ct) + decryptor.finalize()
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
data = unpadder.update(padded_data) + unpadder.finalize()
|
||||
return data.decode()
|
||||
|
||||
class EeroClient:
|
||||
def __init__(self, session_token=""):
|
||||
self.session_token = session_token
|
||||
self.headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
self.headers["Cookie"] = f"s={session_token}"
|
||||
|
||||
def get_account(self):
|
||||
url = f"{BASE_URL}account"
|
||||
response = requests.get(url, headers=self.headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()["data"]
|
||||
else:
|
||||
raise Exception(f"Account retrieval failed: {response.text}")
|
||||
|
||||
def get_networks(self):
|
||||
account_data = self.get_account()
|
||||
return account_data["networks"]["data"]
|
||||
|
||||
def get_devices(self):
|
||||
networks = self.get_networks()
|
||||
devices = []
|
||||
for network in networks:
|
||||
network_id = network["url"].split("/")[-1]
|
||||
url = f"{BASE_URL}networks/{network_id}/devices"
|
||||
response = requests.get(url, headers=self.headers)
|
||||
if response.status_code == 200:
|
||||
devices.extend(response.json()["data"])
|
||||
else:
|
||||
raise Exception(f"Device retrieval failed: {response.text}")
|
||||
return devices
|
||||
|
||||
@click.group()
|
||||
@click.option('--session-file', default=SESSION_FILE, help='Session file to load/save session token')
|
||||
@click.pass_context
|
||||
def cli(ctx, session_file):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['SESSION_FILE'] = session_file
|
||||
passphrase = getpass("Enter passphrase for decrypting session token: ")
|
||||
if os.path.exists(session_file):
|
||||
with open(session_file, 'r') as file:
|
||||
encrypted_token = file.read().strip()
|
||||
session_token = decrypt_data(encrypted_token, passphrase)
|
||||
ctx.obj['CLIENT'] = EeroClient(session_token)
|
||||
else:
|
||||
ctx.obj['CLIENT'] = EeroClient()
|
||||
|
||||
def save_to_file(data, output_file):
|
||||
if output_file.endswith('.json'):
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(f"Data saved to {output_file}")
|
||||
elif output_file.endswith('.csv'):
|
||||
with open(output_file, 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
if isinstance(data, list):
|
||||
headers = data[0].keys()
|
||||
writer.writerow(headers)
|
||||
for row in data:
|
||||
writer.writerow(row.values())
|
||||
else:
|
||||
writer.writerow(data.keys())
|
||||
writer.writerow(data.values())
|
||||
print(f"Data saved to {output_file}")
|
||||
else:
|
||||
print(f"Unsupported file format: {output_file}")
|
||||
|
||||
@cli.command()
|
||||
@click.option('--output', default=None, help='Output file to save the data in JSON or CSV format')
|
||||
@click.pass_context
|
||||
def account(ctx, output):
|
||||
client = ctx.obj['CLIENT']
|
||||
account_data = client.get_account()
|
||||
if output:
|
||||
save_to_file(account_data, output)
|
||||
else:
|
||||
print(json.dumps(account_data, indent=2))
|
||||
|
||||
@cli.command()
|
||||
@click.option('--output', default=None, help='Output file to save the data in JSON or CSV format')
|
||||
@click.pass_context
|
||||
def devices(ctx, output):
|
||||
client = ctx.obj['CLIENT']
|
||||
devices_data = client.get_devices()
|
||||
if output:
|
||||
save_to_file(devices_data, output)
|
||||
else:
|
||||
print(json.dumps(devices_data, indent=2))
|
||||
|
||||
@cli.command()
|
||||
@click.option('--output', default=None, help='Output file to save the data in JSON or CSV format')
|
||||
@click.pass_context
|
||||
def networks(ctx, output):
|
||||
client = ctx.obj['CLIENT']
|
||||
networks_data = client.get_networks()
|
||||
if output:
|
||||
save_to_file(networks_data, output)
|
||||
else:
|
||||
print(json.dumps(networks_data, indent=2))
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def session(ctx):
|
||||
session_file = ctx.obj['SESSION_FILE']
|
||||
if os.path.exists(session_file):
|
||||
with open(session_file, 'r') as file:
|
||||
encrypted_token = file.read().strip()
|
||||
print(f"Encrypted token: {encrypted_token}")
|
||||
else:
|
||||
print("No session file found.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -0,0 +1,3 @@
|
||||
click
|
||||
cryptography
|
||||
requests
|
||||
Reference in New Issue
Block a user