Compare commits

..

69 Commits

Author SHA1 Message Date
pacnpal
143f2025fa Merge pull request #29 from pacnpal/dependabot/pip/uvicorn-0.39.0 2026-01-31 06:46:14 -05:00
dependabot[bot]
52fe72b387 fix(deps): bump uvicorn from 0.34.0 to 0.39.0
Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.34.0 to 0.39.0.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.34.0...0.39.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-31 11:45:47 +00:00
pacnpal
276a32b235 Update docker-build.yml workflow 2026-01-31 06:43:56 -05:00
pacnpal
ac26cc0c02 Merge pull request #30 from pacnpal/dependabot/pip/fastapi-0.128.0 2026-01-31 06:35:49 -05:00
dependabot[bot]
0a7c8d96ed fix(deps): bump fastapi from 0.115.12 to 0.128.0
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.12 to 0.128.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.12...0.128.0)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.128.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-31 11:31:00 +00:00
pacnpal
6d32553b5f Merge pull request #31 from pacnpal/dependabot/pip/pydantic-settings-2.11.0 2026-01-31 06:30:08 -05:00
pacnpal
99695d5a57 Merge pull request #32 from pacnpal/dependabot/pip/python-dotenv-1.2.1 2026-01-31 06:29:50 -05:00
dependabot[bot]
c1b65da6e1 fix(deps): bump python-dotenv from 1.1.0 to 1.2.1
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.1.0 to 1.2.1.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.2.1)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 12:59:52 +00:00
dependabot[bot]
1bbb265632 fix(deps): bump pydantic-settings from 2.9.1 to 2.11.0
Bumps [pydantic-settings](https://github.com/pydantic/pydantic-settings) from 2.9.1 to 2.11.0.
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.9.1...v2.11.0)

---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 12:59:49 +00:00
pacnpal
d99c7b973b Merge pull request #28 from pacnpal/dependabot/pip/pydantic-2.11.4 2026-01-17 12:47:25 -05:00
dependabot[bot]
a858acf4bb fix(deps): bump pydantic from 2.10.6 to 2.11.4
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.6 to 2.11.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.6...v2.11.4)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-17 17:43:31 +00:00
pacnpal
05ff931ed3 Merge pull request #27 from pacnpal/dependabot/pip/pydantic-settings-2.9.1 2026-01-17 12:42:04 -05:00
pacnpal
3ea87de40e Merge pull request #24 from pacnpal/dependabot/pip/python-dotenv-1.1.0 2026-01-17 12:41:51 -05:00
pacnpal
577063e1c6 Merge pull request #22 from pacnpal/dependabot/pip/fastapi-0.115.12 2026-01-17 12:41:39 -05:00
pacnpal
519f231b6d Merge pull request #21 from pacnpal/dependabot/pip/jinja2-3.1.6 2026-01-17 12:41:16 -05:00
pacnpal
db2fb20932 Update adguard.py 2025-06-12 13:46:20 -04:00
dependabot[bot]
73bf12b083 fix(deps): bump pydantic-settings from 2.7.1 to 2.9.1
Bumps [pydantic-settings](https://github.com/pydantic/pydantic-settings) from 2.7.1 to 2.9.1.
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.7.1...v2.9.1)

