From 078c68ea9e2f6e227bfc0772ae93b3be5e8477cc Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:36:20 +0530 Subject: [PATCH 01/15] Update readme.md --- readme.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 971ce5e..a8f6d10 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,10 @@ -# **Auto Wifi Login** +# **WiFiLoginBot** "Tired of entering the same Wi-Fi credentials every time you join the network? So was I! šŸ˜… At my institute, logging into Wi-Fi manually was a hassle, so I built this Auto WiFi Login Script to automate the process! This script automatically logs into Wi-Fi networks using pre-saved credentials and now comes with SQLite integration to store login attempts. It keeps track of all login activities, captures dynamic session parameters (a), and provides a user-friendly log display for debugging. Ideal for schools, workplaces, or any location with recurring Wi-Fi logins, this script eliminates manual re-authentication and ensures effortless connectivity. It's fully customizable, works across different networks, and can even be automated on startup for a seamless experience. -Say goodbye to typing passwords repeatedly and hello to smart, automated Wi-Fi access - - ## **Features** - Secure credential storage using SQLite database From cab979d5b5ecf5f8eac096fcb923d53078456e96 Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:37:19 +0530 Subject: [PATCH 02/15] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a8f6d10..a1c2d98 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# **WiFiLoginBot** +# **WiFi-Auto-Auth** "Tired of entering the same Wi-Fi credentials every time you join the network? So was I! šŸ˜… At my institute, logging into Wi-Fi manually was a hassle, so I built this Auto WiFi Login Script to automate the process! This script automatically logs into Wi-Fi networks using pre-saved credentials and now comes with SQLite integration to store login attempts. It keeps track of all login activities, captures dynamic session parameters (a), and provides a user-friendly log display for debugging. From de01f8307c29757630c060ebc28fdc0b7f9ec73f Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:38:42 +0530 Subject: [PATCH 03/15] Update readme.md updated the setup.md referral link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a1c2d98..95613c4 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ Before running the script, ensure you have: āœ” Required libraries āœ” WiFi network login page details -### For step-by-step setup instructions, please refer to [setup.md](https://github.com/01bps/RGIPT_Auto_Connect/blob/main/setup.md). +### For step-by-step setup instructions, please refer to [setup.md](https://github.com/01bps/WiFi-Auto-Auth/blob/main/setup.md). ## **Security Notes** - Credentials are securely stored in an SQLite database within your home directory. From 24cbc132c150468a1084865117ea32cd353b8a6c Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:51:41 +0530 Subject: [PATCH 04/15] Update readme.md --- readme.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/readme.md b/readme.md index 95613c4..cbcd0ec 100644 --- a/readme.md +++ b/readme.md @@ -5,22 +5,7 @@ This script automatically logs into Wi-Fi networks using pre-saved credentials a Ideal for schools, workplaces, or any location with recurring Wi-Fi logins, this script eliminates manual re-authentication and ensures effortless connectivity. It's fully customizable, works across different networks, and can even be automated on startup for a seamless experience. -## **Features** - -- Secure credential storage using SQLite database -- Login attempt logging with automatic cleanup -- Support for multiple network configurations -- Automatic error handling and retries -- Detailed status tracking and logging - -## **Requirements** - -Before running the script, ensure you have: -āœ” Python 3 installed -āœ” Required libraries -āœ” WiFi network login page details - -### For step-by-step setup instructions, please refer to [setup.md](https://github.com/01bps/WiFi-Auto-Auth/blob/main/setup.md). +### **For step-by-step setup instructions, please refer to [setup.md](https://github.com/01bps/WiFi-Auto-Auth/blob/main/setup.md)** ## **Security Notes** - Credentials are securely stored in an SQLite database within your home directory. From 185a7976ed0a714e5b6950bea0a84fa2bc161b7b Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:59:34 +0530 Subject: [PATCH 05/15] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index cbcd0ec..d1bffdd 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # **WiFi-Auto-Auth** -"Tired of entering the same Wi-Fi credentials every time you join the network? So was I! šŸ˜… At my institute, logging into Wi-Fi manually was a hassle, so I built this Auto WiFi Login Script to automate the process! +Tired of entering the same Wi-Fi credentials every time you join the network? So was I! At my institute, logging into Wi-Fi manually was a hassle, so I built this Auto WiFi Login Script to automate the process with the help of Python,SQLite and Crontab! -This script automatically logs into Wi-Fi networks using pre-saved credentials and now comes with SQLite integration to store login attempts. It keeps track of all login activities, captures dynamic session parameters (a), and provides a user-friendly log display for debugging. +This script automatically logs into Wi-Fi networks using pre-saved credentials and now comes with SQLite integration to store login attempts and all payload parameters. It keeps track of all login activities, captures dynamic session parameters (a), and provides a user-friendly log display for debugging. Ideal for schools, workplaces, or any location with recurring Wi-Fi logins, this script eliminates manual re-authentication and ensures effortless connectivity. It's fully customizable, works across different networks, and can even be automated on startup for a seamless experience. From 9b1eaf12fab9920437cab5507e60f6f70c961f62 Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:16:11 +0530 Subject: [PATCH 06/15] Create CONTRIBUTING.md --- CONTRIBUTING.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..95c8ba9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to WiFi-Auto-Auth + +Welcome! We're excited to have you contribute to this project. Don't worry if you're new to open source - we're here to help! + +## First Things First - Star This Repo! + +Click the ⭐ button at the top right of this page. It helps others find this project! + +--- + +## Quick Start (5 Simple Steps) + +### Step 1: Pick an Issue +- Go to the [Issues tab](../../issues) +- Look for `good first issue` label if you're new +- Comment "I'd like to work on this!" on the issue + +### Step 2: Set Up Your Workspace +```bash +# Fork this repo (click Fork button on GitHub), then: +git clone https://github.com/01bps/WiFi-Auto-Auth.git +cd WiFi-Auto-Auth +git checkout -b my-new-feature +``` +### Step 3: Make Your Changes + +Write your code +Test it locally +Make sure it works! + +### Step 4: Document What You Did +Open HACKTOBERFEST.md and add your contribution at the bottom: +## What I Added/Fixed +**Contributor:** @your-github-username +**Issue:** #issue-number + +### What I Changed: +- Added/Fixed/Improved [describe in simple words] +- Modified these files: `filename.py` + +--- +### Step 5: Submit Your Work +```bash +git add . +git commit -m "Added [brief description] (fixes #issue-number)" +git push origin my-new-feature +``` + +Then go to GitHub and click "Create Pull Request" From ecfb1041da5298e1a7de5a4b244fdbaa00954c17 Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:20:02 +0530 Subject: [PATCH 07/15] Create HACKTOBERFEST.md --- HACKTOBERFEST.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 HACKTOBERFEST.md diff --git a/HACKTOBERFEST.md b/HACKTOBERFEST.md new file mode 100644 index 0000000..b84cf80 --- /dev/null +++ b/HACKTOBERFEST.md @@ -0,0 +1,51 @@ + +# Hacktoberfest 2025 - Our Contributors + +This page celebrates everyone who contributed to WiFi-Auto-Auth during Hacktoberfest! + +## ⭐ Star This Repo! + +Show your support by starring this repository! + +--- + +## šŸ“Š Stats + +- **Contributors:** 0 +- **Issues Fixed:** 0 +- **PRs Merged:** 0 + +--- + +## šŸ† All Contributions + +We'll keep updating this once a while. + + +--- + +## Configuration File Support +**Contributor:** +**Issue:** #1 + +### What I Changed: +- Created config.json file for storing WiFi credentials +- Updated login script to read from config file +- Added example config file + +**Files Changed:** `config.py`, `wifi_auto_login.py`, `config.example.json` + +--- + +## šŸ™ Thank You! + +Every contribution makes this project better. We appreciate you! + +**Don't forget to ⭐ star this repository!** + +--- + +*Updated: October 2025* From 0d0dfb804fd1502d88e3361ba87ce8456fed3810 Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:22:31 +0530 Subject: [PATCH 08/15] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95c8ba9..4171892 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ Test it locally Make sure it works! ### Step 4: Document What You Did -Open HACKTOBERFEST.md and add your contribution at the bottom: +Open [HACKTOBERFEST.md](HACKTOBERFEST.md) and add your contribution at the bottom: ## What I Added/Fixed **Contributor:** @your-github-username **Issue:** #issue-number From b5c8e17f8325c30026d9cb8bc57e91de8c4d36df Mon Sep 17 00:00:00 2001 From: Bagwan <129797926+01bps@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:25:05 +0530 Subject: [PATCH 09/15] Update HACKTOBERFEST.md --- HACKTOBERFEST.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/HACKTOBERFEST.md b/HACKTOBERFEST.md index b84cf80..6d81f2f 100644 --- a/HACKTOBERFEST.md +++ b/HACKTOBERFEST.md @@ -19,17 +19,11 @@ Show your support by starring this repository! ## šŸ† All Contributions -We'll keep updating this once a while. - +Keep Adding your contribution in the below template,One after the other: ---- - -## Configuration File Support -**Contributor:** -**Issue:** #1 +## Your Issue Title +**Contributor ID:** +**Issue reference No.:** #1 ### What I Changed: - Created config.json file for storing WiFi credentials From 84379a296418322733c8c24d400dbd02b189114d Mon Sep 17 00:00:00 2001 From: Aarya Shetiye Date: Thu, 2 Oct 2025 16:00:11 +0530 Subject: [PATCH 10/15] Replace hardcoded credentials with config file (fixes #3) --- config.example.json | 6 ++++++ wifi_auto_login.py | 50 +++++++++++++++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 config.example.json diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..b4015fa --- /dev/null +++ b/config.example.json @@ -0,0 +1,6 @@ +{ + "wifi_url": "http://192.168.1.1/login", + "username": "your_username", + "password": "your_password", + "product_type": "router" +} diff --git a/wifi_auto_login.py b/wifi_auto_login.py index c2a4153..95747ea 100644 --- a/wifi_auto_login.py +++ b/wifi_auto_login.py @@ -2,8 +2,27 @@ import requests import datetime import re +import json +import os -# Database setup +# --- CONFIGURATION --- +CONFIG_PATH = "config.json" + +# Error handling if config.json is missing +if not os.path.exists(CONFIG_PATH): + raise FileNotFoundError( + "Missing config.json. Please copy config.example.json to config.json and fill in your details." + ) + +with open(CONFIG_PATH, "r") as f: + config = json.load(f) + +URL = config["wifi_url"] +USERNAME = config["username"] +PASSWORD = config["password"] +PRODUCT_TYPE = config.get("product_type", "0") # Default to "0" if not provided + +# --- DATABASE SETUP --- DB_NAME = "wifi_log.db" def setup_database(): @@ -35,46 +54,46 @@ def log_attempt(username, password, a, response_status, response_message): conn.commit() conn.close() +# --- HELPER FUNCTIONS --- def extract_message(response_text): """Extracts the meaningful message from the XML response.""" match = re.search(r"", response_text) return match.group(1) if match else "Unknown response" +# --- MAIN WIFI LOGIN FUNCTION --- def wifi_login(): """Perform the WiFi login request and log the result.""" - url = "POST url from the inspect element" # Change Required - username = "username" - password = "password" - a_value = str(int(datetime.datetime.now().timestamp())) # Generate dynamic 'a' value, you may refer to the screenshots in the setup.md file + a_value = str(int(datetime.datetime.now().timestamp())) # Generate dynamic 'a' value payload = { "mode": "191", - "username": username, - "password": password, + "username": USERNAME, + "password": PASSWORD, "a": a_value, - "producttype": "0" + "producttype": PRODUCT_TYPE } try: - response = requests.post(url, data=payload) + response = requests.post(URL, data=payload) response_status = response.status_code response_message = extract_message(response.text) print(f"\nšŸ“Œ Login Attempt") print(f"Time: {datetime.datetime.now()}") - print(f"Username: {username}") + print(f"Username: {USERNAME}") print(f"Session ID (a): {a_value}") print(f"Status: {response_status}") print(f"Message: {response_message}") print("-" * 80) # Log the attempt in SQLite - log_attempt(username, password, a_value, response_status, response_message) + log_attempt(USERNAME, PASSWORD, a_value, response_status, response_message) except requests.exceptions.RequestException as e: print(f"āŒ Error: {e}") - log_attempt(username, password, a_value, "FAILED", str(e)) + log_attempt(USERNAME, PASSWORD, a_value, "FAILED", str(e)) +# --- VIEW LOGIN LOGS --- def view_logs(limit=5): """Display login logs in a readable format.""" conn = sqlite3.connect(DB_NAME) @@ -105,7 +124,8 @@ def view_logs(limit=5): print(f"Message: {message}") print("-" * 80) +# --- MAIN EXECUTION --- if __name__ == "__main__": - setup_database() # Ensure the database is set up - wifi_login() # Attempt login - view_logs(5) # Show last 5 login attempts + setup_database() # Ensure the database is set up + wifi_login() # Attempt login + view_logs(5) # Show last 5 login attempts From 6b0e2db50d7650be04313702bd0ba288b39ffdc6 Mon Sep 17 00:00:00 2001 From: zendrix396 Date: Thu, 2 Oct 2025 16:03:27 +0530 Subject: [PATCH 11/15] Feat: Add Command-Line Interface --- setup.md | 27 ++++++++++++++ wifi_auto_login.py | 90 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/setup.md b/setup.md index 63947d1..108ab91 100644 --- a/setup.md +++ b/setup.md @@ -7,6 +7,7 @@ Run the following command to install the requirements: ```bash pip install -r requirements.txt ``` + ## 2. Find Your Network's Login URL and Payload ``` 2.1 Connect to your WiFi network manually. @@ -15,6 +16,7 @@ pip install -r requirements.txt 2.4 Find the POST request URL inside the Network tab(should look like http://192.168.x.x:8090/login.xml). 2.5 Copy the form data parameters (like username, password, a, etc.). ``` + ## 3. Edit ```wifi_auto_login.py``` file Modify the ```def wifi_login()``` function to match your payload parameters. i.e: @@ -96,3 +98,28 @@ python wifi_auto_login.py We have succesfully setup the script now the wifi or LAN will get connected **automatically on system startup**! +## **Command-Line Interface (CLI) Usage** + +This script now includes a command-line interface for easier interaction. Here are the available options: + +| Command | Description | +| ---------------------- | ----------------------------------------------------------- | +| `python wifi_auto_login.py --login` | Performs a login attempt. This is the default action. | +| `python wifi_auto_login.py --view-logs` | Shows the 5 most recent login attempts. | +| `python wifi_auto_login.py --view-logs 10` | Shows the specified number of recent login attempts. | +| `python wifi_auto_login.py --setup` | Launches an interactive wizard to guide you through setup. | +| `python wifi_auto_login.py --test` | Tests the connection to the login server without logging in. | +| `python wifi_auto_login.py --clear-logs` | Deletes all stored login logs from the database. | +| `python wifi_auto_login.py --help` | Displays the help menu with all available commands. | + +### **Examples** + +**To perform a login:** +```bash +python wifi_auto_login.py --login +``` + +**To view the last 3 login attempts:** +```bash +python wifi_auto_login.py --view-logs 3 +``` diff --git a/wifi_auto_login.py b/wifi_auto_login.py index c2a4153..a91cc9e 100644 --- a/wifi_auto_login.py +++ b/wifi_auto_login.py @@ -2,6 +2,7 @@ import requests import datetime import re +import argparse # Database setup DB_NAME = "wifi_log.db" @@ -105,7 +106,90 @@ def view_logs(limit=5): print(f"Message: {message}") print("-" * 80) +def clear_logs(): + """Deletes all logs from the login_attempts table.""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("DELETE FROM login_attempts") + conn.commit() + conn.close() + print("āœ… All logs have been cleared.") + +def test_connection(): + """Tests if the login URL is reachable.""" + # This URL should eventually come from a config file + url = "POST url from the inspect element" # The same URL from wifi_login() + print(f" testing connection to {url}...") + try: + response = requests.head(url, timeout=5) # Use HEAD to be efficient + if response.status_code == 200: + print(f"āœ… Connection successful! The server responded with status {response.status_code}.") + else: + print(f"āš ļø Connection successful, but the server responded with status {response.status_code}.") + except requests.exceptions.RequestException as e: + print(f"āŒ Connection failed: {e}") + +def run_setup_wizard(): + """Guides the user through an interactive setup process.""" + print("--- WiFi-Auto-Auth Interactive Setup ---") + print("This wizard will help you configure the script.") + + url = input("1. Enter the POST request URL from your network's login page: ") + username = input("2. Enter your login username: ") + password = input("3. Enter your login password: ") + + print("\nSetup Complete!") + if __name__ == "__main__": - setup_database() # Ensure the database is set up - wifi_login() # Attempt login - view_logs(5) # Show last 5 login attempts + parser = argparse.ArgumentParser( + description="A script to automatically log into captive portal WiFi networks." + ) + + parser.add_argument( + '--login', + action='store_true', + help="Perform a login attempt." + ) + parser.add_argument( + '--view-logs', + nargs='?', + const=5, + type=int, + metavar='N', + help="View the last N login attempts. Defaults to 5 if no number is provided." + ) + parser.add_argument( + '--setup', + action='store_true', + help="Run the interactive setup wizard to configure credentials." + ) + parser.add_argument( + '--test', + action='store_true', + help="Test the connection to the login URL without logging in." + ) + parser.add_argument( + '--clear-logs', + action='store_true', + help="Clear all login logs from the database." + ) + + args = parser.parse_args() + + setup_database() # Ensure the database is always set up + + if args.login: + wifi_login() + elif args.view_logs is not None: + view_logs(args.view_logs) + elif args.setup: + run_setup_wizard() + elif args.test: + test_connection() + elif args.clear_logs: + clear_logs() + else: + print("No arguments provided. Performing default login action.") + wifi_login() + view_logs(1) + From 113d204d6f9b718360efc20064437426b3891268 Mon Sep 17 00:00:00 2001 From: Carlos Marchena Date: Thu, 2 Oct 2025 12:37:01 +0200 Subject: [PATCH 12/15] feat: implement professional logging system - Replace all print() statements with structured logging using Python logging module - Add centralized logging configuration (config/logging_config.py) with factory pattern - Implement dual output handlers: console (simple format) and file (detailed format) - Add log rotation using RotatingFileHandler (10MB files, 5 backups by default) - Support multiple log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL - Add comprehensive CLI argument parsing for runtime configuration - Support environment variable configuration for containerized deployments - Include detailed file logging with module:function:line information - Add utility functions for common logging patterns (function entry/exit, exceptions) - Maintain backward compatibility with existing application functionality Features: - Configurable via CLI args (--log-level, --log-dir, --view-logs, --no-console-logging) - Environment variable support (LOG_LEVEL, LOG_DIR, LOG_MAX_BYTES, etc.) - Professional log formatting with timestamps and context information - Separate formatters for console (readable) and file (detailed debugging) - Comprehensive error handling and application flow logging Closes #original-issue: Professional logging implementation with configurable verbosity levels --- config/logging_config.py | 186 +++++++++++++++++++++++++++++++++++++++ wifi_auto_login.py | 134 +++++++++++++++++++++++----- 2 files changed, 300 insertions(+), 20 deletions(-) create mode 100644 config/logging_config.py diff --git a/config/logging_config.py b/config/logging_config.py new file mode 100644 index 0000000..feab2dc --- /dev/null +++ b/config/logging_config.py @@ -0,0 +1,186 @@ +""" +Professional logging configuration for WiFi Auto Auth application. +Provides structured, configurable logging with multiple output handlers and log rotation. +""" + +import os +import sys +import logging +import logging.handlers +from pathlib import Path +from typing import Optional + + +class LoggerFactory: + """Factory class for creating and managing loggers.""" + + _loggers = {} + _configured = False + + @classmethod + def get_logger(cls, name: str) -> logging.Logger: + """ + Get or create a logger with the specified name. + + Args: + name: Logger name (typically __name__ of the calling module) + + Returns: + Configured logger instance + """ + if not cls._configured: + cls._setup_logging() + + if name not in cls._loggers: + cls._loggers[name] = logging.getLogger(name) + + return cls._loggers[name] + + @classmethod + def _setup_logging(cls) -> None: + """Set up the global logging configuration.""" + if cls._configured: + return + + # Create formatters + simple_formatter = logging.Formatter( + fmt='%(asctime)s [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + detailed_formatter = logging.Formatter( + fmt='%(asctime)s [%(levelname)s] [%(name)s:%(funcName)s:%(lineno)d] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Set up root logger + root_logger = logging.getLogger() + root_logger.setLevel(cls._get_log_level()) + + # Clear existing handlers to avoid duplicates + root_logger.handlers.clear() + + # Console handler + if cls._is_console_logging_enabled(): + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(cls._get_console_log_level()) + console_handler.setFormatter(simple_formatter) + root_logger.addHandler(console_handler) + + # File handler with rotation + if cls._is_file_logging_enabled(): + file_handler = cls._create_file_handler(detailed_formatter) + file_handler.setLevel(logging.DEBUG) # Always capture all levels in file + root_logger.addHandler(file_handler) + + cls._configured = True + + @classmethod + def _get_log_level(cls) -> int: + """Get the overall log level from environment variables.""" + level_str = os.getenv('LOG_LEVEL', 'INFO').upper() + return getattr(logging, level_str, logging.INFO) + + @classmethod + def _get_console_log_level(cls) -> int: + """Get the console log level from environment variables.""" + level_str = os.getenv('CONSOLE_LOG_LEVEL', os.getenv('LOG_LEVEL', 'INFO')).upper() + return getattr(logging, level_str, logging.INFO) + + @classmethod + def _is_console_logging_enabled(cls) -> bool: + """Check if console logging is enabled.""" + return os.getenv('CONSOLE_LOGGING_ENABLED', 'true').lower() == 'true' + + @classmethod + def _is_file_logging_enabled(cls) -> bool: + """Check if file logging is enabled.""" + return os.getenv('LOG_FILE_ENABLED', 'true').lower() == 'true' + + @classmethod + def _create_file_handler(cls, formatter: logging.Formatter) -> logging.Handler: + """Create a rotating file handler.""" + log_dir = Path(os.getenv('LOG_DIR', './logs')) + log_dir.mkdir(exist_ok=True) + + log_file = log_dir / 'wifi_auto_auth.log' + + # Get rotation settings from environment + max_bytes = int(os.getenv('LOG_MAX_BYTES', '10485760')) # 10MB default + backup_count = int(os.getenv('LOG_BACKUP_COUNT', '5')) + + # Use RotatingFileHandler for size-based rotation + handler = logging.handlers.RotatingFileHandler( + log_file, + maxBytes=max_bytes, + backupCount=backup_count, + encoding='utf-8' + ) + + handler.setFormatter(formatter) + return handler + + @classmethod + def configure_from_args(cls, args) -> None: + """ + Configure logging based on command line arguments. + + Args: + args: Parsed command line arguments with logging options + """ + if hasattr(args, 'log_level'): + os.environ['LOG_LEVEL'] = args.log_level + if hasattr(args, 'log_file'): + os.environ['LOG_FILE_ENABLED'] = 'true' if args.log_file else 'false' + if hasattr(args, 'log_dir'): + os.environ['LOG_DIR'] = args.log_dir + if hasattr(args, 'console_logging'): + os.environ['CONSOLE_LOGGING_ENABLED'] = 'true' if args.console_logging else 'false' + + # Reconfigure logging with new settings + cls._configured = False + cls._setup_logging() + + +def get_logger(name: str) -> logging.Logger: + """ + Convenience function to get a logger. + + Args: + name: Logger name (typically __name__ of the calling module) + + Returns: + Configured logger instance + """ + return LoggerFactory.get_logger(name) + + +def setup_logging_from_env() -> None: + """Set up logging configuration from environment variables.""" + LoggerFactory._setup_logging() + + +# Convenience functions for common logging patterns +def log_function_entry(logger: logging.Logger, func_name: str, *args, **kwargs) -> None: + """Log function entry with parameters.""" + params = [] + if args: + params.append(f"args={args}") + if kwargs: + params.append(f"kwargs={kwargs}") + + param_str = ", ".join(params) if params else "no parameters" + logger.debug(f"Entering {func_name}({param_str})") + + +def log_function_exit(logger: logging.Logger, func_name: str, return_value=None) -> None: + """Log function exit with return value.""" + if return_value is not None: + logger.debug(f"Exiting {func_name} with return value: {return_value}") + else: + logger.debug(f"Exiting {func_name}") + + +def log_exception(logger: logging.Logger, exc: Exception, message: str = "Exception occurred") -> None: + """Log an exception with full traceback.""" + logger.exception(f"{message}: {exc}") \ No newline at end of file diff --git a/wifi_auto_login.py b/wifi_auto_login.py index c2a4153..7bcf5e3 100644 --- a/wifi_auto_login.py +++ b/wifi_auto_login.py @@ -2,10 +2,22 @@ import requests import datetime import re +import sys +import os +import argparse + +# Add the current directory to the path so we can import config +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from config.logging_config import get_logger, setup_logging_from_env, LoggerFactory # Database setup DB_NAME = "wifi_log.db" +# Initialize logging +setup_logging_from_env() +logger = get_logger(__name__) + def setup_database(): """Create the database and table if they do not exist.""" conn = sqlite3.connect(DB_NAME) @@ -60,19 +72,17 @@ def wifi_login(): response_status = response.status_code response_message = extract_message(response.text) - print(f"\nšŸ“Œ Login Attempt") - print(f"Time: {datetime.datetime.now()}") - print(f"Username: {username}") - print(f"Session ID (a): {a_value}") - print(f"Status: {response_status}") - print(f"Message: {response_message}") - print("-" * 80) + logger.info("WiFi login attempt completed") + logger.info(f"Username: {username}") + logger.info(f"Session ID (a): {a_value}") + logger.info(f"Response status: {response_status}") + logger.info(f"Response message: {response_message}") # Log the attempt in SQLite log_attempt(username, password, a_value, response_status, response_message) except requests.exceptions.RequestException as e: - print(f"āŒ Error: {e}") + logger.error(f"WiFi login request failed: {e}") log_attempt(username, password, a_value, "FAILED", str(e)) def view_logs(limit=5): @@ -90,22 +100,106 @@ def view_logs(limit=5): conn.close() if not logs: - print("No login attempts found.") + logger.info("No login attempts found in database") return - print("\nšŸ“Œ Recent Login Attempts") - print("=" * 80) + logger.info("Recent login attempts retrieved from database") + logger.info("=" * 80) for log in logs: timestamp, username, a, status, message = log - print(f"Time: {timestamp}") - print(f"Username: {username}") - print(f"Session ID (a): {a}") - print(f"Status: {status}") - print(f"Message: {message}") - print("-" * 80) + logger.info(f"Time: {timestamp}") + logger.info(f"Username: {username}") + logger.info(f"Session ID (a): {a}") + logger.info(f"Status: {status}") + logger.info(f"Message: {message}") + logger.info("-" * 80) + +def parse_arguments(): + """Parse command line arguments for logging configuration.""" + parser = argparse.ArgumentParser(description='WiFi Auto Login with Professional Logging') + + # Logging configuration arguments + parser.add_argument( + '--log-level', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], + help='Set the logging level (default: INFO)' + ) + parser.add_argument( + '--log-file', + action='store_true', + default=True, + help='Enable file logging (default: enabled)' + ) + parser.add_argument( + '--no-log-file', + action='store_false', + dest='log_file', + help='Disable file logging' + ) + parser.add_argument( + '--log-dir', + default='./logs', + help='Directory for log files (default: ./logs)' + ) + parser.add_argument( + '--console-logging', + action='store_true', + default=True, + help='Enable console logging (default: enabled)' + ) + parser.add_argument( + '--no-console-logging', + action='store_false', + dest='console_logging', + help='Disable console logging' + ) + + # Application arguments + parser.add_argument( + '--view-logs', + type=int, + metavar='N', + help='View last N login attempts instead of performing login' + ) + parser.add_argument( + '--max-attempts', + type=int, + default=5, + help='Maximum number of login attempts to show when viewing logs (default: 5)' + ) + + return parser.parse_args() + if __name__ == "__main__": - setup_database() # Ensure the database is set up - wifi_login() # Attempt login - view_logs(5) # Show last 5 login attempts + # Parse command line arguments + args = parse_arguments() + + # Configure logging from command line arguments + LoggerFactory.configure_from_args(args) + + # Get logger after configuration + logger = get_logger(__name__) + + logger.info("WiFi Auto Login application started") + + try: + setup_database() # Ensure the database is set up + + if args.view_logs is not None: + # View logs only + logger.info(f"Viewing last {args.max_attempts} login attempts") + view_logs(args.max_attempts) + else: + # Perform login and show recent logs + logger.info("Performing WiFi login attempt") + wifi_login() # Attempt login + logger.info("Login attempt completed, retrieving recent logs") + view_logs(args.max_attempts) # Show last 5 login attempts + + except Exception as e: + logger.critical(f"Application error: {e}", exc_info=True) + sys.exit(1) + + logger.info("WiFi Auto Login application completed successfully") From 4426b9328f03b160bee4d20f812886c124b866dd Mon Sep 17 00:00:00 2001 From: Carlos Marchena Date: Fri, 3 Oct 2025 09:35:44 +0200 Subject: [PATCH 13/15] docs: update README with logging options and create .gitignore --- .gitignore | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ HACKTOBERFEST.md | 19 +++++++++------- readme.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..909ecef --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Logs +*.log +logs/ +wifi_auto_auth.log* + +# Database files +*.db +wifi_log.db + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ +.env + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/HACKTOBERFEST.md b/HACKTOBERFEST.md index 6d81f2f..eda7304 100644 --- a/HACKTOBERFEST.md +++ b/HACKTOBERFEST.md @@ -21,16 +21,19 @@ Show your support by starring this repository! Keep Adding your contribution in the below template,One after the other: -## Your Issue Title -**Contributor ID:** -**Issue reference No.:** #1 +## Professional Logging System Implementation +**Contributor ID:** cmarchena +**Issue reference No.:** #6 ### What I Changed: -- Created config.json file for storing WiFi credentials -- Updated login script to read from config file -- Added example config file - -**Files Changed:** `config.py`, `wifi_auto_login.py`, `config.example.json` +- Implemented comprehensive professional logging system with configurable log levels +- Added CLI arguments for logging configuration (--log-level, --view-logs, etc.) +- Created logging configuration module with environment variable support +- Added log rotation for automatic file management +- Updated README with detailed logging options documentation +- Created .gitignore file to exclude logs, cache, and sensitive files + +**Files Changed:** `wifi_auto_login.py`, `config/logging_config.py`, `readme.md`, `.gitignore` --- diff --git a/readme.md b/readme.md index d1bffdd..29b1f56 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,63 @@ This script automatically logs into Wi-Fi networks using pre-saved credentials a Ideal for schools, workplaces, or any location with recurring Wi-Fi logins, this script eliminates manual re-authentication and ensures effortless connectivity. It's fully customizable, works across different networks, and can even be automated on startup for a seamless experience. +## **Logging Options** + +This application features a comprehensive professional logging system that provides detailed insights into login attempts, debugging information, and system status. The logging system supports multiple output destinations, configurable log levels, and automatic log rotation. + +### **Command Line Arguments** + +The script accepts various logging-related command line arguments: + +- `--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}`: Set the logging level (default: INFO) +- `--log-file` / `--no-log-file`: Enable or disable file logging (default: enabled) +- `--log-dir DIR`: Directory for log files (default: ./logs) +- `--console-logging` / `--no-console-logging`: Enable or disable console logging (default: enabled) +- `--view-logs N`: View last N login attempts instead of performing login +- `--max-attempts N`: Maximum number of login attempts to show (default: 5) + +**Usage Examples:** + +```bash +# Run with debug logging +python wifi_auto_login.py --log-level DEBUG + +# View recent login attempts +python wifi_auto_login.py --view-logs 10 + +# Disable file logging, only console output +python wifi_auto_login.py --no-log-file + +# Custom log directory +python wifi_auto_login.py --log-dir /var/log/wifi-auth +``` + +### **Environment Variables** + +Configure logging behavior using environment variables: + +- `LOG_LEVEL`: Overall logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) +- `CONSOLE_LOG_LEVEL`: Separate level for console output +- `CONSOLE_LOGGING_ENABLED`: Enable/disable console logging (true/false) +- `LOG_FILE_ENABLED`: Enable/disable file logging (true/false) +- `LOG_DIR`: Directory for log files (default: ./logs) +- `LOG_MAX_BYTES`: Maximum log file size before rotation (default: 10485760 = 10MB) +- `LOG_BACKUP_COUNT`: Number of backup log files to keep (default: 5) + +**Example:** + +```bash +export LOG_LEVEL=DEBUG +export LOG_DIR=/home/user/logs +python wifi_auto_login.py +``` + +### **Log Rotation** + +The application automatically rotates log files when they reach the maximum size (default 10MB). It keeps up to 5 backup files, ensuring logs don't consume excessive disk space while maintaining historical data. + +Log files are stored in the configured log directory with the name `wifi_auto_auth.log`, and rotated files are named `wifi_auto_auth.log.1`, `wifi_auto_auth.log.2`, etc. + ### **For step-by-step setup instructions, please refer to [setup.md](https://github.com/01bps/WiFi-Auto-Auth/blob/main/setup.md)** ## **Security Notes** From 378d9a87635d235ffe2bb21adba9376dc930d253 Mon Sep 17 00:00:00 2001 From: Princy Ballabh Date: Fri, 3 Oct 2025 20:19:56 +0530 Subject: [PATCH 14/15] Add web dashboard feature --- .gitignore | 3 + DASHBOARD.md | 342 +++++++++++++++++++++++++++++ config.example.json | 8 +- dashboard.py | 347 ++++++++++++++++++++++++++++++ readme.md | 28 +++ requirements.txt | 6 +- static/dashboard.css | 325 ++++++++++++++++++++++++++++ templates/dashboard.html | 453 +++++++++++++++++++++++++++++++++++++++ templates/login.html | 143 ++++++++++++ wifi_auto_login.py | 111 +++++++--- 10 files changed, 1733 insertions(+), 33 deletions(-) create mode 100644 DASHBOARD.md create mode 100644 dashboard.py create mode 100644 static/dashboard.css create mode 100644 templates/dashboard.html create mode 100644 templates/login.html diff --git a/.gitignore b/.gitignore index 909ecef..cf43036 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ wifi_auto_auth.log* *.db wifi_log.db +# Configuration files (personal) +config.json + # Python __pycache__/ *.pyc diff --git a/DASHBOARD.md b/DASHBOARD.md new file mode 100644 index 0000000..c4d9873 --- /dev/null +++ b/DASHBOARD.md @@ -0,0 +1,342 @@ +# WiFi Auto Auth Dashboard + +A beautiful web-based monitoring interface for WiFi Auto Auth that provides real-time visualization of login attempts, statistics, and historical data. + +## šŸš€ Quick Start + +1. **Install dependencies**: `pip install -r requirements.txt` +2. **Copy config**: `cp config.example.json config.json` (edit with your WiFi details) +3. **Generate data**: `python wifi_auto_login.py --login` (run a few times) +4. **Start dashboard**: `python wifi_auto_login.py --dashboard` +5. **Access**: Open http://127.0.0.1:8000 (username: admin, password: admin123) + +## Features + +### šŸŽÆ Core Features +- **Real-time Dashboard**: Live monitoring of WiFi login attempts +- **Interactive Charts**: Time-based visualizations using Chart.js +- **Advanced Filtering**: Filter by date range, status, and more +- **Responsive Design**: Works perfectly on desktop and mobile devices +- **Secure Access**: HTTP Basic Authentication protection +- **RESTful API**: Complete API for data access and integration + +### šŸ“Š Dashboard Components +- **Statistics Cards**: Total attempts, success rate, failure count +- **Login Attempts Table**: Detailed view of recent attempts +- **Time-based Charts**: Visual representation of login patterns +- **Success Rate Pie Chart**: Quick overview of success vs failure rates +- **Real-time Updates**: Auto-refresh every 30 seconds + +### šŸ”’ Security Features +- HTTP Basic Authentication +- Configurable credentials +- Session management +- Secure API endpoints + +## Installation + +### Prerequisites +- Python 3.7 or higher +- All dependencies from `requirements.txt` + +### Install Dependencies + +```bash +pip install -r requirements.txt +``` + +The dashboard requires these additional packages: +- `fastapi>=0.104.0` - Modern web framework +- `uvicorn>=0.24.0` - ASGI server +- `jinja2>=3.1.0` - Template engine +- `python-multipart>=0.0.6` - Form data handling +- `aiofiles>=23.2.0` - Async file operations + +## Configuration + +### Dashboard Settings + +Add dashboard configuration to your `config.json`: + +```json +{ + "wifi_url": "http://192.168.1.1/login", + "username": "your_username", + "password": "your_password", + "product_type": "router", + "dashboard": { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123" + } +} +``` + +### Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| `host` | Dashboard server host | `127.0.0.1` | +| `port` | Dashboard server port | `8000` | +| `username` | Dashboard login username | `admin` | +| `password` | Dashboard login password | `admin123` | + +## Usage + +### Starting the Dashboard + +#### Method 1: Using CLI Command +```bash +python wifi_auto_login.py --dashboard +``` + +#### Method 2: Direct Server Start +```bash +python dashboard.py +``` + +#### Method 3: Custom Host/Port +```bash +python dashboard.py --host 0.0.0.0 --port 8080 +``` + +### Accessing the Dashboard + +1. **Open your browser** and navigate to: `http://127.0.0.1:8000` +2. **Enter credentials** when prompted: + - Username: `admin` (or your configured username) + - Password: `admin123` (or your configured password) +3. **Explore the dashboard** - you'll see all your WiFi login statistics and history + +### Dashboard Sections + +#### šŸ“ˆ Statistics Overview +- **Total Attempts**: All login attempts recorded +- **Successful**: Number of successful logins +- **Failed**: Number of failed attempts +- **Success Rate**: Percentage of successful logins + +#### šŸ” Filtering Options +- **Date Range**: Filter attempts by start and end date +- **Status Filter**: Show only successful or failed attempts +- **Result Limit**: Control number of results displayed (25, 50, 100, 200) + +#### šŸ“Š Visualizations +- **Login Attempts Over Time**: Line chart showing hourly login patterns +- **Success Rate Distribution**: Pie chart showing success vs failure ratio +- **Real-time Updates**: Charts refresh automatically every 30 seconds + +#### šŸ“‹ Login Attempts Table +- **Timestamp**: When the login attempt occurred +- **Username**: User account used for login +- **Session ID**: Unique session identifier +- **Status**: Success (200) or failure status codes +- **Message**: Response message from the WiFi system + +## API Endpoints + +The dashboard provides a RESTful API for integration: + +### Authentication +All API endpoints require HTTP Basic Authentication using the same credentials as the dashboard. + +### Available Endpoints + +#### `GET /` +Main dashboard page (HTML) + +#### `GET /api/attempts` +Get login attempts with optional filters + +**Query Parameters:** +- `start_date`: Filter attempts after this date (ISO format) +- `end_date`: Filter attempts before this date (ISO format) +- `status_filter`: `success` or `failed` +- `limit`: Maximum number of results (default: 50) + +**Example:** +```bash +curl -u admin:admin123 "http://localhost:8000/api/attempts?limit=10&status_filter=success" +``` + +#### `GET /api/stats` +Get dashboard statistics + +**Response:** +```json +{ + "total_attempts": 150, + "successful_attempts": 145, + "failed_attempts": 5, + "success_rate": 96.67, + "last_attempt": "2025-10-03T10:30:45" +} +``` + +#### `GET /api/hourly-stats` +Get hourly statistics for charts + +**Query Parameters:** +- `days`: Number of days to include (default: 7) + +**Example:** +```bash +curl -u admin:admin123 "http://localhost:8000/api/hourly-stats?days=3" +``` + +#### `GET /health` +Health check endpoint (no authentication required) + +## Development + +### Running in Development Mode + +```bash +python dashboard.py --debug +``` + +This enables: +- Auto-reload on code changes +- Detailed error messages +- Debug logging + +### Custom Styling + +The dashboard uses Bootstrap 5 and custom CSS. You can modify: +- `static/dashboard.css` - Custom styles +- `templates/dashboard.html` - Main dashboard template +- `templates/login.html` - Login page template + +### Adding New Features + +The dashboard is built with FastAPI, making it easy to extend: + +1. **Add new API endpoints** in `dashboard.py` +2. **Modify database queries** in the database functions +3. **Update templates** for new UI components +4. **Add JavaScript** for new interactive features + +## Troubleshooting + +### Common Issues + +#### "Database not found" Error +- **Cause**: The database hasn't been created yet +- **Solution**: Run the main script first: `python wifi_auto_login.py --login` + +#### "Port already in use" Error +- **Cause**: Another process is using port 8000 +- **Solution**: Use a different port: `python dashboard.py --port 8080` + +#### Authentication Issues +- **Cause**: Incorrect credentials +- **Solution**: Check your `config.json` dashboard settings + +#### Charts Not Loading +- **Cause**: No login data available +- **Solution**: Run some login attempts first to populate the database + +#### Permission Denied +- **Cause**: Insufficient permissions to bind to the specified host/port +- **Solution**: Use `127.0.0.1` instead of `0.0.0.0`, or run with appropriate permissions + +### Debug Mode + +Enable debug mode for detailed error information: + +```bash +python dashboard.py --debug +``` + +### Logs + +The dashboard uses the same logging configuration as the main application. Check: +- Console output for real-time logs +- Log files in the `logs/` directory (if configured) + +## Security Considerations + +### Production Deployment + +āš ļø **Important**: The default credentials are for development only! + +For production deployment: + +1. **Change default credentials** in `config.json` +2. **Use HTTPS** with a reverse proxy (nginx, Apache) +3. **Restrict access** by IP address if needed +4. **Use strong passwords** (12+ characters, mixed case, numbers, symbols) +5. **Consider additional authentication** methods if required + +### Network Security + +- The dashboard binds to `127.0.0.1` by default (localhost only) +- To allow network access, bind to `0.0.0.0` but ensure proper firewall rules +- Consider using a VPN for remote access + +## Performance + +### Optimization Tips + +- **Limit results** using the limit parameter for better performance +- **Use date filters** to reduce database query time +- **Regular maintenance** - consider archiving old login attempts +- **Monitor database size** - SQLite performance degrades with very large databases + +### Resource Usage + +- **Memory**: ~50MB for typical usage +- **CPU**: Minimal when idle, moderate during chart updates +- **Database**: Grows ~1KB per login attempt +- **Network**: ~100KB per dashboard page load + +## Integration Examples + +### Monitoring Scripts + +```python +import requests +from requests.auth import HTTPBasicAuth + +# Get current statistics +response = requests.get( + 'http://localhost:8000/api/stats', + auth=HTTPBasicAuth('admin', 'admin123') +) +stats = response.json() +print(f"Success rate: {stats['success_rate']}%") +``` + +### Automated Reporting + +```python +import requests +from requests.auth import HTTPBasicAuth +from datetime import datetime, timedelta + +# Get yesterday's login attempts +yesterday = datetime.now() - timedelta(days=1) +start_date = yesterday.strftime('%Y-%m-%dT00:00:00') +end_date = yesterday.strftime('%Y-%m-%dT23:59:59') + +response = requests.get( + f'http://localhost:8000/api/attempts?start_date={start_date}&end_date={end_date}', + auth=HTTPBasicAuth('admin', 'admin123') +) +attempts = response.json()['attempts'] +print(f"Yesterday's login attempts: {len(attempts)}") +``` + +## Support + +For issues, questions, or contributions: + +1. Check the troubleshooting section above +2. Review the application logs +3. Create an issue on the project repository +4. Include detailed error messages and system information + +## License + +This dashboard is part of the WiFi Auto Auth project and follows the same license terms. \ No newline at end of file diff --git a/config.example.json b/config.example.json index b4015fa..4831e44 100644 --- a/config.example.json +++ b/config.example.json @@ -2,5 +2,11 @@ "wifi_url": "http://192.168.1.1/login", "username": "your_username", "password": "your_password", - "product_type": "router" + "product_type": "router", + "dashboard": { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123" + } } diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..c0a654b --- /dev/null +++ b/dashboard.py @@ -0,0 +1,347 @@ +""" +WiFi Auto Auth Dashboard - Web-based monitoring interface for WiFi login attempts +""" + +import sqlite3 +import json +import os +from datetime import datetime, timedelta +from typing import List, Dict, Optional +from fastapi import FastAPI, Request, Depends, HTTPException, status, Form +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from pydantic import BaseModel +import uvicorn +import secrets +from pathlib import Path + +# Import existing logging configuration +try: + from config.logging_config import get_logger + logger = get_logger(__name__) +except ImportError: + import logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + +# --- CONFIGURATION --- +CONFIG_PATH = "config.json" +DB_NAME = "wifi_log.db" + +# Load configuration +def load_dashboard_config(): + """Load dashboard configuration from config.json""" + if not os.path.exists(CONFIG_PATH): + # Use default configuration if config.json doesn't exist + return { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123", + "secret_key": secrets.token_urlsafe(32) + } + + try: + with open(CONFIG_PATH, "r") as f: + config = json.load(f) + + return config.get("dashboard", { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123", + "secret_key": secrets.token_urlsafe(32) + }) + except (json.JSONDecodeError, KeyError): + logger.warning("Invalid config.json, using default dashboard configuration") + return { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123", + "secret_key": secrets.token_urlsafe(32) + } + +# Dashboard configuration +DASHBOARD_CONFIG = load_dashboard_config() + +# --- PYDANTIC MODELS --- +class LoginAttempt(BaseModel): + id: int + timestamp: str + username: str + a: str + response_status: str + response_message: str + +class DashboardStats(BaseModel): + total_attempts: int + successful_attempts: int + failed_attempts: int + success_rate: float + last_attempt: Optional[str] + +class FilterParams(BaseModel): + start_date: Optional[str] = None + end_date: Optional[str] = None + status_filter: Optional[str] = None + limit: int = 50 + +# --- FASTAPI APP SETUP --- +app = FastAPI(title="WiFi Auto Auth Dashboard", version="1.0.0") + +# Setup templates and static files +templates = Jinja2Templates(directory="templates") + +# Create static directory if it doesn't exist +static_dir = Path("static") +static_dir.mkdir(exist_ok=True) + +app.mount("/static", StaticFiles(directory="static"), name="static") + +# Simple authentication +security = HTTPBasic() + +def authenticate(credentials: HTTPBasicCredentials = Depends(security)): + """Simple HTTP Basic Authentication""" + correct_username = secrets.compare_digest(credentials.username, DASHBOARD_CONFIG["username"]) + correct_password = secrets.compare_digest(credentials.password, DASHBOARD_CONFIG["password"]) + + if not (correct_username and correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + +# --- DATABASE FUNCTIONS --- +def get_db_connection(): + """Get database connection""" + if not os.path.exists(DB_NAME): + logger.warning(f"Database {DB_NAME} not found. Creating empty database.") + # Create an empty database with the required table structure + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS login_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT, + username TEXT, + password TEXT, + a TEXT, + response_status TEXT, + response_message TEXT + ) + """) + conn.commit() + return conn + + return sqlite3.connect(DB_NAME) + +def get_login_attempts(filters: FilterParams) -> List[Dict]: + """Get login attempts with filters""" + conn = get_db_connection() + cursor = conn.cursor() + + query = """ + SELECT id, timestamp, username, a, response_status, response_message + FROM login_attempts + WHERE 1=1 + """ + params = [] + + if filters.start_date: + query += " AND timestamp >= ?" + params.append(filters.start_date) + + if filters.end_date: + query += " AND timestamp <= ?" + params.append(filters.end_date) + + if filters.status_filter: + if filters.status_filter == "success": + query += " AND response_status = '200'" + elif filters.status_filter == "failed": + query += " AND response_status != '200'" + + query += " ORDER BY timestamp DESC LIMIT ?" + params.append(filters.limit) + + cursor.execute(query, params) + rows = cursor.fetchall() + conn.close() + + return [ + { + "id": row[0], + "timestamp": row[1], + "username": row[2], + "a": row[3], + "response_status": row[4], + "response_message": row[5] + } + for row in rows + ] + +def get_dashboard_stats() -> DashboardStats: + """Get dashboard statistics""" + conn = get_db_connection() + cursor = conn.cursor() + + # Total attempts + cursor.execute("SELECT COUNT(*) FROM login_attempts") + total_attempts = cursor.fetchone()[0] + + # Successful attempts (assuming 200 is success) + cursor.execute("SELECT COUNT(*) FROM login_attempts WHERE response_status = '200'") + successful_attempts = cursor.fetchone()[0] + + # Failed attempts + failed_attempts = total_attempts - successful_attempts + + # Success rate + success_rate = (successful_attempts / total_attempts * 100) if total_attempts > 0 else 0 + + # Last attempt + cursor.execute("SELECT timestamp FROM login_attempts ORDER BY timestamp DESC LIMIT 1") + last_attempt_row = cursor.fetchone() + last_attempt = last_attempt_row[0] if last_attempt_row else None + + conn.close() + + return DashboardStats( + total_attempts=total_attempts, + successful_attempts=successful_attempts, + failed_attempts=failed_attempts, + success_rate=round(success_rate, 2), + last_attempt=last_attempt + ) + +def get_hourly_stats(days: int = 7) -> List[Dict]: + """Get hourly login attempt statistics for the last N days""" + conn = get_db_connection() + cursor = conn.cursor() + + start_date = datetime.now() - timedelta(days=days) + + query = """ + SELECT + strftime('%Y-%m-%d %H', timestamp) as hour, + COUNT(*) as total_attempts, + SUM(CASE WHEN response_status = '200' THEN 1 ELSE 0 END) as successful_attempts + FROM login_attempts + WHERE timestamp >= ? + GROUP BY strftime('%Y-%m-%d %H', timestamp) + ORDER BY hour + """ + + cursor.execute(query, (start_date.isoformat(),)) + rows = cursor.fetchall() + conn.close() + + return [ + { + "hour": row[0], + "total_attempts": row[1], + "successful_attempts": row[2], + "failed_attempts": row[1] - row[2] + } + for row in rows + ] + +# --- ROUTES --- +@app.get("/", response_class=HTMLResponse) +async def dashboard(request: Request, username: str = Depends(authenticate)): + """Main dashboard page""" + logger.info(f"Dashboard accessed by user: {username}") + + # Get recent login attempts + filters = FilterParams(limit=10) + recent_attempts = get_login_attempts(filters) + + # Get statistics + stats = get_dashboard_stats() + + return templates.TemplateResponse("dashboard.html", { + "request": request, + "recent_attempts": recent_attempts, + "stats": stats, + "username": username + }) + +@app.get("/api/attempts") +async def get_attempts_api( + start_date: Optional[str] = None, + end_date: Optional[str] = None, + status_filter: Optional[str] = None, + limit: int = 50, + username: str = Depends(authenticate) +): + """API endpoint to get login attempts with filters""" + filters = FilterParams( + start_date=start_date, + end_date=end_date, + status_filter=status_filter, + limit=limit + ) + + attempts = get_login_attempts(filters) + logger.info(f"API call: Retrieved {len(attempts)} login attempts") + + return {"attempts": attempts} + +@app.get("/api/stats") +async def get_stats_api(username: str = Depends(authenticate)): + """API endpoint to get dashboard statistics""" + stats = get_dashboard_stats() + logger.info("API call: Retrieved dashboard statistics") + return stats + +@app.get("/api/hourly-stats") +async def get_hourly_stats_api(days: int = 7, username: str = Depends(authenticate)): + """API endpoint to get hourly statistics""" + stats = get_hourly_stats(days) + logger.info(f"API call: Retrieved hourly statistics for last {days} days") + return {"hourly_stats": stats} + +@app.get("/login", response_class=HTMLResponse) +async def login_page(request: Request): + """Login page (for custom authentication if needed)""" + return templates.TemplateResponse("login.html", {"request": request}) + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy", "timestamp": datetime.now().isoformat()} + +# --- SERVER MANAGEMENT --- +def start_dashboard_server(host: str = None, port: int = None, debug: bool = False): + """Start the dashboard server""" + host = host or DASHBOARD_CONFIG["host"] + port = port or DASHBOARD_CONFIG["port"] + + logger.info(f"Starting WiFi Auto Auth Dashboard on http://{host}:{port}") + logger.info(f"Username: {DASHBOARD_CONFIG['username']}") + logger.info(f"Password: {DASHBOARD_CONFIG['password']}") + + uvicorn.run( + "dashboard:app", + host=host, + port=port, + reload=debug, + log_level="info" + ) + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="WiFi Auto Auth Dashboard") + parser.add_argument("--host", default="127.0.0.1", help="Host to bind the server") + parser.add_argument("--port", type=int, default=8000, help="Port to bind the server") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") + + args = parser.parse_args() + start_dashboard_server(args.host, args.port, args.debug) \ No newline at end of file diff --git a/readme.md b/readme.md index 29b1f56..dca8562 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,31 @@ This script automatically logs into Wi-Fi networks using pre-saved credentials a Ideal for schools, workplaces, or any location with recurring Wi-Fi logins, this script eliminates manual re-authentication and ensures effortless connectivity. It's fully customizable, works across different networks, and can even be automated on startup for a seamless experience. +## **šŸš€ New: Web Dashboard** + +**Beautiful web-based monitoring interface with real-time statistics and interactive charts!** + +### **Dashboard Features** +- šŸ“Š Real-time statistics & success rates +- šŸ“ˆ Interactive time-based visualizations +- šŸ” Advanced filtering & search +- šŸ“± Mobile-responsive design +- šŸ”’ Secure authentication +- ⚔ Auto-refresh every 30 seconds + +### **Quick Start** +```bash +# Install dependencies +pip install -r requirements.txt + +# Start dashboard +python wifi_auto_login.py --dashboard + +# Access: http://127.0.0.1:8000 (admin/admin123) +``` + +**šŸ“– Full documentation: [DASHBOARD.md](DASHBOARD.md)** + ## **Logging Options** This application features a comprehensive professional logging system that provides detailed insights into login attempts, debugging information, and system status. The logging system supports multiple output destinations, configurable log levels, and automatic log rotation. @@ -29,6 +54,9 @@ python wifi_auto_login.py --log-level DEBUG # View recent login attempts python wifi_auto_login.py --view-logs 10 +# Start the web dashboard +python wifi_auto_login.py --dashboard + # Disable file logging, only console output python wifi_auto_login.py --no-log-file diff --git a/requirements.txt b/requirements.txt index 64851aa..5f2396a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -python>=3.6 requests>=2.25.1 -sqlite3 +fastapi>=0.104.0 +uvicorn>=0.24.0 +jinja2>=3.1.0 +python-multipart>=0.0.6 diff --git a/static/dashboard.css b/static/dashboard.css new file mode 100644 index 0000000..8198eca --- /dev/null +++ b/static/dashboard.css @@ -0,0 +1,325 @@ +/* WiFi Auto Auth Dashboard - Custom Styles */ + +:root { + --primary-color: #667eea; + --secondary-color: #764ba2; + --success-color: #28a745; + --danger-color: #dc3545; + --warning-color: #ffc107; + --info-color: #17a2b8; + --light-color: #f8f9fa; + --dark-color: #343a40; + + --gradient-primary: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + --gradient-success: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%); + --gradient-danger: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%); + --gradient-info: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + + --border-radius: 10px; + --box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + --transition: all 0.3s ease; +} + +/* Global Styles */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; +} + +/* Navigation Enhancements */ +.navbar-brand { + font-weight: 700; + font-size: 1.5rem; +} + +.navbar-brand i { + margin-right: 0.5rem; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +/* Card Enhancements */ +.card { + border: none; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + transition: var(--transition); + overflow: hidden; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.card-header { + background: var(--gradient-primary); + color: white; + border-bottom: none; + padding: 1rem 1.5rem; +} + +.card-header h5 { + margin: 0; + font-weight: 600; +} + +.card-body { + padding: 1.5rem; +} + +/* Statistics Cards */ +.card.bg-primary { + background: var(--gradient-primary) !important; + border: none; +} + +.card.bg-success { + background: var(--gradient-success) !important; + border: none; +} + +.card.bg-danger { + background: var(--gradient-danger) !important; + border: none; +} + +.card.bg-info { + background: var(--gradient-info) !important; + border: none; +} + +.card .fs-1 { + opacity: 0.8; + transition: var(--transition); +} + +.card:hover .fs-1 { + opacity: 1; + transform: scale(1.1); +} + +/* Form Enhancements */ +.form-control, .form-select { + border-radius: var(--border-radius); + border: 2px solid #e9ecef; + transition: var(--transition); + padding: 0.75rem 1rem; +} + +.form-control:focus, .form-select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); +} + +.form-label { + font-weight: 600; + color: var(--dark-color); + margin-bottom: 0.5rem; +} + +/* Button Enhancements */ +.btn { + border-radius: var(--border-radius); + font-weight: 600; + padding: 0.5rem 1.5rem; + transition: var(--transition); + border: none; +} + +.btn-primary { + background: var(--gradient-primary); + border: none; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3); +} + +.btn-secondary:hover { + transform: translateY(-2px); +} + +.btn-outline-light:hover { + transform: translateY(-2px); +} + +/* Table Enhancements */ +.table { + border-radius: var(--border-radius); + overflow: hidden; + margin-bottom: 0; +} + +.table thead th { + border-bottom: none; + font-weight: 700; + text-transform: uppercase; + font-size: 0.85rem; + letter-spacing: 0.5px; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.02); +} + +.table-hover tbody tr:hover { + background-color: rgba(102, 126, 234, 0.05); + transform: scale(1.002); +} + +/* Badge Enhancements */ +.badge { + border-radius: 20px; + padding: 0.5em 0.8em; + font-weight: 600; + font-size: 0.75rem; +} + +.badge.bg-success { + background: var(--gradient-success) !important; +} + +.badge.bg-danger { + background: var(--gradient-danger) !important; +} + +.badge.bg-primary { + background: var(--gradient-primary) !important; +} + +/* Alert Enhancements */ +.alert { + border: none; + border-radius: var(--border-radius); + padding: 1rem 1.5rem; +} + +.alert-info { + background: linear-gradient(135deg, rgba(23, 162, 184, 0.1) 0%, rgba(23, 162, 184, 0.05) 100%); + color: #0c5460; + border-left: 4px solid var(--info-color); +} + +/* Chart Container Enhancements */ +canvas { + max-height: 400px !important; +} + +.card .card-body canvas { + background: rgba(255, 255, 255, 0.8); + border-radius: var(--border-radius); + padding: 1rem; +} + +/* Code Styling */ +code { + background: rgba(102, 126, 234, 0.1); + color: var(--primary-color); + padding: 0.2em 0.4em; + border-radius: 4px; + font-size: 0.85em; + font-weight: 600; +} + +/* Responsive Enhancements */ +@media (max-width: 768px) { + .card-body { + padding: 1rem; + } + + .table-responsive { + border-radius: var(--border-radius); + } + + .row.g-3 > * { + margin-bottom: 1rem; + } + + .btn { + width: 100%; + margin-bottom: 0.5rem; + } + + .d-flex.justify-content-between { + flex-direction: column; + align-items: flex-start !important; + } + + .navbar-nav.ms-auto { + margin-top: 1rem; + } +} + +/* Loading Animation */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Utility Classes */ +.text-gradient { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; +} + +.glass-effect { + backdrop-filter: blur(10px); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.shadow-custom { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +/* Dark Theme Support (Optional) */ +@media (prefers-color-scheme: dark) { + :root { + --light-color: #1a1a1a; + --dark-color: #f8f9fa; + } + + body { + background-color: #1a1a1a; + color: #f8f9fa; + } + + .card { + background-color: #2d2d2d; + color: #f8f9fa; + } + + .table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); + } + + .form-control, .form-select { + background-color: #2d2d2d; + border-color: #404040; + color: #f8f9fa; + } + + .form-control:focus, .form-select:focus { + background-color: #2d2d2d; + color: #f8f9fa; + } +} \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..6eb39b8 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,453 @@ + + + + + + WiFi Auto Auth Dashboard + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+

