diff --git a/README.md b/README.md index e6cd521..1c4fb6b 100644 --- a/README.md +++ b/README.md @@ -38,23 +38,43 @@ You also need to have a valid Amazon account and access to the account you want 6. Let the page load completely. 7. Delete a device using the Alexa app. 8. Stop the capture in the HTTP Sniffer. -9. Search for the `GET /api/behaviors/entities` request in the HTTP Sniffer. -10. Copy the value of the `Cookie` header and paste it into the `COOKIE` variable in the script (Most likely, you will find the cookie value to be very long). -11. Copy the value of the `x-amzn-alexa-app` header and paste it into the `X_AMZN_ALEXA_APP` variable in the script. -12. Copy the CSRF value found at the end of the cookie and paste it into the `CSRF` variable -13. Look for a `DELETE` request containing `/api/phoenix/appliance/` -14. Copy the part after `api/phoenix/appliance/` but before `%3D%3D_` and set `DELETE_SKILL` variable to that - - e.g. SKILL_abc123abc (much longer) -16. Update the `HOST` to match the host your Alexa App is making requests to - - e.g. `eu-api-alexa.amazon.co.uk` -18. You can now try and run the script. If it works, you should see a list of all devices connected to the account you are logged in with. If you get an error, see the [Troubleshooting](#troubleshooting) section for more information. +9. Extract the following values from your HTTP Sniffer: + - **HOST**: The host from the `GET /api/behaviors/entities` request (e.g., `na-api-alexa.amazon.com` or `eu-api-alexa.amazon.co.uk`) + - **COOKIE**: The full `Cookie` header value from the `GET /api/behaviors/entities` request (will be very long) + - **X_AMZN_ALEXA_APP**: The `x-amzn-alexa-app` header value from the `GET /api/behaviors/entities` request + - **DELETE_SKILL**: From the `DELETE` request containing `/api/phoenix/appliance/`, copy the part after `api/phoenix/appliance/` but before `%3D%3D_` (e.g., `SKILL_abc123abc...`) + +10. Run the script. You can provide values in three ways: + + **Interactive mode** (prompts for all values): + ```bash + python main.py + ``` + The script will prompt you for each value. CSRF is automatically extracted from the cookie. + + **Command-line arguments** (skip prompts): + ```bash + python main.py --host na-api-alexa.amazon.com --cookie "..." --alexa-app "..." --delete-skill "..." + ``` + + **Environment variables** (skip prompts): + ```bash + export ALEXA_HOST="na-api-alexa.amazon.com" + export ALEXA_COOKIE="..." + export ALEXA_APP="..." + export ALEXA_DELETE_SKILL="..." + python main.py + ``` + + You can mix and match - provide some values via arguments/env vars and the script will only prompt for the missing ones. + +If it works, you should see a list of all devices connected to the account you are logged in with. If you get an error, see the [Troubleshooting](#troubleshooting) section for more information. ## Troubleshooting -1. Try and change the `HOST` address in the script to your local Amazon address. You can find it in the HTTP Sniffer in both the requests you copied the headers from. -2. Try and change the `USER_AGENT` variable in the script to the one you find in the HTTP Sniffer in both the requests you copied the headers from. -3. If you used step 11.1, try and change the `CSRF` variable in the script to the one you find in the HTTP Sniffer in the `DELETE` request. -4. If you used the script some time ago, try and update the `COOKIE` variable in the script to the one you find in the HTTP Sniffer in the `GET` and/or `DELETE` request. +1. Try changing the `HOST` address to your local Amazon address. You can find it in the HTTP Sniffer in both the requests you copied the headers from. Provide it via `--host` argument or `ALEXA_HOST` environment variable. +2. If you used the script some time ago, try updating the `COOKIE` value - cookies expire. Provide it via `--cookie` argument or `ALEXA_COOKIE` environment variable. +3. Make sure you copied the complete cookie string - it should be very long. The CSRF token is automatically extracted from the cookie, so you don't need to provide it separately. ## Inspiration diff --git a/main.py b/main.py index b5fad0a..a2390b6 100644 --- a/main.py +++ b/main.py @@ -25,20 +25,237 @@ import time # only needed if you want to add a delay between each delete request import requests import uuid +import readline # Fixes macOS terminal input buffer limit (1024 chars) +import argparse +import os # Settings DEBUG = False # set this to True if you want to see more output SHOULD_SLEEP = False # set this to True if you want to add a delay between each delete request DESCRIPTION_FILTER_TEXT = "Home Assistant" -# CHANGE THESE TO MATCH YOUR SETUP -HOST = "na-api-alexa.amazon.ca" +def extract_csrf_from_cookie(cookie): + """ + Extracts the CSRF token from a Cookie header string. + + Cookie headers use a simpler format than Set-Cookie: name=value; name2=value2 + This function parses the Cookie header format correctly. + + Args: + cookie (str): The cookie string containing csrf=value + + Returns: + str: The CSRF token value, or None if not found + """ + # Strip leading/trailing whitespace and semicolons + cookie = cookie.strip().strip(';') + + # Split by semicolon to get individual cookie pairs + cookie_pairs = cookie.split(';') + + for pair in cookie_pairs: + # Strip whitespace from each pair + pair = pair.strip() + # Split on first '=' to separate name and value + if '=' in pair: + name, value = pair.split('=', 1) + name = name.strip() + # Check if this is the csrf cookie + if name == 'csrf': + return value.strip() + + return None + +def transform_device_id_for_url(description): + """ + Transforms a device description into the format needed for DELETE URL. + + Args: + description: Device description string + + Returns: + str: Transformed device ID for URL + """ + return description.replace(".", "%23").replace(" via Home Assistant", "").lower() + +def build_base_headers(): + """Returns common headers used across requests.""" + return { + "Host": HOST, + "Cookie": COOKIE, + "Connection": "keep-alive", + "Accept": ACCEPT_HEADER, + "Accept-Language": "en-CA,en-CA;q=1.0,ar-CA;q=0.9", + "User-Agent": USER_AGENT, + "x-amzn-alexa-app": X_AMZN_ALEXA_APP, + } + +def build_get_headers(): + """Returns headers for GET requests.""" + headers = build_base_headers() + headers["Routines-Version"] = ROUTINE_VERSION + return headers + +def build_delete_headers(): + """Returns headers for DELETE requests.""" + headers = build_base_headers() + headers["Content-Length"] = "0" + headers["csrf"] = CSRF + return headers + +def build_graphql_headers(): + """Returns headers for GraphQL POST requests.""" + headers = build_base_headers() + headers.update({ + "csrf": CSRF, + "Content-Type": "application/json; charset=utf-8", + "Accept-Encoding": "gzip, deflate, br", + }) + return headers + +def parse_arguments(): + """ + Parses command-line arguments and environment variables for configuration values. + + Returns: + dict: Dictionary with HOST, COOKIE, X_AMZN_ALEXA_APP, DELETE_SKILL (or None if not provided) + """ + parser = argparse.ArgumentParser( + description="Delete Alexa devices. Provide values via command-line args or environment variables to skip prompts." + ) + parser.add_argument("--host", help="Amazon API host (e.g., na-api-alexa.amazon.com)") + parser.add_argument("--cookie", help="Full Cookie header value") + parser.add_argument("--alexa-app", dest="alexa_app", help="x-amzn-alexa-app header value") + parser.add_argument("--delete-skill", dest="delete_skill", help="DELETE_SKILL value") + + args = parser.parse_args() + + # Check environment variables if not provided via command line + config = { + "HOST": args.host or os.environ.get("ALEXA_HOST"), + "COOKIE": args.cookie or os.environ.get("ALEXA_COOKIE"), + "X_AMZN_ALEXA_APP": args.alexa_app or os.environ.get("ALEXA_APP"), + "DELETE_SKILL": args.delete_skill or os.environ.get("ALEXA_DELETE_SKILL"), + } + + return config + +def prompt_user_input(provided_config=None): + """ + Prompts the user for required configuration values, skipping those already provided. + + Args: + provided_config: Optional dict with HOST, COOKIE, X_AMZN_ALEXA_APP, DELETE_SKILL + + Returns: + tuple: (HOST, COOKIE, X_AMZN_ALEXA_APP, DELETE_SKILL, CSRF) + """ + if provided_config is None: + provided_config = {} + + print("=" * 80) + print("Alexa Device Deletion Script - Configuration") + print("=" * 80) + print() + + # Only show setup instructions if we need to prompt for values + if not all([provided_config.get("HOST"), provided_config.get("COOKIE"), + provided_config.get("X_AMZN_ALEXA_APP"), provided_config.get("DELETE_SKILL")]): + print("HTTP Sniffer Setup (do this first):") + print(" 1. Open Alexa app and navigate to Devices tab") + print(" 2. Start HTTP Sniffer capture (e.g., HTTP Catcher, Proxyman, HTTP Toolkit)") + print(" 3. Refresh device list in Alexa app") + print(" 4. Delete a device using the Alexa app (to capture DELETE request)") + print(" 5. Stop the capture in your HTTP Sniffer") + print() + print("Now extract the following values from your HTTP Sniffer:") + print() + + # Prompt for HOST + default_host = "na-api-alexa.amazon.com" + if provided_config.get("HOST"): + HOST = provided_config["HOST"] + print(f"✓ Using HOST from arguments: {HOST}") + else: + print("HOST:") + print(" The Amazon API host for your region.") + print(" Find in: GET /api/behaviors/entities request") + print(" Examples: 'na-api-alexa.amazon.com', 'eu-api-alexa.amazon.co.uk'") + print() + user_input = input(f"Enter HOST (default: {default_host}): ").strip() + HOST = user_input if user_input else default_host + print() + + # Prompt for COOKIE + if provided_config.get("COOKIE"): + COOKIE = provided_config["COOKIE"] + print(f"✓ Using COOKIE from arguments (length: {len(COOKIE)} characters)") + else: + print("COOKIE:") + print(" The full Cookie header value (will be very long).") + print(" Find in: Cookie header from GET /api/behaviors/entities request") + print() + COOKIE = input("Enter COOKIE: ").strip() + if not COOKIE: + raise ValueError("Cookie value cannot be empty. Please provide a valid cookie string.") + print() + + # Extract CSRF from cookie + CSRF = extract_csrf_from_cookie(COOKIE) + if not CSRF: + raise ValueError( + "ERROR: Could not extract CSRF token from cookie. " + "Please ensure your cookie contains a 'csrf=value' entry. " + "Make sure you copied the complete Cookie header from your HTTP Sniffer." + ) + print(f"✓ CSRF token automatically extracted from cookie: {CSRF}") + print() + + # Prompt for X_AMZN_ALEXA_APP + if provided_config.get("X_AMZN_ALEXA_APP"): + X_AMZN_ALEXA_APP = provided_config["X_AMZN_ALEXA_APP"] + print(f"✓ Using X_AMZN_ALEXA_APP from arguments") + else: + print("X_AMZN_ALEXA_APP:") + print(" The x-amzn-alexa-app header value (base64 encoded app info).") + print(" Find in: x-amzn-alexa-app header from GET /api/behaviors/entities request") + print() + X_AMZN_ALEXA_APP = input("Enter X_AMZN_ALEXA_APP: ").strip() + print() + + # Prompt for DELETE_SKILL + if provided_config.get("DELETE_SKILL"): + DELETE_SKILL = provided_config["DELETE_SKILL"] + print(f"✓ Using DELETE_SKILL from arguments") + else: + print("DELETE_SKILL:") + print(" The skill identifier used in DELETE requests.") + print(" Find in: DELETE request containing '/api/phoenix/appliance/'") + print(" Copy the part after 'api/phoenix/appliance/' but before '%3D%3D_'") + print(" Example format: 'SKILL_abc123abc...' (much longer)") + print() + DELETE_SKILL = input("Enter DELETE_SKILL: ").strip() + print() + + print("=" * 80) + print("Configuration complete!") + print("=" * 80) + print() + + return HOST, COOKIE, X_AMZN_ALEXA_APP, DELETE_SKILL, CSRF + +# Get configuration from command-line args, environment variables, or user prompts +provided_config = parse_arguments() +HOST, COOKIE, X_AMZN_ALEXA_APP, DELETE_SKILL, CSRF = prompt_user_input(provided_config) + +# Validate that we have all required values +if not all([HOST, COOKIE, X_AMZN_ALEXA_APP, DELETE_SKILL, CSRF]): + print("ERROR: Missing required configuration values. Please run the script again.") + exit(1) + +# Constants USER_AGENT = "AppleWebKit PitanguiBridge/2.2.635412.0-[HARDWARE=iPhone17_3][SOFTWARE=18.2][DEVICE=iPhone]" ROUTINE_VERSION = "3.0.255246" -COOKIE = ';at-acbca="LONG STRING";sess-at-acbca="SHORT STRING";session-id=000-0000000-0000000;session-id-time=2366612930l;session-token=LOING_STRING;ubid-acbca=000-0000000-00000;x-acbca="SHORT_STRING";csrf=NUMBER' -X_AMZN_ALEXA_APP = "LONG_STRING" -CSRF = "NUMBER" # should look something like this: 'somenumber'; should match the cookie -DELETE_SKILL = "SKILL_LONG_STRING" # Constants DATA_FILE = "data.json" @@ -59,15 +276,7 @@ def get_entities(url = GET_URL): Returns: dict: The JSON response from the GET request. """ - GET_HEADERS = { - "Host": HOST, - "Routines-Version": ROUTINE_VERSION , - "Cookie": COOKIE, - "Connection": "keep-alive", - "x-amzn-alexa-app": X_AMZN_ALEXA_APP, - "Accept": ACCEPT_HEADER, - "User-Agent": USER_AGENT, - } + GET_HEADERS = build_get_headers() parameters = { "skillId": "amzn1.ask.1p.smarthome" @@ -99,15 +308,8 @@ def check_device_deleted(entity_id): bool: True if the device was deleted, False otherwise. """ url = f"https://{HOST}/api/smarthome/v1/presentation/devices/control/{entity_id}" - headers = { - "x-amzn-RequestId": str(uuid.uuid4()), - "Host": HOST, - "User-Agent": USER_AGENT, - "Cookie": COOKIE, - "Connection": "keep-alive", - "Accept": ACCEPT_HEADER, - "x-amzn-alexa-app": X_AMZN_ALEXA_APP - } + headers = build_get_headers() + headers["x-amzn-RequestId"] = str(uuid.uuid4()) response = requests.get(url, headers=headers, timeout=10) if DEBUG: print(f"Check device deleted response status code: {response.status_code}") @@ -126,15 +328,7 @@ def delete_entities(): list: A list of dictionaries containing information about failed deletions. """ failed_deletions = [] - DELETE_HEADERS = { - "Host": HOST, - "Content-Length": "0", - "x-amzn-alexa-app": X_AMZN_ALEXA_APP, - "Connection": "keep-alive", - "Accept": ACCEPT_HEADER, - "User-Agent": USER_AGENT, - "csrf": CSRF, - "Cookie": COOKIE} + DELETE_HEADERS = build_delete_headers() # Open the file for reading with open(DATA_FILE, 'r', encoding="utf_8") as file: # Load the JSON data from the file @@ -144,7 +338,7 @@ def delete_entities(): if DESCRIPTION_FILTER_TEXT in description: entity_id = item["id"] name = item["displayName"] - device_id_for_url = (description).replace(".", "%23").replace(" via Home Assistant","").lower() + device_id_for_url = transform_device_id_for_url(description) print(f"Name: '{name}', Entity ID: '{entity_id}', Device ID: '{device_id_for_url}', Description: '{description}'") url = f"{DELETE_URL}{device_id_for_url}" @@ -168,7 +362,8 @@ def delete_entities(): break else: print(f"Entity {name}:{entity_id} was not deleted. Attempt {attempt + 1}.") - break + # Continue to next attempt instead of breaking + if SHOULD_SLEEP: time.sleep(.2) @@ -197,20 +392,8 @@ def get_graphql_endpoints(): dict: The JSON response from the POST request. """ url = f"https://{HOST}/nexus/v1/graphql" - headers = { - "Content-Length": "1839", - "Cookie": COOKIE, - "Host": HOST, - "Connection": "keep-alive", - "Accept-Language": "en-CA,en-CA;q=1.0,ar-CA;q=0.9", - "csrf": CSRF, - "Content-Type": "application/json; charset=utf-8", - "x-amzn-RequestId": str(uuid.uuid4()), - "User-Agent": USER_AGENT, - "Accept-Encoding": "gzip, deflate, br", - "x-amzn-alexa-app": X_AMZN_ALEXA_APP, - "Accept": ACCEPT_HEADER - } + headers = build_graphql_headers() + headers["x-amzn-RequestId"] = str(uuid.uuid4()) data = { "query": """ query CustomerSmartHome { @@ -255,16 +438,7 @@ def delete_endpoints(): list: A list of dictionaries containing information about failed deletions. """ failed_deletions = [] - DELETE_HEADERS = { - "Host": HOST, - "Content-Length": "0", - "x-amzn-alexa-app": X_AMZN_ALEXA_APP, - "Connection": "keep-alive", - "Accept": ACCEPT_HEADER, - "User-Agent": "AppleWebKit PitanguiBridge/2.2.635412.0-[HARDWARE=iPhone17_3][SOFTWARE=18.2][DEVICE=iPhone]", - "Accept-Language": "en-CA,en-CA;q=1.0,ar-CA;q=0.9", - "csrf": CSRF, - "Cookie": COOKIE} + DELETE_HEADERS = build_delete_headers() # Open the file for reading with open(GRAPHQL_FILE, 'r', encoding="utf_8") as file: # Load the JSON data from the file @@ -275,7 +449,7 @@ def delete_endpoints(): if DESCRIPTION_FILTER_TEXT in manufacturer_name: entity_id = item["legacyAppliance"]["applianceKey"] name = item["friendlyName"] - device_id_for_url = (description).replace(".", "%23").replace(" via Home Assistant","").lower() + device_id_for_url = transform_device_id_for_url(description) print(f"Name: '{name}', Entity ID: '{entity_id}', Device ID: '{device_id_for_url}', Description: '{description}'") url = f"{DELETE_URL}{device_id_for_url}" @@ -299,7 +473,8 @@ def delete_endpoints(): break else: print(f"Entity {name}:{entity_id} was not deleted. Attempt {attempt + 1}.") - break + # Continue to next attempt instead of breaking + if SHOULD_SLEEP: time.sleep(.2)