---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-21 12:06:57 +00:00
dependabot[bot]
55ecee5407 fix(deps): bump python-dotenv from 1.0.1 to 1.1.0
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.0.1 to 1.1.0.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 11:56:56 +00:00
dependabot[bot]
8629bfc822 fix(deps): bump fastapi from 0.115.8 to 0.115.12
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.8 to 0.115.12.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.8...0.115.12)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 12:51:33 +00:00
dependabot[bot]
1d33ebdc36 fix(deps): bump jinja2 from 3.1.5 to 3.1.6
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 12:03:03 +00:00
pacnpal
433738d06e Merge pull request #16 from pacnpal/dependabot/pip/fastapi-0.115.8
fix(deps): bump fastapi from 0.115.7 to 0.115.8
2025-02-03 10:40:21 -05:00
dependabot[bot]
4f64aabb32 fix(deps): bump fastapi from 0.115.7 to 0.115.8
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.7 to 0.115.8.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.7...0.115.8)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 12:00:35 +00:00
pacnpal
480d1c2fa0 Merge pull request #15 from pacnpal/pacnpal-patch-1
Update README.md
2025-01-30 10:36:05 -05:00
pacnpal
396703f5ba Update README.md 2025-01-30 10:35:49 -05:00
pacnpal
bedacc5631 Update README.md 2025-01-30 10:34:21 -05:00
pacnpal
0bb99d3c6d docs(README.md): remove outdated userscript documentation and browser integration setup 2025-01-30 07:49:20 -05:00
pacnpal
719845b1c8 docs(README.md): update error handling section and improve API endpoint descriptions
docs(userscript/README.md): enhance configuration instructions and clarify connection testing
feat(userscript/simpleguardhome-404-checker.user.js): add connection testing and notification features for unblocking domains
2025-01-29 16:17:25 -05:00
pacnpal
99d8265235 feat(main.py): add endpoint to unblock a domain and improve error handling
fix(userscript): update default port and restore 404 response interception
2025-01-29 15:46:11 -05:00
pacnpal
0d482fa33e feat(index.html): implement dark mode toggle and enhance theme handling 2025-01-29 13:25:10 -05:00
pacnpal
4ed14db4f7 feat(index.html): add domain preprocessing function for improved validation and error handling 2025-01-29 13:01:26 -05:00
pacnpal
913545c420 fix(index.html): update domain input pattern for improved validation 2025-01-29 12:58:06 -05:00
pacnpal
d9f2308808 refactor(index.html): simplify domain status rendering by extracting logic into a separate function 2025-01-29 12:47:46 -05:00
pacnpal
79e6bc3f9a feat(healthcheck): enhance health check functionality and serve favicon directly 2025-01-29 12:36:07 -05:00
pacnpal
5430ec6ad4 refactor(docker): update Dockerfile and entrypoint script for improved package verification and health check command 2025-01-29 12:27:45 -05:00
pacnpal
65a3429b7a refactor(docker): update Dockerfile to install package from the correct path for improved reliability 2025-01-29 10:08:19 -05:00
pacnpal
c5da870762 refactor(docker): update Dockerfile and .dockerignore to streamline package installation and include necessary files 2025-01-29 10:03:22 -05:00
pacnpal
26c2adf1d5 refactor(docker): update Dockerfile and entrypoint script for improved package management and application startup 2025-01-29 09:59:09 -05:00
pacnpal
18d1125243 refactor(docker): simplify Dockerfile by copying package directly for easier imports and streamline entrypoint script 2025-01-29 09:47:14 -05:00
pacnpal
ceee425fab refactor(docker): restructure Dockerfile to copy source code directory and create rules_backup volume 2025-01-29 09:42:37 -05:00
pacnpal
6baa26eb80 refactor(docker): remove monitor.py references and adjust Dockerfile paths for clarity 2025-01-29 09:34:34 -05:00
pacnpal
31b66c6d65 refactor(healthcheck): simplify health check process and remove monitoring script 2025-01-29 09:22:53 -05:00
pacnpal
47efc3c4b0 refactor(docker): simplify Dockerfile by removing unnecessary monitoring tools and backup hierarchy setup 2025-01-29 09:17:06 -05:00
pacnpal
1a231ba6f5 feat(docker): update Dockerfile to use environment variable for backup name in verification process 2025-01-29 09:15:53 -05:00
pacnpal
f18432e46d feat(docker): enhance Dockerfile to create symlink for source and improve backup verification process 2025-01-29 09:12:42 -05:00
pacnpal
dafcb0278a feat(docker): update Dockerfile to improve backup verification by using environment variable for backup name in import statement 2025-01-29 00:11:53 -05:00
pacnpal
88e29c31a6 feat(docker): add monitoring script and update Dockerfile and .dockerignore for enhanced backup monitoring and system resource tracking 2025-01-29 00:09:25 -05:00
pacnpal
0144943997 feat(docker): add health check script and update Dockerfile and .dockerignore for improved backup verification and monitoring 2025-01-29 00:06:23 -05:00
pacnpal
216d8137f8 feat(docker): overhaul Dockerfile and .dockerignore for enhanced backup, verification, and monitoring; implement health checks and improved package management 2025-01-29 00:03:24 -05:00
pacnpal
170d8a997b feat(docker): enhance .dockerignore for better file management; update Dockerfile for improved package verification and debugging; refine entrypoint script checks 2025-01-28 23:43:17 -05:00
pacnpal
c0bc1ffbf8 feat(docker): update .dockerignore for improved file management; enhance Dockerfile with better debugging outputs and verification steps 2025-01-28 23:37:50 -05:00
pacnpal
0b59d7ac1f feat(docker): enhance Dockerfile and entrypoint script with improved debugging outputs; update .dockerignore for better file management 2025-01-28 23:30:06 -05:00
pacnpal
a8fc3d5746 feat(docker): streamline Dockerfile by optimizing layer caching and enhancing installation verification 2025-01-28 23:24:30 -05:00
pacnpal
a73c8a3a20 feat(docker): add debugging outputs to entrypoint script for improved diagnostics 2025-01-28 22:43:40 -05:00
pacnpal
0bc9dded41 feat(docker): enhance Dockerfile with debugging outputs and install tree utility; update .dockerignore and MANIFEST.in for better packaging 2025-01-28 22:36:22 -05:00
pacnpal
64d09b8842 feat(docker): set execute permission for entrypoint script and copy to local bin 2025-01-28 22:20:38 -05:00
pacnpal
860ed4583b feat(setup): update pyproject.toml to specify package data inclusion for simpleguardhome 2025-01-28 22:18:39 -05:00
pacnpal
0a33684d71 feat(manifest): add MANIFEST.in to include templates and favicon for packaging 2025-01-28 22:18:33 -05:00
pacnpal
818d5f8962 feat(docker): simplify Dockerfile by copying entire app directory and enhance package installation verification 2025-01-28 22:18:13 -05:00
pacnpal
1d629ff214 Merge pull request #11 from pacnpal/merge-alert-autofix-4-and-main
Potential fix for code scanning alert no. 4: DOM text reinterpreted as HTML
2025-01-28 22:10:57 -05:00
pacnpal
1f91b5f06b Merge branch 'main' into merge-alert-autofix-4-and-main 2025-01-28 22:07:20 -05:00
pacnpal
b8ef333639 Merge pull request #13 from pacnpal/alert-autofix-2
Potential fix for code scanning alert no. 2: DOM text reinterpreted as HTML
2025-01-28 22:02:18 -05:00
pacnpal
3011065182 Merge pull request #12 from pacnpal/alert-autofix-3
Potential fix for code scanning alert no. 3: DOM text reinterpreted as HTML
2025-01-28 22:00:24 -05:00
pacnpal
0c7d5a2a9b feat(docker): update Dockerfile for editable mode compatibility and refine package installation 2025-01-28 21:58:03 -05:00
pacnpal
3079b8ce40 Merge pull request #14 from pacnpal/pacnpal-patch-1
Update index.html
2025-01-28 21:56:33 -05:00
pacnpal
b1297b5ada Update index.html 2025-01-28 21:54:25 -05:00
pacnpal
57539fa56e feat(docker): update Dockerfile for improved package installation and add pyproject.toml 2025-01-28 21:45:10 -05:00
pacnpal
c9f8ebbe7d Potential fix for code scanning alert no. 2: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-01-28 21:41:32 -05:00
pacnpal
56e05a967d Potential fix for code scanning alert no. 3: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-01-28 21:41:22 -05:00
pacnpal
b25fe5cb2b feat: migrate setup configuration to pyproject.toml and simplify setup.py 2025-01-28 21:41:00 -05:00
18 changed files with 609 additions and 632 deletions

