diff --git a/README.md b/README.md index 4c57a58..07a0f3f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Version 0.1.0 MIT License - Python 3.9+ + Python 3.7+

A modern web application for checking and managing domain filtering in AdGuard Home. Built with FastAPI and modern JavaScript, following the official AdGuard Home OpenAPI specification. @@ -27,9 +27,11 @@ Then visit `http://localhost:8000` to start managing your AdGuard Home filtering ## Features +### Core Features - 🔍 Real-time domain filtering status checks - 🚫 One-click domain unblocking - 💻 Modern, responsive web interface with Tailwind CSS +- 🌓 Support for light and dark modes - 🔄 Live feedback and error handling - 📝 Comprehensive logging - 🏥 Health monitoring endpoint @@ -38,10 +40,18 @@ Then visit `http://localhost:8000` to start managing your AdGuard Home filtering - ✅ Implements official AdGuard Home API spec - 🐳 Docker support +### Browser Integration +- 🔎 404 Page Checker Userscript + - Automatically detects 404 responses while browsing + - Checks if failed domains are blocked by AdGuard Home + - Shows unblock notifications with one-click actions + - Configurable settings and caching system + - Tampermonkey integration for all major browsers + ## Requirements ### System Requirements -- Python 3.9 or higher (for local installation) +- Python 3.7 or higher (for local installation) - Running AdGuard Home instance - AdGuard Home API credentials - Docker (optional, for containerized deployment) @@ -148,13 +158,32 @@ python -m uvicorn src.simpleguardhome.main:app --reload The application will be available at `http://localhost:8000` +## Browser Integration Setup + +1. Install the [Tampermonkey](https://www.tampermonkey.net/) browser extension +2. Navigate to the `userscript` directory in this repository +3. Install the `simpleguardhome-404-checker.user.js` script +4. Configure the script with your AdGuard Home instance details + +The userscript will automatically: +- Monitor web requests for 404 responses +- Check if failed domains are blocked +- Show notifications for blocked domains +- Provide quick unblock options ## API Documentation -The API documentation is available at: -- Swagger UI: `http://localhost:8000/api/docs` -- ReDoc: `http://localhost:8000/api/redoc` -- OpenAPI Schema: `http://localhost:8000/api/openapi.json` +The API documentation is automatically generated by FastAPI using: +- Type hints in endpoint definitions +- Pydantic models for request/response validation +- Function docstrings for descriptions +- Response models and status codes +Documentation is available at: +- Swagger UI: `http://localhost:8000/api/docs` - Interactive API documentation +- ReDoc: `http://localhost:8000/api/redoc` - Alternative documentation UI +- OpenAPI Schema: `http://localhost:8000/api/openapi.json` - Raw OpenAPI specification + +### API Endpoints ### API Endpoints All endpoints follow the official AdGuard Home API specification: @@ -163,13 +192,19 @@ All endpoints follow the official AdGuard Home API specification: - `GET /` - Main web interface for domain checking and unblocking #### Filtering Endpoints -- `POST /control/filtering/check_host` - Check if a domain is blocked +- `GET /control/filtering/check_host` - Check if a domain is blocked - Parameters: `name` (query parameter) - Returns: Detailed filtering status and rules -- `POST /control/filtering/whitelist/add` - Add a domain to the allowed list - - Parameters: `name` (JSON body) - - Returns: Success status +- `GET /control/filtering/unblock_host` - Unblock a domain by adding it to whitelist + - Parameters: `name` (query parameter) + - Returns: Success message with domain status + - Status: Returns whether domain was unblocked, already unblocked, or not blocked + +- `POST /control/filtering/set_rules` - Add domains to the filtering rules + - Parameters: Array of rules in request body + - Returns: Success message on successful update + - Note: Used internally by unblock_host endpoint - `GET /control/filtering/status` - Get current filtering configuration - Returns: Complete filtering status including rules and filters @@ -178,6 +213,63 @@ All endpoints follow the official AdGuard Home API specification: - `GET /control/status` - Check application and AdGuard Home connection status - Returns: Health status with filtering state +## Project Structure + +``` +simpleguardhome/ +├── src/ +│ └── simpleguardhome/ +│ ├── __init__.py +│ ├── main.py # FastAPI application +│ ├── config.py # Configuration management +│ ├── adguard.py # AdGuard Home API client +│ └── templates/ +│ └── index.html # Web interface +├── static/ +│ └── simpleguardhome.png # Project logo +├── userscript/ +│ ├── README.md # Userscript documentation +│ └── simpleguardhome-404-checker.user.js +├── rules_backup/ # Backup storage location +├── requirements.txt +├── setup.py +├── pyproject.toml # Project metadata and dependencies +├── .env.example +├── Dockerfile +└── README.md +``` + +## Security Notes + +- API credentials are handled via environment variables +- Connections use proper error handling and timeouts +- Input validation is performed on all endpoints +- CORS protection with proper headers +- Rate limiting on sensitive endpoints +- Session-based authentication with AdGuard Home +- Sensitive information is not exposed in responses + +## Error Handling + +The application implements comprehensive error handling for all endpoints: + +- 400 Bad Request + - Invalid domain format + - Missing required parameters + - Invalid whitelist rule format +- 500 Internal Server Error + - Failed to add domain to whitelist + - Other internal processing errors +- 502 Bad Gateway + - AdGuard Home API errors + - Invalid API responses +- 503 Service Unavailable + - AdGuard Home service unreachable + - Connection timeouts + - Network errors + +All errors return an ErrorResponse object with a descriptive message. + ## Response Models The application uses Pydantic models that match the AdGuard Home API specification: @@ -200,74 +292,37 @@ The application uses Pydantic models that match the AdGuard Home API specificati } ``` -### DomainCheckResult +### FilterCheckHostResponse ```python { - "reason": str, # Filtering status (e.g., "FilteredBlackList") - "rule": str, # Applied filtering rule - "filter_id": int, # ID of the filter containing the rule - "service_name": str, # For blocked services - "cname": str, # For CNAME rewrites - "ip_addrs": List[str] # For A/AAAA rewrites + "reason": str, # Filtering status (e.g., "FilteredBlackList", "NotFilteredNotFound") + "filter_id": int, # Optional: ID of the filter containing the rule (deprecated) + "rule": str, # Optional: Applied filtering rule (deprecated) + "rules": [ # List of applied rules with details + { + "filter_list_id": int, # Filter list ID + "text": str # Rule text + } + ], + "service_name": str, # Optional: For blocked services + "cname": str, # Optional: For CNAME rewrites + "ip_addrs": List[str] # Optional: For A/AAAA rewrites } ``` -## Error Handling - -The application implements proper error handling according to the AdGuard Home API spec: - -- 400 Bad Request - Invalid input -- 401 Unauthorized - Authentication required -- 403 Forbidden - Authentication failed -- 502 Bad Gateway - AdGuard Home API error -- 503 Service Unavailable - AdGuard Home unreachable - -## Development - -### Project Structure - -``` -simpleguardhome/ -├── src/ -│ └── simpleguardhome/ -│ ├── __init__.py -│ ├── main.py # FastAPI application -│ ├── config.py # Configuration management -│ ├── adguard.py # AdGuard Home API client -│ └── templates/ -│ └── index.html # Web interface -├── static/ -│ └── simpleguardhome.png # Project logo -├── requirements.txt -├── setup.py -├── .env.example -├── Dockerfile -├── docker-compose.yml -└── README.md +### SetRulesRequest +```python +{ + "rules": List[str] # List of filtering rules to set +} ``` -### Adding New Features - -1. Backend Changes: - - Add routes in `main.py` - - Extend AdGuard client in `adguard.py` - - Update configuration in `config.py` - - Follow AdGuard Home OpenAPI spec - -2. Frontend Changes: - - Modify `templates/index.html` - - Use Tailwind CSS for styling - - Follow existing error handling patterns - -## Security Notes - -- API credentials are handled via environment variables -- Connections use proper error handling and timeouts -- Input validation is performed on all endpoints -- CORS protection with proper headers -- Rate limiting on sensitive endpoints -- Session-based authentication with AdGuard Home -- Sensitive information is not exposed in responses +### ErrorResponse +```python +{ + "message": str # Error description +} +``` ## License diff --git a/setup.py b/setup.py index e1aff0a..9914cbd 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_namespace_packages +from setuptools import setup, find_namespace_packages # type: ignore if __name__ == "__main__": try: diff --git a/src/simpleguardhome/main.py b/src/simpleguardhome/main.py index 86c2440..16de2b9 100644 --- a/src/simpleguardhome/main.py +++ b/src/simpleguardhome/main.py @@ -192,6 +192,59 @@ async def get_filtering_status() -> FilterStatus: raise +@app.get( + "/control/filtering/unblock_host", + response_model=Dict, + responses={ + 200: {"description": "OK"}, + 400: {"description": "Bad Request", "model": ErrorResponse}, + 503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse} + }, + tags=["filtering"] +) +async def unblock_host(name: str) -> Dict: + """Unblock a domain by adding it to the whitelist if it's blocked.""" + if not name: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Domain name is required" + ) + + logger.info(f"Checking domain status: {name}") + try: + async with adguard.AdGuardClient() as client: + # First check if domain is blocked + check_result = await client.check_domain(name) + + # If domain isn't blocked, no need to check whitelist or do anything else + if check_result.reason != "FilteredBlackList": + return {"message": f"Domain {name} is not blocked (Status: {check_result.reason})"} + + # Domain is blocked, check if it's already in whitelist + status_rules = await client.get_filter_status() + whitelist_rule = f"@@||{name}^" + if status_rules.user_rules and whitelist_rule in status_rules.user_rules: + return {"message": f"Domain {name} is already unblocked"} + + # Domain is blocked and not in whitelist, proceed with unblocking + success = await client.add_allowed_domain(name) + if success: + return {"message": f"Domain {name} has been unblocked"} + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to unblock domain" + ) + except AdGuardValidationError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) from e + except Exception as e: + logger.error(f"Error unblocking domain: {str(e)}") + raise + + @app.exception_handler(AdGuardError) async def adguard_exception_handler(_request: Request, exc: AdGuardError) -> JSONResponse: """Handle AdGuard-related exceptions according to spec.""" diff --git a/userscript/simpleguardhome-404-checker.user.js b/userscript/simpleguardhome-404-checker.user.js index 4f51e76..741bfb0 100644 --- a/userscript/simpleguardhome-404-checker.user.js +++ b/userscript/simpleguardhome-404-checker.user.js @@ -19,7 +19,7 @@ // Default configuration const DEFAULT_CONFIG = { host: 'http://localhost', - port: 3000 + port: 8000 }; // Get current configuration @@ -51,6 +51,52 @@ // Store check results to avoid repeated API calls const checkedDomains = new Map(); + // Check if domain is blocked by AdGuard Home + async function checkDomain(domain) { + // Skip if already checked recently + if (checkedDomains.has(domain)) { + const cachedResult = checkedDomains.get(domain); + if (Date.now() - cachedResult.timestamp < 3600000) { // Cache for 1 hour + return; + } + } + + try { + const config = getConfig(); + const apiUrl = `${config.host}:${config.port}/control/filtering/check_host?name=${encodeURIComponent(domain)}`; + + GM_xmlhttpRequest({ + method: 'GET', + url: apiUrl, + headers: {'Accept': 'application/json'}, + onload: function(response) { + try { + const data = JSON.parse(response.responseText); + const isBlocked = data.reason.startsWith('Filtered'); + + // Cache the result + checkedDomains.set(domain, { + isBlocked, + timestamp: Date.now() + }); + + // If blocked, redirect to SimpleGuardHome interface + if (isBlocked) { + window.location.href = apiUrl; + } + } catch (error) { + console.error('SimpleGuardHome parsing error:', error); + } + }, + onerror: function(error) { + console.error('SimpleGuardHome API error:', error); + } + }); + } catch (error) { + console.error('SimpleGuardHome check error:', error); + } + } + // Intercept 404 responses using a fetch handler const originalFetch = window.fetch; window.fetch = async function(...args) { @@ -82,116 +128,4 @@ }; originalXHROpen.apply(this, [method, url, ...rest]); }; - - // Check if domain is blocked by AdGuard Home - async function checkDomain(domain) { - // Skip if already checked recently - if (checkedDomains.has(domain)) { - const cachedResult = checkedDomains.get(domain); - if (Date.now() - cachedResult.timestamp < 3600000) { // Cache for 1 hour - return; - } - } - - try { - const config = getConfig(); - const apiUrl = `${config.host}:${config.port}/control/filtering/check_host?name=${encodeURIComponent(domain)}`; - - GM_xmlhttpRequest({ - method: 'GET', - url: apiUrl, - headers: { - 'Accept': 'application/json' - }, - onload: function(response) { - try { - const data = JSON.parse(response.responseText); - const isBlocked = data.reason.startsWith('Filtered'); - - // Cache the result - checkedDomains.set(domain, { - isBlocked, - reason: data.reason, - rules: data.rules, - timestamp: Date.now() - }); - - // Show notification if blocked - if (isBlocked) { - showNotification(domain, data); - } - } catch (error) { - console.error('SimpleGuardHome parsing error:', error); - } - }, - onerror: function(error) { - console.error('SimpleGuardHome API error:', error); - showNotification(domain, null, 'Unable to connect to SimpleGuardHome instance. Please check your configuration.'); - }, - onabort: function() { - console.error('SimpleGuardHome API request aborted'); - showNotification(domain, null, 'Request to SimpleGuardHome instance was aborted. Please check your configuration.'); - }, - ontimeout: function() { - console.error('SimpleGuardHome API request timed out'); - showNotification(domain, null, 'Request to SimpleGuardHome instance timed out. Please check your configuration.'); - } - }); - } catch (error) { - console.error('SimpleGuardHome check error:', error); - } - } - - // Show a notification when a blocked domain is detected - function showNotification(domain, data, error = null) { - const notification = document.createElement('div'); - const config = getConfig(); - - notification.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 15px; - background: ${error ? '#fff3cd' : '#f8d7d9'}; - border-left: 4px solid ${error ? '#ffc107' : '#dc3545'}; - border-radius: 4px; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - z-index: 9999; - max-width: 400px; - font-family: system-ui, -apple-system, sans-serif; - `; - - if (error) { - notification.innerHTML = ` -
SimpleGuardHome Error
-
${error}
- - `; - - const configButton = notification.querySelector('button'); - configButton.addEventListener('click', () => { - showConfigDialog(); - notification.remove(); - }); - } else { - notification.innerHTML = ` -
404 Domain is Blocked
-
${domain}
-
Reason: ${data.reason}
- ${data.rules?.length ? `
Rule: ${data.rules[0].text}
` : ''} - - `; - - const unblockButton = notification.querySelector('button'); - unblockButton.addEventListener('click', () => { - window.open(`${config.host}:${config.port}/?domain=${encodeURIComponent(domain)}`, '_blank'); - notification.remove(); - }); - } - - // Auto-remove after 10 seconds - setTimeout(() => notification.remove(), 10000); - - document.body.appendChild(notification); - } })(); \ No newline at end of file