{{ stats.total_attempts }}

+

Total Attempts

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

{{ stats.successful_attempts }}

+

Successful

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

{{ stats.failed_attempts }}

+

Failed

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

{{ stats.success_rate|round(1) }}%

+

Success Rate

+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+ Filters +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ +
+
+
+
+ Login Attempts Over Time +
+
+
+ +
+
+
+ + +
+
+
+
+ Success Rate Distribution +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Recent Login Attempts +
+
{{ recent_attempts|length }} results
+
+
+
+ + + + + + + + + + + + {% for attempt in recent_attempts %} + + + + + + + + {% endfor %} + +
TimestampUsernameSession IDStatusMessage
+ {{ attempt.timestamp }} + + {{ attempt.username }} + + {{ attempt.a }} + + {% if attempt.response_status == "200" %} + + {{ attempt.response_status }} + + {% else %} + + {{ attempt.response_status }} + + {% endif %} + + {{ attempt.response_message }} +
+
+
+
+
+
+ + {% if stats.last_attempt %} +
+
+
+ + Last Activity: {{ stats.last_attempt }} +
+
+
+ {% endif %} +
+ + + + + + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..b8c5622 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,143 @@ + + + + + + WiFi Auto Auth Dashboard - Login + + + + + + + + + +
+
+
+ + +
+ + WiFi Auto Auth Dashboard v1.0.0 + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/wifi_auto_login.py b/wifi_auto_login.py index 0d1ead1..bde7b3d 100644 --- a/wifi_auto_login.py +++ b/wifi_auto_login.py @@ -10,24 +10,29 @@ # --- CONFIGURATION --- CONFIG_PATH = "config.json" -# Error handling if config.json is missing -if not os.path.exists(CONFIG_PATH): - raise FileNotFoundError( - "Missing config.json. Please copy config.example.json to config.json and fill in your details." - ) - -with open(CONFIG_PATH, "r") as f: - config = json.load(f) +def load_config(): + """Load configuration file and return config dict""" + if not os.path.exists(CONFIG_PATH): + raise FileNotFoundError( + "Missing config.json. Please copy config.example.json to config.json and fill in your details." + ) + + with open(CONFIG_PATH, "r") as f: + config = json.load(f) + + return config -URL = config["wifi_url"] -USERNAME = config["username"] -PASSWORD = config["password"] -PRODUCT_TYPE = config.get("product_type", "0") # Default to "0" if not provided +# Global variables - will be loaded when needed +URL = None +USERNAME = None +PASSWORD = None +PRODUCT_TYPE = None # --- DATABASE SETUP --- DB_NAME = "wifi_log.db" # Initialize logging +from config.logging_config import setup_logging_from_env, get_logger setup_logging_from_env() logger = get_logger(__name__) @@ -69,6 +74,14 @@ def extract_message(response_text): # --- MAIN WIFI LOGIN FUNCTION --- def wifi_login(): """Perform the WiFi login request and log the result.""" + # Load config when needed + config = load_config() + global URL, USERNAME, PASSWORD, PRODUCT_TYPE + URL = config["wifi_url"] + USERNAME = config["username"] + PASSWORD = config["password"] + PRODUCT_TYPE = config.get("product_type", "0") + a_value = str(int(datetime.datetime.now().timestamp())) # Generate dynamic 'a' value payload = { @@ -198,9 +211,9 @@ def clear_logs(): def test_connection(): """Tests if the login URL is reachable.""" - # This URL should eventually come from a config file - url = "POST url from the inspect element" # The same URL from wifi_login() - print(f" testing connection to {url}...") + config = load_config() + url = config["wifi_url"] + print(f"šŸ”— Testing connection to {url}...") try: response = requests.head(url, timeout=5) # Use HEAD to be efficient if response.status_code == 200: @@ -221,6 +234,27 @@ def run_setup_wizard(): print("\nSetup Complete!") +def start_dashboard(): + """Start the web dashboard server.""" + try: + import subprocess + import sys + print("šŸš€ Starting WiFi Auto Auth Dashboard...") + print("šŸ“Š Dashboard will be available at: http://127.0.0.1:8000") + print("šŸ”‘ Default credentials: admin / admin123") + print("šŸ›‘ Press Ctrl+C to stop the server") + + # Start the dashboard server + subprocess.run([sys.executable, "dashboard.py"], check=True) + except subprocess.CalledProcessError as e: + print(f"āŒ Error starting dashboard: {e}") + except KeyboardInterrupt: + print("\nšŸ›‘ Dashboard server stopped.") + except ImportError: + print("āŒ Dashboard dependencies not installed. Please run: pip install -r requirements.txt") + except FileNotFoundError: + print("āŒ Dashboard server not found. Please ensure dashboard.py exists.") + if __name__ == "__main__": parser = argparse.ArgumentParser( description="A script to automatically log into captive portal WiFi networks." @@ -254,21 +288,38 @@ def run_setup_wizard(): action='store_true', help="Clear all login logs from the database." ) + parser.add_argument( + '--dashboard', + action='store_true', + help="Start the web dashboard server for monitoring login attempts." + ) args = parser.parse_args() - setup_database() # Ensure the database is always set up - - if args.login: - wifi_login() - elif args.view_logs is not None: - view_logs(args.view_logs) - elif args.setup: - run_setup_wizard() - elif args.test: - test_connection() - elif args.clear_logs: - clear_logs() + + # For operations that don't need config, handle them first + if args.setup: + run_setup_wizard() + elif args.dashboard: + start_dashboard() else: - print("No arguments provided. Performing default login action.") - wifi_login() - view_logs(1) + # For operations that need database/config + try: + setup_database() # Ensure the database is always set up + + if args.login: + wifi_login() + elif args.view_logs is not None: + view_logs(args.view_logs) + elif args.test: + test_connection() + elif args.clear_logs: + clear_logs() + else: + print("No arguments provided. Performing default login action.") + wifi_login() + view_logs(1) + + except FileNotFoundError as e: + print(f"āŒ Configuration Error: {e}") + print("šŸ’” Run 'python wifi_auto_login.py --setup' to configure the application.") + print("šŸ“– Or copy config.example.json to config.json and edit it manually.") From a5158e21363429127c65fcad5cd14602919846b5 Mon Sep 17 00:00:00 2001 From: Sangam Paudel Date: Mon, 6 Oct 2025 12:50:24 +0545 Subject: [PATCH 15/15] Add Multi-Network-Support --- MULTI_NETWORK.md | 343 ++++++++++++++++++++++++++++++++++++++ dashboard.py | 171 ++++++++++++++++--- network_utils.py | 300 +++++++++++++++++++++++++++++++++ readme.md | 32 ++++ wifi_auto_login.py | 403 ++++++++++++++++++++++++++++++++++++++------- 5 files changed, 1166 insertions(+), 83 deletions(-) create mode 100644 MULTI_NETWORK.md create mode 100644 network_utils.py diff --git a/MULTI_NETWORK.md b/MULTI_NETWORK.md new file mode 100644 index 0000000..e01d8b6 --- /dev/null +++ b/MULTI_NETWORK.md @@ -0,0 +1,343 @@ +# Multi-Network Support Guide + +WiFi Auto Auth now supports multiple network profiles, allowing you to automatically connect to different WiFi networks (home, work, school, etc.) with different credentials and settings. + +## Features + +- **Auto-Detection**: Automatically detects current network SSID and selects appropriate profile +- **Manual Selection**: Override auto-detection by specifying a network profile +- **Network-Specific Logging**: Track login attempts per network +- **Dashboard Integration**: View network-specific statistics in the web dashboard +- **Backward Compatibility**: Existing single-network configurations continue to work + +## Configuration + +### New Multi-Network Format + +Create or update your `config.json` file with the new multi-network format: + +```json +{ + "default_network": "home", + "networks": { + "home": { + "ssid": "HomeWiFi", + "wifi_url": "http://192.168.1.1/login", + "username": "your_home_username", + "password": "your_home_password", + "product_type": "router", + "description": "Home WiFi network" + }, + "work": { + "ssid": "OfficeWiFi", + "wifi_url": "http://10.0.0.1/login", + "username": "your_work_username", + "password": "your_work_password", + "product_type": "enterprise", + "description": "Work WiFi network" + }, + "school": { + "ssid": "SchoolWiFi", + "wifi_url": "http://172.16.1.1/login", + "username": "your_school_username", + "password": "your_school_password", + "product_type": "edu", + "description": "School WiFi network" + } + }, + "dashboard": { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123" + } +} +``` + +### Legacy Configuration Support + +Existing single-network configurations will continue to work: + +```json +{ + "wifi_url": "http://192.168.1.1/login", + "username": "your_username", + "password": "your_password", + "product_type": "router" +} +``` + +## Usage + +### Auto-Detection (Recommended) + +The script automatically detects your current network SSID and selects the appropriate profile: + +```bash +# Auto-detect current network and login +python wifi_auto_login.py --login + +# Auto-detect and show recent logs +python wifi_auto_login.py +``` + +### Manual Network Selection + +Override auto-detection by specifying a network profile: + +```bash +# Login using specific network profile +python wifi_auto_login.py --login --network work + +# Test connection for specific network +python wifi_auto_login.py --test --network home +``` + +### Network Management Commands + +```bash +# List all configured network profiles +python wifi_auto_login.py --list-networks + +# Detect current network and show matching profile +python wifi_auto_login.py --detect-network + +# View logs for specific network +python wifi_auto_login.py --view-logs 10 --network-filter work +``` + +## Command Reference + +### New Commands + +| Command | Description | +|---------|-------------| +| `--network PROFILE` | Use specific network profile | +| `--list-networks` | List all configured networks | +| `--detect-network` | Detect current network | +| `--network-filter PROFILE` | Filter logs by network | + +### Updated Commands + +| Command | Description | +|---------|-------------| +| `--login` | Auto-detects network or uses --network | +| `--test` | Tests connection for detected/specified network | +| `--view-logs N` | Shows network info in logs | + +## Network Detection + +The script uses platform-specific methods to detect your current WiFi network: + +### Windows +- Uses `netsh wlan show interfaces` command +- Requires WiFi adapter to be connected + +### macOS +- Uses `networksetup -getairportnetwork en0` command +- Falls back to `airport -I` utility + +### Linux +- Tries multiple methods: `iwgetid`, `nmcli`, `iwconfig` +- Requires appropriate network utilities installed + +## Database Schema + +The database now includes network information: + +```sql +CREATE TABLE login_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT, + network_name TEXT, -- New: Network profile name + network_ssid TEXT, -- New: Actual SSID + username TEXT, + password TEXT, + a TEXT, + response_status TEXT, + response_message TEXT +); +``` + +Existing databases are automatically upgraded with new columns. + +## Dashboard Features + +### Network Statistics +- Per-network success rates +- Network-specific attempt counts +- Last login time per network + +### Filtering +- Filter logs by network profile +- Network-specific historical data +- Multi-network overview + +### API Endpoints + +| Endpoint | Description | +|----------|-------------| +| `/api/network-stats` | Get per-network statistics | +| `/api/attempts?network_filter=PROFILE` | Filtered login attempts | + +## Migration Guide + +### From Single Network to Multi-Network + +1. **Backup your current config.json**: + ```bash + cp config.json config.json.backup + ``` + +2. **Update configuration format**: + ```bash + # Use the new example as template + cp config.example.json config.json + # Edit config.json with your networks + ``` + +3. **Test the configuration**: + ```bash + python wifi_auto_login.py --list-networks + python wifi_auto_login.py --detect-network + ``` + +### Gradual Migration + +You can keep using legacy format while adding new networks: + +```json +{ + "wifi_url": "http://192.168.1.1/login", + "username": "legacy_user", + "password": "legacy_pass", + "networks": { + "work": { + "ssid": "OfficeWiFi", + "wifi_url": "http://10.0.0.1/login", + "username": "work_user", + "password": "work_pass" + } + } +} +``` + +## Troubleshooting + +### Network Detection Issues + +1. **No SSID detected**: + ```bash + # Check if WiFi is connected + python wifi_auto_login.py --detect-network + + # Manually specify network + python wifi_auto_login.py --login --network home + ``` + +2. **Platform not supported**: + - Windows: Ensure you have admin privileges for netsh + - macOS: Check if networksetup is available + - Linux: Install wireless-tools or network-manager + +3. **Profile not found**: + ```bash + # List available profiles + python wifi_auto_login.py --list-networks + + # Check SSID matches exactly + python wifi_auto_login.py --detect-network + ``` + +### Configuration Issues + +1. **Invalid JSON**: + ```bash + # Validate JSON syntax + python -m json.tool config.json + ``` + +2. **Missing network profile**: + - Add the network to your config.json + - Ensure SSID matches exactly (case-sensitive) + +3. **Legacy compatibility**: + - Old format still works + - Gradually migrate to new format + +## Best Practices + +### Network Profile Design + +1. **Use descriptive names**: `home`, `work`, `coffee-shop` +2. **Include descriptions**: Help identify networks later +3. **Set default network**: For fallback when detection fails +4. **Test each profile**: Verify credentials before deployment + +### Security Considerations + +1. **Protect config.json**: Contains passwords in plain text +2. **Use different passwords**: Don't reuse across networks +3. **Regular updates**: Change passwords periodically +4. **Backup configurations**: Keep secure backups + +### Monitoring + +1. **Use dashboard**: Monitor per-network success rates +2. **Check logs regularly**: Identify authentication issues +3. **Network-specific analysis**: Filter logs by network +4. **Set up alerts**: Monitor for failure patterns + +## Examples + +### Complete Multi-Network Setup + +```bash +# 1. Set up configuration +cp config.example.json config.json +# Edit config.json with your networks + +# 2. Test configuration +python wifi_auto_login.py --list-networks +python wifi_auto_login.py --detect-network + +# 3. Test each network +python wifi_auto_login.py --test --network home +python wifi_auto_login.py --test --network work + +# 4. Set up automatic login +python wifi_auto_login.py --login + +# 5. Monitor via dashboard +python wifi_auto_login.py --dashboard +``` + +### Automated Network Switching + +```bash +#!/bin/bash +# Example script for automated network handling + +# Detect current network +CURRENT_NETWORK=$(python wifi_auto_login.py --detect-network 2>/dev/null | grep "Found matching profile" | cut -d: -f2 | xargs) + +if [ -n "$CURRENT_NETWORK" ]; then + echo "Logging into detected network: $CURRENT_NETWORK" + python wifi_auto_login.py --login --network "$CURRENT_NETWORK" +else + echo "No matching network profile found, using auto-detection" + python wifi_auto_login.py --login +fi +``` + +## Support + +If you encounter issues with multi-network support: + +1. Check the troubleshooting section above +2. Enable debug logging: `python wifi_auto_login.py --log-level DEBUG` +3. Test with single network first +4. Report issues with network detection details +5. Include platform information (Windows/macOS/Linux) + +The multi-network feature is designed to be backward compatible while providing powerful new capabilities for managing multiple WiFi environments. \ No newline at end of file diff --git a/dashboard.py b/dashboard.py index c0a654b..a77948b 100644 --- a/dashboard.py +++ b/dashboard.py @@ -71,6 +71,8 @@ def load_dashboard_config(): class LoginAttempt(BaseModel): id: int timestamp: str + network_name: Optional[str] = None + network_ssid: Optional[str] = None username: str a: str response_status: str @@ -87,6 +89,7 @@ class FilterParams(BaseModel): start_date: Optional[str] = None end_date: Optional[str] = None status_filter: Optional[str] = None + network_filter: Optional[str] = None limit: int = 50 # --- FASTAPI APP SETUP --- @@ -120,15 +123,26 @@ def authenticate(credentials: HTTPBasicCredentials = Depends(security)): # --- DATABASE FUNCTIONS --- def get_db_connection(): """Get database connection""" + conn = sqlite3.connect(DB_NAME) + if not os.path.exists(DB_NAME): - logger.warning(f"Database {DB_NAME} not found. Creating empty database.") - # Create an empty database with the required table structure - conn = sqlite3.connect(DB_NAME) - cursor = conn.cursor() + logger.warning(f"Database {DB_NAME} not found. Creating new database.") + + # Ensure table exists with multi-network support + cursor = conn.cursor() + + # Check if table exists and get its schema + cursor.execute("PRAGMA table_info(login_attempts)") + columns = [row[1] for row in cursor.fetchall()] + + if not columns: + # Create new table with network support cursor.execute(""" - CREATE TABLE IF NOT EXISTS login_attempts ( + CREATE TABLE login_attempts ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, + network_name TEXT, + network_ssid TEXT, username TEXT, password TEXT, a TEXT, @@ -136,21 +150,43 @@ def get_db_connection(): response_message TEXT ) """) - conn.commit() - return conn + logger.info("Created new login_attempts table with network support") + else: + # Check if we need to add network columns to existing table + if 'network_name' not in columns: + cursor.execute("ALTER TABLE login_attempts ADD COLUMN network_name TEXT") + logger.info("Added network_name column to existing table") + + if 'network_ssid' not in columns: + cursor.execute("ALTER TABLE login_attempts ADD COLUMN network_ssid TEXT") + logger.info("Added network_ssid column to existing table") - return sqlite3.connect(DB_NAME) + conn.commit() + return conn -def get_login_attempts(filters: FilterParams) -> List[Dict]: +def get_login_attempts(filters: FilterParams, network_filter: Optional[str] = None) -> List[Dict]: """Get login attempts with filters""" conn = get_db_connection() cursor = conn.cursor() - query = """ - SELECT id, timestamp, username, a, response_status, response_message - FROM login_attempts - WHERE 1=1 - """ + # Check if table has network columns + cursor.execute("PRAGMA table_info(login_attempts)") + columns = [row[1] for row in cursor.fetchall()] + has_network_columns = 'network_name' in columns and 'network_ssid' in columns + + if has_network_columns: + query = """ + SELECT id, timestamp, network_name, network_ssid, username, a, response_status, response_message + FROM login_attempts + WHERE 1=1 + """ + else: + query = """ + SELECT id, timestamp, username, a, response_status, response_message + FROM login_attempts + WHERE 1=1 + """ + params = [] if filters.start_date: @@ -167,6 +203,10 @@ def get_login_attempts(filters: FilterParams) -> List[Dict]: elif filters.status_filter == "failed": query += " AND response_status != '200'" + if network_filter and has_network_columns: + query += " AND network_name = ?" + params.append(network_filter) + query += " ORDER BY timestamp DESC LIMIT ?" params.append(filters.limit) @@ -174,17 +214,34 @@ def get_login_attempts(filters: FilterParams) -> List[Dict]: rows = cursor.fetchall() conn.close() - return [ - { - "id": row[0], - "timestamp": row[1], - "username": row[2], - "a": row[3], - "response_status": row[4], - "response_message": row[5] - } - for row in rows - ] + if has_network_columns: + return [ + { + "id": row[0], + "timestamp": row[1], + "network_name": row[2], + "network_ssid": row[3], + "username": row[4], + "a": row[5], + "response_status": row[6], + "response_message": row[7] + } + for row in rows + ] + else: + return [ + { + "id": row[0], + "timestamp": row[1], + "network_name": "Legacy", + "network_ssid": "Unknown", + "username": row[2], + "a": row[3], + "response_status": row[4], + "response_message": row[5] + } + for row in rows + ] def get_dashboard_stats() -> DashboardStats: """Get dashboard statistics""" @@ -220,6 +277,54 @@ def get_dashboard_stats() -> DashboardStats: last_attempt=last_attempt ) +def get_network_stats() -> List[Dict]: + """Get statistics per network profile""" + conn = get_db_connection() + cursor = conn.cursor() + + # Check if table has network columns + cursor.execute("PRAGMA table_info(login_attempts)") + columns = [row[1] for row in cursor.fetchall()] + has_network_columns = 'network_name' in columns and 'network_ssid' in columns + + if not has_network_columns: + return [] + + query = """ + SELECT + network_name, + network_ssid, + COUNT(*) as total_attempts, + SUM(CASE WHEN response_status = '200' THEN 1 ELSE 0 END) as successful_attempts, + MAX(timestamp) as last_attempt + FROM login_attempts + WHERE network_name IS NOT NULL + GROUP BY network_name, network_ssid + ORDER BY total_attempts DESC + """ + + cursor.execute(query) + rows = cursor.fetchall() + conn.close() + + stats = [] + for row in rows: + network_name, network_ssid, total, successful, last_attempt = row + failed = total - successful + success_rate = (successful / total * 100) if total > 0 else 0 + + stats.append({ + "network_name": network_name, + "network_ssid": network_ssid, + "total_attempts": total, + "successful_attempts": successful, + "failed_attempts": failed, + "success_rate": round(success_rate, 2), + "last_attempt": last_attempt + }) + + return stats + def get_hourly_stats(days: int = 7) -> List[Dict]: """Get hourly login attempt statistics for the last N days""" conn = get_db_connection() @@ -264,11 +369,13 @@ async def dashboard(request: Request, username: str = Depends(authenticate)): # Get statistics stats = get_dashboard_stats() + network_stats = get_network_stats() return templates.TemplateResponse("dashboard.html", { "request": request, "recent_attempts": recent_attempts, "stats": stats, + "network_stats": network_stats, "username": username }) @@ -277,6 +384,7 @@ async def get_attempts_api( start_date: Optional[str] = None, end_date: Optional[str] = None, status_filter: Optional[str] = None, + network_filter: Optional[str] = None, limit: int = 50, username: str = Depends(authenticate) ): @@ -285,10 +393,11 @@ async def get_attempts_api( start_date=start_date, end_date=end_date, status_filter=status_filter, + network_filter=network_filter, limit=limit ) - attempts = get_login_attempts(filters) + attempts = get_login_attempts(filters, network_filter) logger.info(f"API call: Retrieved {len(attempts)} login attempts") return {"attempts": attempts} @@ -298,6 +407,16 @@ async def get_stats_api(username: str = Depends(authenticate)): """API endpoint to get dashboard statistics""" stats = get_dashboard_stats() logger.info("API call: Retrieved dashboard statistics") + + return {"stats": stats} + +@app.get("/api/network-stats") +async def get_network_stats_api(username: str = Depends(authenticate)): + """API endpoint to get network-specific statistics""" + network_stats = get_network_stats() + logger.info("API call: Retrieved network statistics") + + return {"network_stats": network_stats} return stats @app.get("/api/hourly-stats") diff --git a/network_utils.py b/network_utils.py new file mode 100644 index 0000000..dc1074f --- /dev/null +++ b/network_utils.py @@ -0,0 +1,300 @@ +""" +Network utilities for WiFi Auto Auth - Multi-Network Support +Handles network detection, SSID identification, and network profile management. +""" + +import subprocess +import platform +import re +import json +import os +from typing import Optional, Dict, List, Tuple +from config.logging_config import get_logger + +logger = get_logger(__name__) + +class NetworkDetector: + """Handles network detection and SSID identification across different platforms.""" + + def __init__(self): + self.platform = platform.system().lower() + logger.debug(f"Initialized NetworkDetector for platform: {self.platform}") + + def get_current_ssid(self) -> Optional[str]: + """ + Get the SSID of the currently connected WiFi network. + + Returns: + str: SSID of current network, or None if not connected to WiFi + """ + try: + if self.platform == "windows": + return self._get_ssid_windows() + elif self.platform == "darwin": # macOS + return self._get_ssid_macos() + elif self.platform == "linux": + return self._get_ssid_linux() + else: + logger.warning(f"Unsupported platform: {self.platform}") + return None + except Exception as e: + logger.error(f"Failed to get current SSID: {e}") + return None + + def _get_ssid_windows(self) -> Optional[str]: + """Get SSID on Windows using netsh command.""" + try: + # Use netsh to get WiFi profile information + cmd = ["netsh", "wlan", "show", "profiles"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Get the currently connected profile + cmd_interfaces = ["netsh", "wlan", "show", "interfaces"] + interfaces_result = subprocess.run(cmd_interfaces, capture_output=True, text=True, check=True) + + # Parse the SSID from the interfaces output + for line in interfaces_result.stdout.split('\n'): + if 'SSID' in line and 'BSSID' not in line: + # Extract SSID (format: " SSID : NetworkName") + match = re.search(r'SSID\s*:\s*(.+)', line.strip()) + if match: + ssid = match.group(1).strip() + logger.debug(f"Detected Windows SSID: {ssid}") + return ssid + + logger.debug("No active WiFi connection found on Windows") + return None + + except subprocess.CalledProcessError as e: + logger.error(f"Windows netsh command failed: {e}") + return None + except Exception as e: + logger.error(f"Error getting Windows SSID: {e}") + return None + + def _get_ssid_macos(self) -> Optional[str]: + """Get SSID on macOS using airport utility.""" + try: + # Try using networksetup first + cmd = ["networksetup", "-getairportnetwork", "en0"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Parse output (format: "Current Wi-Fi Network: NetworkName") + if "Current Wi-Fi Network:" in result.stdout: + ssid = result.stdout.split("Current Wi-Fi Network:")[-1].strip() + if ssid and ssid != "You are not associated with an AirPort network.": + logger.debug(f"Detected macOS SSID: {ssid}") + return ssid + + # Fallback to airport utility + airport_cmd = ["/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport", "-I"] + airport_result = subprocess.run(airport_cmd, capture_output=True, text=True, check=True) + + for line in airport_result.stdout.split('\n'): + if 'SSID:' in line: + ssid = line.split('SSID:')[-1].strip() + logger.debug(f"Detected macOS SSID via airport: {ssid}") + return ssid + + logger.debug("No active WiFi connection found on macOS") + return None + + except subprocess.CalledProcessError as e: + logger.error(f"macOS network command failed: {e}") + return None + except Exception as e: + logger.error(f"Error getting macOS SSID: {e}") + return None + + def _get_ssid_linux(self) -> Optional[str]: + """Get SSID on Linux using various methods.""" + methods = [ + self._linux_iwgetid, + self._linux_nmcli, + self._linux_iwconfig + ] + + for method in methods: + try: + ssid = method() + if ssid: + logger.debug(f"Detected Linux SSID: {ssid}") + return ssid + except Exception as e: + logger.debug(f"Linux method {method.__name__} failed: {e}") + continue + + logger.debug("No active WiFi connection found on Linux") + return None + + def _linux_iwgetid(self) -> Optional[str]: + """Get SSID using iwgetid command.""" + cmd = ["iwgetid", "-r"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + ssid = result.stdout.strip() + return ssid if ssid else None + + def _linux_nmcli(self) -> Optional[str]: + """Get SSID using NetworkManager's nmcli.""" + cmd = ["nmcli", "-t", "-f", "active,ssid", "dev", "wifi"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + for line in result.stdout.split('\n'): + if line.startswith('yes:'): + ssid = line.split(':', 1)[1] + return ssid if ssid else None + return None + + def _linux_iwconfig(self) -> Optional[str]: + """Get SSID using iwconfig command.""" + cmd = ["iwconfig"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + for line in result.stdout.split('\n'): + if 'ESSID:' in line: + match = re.search(r'ESSID:"([^"]*)"', line) + if match: + return match.group(1) + return None + + +class NetworkProfileManager: + """Manages network profiles and configuration loading.""" + + def __init__(self, config_path: str = "config.json"): + self.config_path = config_path + self.detector = NetworkDetector() + logger.debug(f"Initialized NetworkProfileManager with config: {config_path}") + + def load_config(self) -> Dict: + """Load and parse configuration file.""" + if not os.path.exists(self.config_path): + raise FileNotFoundError( + f"Missing {self.config_path}. Please copy config.example.json to {self.config_path} and configure your networks." + ) + + with open(self.config_path, "r") as f: + config = json.load(f) + + logger.debug(f"Loaded configuration from {self.config_path}") + return config + + def get_available_networks(self) -> List[str]: + """Get list of configured network profile names.""" + try: + config = self.load_config() + + # Handle both new multi-network format and legacy format + if "networks" in config: + networks = list(config["networks"].keys()) + logger.debug(f"Found configured networks: {networks}") + return networks + else: + # Legacy single network configuration + logger.debug("Using legacy single network configuration") + return ["default"] + except Exception as e: + logger.error(f"Error getting available networks: {e}") + return [] + + def get_network_profile(self, network_name: Optional[str] = None, auto_detect: bool = True) -> Tuple[str, Dict]: + """ + Get network configuration for specified network or auto-detect current network. + + Args: + network_name: Specific network profile to use + auto_detect: Whether to auto-detect current network if network_name is None + + Returns: + Tuple of (network_name, network_config) + """ + config = self.load_config() + + # Handle legacy configuration format + if "networks" not in config: + logger.info("Using legacy configuration format") + legacy_config = { + "ssid": config.get("ssid", "Unknown"), + "wifi_url": config["wifi_url"], + "username": config["username"], + "password": config["password"], + "product_type": config.get("product_type", "0"), + "description": "Legacy configuration" + } + return "legacy", legacy_config + + networks = config["networks"] + + # If specific network requested, use it + if network_name: + if network_name in networks: + logger.info(f"Using specified network profile: {network_name}") + return network_name, networks[network_name] + else: + raise ValueError(f"Network profile '{network_name}' not found in configuration") + + # Auto-detect current network + if auto_detect: + current_ssid = self.detector.get_current_ssid() + if current_ssid: + logger.info(f"Detected current SSID: {current_ssid}") + + # Find matching network profile by SSID + for profile_name, profile_config in networks.items(): + if profile_config.get("ssid") == current_ssid: + logger.info(f"Found matching network profile: {profile_name}") + return profile_name, profile_config + + logger.warning(f"No network profile found for SSID: {current_ssid}") + else: + logger.warning("Could not detect current network SSID") + + # Fall back to default network + default_network = config.get("default_network", list(networks.keys())[0]) + if default_network in networks: + logger.info(f"Using default network profile: {default_network}") + return default_network, networks[default_network] + + # If default not found, use first available + first_network = list(networks.keys())[0] + logger.info(f"Using first available network profile: {first_network}") + return first_network, networks[first_network] + + def list_networks(self) -> Dict[str, Dict]: + """Get detailed information about all configured networks.""" + try: + config = self.load_config() + + if "networks" not in config: + # Legacy format + return { + "legacy": { + "ssid": config.get("ssid", "Unknown"), + "wifi_url": config["wifi_url"], + "description": "Legacy configuration" + } + } + + return config["networks"] + except Exception as e: + logger.error(f"Error listing networks: {e}") + return {} + + +# Convenience functions for backward compatibility +def get_current_ssid() -> Optional[str]: + """Get the SSID of the currently connected WiFi network.""" + detector = NetworkDetector() + return detector.get_current_ssid() + + +def get_network_profile(network_name: Optional[str] = None, auto_detect: bool = True) -> Tuple[str, Dict]: + """Get network configuration for specified network or auto-detect current network.""" + manager = NetworkProfileManager() + return manager.get_network_profile(network_name, auto_detect) + + +def list_available_networks() -> List[str]: + """Get list of configured network profile names.""" + manager = NetworkProfileManager() + return manager.get_available_networks() \ No newline at end of file diff --git a/readme.md b/readme.md index dca8562..a58781c 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,38 @@ python wifi_auto_login.py --dashboard **šŸ“– Full documentation: [DASHBOARD.md](DASHBOARD.md)** +## **🌐 NEW: Multi-Network Support** + +**Automatically handle multiple WiFi networks with intelligent auto-detection!** + +### **Multi-Network Features** +- šŸ  **Multiple Profiles**: Configure home, work, school networks +- šŸ” **Auto-Detection**: Automatically detects current SSID +- šŸ“± **Smart Selection**: Chooses appropriate credentials +- šŸ“Š **Network Analytics**: Per-network statistics in dashboard +- šŸ”„ **Seamless Switching**: No manual intervention needed +- šŸ“‹ **Easy Management**: List, detect, and filter by network + +### **Quick Multi-Network Setup** +```bash +# Copy and configure multi-network template +cp config.example.json config.json + +# List configured networks +python wifi_auto_login.py --list-networks + +# Auto-detect current network +python wifi_auto_login.py --detect-network + +# Login with auto-detection +python wifi_auto_login.py --login + +# Use specific network profile +python wifi_auto_login.py --login --network work +``` + +**šŸ“– Complete guide: [MULTI_NETWORK.md](MULTI_NETWORK.md)** + ## **Logging Options** This application features a comprehensive professional logging system that provides detailed insights into login attempts, debugging information, and system status. The logging system supports multiple output destinations, configurable log levels, and automatic log rotation. diff --git a/wifi_auto_login.py b/wifi_auto_login.py index bde7b3d..0539d61 100644 --- a/wifi_auto_login.py +++ b/wifi_auto_login.py @@ -22,6 +22,20 @@ def load_config(): return config +# Initialize logging first +from config.logging_config import setup_logging_from_env, get_logger +setup_logging_from_env() +logger = get_logger(__name__) + +# Import network utilities for multi-network support +try: + from network_utils import NetworkProfileManager, get_current_ssid + MULTI_NETWORK_SUPPORT = True + logger.info("Multi-network support enabled") +except ImportError as e: + logger.warning(f"Multi-network support disabled: {e}") + MULTI_NETWORK_SUPPORT = False + # Global variables - will be loaded when needed URL = None USERNAME = None @@ -31,37 +45,51 @@ def load_config(): # --- DATABASE SETUP --- DB_NAME = "wifi_log.db" -# Initialize logging -from config.logging_config import setup_logging_from_env, get_logger -setup_logging_from_env() -logger = get_logger(__name__) - def setup_database(): """Create the database and table if they do not exist.""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() - cursor.execute(""" - CREATE TABLE IF NOT EXISTS login_attempts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp TEXT, - username TEXT, - password TEXT, - a TEXT, - response_status TEXT, - response_message TEXT - ) - """) + + # Check if the table exists and get its schema + cursor.execute("PRAGMA table_info(login_attempts)") + columns = [row[1] for row in cursor.fetchall()] + + if not columns: + # Create new table with network support + cursor.execute(""" + CREATE TABLE login_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT, + network_name TEXT, + network_ssid TEXT, + username TEXT, + password TEXT, + a TEXT, + response_status TEXT, + response_message TEXT + ) + """) + logger.info("Created new login_attempts table with network support") + else: + # Check if we need to add network columns to existing table + if 'network_name' not in columns: + cursor.execute("ALTER TABLE login_attempts ADD COLUMN network_name TEXT") + logger.info("Added network_name column to existing table") + + if 'network_ssid' not in columns: + cursor.execute("ALTER TABLE login_attempts ADD COLUMN network_ssid TEXT") + logger.info("Added network_ssid column to existing table") conn.commit() conn.close() -def log_attempt(username, password, a, response_status, response_message): +def log_attempt(username, password, a, response_status, response_message, network_name=None, network_ssid=None): """Log each login attempt in the database.""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() cursor.execute(""" - INSERT INTO login_attempts (timestamp, username, password, a, response_status, response_message) - VALUES (?, ?, ?, ?, ?, ?) - """, (datetime.datetime.now(), username, "******", a, response_status, response_message)) + INSERT INTO login_attempts (timestamp, network_name, network_ssid, username, password, a, response_status, response_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (datetime.datetime.now(), network_name, network_ssid, username, "******", a, response_status, response_message)) conn.commit() conn.close() @@ -72,15 +100,40 @@ def extract_message(response_text): return match.group(1) if match else "Unknown response" # --- MAIN WIFI LOGIN FUNCTION --- -def wifi_login(): +def wifi_login(network_name=None): """Perform the WiFi login request and log the result.""" - # Load config when needed - config = load_config() - global URL, USERNAME, PASSWORD, PRODUCT_TYPE - URL = config["wifi_url"] - USERNAME = config["username"] - PASSWORD = config["password"] - PRODUCT_TYPE = config.get("product_type", "0") + network_profile_name = "legacy" + network_ssid = "Unknown" + + try: + if MULTI_NETWORK_SUPPORT: + # Use multi-network configuration + manager = NetworkProfileManager() + network_profile_name, network_config = manager.get_network_profile(network_name, auto_detect=True) + network_ssid = network_config.get("ssid", "Unknown") + + URL = network_config["wifi_url"] + USERNAME = network_config["username"] + PASSWORD = network_config["password"] + PRODUCT_TYPE = network_config.get("product_type", "0") + + print(f"\n🌐 Using Network Profile: {network_profile_name}") + print(f"šŸ“” Network SSID: {network_ssid}") + print(f"šŸ”— Login URL: {URL}") + else: + # Fallback to legacy single network configuration + config = load_config() + URL = config["wifi_url"] + USERNAME = config["username"] + PASSWORD = config["password"] + PRODUCT_TYPE = config.get("product_type", "0") + network_ssid = config.get("ssid", "Unknown") + print(f"\n🌐 Using Legacy Configuration") + + except Exception as e: + logger.error(f"Configuration error: {e}") + print(f"āŒ Configuration Error: {e}") + return a_value = str(int(datetime.datetime.now().timestamp())) # Generate dynamic 'a' value @@ -105,42 +158,74 @@ def wifi_login(): print(f"Message: {response_message}") print("-" * 80) - # Log the attempt in SQLite - log_attempt(USERNAME, PASSWORD, a_value, response_status, response_message) + # Log the attempt in SQLite with network information + log_attempt(USERNAME, PASSWORD, a_value, response_status, response_message, + network_profile_name, network_ssid) except requests.exceptions.RequestException as e: print(f"āŒ Error: {e}") - log_attempt(USERNAME, PASSWORD, a_value, "FAILED", str(e)) + log_attempt(USERNAME, PASSWORD, a_value, "FAILED", str(e), + network_profile_name, network_ssid) # --- VIEW LOGIN LOGS --- -def view_logs(limit=5): +def view_logs(limit=5, network_filter=None): """Display login logs in a readable format.""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() - cursor.execute(""" - SELECT timestamp, username, a, response_status, response_message - FROM login_attempts - ORDER BY timestamp DESC - LIMIT ? - """, (limit,)) + + # Check if table has new network columns + cursor.execute("PRAGMA table_info(login_attempts)") + columns = [row[1] for row in cursor.fetchall()] + has_network_columns = 'network_name' in columns and 'network_ssid' in columns + + if has_network_columns: + base_query = """ + SELECT timestamp, network_name, network_ssid, username, a, response_status, response_message + FROM login_attempts + """ + if network_filter: + query = base_query + "WHERE network_name = ? ORDER BY timestamp DESC LIMIT ?" + cursor.execute(query, (network_filter, limit)) + else: + query = base_query + "ORDER BY timestamp DESC LIMIT ?" + cursor.execute(query, (limit,)) + else: + # Legacy table structure + cursor.execute(""" + SELECT timestamp, username, a, response_status, response_message + FROM login_attempts + ORDER BY timestamp DESC + LIMIT ? + """, (limit,)) logs = cursor.fetchall() conn.close() if not logs: - logger.info("No login attempts found in database") + filter_msg = f" for network '{network_filter}'" if network_filter else "" + logger.info(f"No login attempts found in database{filter_msg}") return - logger.info("Recent login attempts retrieved from database") + filter_msg = f" for network '{network_filter}'" if network_filter else "" + logger.info(f"Recent login attempts retrieved from database{filter_msg}") logger.info("=" * 80) for log in logs: - timestamp, username, a, status, message = log - logger.info(f"Time: {timestamp}") - logger.info(f"Username: {username}") - logger.info(f"Session ID (a): {a}") - logger.info(f"Status: {status}") - logger.info(f"Message: {message}") + if has_network_columns: + timestamp, network_name, network_ssid, username, a, status, message = log + logger.info(f"Time: {timestamp}") + logger.info(f"Network: {network_name} ({network_ssid})") + logger.info(f"Username: {username}") + logger.info(f"Session ID (a): {a}") + logger.info(f"Status: {status}") + logger.info(f"Message: {message}") + else: + timestamp, username, a, status, message = log + logger.info(f"Time: {timestamp}") + logger.info(f"Username: {username}") + logger.info(f"Session ID (a): {a}") + logger.info(f"Status: {status}") + logger.info(f"Message: {message}") logger.info("-" * 80) def parse_arguments(): @@ -209,12 +294,19 @@ def clear_logs(): conn.close() print("āœ… All logs have been cleared.") -def test_connection(): +def test_connection(network_name=None): """Tests if the login URL is reachable.""" - config = load_config() - url = config["wifi_url"] - print(f"šŸ”— Testing connection to {url}...") try: + if MULTI_NETWORK_SUPPORT: + manager = NetworkProfileManager() + network_profile_name, network_config = manager.get_network_profile(network_name, auto_detect=True) + url = network_config["wifi_url"] + print(f"šŸ”— Testing connection for network '{network_profile_name}' to {url}...") + else: + config = load_config() + url = config["wifi_url"] + print(f"šŸ”— Testing connection to {url}...") + response = requests.head(url, timeout=5) # Use HEAD to be efficient if response.status_code == 200: print(f"āœ… Connection successful! The server responded with status {response.status_code}.") @@ -222,17 +314,188 @@ def test_connection(): print(f"āš ļø Connection successful, but the server responded with status {response.status_code}.") except requests.exceptions.RequestException as e: print(f"āŒ Connection failed: {e}") + except Exception as e: + print(f"āŒ Configuration error: {e}") def run_setup_wizard(): """Guides the user through an interactive setup process.""" print("--- WiFi-Auto-Auth Interactive Setup ---") print("This wizard will help you configure the script.") + print() + + # Ask about multi-network setup + print("Choose configuration type:") + print("1. Single Network (Legacy)") + print("2. Multi-Network (Recommended)") + + choice = input("\nEnter your choice (1 or 2): ").strip() + + if choice == "2": + setup_multi_network() + else: + setup_single_network() + +def setup_single_network(): + """Set up single network configuration.""" + print("\n--- Single Network Setup ---") url = input("1. Enter the POST request URL from your network's login page: ") username = input("2. Enter your login username: ") password = input("3. Enter your login password: ") + product_type = input("4. Enter product type (optional, press Enter for default): ") or "0" + + config = { + "wifi_url": url, + "username": username, + "password": password, + "product_type": product_type, + "dashboard": { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123" + } + } + + save_config(config) + print("\nāœ… Single network setup complete!") + +def setup_multi_network(): + """Set up multi-network configuration.""" + print("\n--- Multi-Network Setup ---") + + networks = {} + default_network = None + + while True: + print(f"\n--- Network Profile #{len(networks) + 1} ---") + + profile_name = input("Enter network profile name (e.g., home, work, school): ").strip() + if not profile_name: + break + + ssid = input(f"Enter SSID for {profile_name}: ").strip() + url = input(f"Enter login URL for {profile_name}: ").strip() + username = input(f"Enter username for {profile_name}: ").strip() + password = input(f"Enter password for {profile_name}: ").strip() + product_type = input(f"Enter product type for {profile_name} (optional): ").strip() or "0" + description = input(f"Enter description for {profile_name} (optional): ").strip() or f"{profile_name.title()} network" + + networks[profile_name] = { + "ssid": ssid, + "wifi_url": url, + "username": username, + "password": password, + "product_type": product_type, + "description": description + } + + if not default_network: + default_network = profile_name + + add_more = input(f"\nAdd another network profile? (y/N): ").strip().lower() + if add_more not in ['y', 'yes']: + break + + if not networks: + print("No networks configured. Falling back to single network setup.") + setup_single_network() + return + + # Ask for default network + if len(networks) > 1: + print(f"\nAvailable networks: {', '.join(networks.keys())}") + default_choice = input(f"Enter default network (press Enter for '{default_network}'): ").strip() + if default_choice in networks: + default_network = default_choice + + config = { + "default_network": default_network, + "networks": networks, + "dashboard": { + "host": "127.0.0.1", + "port": 8000, + "username": "admin", + "password": "admin123" + } + } + + save_config(config) + print(f"\nāœ… Multi-network setup complete! {len(networks)} networks configured.") + print(f"Default network: {default_network}") - print("\nSetup Complete!") +def save_config(config): + """Save configuration to config.json file.""" + try: + with open(CONFIG_PATH, 'w') as f: + json.dump(config, f, indent=2) + print(f"\nšŸ’¾ Configuration saved to {CONFIG_PATH}") + except Exception as e: + print(f"\nāŒ Error saving configuration: {e}") + +def list_networks(): + """List all configured network profiles.""" + if not MULTI_NETWORK_SUPPORT: + print("āŒ Multi-network support not available. Using legacy configuration.") + return + + try: + manager = NetworkProfileManager() + networks = manager.list_networks() + current_ssid = get_current_ssid() + + print("\nšŸ“¶ Configured Network Profiles:") + print("=" * 60) + + for name, config in networks.items(): + ssid = config.get("ssid", "Unknown") + url = config.get("wifi_url", "Unknown") + description = config.get("description", "No description") + + # Mark current network + current_marker = " šŸ“ CURRENT" if ssid == current_ssid else "" + + print(f"🌐 Network: {name}{current_marker}") + print(f" SSID: {ssid}") + print(f" URL: {url}") + print(f" Description: {description}") + print("-" * 60) + + if current_ssid: + print(f"\nšŸ“” Currently connected to: {current_ssid}") + else: + print("\nšŸ“” No WiFi connection detected") + + except Exception as e: + print(f"āŒ Error listing networks: {e}") + +def detect_network(): + """Detect current network and show matching profile.""" + if not MULTI_NETWORK_SUPPORT: + print("āŒ Multi-network support not available.") + return + + try: + current_ssid = get_current_ssid() + + if not current_ssid: + print("šŸ“” No WiFi connection detected") + return + + print(f"šŸ“” Current SSID: {current_ssid}") + + manager = NetworkProfileManager() + try: + network_name, network_config = manager.get_network_profile(auto_detect=True) + print(f"āœ… Found matching profile: {network_name}") + print(f" Description: {network_config.get('description', 'No description')}") + print(f" Login URL: {network_config.get('wifi_url', 'Unknown')}") + except Exception: + print("āš ļø No matching network profile found") + print("šŸ’” You may need to add this network to your config.json") + + except Exception as e: + print(f"āŒ Error detecting network: {e}") def start_dashboard(): """Start the web dashboard server.""" @@ -257,7 +520,7 @@ def start_dashboard(): if __name__ == "__main__": parser = argparse.ArgumentParser( - description="A script to automatically log into captive portal WiFi networks." + description="A script to automatically log into captive portal WiFi networks with multi-network support." ) parser.add_argument( @@ -265,6 +528,12 @@ def start_dashboard(): action='store_true', help="Perform a login attempt." ) + parser.add_argument( + '--network', '-n', + type=str, + metavar='PROFILE', + help="Specify which network profile to use (overrides auto-detection)." + ) parser.add_argument( '--view-logs', nargs='?', @@ -273,6 +542,22 @@ def start_dashboard(): metavar='N', help="View the last N login attempts. Defaults to 5 if no number is provided." ) + parser.add_argument( + '--network-filter', + type=str, + metavar='PROFILE', + help="Filter logs by network profile name." + ) + parser.add_argument( + '--list-networks', + action='store_true', + help="List all configured network profiles." + ) + parser.add_argument( + '--detect-network', + action='store_true', + help="Detect current network and show matching profile." + ) parser.add_argument( '--setup', action='store_true', @@ -301,23 +586,27 @@ def start_dashboard(): run_setup_wizard() elif args.dashboard: start_dashboard() + elif args.list_networks: + list_networks() + elif args.detect_network: + detect_network() else: # For operations that need database/config try: setup_database() # Ensure the database is always set up if args.login: - wifi_login() + wifi_login(args.network) elif args.view_logs is not None: - view_logs(args.view_logs) + view_logs(args.view_logs, args.network_filter) elif args.test: - test_connection() + test_connection(args.network) elif args.clear_logs: clear_logs() else: print("No arguments provided. Performing default login action.") - wifi_login() - view_logs(1) + wifi_login(args.network) + view_logs(1, args.network_filter) except FileNotFoundError as e: print(f"āŒ Configuration Error: {e}")