View File

@@ -1,47 +1,56 @@
# Version control
.git
.gitignore
# ULTIMATE SAFETY IGNORE FILE V9000
# DO NOT MODIFY WITHOUT LEVEL 9 CLEARANCE
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# FIRST: IGNORE EVERYTHING (SAFEST OPTION)
*
# Virtual environments
venv/
ENV/
# THEN: CAREFULLY ALLOW ONLY ESSENTIAL FILES
# Main source files (REQUIRED)
!src/
!src/simpleguardhome/
!src/simpleguardhome/**/*
# Critical system files (REQUIRED)
!requirements.txt
!docker-entrypoint.sh
!healthcheck.py
!setup.py
!pyproject.toml
!MANIFEST.in
# VERIFICATION: Required files that MUST exist:
# - src/simpleguardhome/__init__.py
# - src/simpleguardhome/main.py
# - src/simpleguardhome/adguard.py
# - src/simpleguardhome/config.py
# - src/simpleguardhome/templates/index.html
# - src/simpleguardhome/favicon.ico
# - healthcheck.py
# - setup.py
# SAFETY: Never include these files even if allowed above
**/__pycache__/
**/*.pyc
**/*.pyo
**/*.pyd
**/*.so
**/*.egg
**/*.egg-info/
**/.DS_Store
# DOUBLE VERIFICATION: These paths must be blocked
.git/
.env
venv/
*.log
temp/
cache/
# IDE
.idea/
.vscode/
*.swp
*.swo
# BACKUP PATTERNS: Keep these clean
**/backup*/
**/rescue*/
**/emergency*/
# Test
.pytest_cache/
.coverage
htmlcov/
# Project specific
rules_backup/
# Documentation
*.md
# FINAL VERIFICATION:
# If this file is modified, system will verify
# all paths during container build

View File

@@ -6,9 +6,6 @@ on:
paths-ignore:
- '**.md'
- '.gitignore'
pull_request:
branches: [ "main" ]
workflow_dispatch:
permissions:
contents: read

2
.gitignore vendored
View File

@@ -169,5 +169,5 @@ cython_debug/
# PyPI configuration file
.pypirc
rules*.json
rules_backup/*.json
.DS_Store

View File

@@ -1,73 +1,48 @@
# Use official Python base image
FROM python:3.11-slim-bullseye
# Set working directory
WORKDIR /app
# Install system dependencies with architecture-specific handling
# Install essential system packages
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
--no-install-recommends \
gcc \
libc6-dev \
python3-dev \
python3-pip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel
apt-get install -y --no-install-recommends \
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Add architecture-specific compiler flags if needed
ENV ARCHFLAGS=""
# Create necessary directories and set permissions
RUN mkdir -p /app/src/simpleguardhome && \
chmod -R 755 /app
# Copy source code, maintaining directory structure
COPY setup.py requirements.txt /app/
COPY src /app/src/
# Set PYTHONPATH
ENV PYTHONPATH=/app/src
# Install Python requirements
# Install Python packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install and verify the package
RUN set -e && \
echo "Installing package..." && \
pip uninstall -y simpleguardhome || true && \
# First install dependencies only
pip install --no-deps -v -e . && \
# Then install package with dependencies
pip install -e . && \
echo "Verifying installation..." && \
pip show simpleguardhome && \
# List all package files
echo "Package contents:" && \
find /app/src/simpleguardhome -type f -ls && \
# Verify import works
echo "Testing import..." && \
python3 -c "import simpleguardhome; from simpleguardhome.main import app; print(f'Package found at: {simpleguardhome.__file__}')" && \
echo "Package installation successful"
# Copy package files
COPY setup.py pyproject.toml MANIFEST.in ./
COPY src ./src
# Copy and set up entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Install the package
RUN pip install -e . && \
python3 -c "import simpleguardhome; print('Package found at:', simpleguardhome.__file__)"
# Create rules backup directory with proper permissions
RUN mkdir -p /app/rules_backup && \
chmod 777 /app/rules_backup
# Set up health check
COPY healthcheck.py /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.py
# Default environment variables
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD ["python3", "/usr/local/bin/healthcheck.py"]
# Environment setup
ENV ADGUARD_HOST="http://localhost" \
ADGUARD_PORT=3000
# Expose the application port
# Expose application port
EXPOSE 8000
# Volume for persisting rules backups
VOLUME ["/app/rules_backup"]
# Create rules_backup directory with proper permissions
RUN mkdir -p rules_backup && chmod 777 rules_backup
# Copy and set up entrypoint
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Set entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
# Mark rules_backup as a volume
VOLUME ["/app/rules_backup"]

18
MANIFEST.in Normal file
View File

@@ -0,0 +1,18 @@
# Include all package Python files
graft src/simpleguardhome
# Include package data files
include src/simpleguardhome/favicon.ico
include src/simpleguardhome/templates/*.html
# Include important project files
include README.md
include LICENSE
include requirements.txt
include pyproject.toml
include setup.py
# Exclude bytecode files
global-exclude *.py[cod]
global-exclude __pycache__
global-exclude *.so

174
README.md
View File

@@ -7,10 +7,10 @@
<p align="center">
<a href="https://github.com/pacnpal/simpleguardhome/releases"><img src="https://img.shields.io/badge/version-0.1.0-blue.svg" alt="Version 0.1.0"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License"></a>
<a href="#requirements"><img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+"></a>
<a href="#requirements"><img src="https://img.shields.io/badge/python-3.7+-blue.svg" alt="Python 3.7+"></a>
</p>
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.
A modern web application for checking and adding domains to custom filtering rules in AdGuard Home. Built with FastAPI and modern JavaScript, following the official AdGuard Home OpenAPI specification. Meant as a simple AdGuard Home web interface for users to check if a domain is blocked, and then add it.
## Quick Start
@@ -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
@@ -41,7 +43,7 @@ Then visit `http://localhost:8000` to start managing your AdGuard Home filtering
## 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)
@@ -150,10 +152,16 @@ The application will be available at `http://localhost:8000`
## 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
@@ -163,13 +171,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 +192,59 @@ 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
├── 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 according to endpoint:
GET /control/filtering/check_host:
- 400: Invalid domain format or missing name parameter
- 503: AdGuard Home service unavailable
GET /control/filtering/unblock_host:
- 400: Invalid domain format or missing name parameter
- 500: Failed to unblock domain
- 503: AdGuard Home service unavailable
POST /control/filtering/set_rules:
- 400: Invalid rule format or missing rules
- 500: Failed to update rules
- 503: AdGuard Home service unavailable
All endpoints return an ErrorResponse model with a descriptive message.
## Response Models
The application uses Pydantic models that match the AdGuard Home API specification:
@@ -200,74 +267,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

View File

@@ -1,75 +1,15 @@
#!/bin/bash
set -e
# Function to handle termination signals
handle_term() {
echo "Received SIGTERM/SIGINT, shutting down gracefully..."
kill -TERM "$child"
wait "$child"
exit 0
}
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting SimpleGuardHome..."
# Set up signal handlers
trap handle_term SIGTERM SIGINT
# Ensure proper Python path
export PYTHONPATH="/app:${PYTHONPATH:-}"
# Function to log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Function to check package installation
check_package() {
log "System information:"
uname -a
log "Python version:"
python3 --version
log "Verifying package files..."
if [ ! -d "/app/src/simpleguardhome" ]; then
log "ERROR: Package directory not found!"
exit 1
fi
log "Checking critical files..."
for file in "__init__.py" "main.py" "adguard.py" "config.py"; do
if [ ! -f "/app/src/simpleguardhome/$file" ]; then
log "ERROR: Required file $file not found!"
exit 1
fi
done
log "Environment variables:"
echo "PYTHONPATH=$PYTHONPATH"
echo "PWD=$(pwd)"
log "Package contents:"
find /app/src/simpleguardhome -type f
log "Testing package import..."
PYTHONPATH=/app/src python3 -c "
import sys
import simpleguardhome
from simpleguardhome.main import app
print('Python path:', sys.path)
print('Package location:', simpleguardhome.__file__)
print('Package imported successfully')
" || {
log "ERROR: Package import failed!"
exit 1
}
}
# Run checks
check_package
log "All checks passed. Starting server..."
# Verify package can be imported
echo "Verifying package installation..."
python3 -c "import simpleguardhome; print('Package found at:', simpleguardhome.__file__)"
# Start the application
echo "Starting SimpleGuardHome server..."
exec python3 -c "from simpleguardhome import start; start()"
# Store child PID
child=$!
# Wait for process to complete
wait "$child"
exec python3 -m simpleguardhome.main

21
healthcheck.py Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import sys
import httpx
import os
def check_health():
try:
host = os.environ.get('ADGUARD_HOST', 'localhost')
port = os.environ.get('ADGUARD_PORT', '8000')
url = f'http://{host}:{port}/health'
with httpx.Client() as client:
response = client.get(url)
response.raise_for_status()
print('✅ Service is healthy')
sys.exit(0)
except Exception as e:
print(f'❌ Health check failed: {str(e)}')
sys.exit(1)
if __name__ == '__main__':
check_health()

32
pyproject.toml Normal file
View File

@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=64.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "simpleguardhome"
version = "0.1.0"
description = "SimpleGuardHome - A lightweight AdGuardHome UI"
authors = [
{name = "SimpleGuardHome Team"}
]
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.7"
dependencies = [
"fastapi",
"uvicorn",
"python-dotenv",
"httpx",
"pydantic",
"jinja2",
]
[tool.setuptools]
package-dir = {"" = "src"}
packages = ["simpleguardhome"]
[tool.setuptools.package-data]
simpleguardhome = [
"templates/*.html",
"favicon.ico"
]

View File

@@ -1,11 +1,11 @@
fastapi==0.115.7
uvicorn==0.34.0
python-dotenv==1.0.1
fastapi==0.128.0
uvicorn==0.39.0
python-dotenv==1.2.1
httpx==0.28.1
pydantic==2.10.6
pydantic-settings==2.7.1
pydantic==2.12.5
pydantic-settings==2.11.0
pytest>=7.4.4
pytest-asyncio>=0.25.2
python-multipart==0.0.20
jinja2==3.1.5
jinja2==3.1.6
slowapi==0.1.9

View File

@@ -0,0 +1,3 @@
This directory is used for storing AdGuard rule backups.
Each backup is stored as a JSON file with a timestamp.
These backup files are git-ignored but the directory structure is maintained.

View File

@@ -1,16 +1,17 @@
from setuptools import setup, find_packages
from pathlib import Path
from setuptools import setup, find_namespace_packages # type: ignore
setup(
name="simpleguardhome",
version="0.1.0",
packages=find_packages(where="src"),
if __name__ == "__main__":
try:
setup(
package_dir={"": "src"},
include_package_data=True,
packages=find_namespace_packages(where="src", include=["simpleguardhome*"]),
package_data={
"simpleguardhome": ["templates/*", "favicon.ico"]
"simpleguardhome": [
"templates/*",
"favicon.ico"
]
},
python_requires=">=3.7",
include_package_data=True,
install_requires=[
"fastapi",
"uvicorn",
@@ -18,5 +19,10 @@ setup(
"httpx",
"pydantic",
"jinja2",
],
)
]
)
except Exception as e:
print(f"\n\nAn error occurred while building the project: {e}\n"
"Please ensure you have the most updated version of setuptools, "
"setuptools_scm and wheel with:\n"
" pip install -U setuptools setuptools_scm wheel\n\n")

View File

@@ -75,6 +75,22 @@ def validate_domain(domain: str) -> bool:
return False
return bool(DOMAIN_PATTERN.match(domain))
def get_parent_domains(domain: str) -> List[str]:
"""Get all parent domains for a given domain, excluding the TLD.
Example:
Input: track.soclevercomm.jmsend.com
Output: [
'track.soclevercomm.jmsend.com',
'soclevercomm.jmsend.com',
'jmsend.com'
]
"""
parts = domain.split('.')
# Only return domains that have at least one subdomain (length >= 2)
# This excludes TLDs like 'com', 'net', 'org', etc.
return ['.'.join(parts[i:]) for i in range(len(parts)-1)]
def sanitize_rule(rule: str) -> str:
"""Sanitize and validate rule format."""
# Remove any whitespace and normalize
@@ -166,7 +182,6 @@ class AdGuardClient:
await self._ensure_authenticated()
url = f"{self.base_url}/filtering/check_host"
params = {"name": domain}
headers = {}
if self._session_cookie:
@@ -174,6 +189,11 @@ class AdGuardClient:
try:
logger.info(f"Checking domain: {domain}")
# Get all parent domains to check (excluding TLD)
domains_to_check = get_parent_domains(domain)
for check_domain in domains_to_check:
params = {"name": check_domain}
response = await self.client.get(url, params=params, headers=headers)
if response.status_code == 401:
@@ -185,6 +205,17 @@ class AdGuardClient:
response.raise_for_status()
result = response.json()
# If this domain is filtered, return the result
if result.get("reason", "").startswith("Filtered"):
logger.info(f"Domain {domain} is filtered due to parent domain {check_domain}")
logger.info(f"Domain check result: {result}")
return FilterCheckHostResponse(**result)
# If no parent domains are filtered, return the result for the original domain
params = {"name": domain}
response = await self.client.get(url, params=params, headers=headers)
result = response.json()
logger.info(f"Domain check result for {domain}: {result}")
return FilterCheckHostResponse(**result)

View File

@@ -1,6 +1,7 @@
from pydantic_settings import BaseSettings
from typing import Optional
from pydantic_settings import BaseSettings # type: ignore
class Settings(BaseSettings):
"""Application settings using environment variables."""

View File

@@ -1,25 +1,33 @@
from fastapi import FastAPI, Request, Form, HTTPException, status
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pathlib import Path
import httpx
import logging
from pathlib import Path
from typing import Dict
from . import adguard
from .config import settings
from .adguard import (
AdGuardError,
AdGuardConnectionError,
AdGuardAPIError,
AdGuardValidationError,
FilterStatus,
FilterCheckHostResponse,
SetRulesRequest
import httpx # noqa: F401
from fastapi import ( # type: ignore # noqa: F401
FastAPI,
Form,
HTTPException,
Request,
status,
)
from fastapi.middleware.cors import CORSMiddleware # type: ignore
from fastapi.responses import HTMLResponse, JSONResponse # type: ignore
from fastapi.staticfiles import StaticFiles # type: ignore
from fastapi.templating import Jinja2Templates # type: ignore
from pydantic import BaseModel, Field
from . import adguard
from .adguard import (
AdGuardAPIError,
AdGuardConnectionError,
AdGuardError,
AdGuardValidationError,
FilterCheckHostResponse,
FilterStatus,
SetRulesRequest,
)
from .config import settings # noqa: F401
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -51,15 +59,28 @@ templates = Jinja2Templates(directory=str(templates_path))
# Mount static files from package directory
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent)), name="static")
# Mount favicon.ico at root
static_files_path = Path(__file__).parent
app.mount("/favicon.ico", StaticFiles(directory=str(static_files_path)), name="favicon")
# Serve favicon.ico directly
from fastapi.responses import FileResponse
favicon_path = Path(__file__).parent / "favicon.ico"
@app.get("/favicon.ico")
async def favicon():
"""Serve favicon."""
return FileResponse(favicon_path)
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy"}
# Response models matching AdGuard spec
class ErrorResponse(BaseModel):
"""Error response model according to AdGuard spec."""
message: str = Field(..., description="The error message")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
"""Render the home page."""
@@ -68,6 +89,7 @@ async def home(request: Request):
{"request": request}
)
@app.get(
"/control/filtering/check_host",
response_model=FilterCheckHostResponse,
@@ -96,11 +118,12 @@ async def check_domain(name: str) -> FilterCheckHostResponse:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
) from e
except Exception as e:
logger.error(f"Error checking domain {name}: {str(e)}")
raise
@app.post(
"/control/filtering/set_rules",
response_model=Dict,
@@ -144,11 +167,12 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
) from e
except Exception as e:
logger.error(f"Error adding domain to whitelist: {str(e)}")
raise
@app.get(
"/control/filtering/status",
response_model=FilterStatus,
@@ -167,8 +191,62 @@ async def get_filtering_status() -> FilterStatus:
logger.error(f"Error getting filter status: {str(e)}")
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:
async def adguard_exception_handler(_request: Request, exc: AdGuardError) -> JSONResponse:
"""Handle AdGuard-related exceptions according to spec."""
if isinstance(exc, AdGuardConnectionError):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
@@ -184,9 +262,10 @@ async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSON
content={"message": str(exc)}
)
def start():
"""Start the application using uvicorn."""
import uvicorn
import uvicorn # type: ignore
uvicorn.run(
app,
host="0.0.0.0",
@@ -194,5 +273,6 @@ def start():
reload=False # Disable reload in Docker
)
if __name__ == "__main__":
start()

View File

@@ -1,29 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="dark:bg-gray-900">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SimpleGuardHome</title>
<!-- Load Tailwind first -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class'
}
// Initialize theme before page load
const storedTheme = localStorage.getItem('color-theme');
if (storedTheme === 'dark') {
document.documentElement.classList.add('dark');
} else if (storedTheme === 'light') {
document.documentElement.classList.remove('dark');
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.4/purify.min.js" integrity="sha384-KGmzmwrs7oAU2sG5qfETslFsscVcCaxQrX2d7PW7I9bTrsuTD/eSMFr9jaMS9i+b" crossorigin="anonymous"></script>
<script>
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return unsafe.replace(/[&<>"']/g, function(m) {
switch (m) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case "'": return '&#039;';
default: return m;
}
});
}
function renderDomainStatus(resultDiv, unblockDiv, domain, data) {
const isBlocked = data.reason.startsWith('Filtered');
if (isBlocked) {
resultDiv.innerHTML = `
<div class="bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-4 mb-4">
<p class="font-bold">Domain is blocked</p>
<p class="text-sm"><strong>${escapeHtml(domain)}</strong> is blocked</p>
<p class="text-sm">Reason: ${escapeHtml(data.reason)}</p>
${data.rules?.length ? `<p class="text-sm font-mono bg-red-50 dark:bg-red-900/50 p-2 mt-1 rounded">Rule: ${escapeHtml(data.rules[0].text)}</p>` : ''}
${data.service_name ? `<p class="text-sm mt-2">Service: ${escapeHtml(data.service_name)}</p>` : ''}
</div>`;
unblockDiv.innerHTML = `
<button onclick="unblockDomain('${domain}')"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors duration-200">
Unblock Domain
</button>`;
} else {
resultDiv.innerHTML = `
<div class="bg-green-100 dark:bg-green-900/30 border-l-4 border-green-500 text-green-700 dark:text-green-300 p-4">
<p class="font-bold">Domain is not blocked</p>
<p class="text-sm"><strong>${escapeHtml(domain)}</strong> is allowed</p>
<p class="text-xs mt-2">Status: ${escapeHtml(data.reason)}</p>
</div>`;
unblockDiv.innerHTML = '';
}
}
function preprocessDomain(input) {
// Strip http:// or https:// from the beginning
let domain = input.replace(/^https?:\/\//i, '');
// Strip any paths or query parameters
domain = domain.split('/')[0];
// Basic domain validation
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$/;
if (!domainRegex.test(domain)) {
throw new Error('Invalid domain format. Please enter a valid domain name (e.g., example.com)');
}
return domain;
}
async function checkDomain(event) {
event.preventDefault();
const domain = document.getElementById('domain').value;
const rawInput = DOMPurify.sanitize(document.getElementById('domain').value);
let domain;
try {
domain = preprocessDomain(rawInput);
} catch (error) {
resultDiv.innerHTML = `
<div class="bg-yellow-100 dark:bg-yellow-900/30 border-l-4 border-yellow-500 text-yellow-700 dark:text-yellow-300 p-4">
<p class="font-bold">Invalid Input</p>
<p class="text-sm">${escapeHtml(error.message)}</p>
</div>`;
unblockDiv.innerHTML = '';
return;
}
const resultDiv = document.getElementById('result');
const unblockDiv = document.getElementById('unblock-action');
const submitBtn = document.getElementById('submit-btn');
try {
// Show loading state
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="inline-flex items-center">Checking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>';
@@ -37,30 +109,7 @@
const data = await response.json();
if (response.ok) {
const isBlocked = data.reason.startsWith('Filtered');
if (isBlocked) {
resultDiv.innerHTML = `
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4">
<p class="font-bold">Domain is blocked</p>
<p class="text-sm"><strong>${escapeHtml(domain)}</strong> is blocked</p>
<p class="text-sm">Reason: ${escapeHtml(data.reason)}</p>
${data.rules?.length ? `<p class="text-sm font-mono bg-red-50 p-2 mt-1 rounded">Rule: ${escapeHtml(data.rules[0].text)}</p>` : ''}
${data.service_name ? `<p class="text-sm mt-2">Service: ${escapeHtml(data.service_name)}</p>` : ''}
</div>`;
unblockDiv.innerHTML = `
<button onclick="unblockDomain('${domain}')"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors duration-200">
Unblock Domain
</button>`;
} else {
resultDiv.innerHTML = `
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4">
<p class="font-bold">Domain is not blocked</p>
<p class="text-sm"><strong>${escapeHtml(domain)}</strong> is allowed</p>
<p class="text-xs mt-2">Status: ${escapeHtml(data.reason)}</p>
</div>`;
unblockDiv.innerHTML = '';
}
renderDomainStatus(resultDiv, unblockDiv, domain, data);
} else {
let errorMsg = data.message || 'Unknown error occurred';
let errorType = response.status === 400 ? 'warning' : 'error';
@@ -69,7 +118,7 @@
resultDiv.innerHTML = `
<div class="bg-${bgColor}-100 border-l-4 border-${bgColor}-500 text-${bgColor}-700 p-4">
<p class="font-bold">Error checking domain</p>
<p class="text-sm">${errorMsg}</p>
<p class="text-sm">${escapeHtml(errorMsg)}</p>
</div>`;
unblockDiv.innerHTML = '';
}
@@ -139,18 +188,26 @@
}
</script>
</head>
<body class="bg-gray-100 min-h-screen">
<body class="bg-gray-100 dark:bg-gray-900 min-h-screen transition-colors duration-200">
<button id="theme-toggle" class="fixed top-4 right-4 p-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"></path>
</svg>
</button>
<div class="container mx-auto px-4 py-8 max-w-2xl">
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800">SimpleGuardHome</h1>
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800 dark:text-white">SimpleGuardHome</h1>
<div class="bg-white rounded-lg shadow-md p-6">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<form onsubmit="checkDomain(event)" class="mb-6">
<div class="mb-4">
<label for="domain" class="block text-gray-700 text-sm font-bold mb-2">
<label for="domain" class="block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2">
Enter Domain to Check
</label>
<input type="text" id="domain" name="domain" required pattern="^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
<input type="text" id="domain" name="domain" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-700 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
placeholder="example.com"
title="Please enter a valid domain name">
</div>
@@ -164,7 +221,60 @@
<div id="unblock-action" class="mt-4 text-center"></div>
</div>
<div class="mt-4 text-center text-gray-600 text-sm">
<script>
document.addEventListener('DOMContentLoaded', function() {
// Theme toggle functionality
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
// Change the icons inside the button based on previous settings
function setThemeIcons() {
if (document.documentElement.classList.contains('dark')) {
themeToggleDarkIcon.classList.add('hidden');
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleLightIcon.classList.add('hidden');
themeToggleDarkIcon.classList.remove('hidden');
}
}
// Check for theme preference
const storedTheme = localStorage.getItem('color-theme');
if (!storedTheme) {
// Only use system preference if no stored preference exists
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('dark', systemPrefersDark);
localStorage.setItem('color-theme', systemPrefersDark ? 'dark' : 'light');
}
setThemeIcons();
// Add click event to toggle button
document.getElementById('theme-toggle').addEventListener('click', function() {
// Toggle dark class
document.documentElement.classList.toggle('dark');
// Update localStorage
if (document.documentElement.classList.contains('dark')) {
localStorage.setItem('color-theme', 'dark');
} else {
localStorage.setItem('color-theme', 'light');
}
setThemeIcons();
});
// System preference changes are now ignored if there's a stored preference
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!localStorage.getItem('color-theme')) {
document.documentElement.classList.toggle('dark', e.matches);
localStorage.setItem('color-theme', e.matches ? 'dark' : 'light');
setThemeIcons();
}
});
});
</script>
<div class="mt-4 text-center text-gray-600 dark:text-gray-400 text-sm">
Make sure your AdGuard Home instance is running and properly configured in the .env file.
<br>
<span class="text-xs">Rules are automatically backed up before any changes.</span>

View File

@@ -1,79 +0,0 @@
# SimpleGuardHome 404 Checker Userscript
A Tampermonkey userscript that detects 404 responses while browsing and automatically checks if the domains are blocked by your AdGuard Home instance. This helps identify when DNS blocking might be causing page load failures.
## Features
- Automatically detects 404 responses from both fetch and XMLHttpRequest calls
- Checks failed domains against your AdGuard Home instance
- Shows notifications for blocked domains with unblock option
- Configurable AdGuard Home instance settings
- Caches results to minimize API calls
- Error handling with configuration shortcuts
## Installation
1. Install the [Tampermonkey](https://www.tampermonkey.net/) browser extension
2. Click on the Tampermonkey icon and select "Create a new script"
3. Copy the contents of `simpleguardhome-404-checker.user.js` into the editor
4. Save the script (Ctrl+S or File -> Save)
## Configuration
1. Click on the Tampermonkey icon in your browser
2. Select "Configure SimpleGuardHome Instance" under the script's menu
3. Enter your AdGuard Home host (e.g., `http://localhost`)
4. Enter your AdGuard Home port (default: 3000)
### Default Settings
- Host: `http://localhost`
- Port: `3000`
## How It Works
1. The script monitors all web requests on any website
2. When a 404 response is detected:
- Extracts the domain from the failed URL
- Checks if the domain is blocked by AdGuard Home
- Shows a notification if the domain is blocked
- Provides a quick "Unblock" button to open SimpleGuardHome
3. Error handling:
- Connection issues show a notification with configuration options
- Results are cached for 1 hour to reduce API load
- Failed requests provide clear error messages
## Technical Details
### Required Permissions
- `GM_xmlhttpRequest`: For making cross-origin requests to AdGuard Home
- `GM_getValue`/`GM_setValue`: For storing configuration
- `GM_registerMenuCommand`: For adding configuration menu
- `@connect *`: For connecting to custom AdGuard Home instances
### Cache System
- Domain check results are cached for 1 hour
- Cache includes:
- Block status
- Blocking reason
- Applied rules
- Timestamp
### Error Handling
- Connection failures
- Request timeouts
- API errors
- JSON parsing errors
## Development
The userscript is part of the SimpleGuardHome project and is designed to complement the main application by providing real-time feedback during web browsing.
To modify or extend the script:
1. Make changes to `simpleguardhome-404-checker.user.js`
2. Update version number in the metadata block
3. Reinstall in Tampermonkey to test changes
## License
Same as the main SimpleGuardHome project

View File

@@ -1,197 +0,0 @@
// ==UserScript==
// @name SimpleGuardHome 404 Checker
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Detects 404 responses and checks if they are blocked by AdGuard Home
// @author SimpleGuardHome
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect *
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// Default configuration
const DEFAULT_CONFIG = {
host: 'http://localhost',
port: 3000
};
// Get current configuration
function getConfig() {
return {
host: GM_getValue('host', DEFAULT_CONFIG.host),
port: GM_getValue('port', DEFAULT_CONFIG.port)
};
}
// Show configuration dialog
function showConfigDialog() {
const config = getConfig();
const host = prompt('Enter SimpleGuardHome host (e.g. http://localhost):', config.host);
if (host === null) return;
const port = prompt('Enter SimpleGuardHome port:', config.port);
if (port === null) return;
GM_setValue('host', host);
GM_setValue('port', parseInt(port, 10) || DEFAULT_CONFIG.port);
alert('Configuration saved! The new settings will be used for future checks.');
}
// Register configuration menu command
GM_registerMenuCommand('Configure SimpleGuardHome Instance', showConfigDialog);
// Store check results to avoid repeated API calls
const checkedDomains = new Map();
// Intercept 404 responses using a fetch handler
const originalFetch = window.fetch;
window.fetch = async function(...args) {
try {
const response = await originalFetch.apply(this, args);
if (response.status === 404) {
const url = new URL(args[0].toString());
checkDomain(url.hostname);
}
return response;
} catch (error) {
console.error('SimpleGuardHome 404 Checker Error:', error);
return originalFetch.apply(this, args);
}
};
// Also intercept XHR for broader compatibility
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
const originalOnReadyStateChange = this.onreadystatechange;
this.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 404) {
const urlObj = new URL(url, window.location.href);
checkDomain(urlObj.hostname);
}
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
};
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 = `
<div style="font-weight: bold; margin-bottom: 5px;">SimpleGuardHome Error</div>
<div style="font-size: 14px;">${error}</div>
<button style="margin-top: 10px; background: #ffc107; color: black; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">Configure Instance</button>
`;
const configButton = notification.querySelector('button');
configButton.addEventListener('click', () => {
showConfigDialog();
notification.remove();
});
} else {
notification.innerHTML = `
<div style="font-weight: bold; margin-bottom: 5px;">404 Domain is Blocked</div>
<div style="font-size: 14px;"><strong>${domain}</strong></div>
<div style="font-size: 12px; margin-top: 5px;">Reason: ${data.reason}</div>
${data.rules?.length ? `<div style="font-size: 12px; margin-top: 5px; background: rgba(0,0,0,0.05); padding: 5px; border-radius: 3px;">Rule: ${data.rules[0].text}</div>` : ''}
<button style="margin-top: 10px; background: #0d6efd; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">Unblock Domain</button>
`;
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);
}
})();