diff --git a/README.md b/README.md index e5ba805..a7b5893 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SimpleGuardHome -A modern web application for checking and managing domain filtering in AdGuard Home. Built with FastAPI and modern JavaScript. +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. ## Features @@ -11,6 +11,8 @@ A modern web application for checking and managing domain filtering in AdGuard H - 📝 Comprehensive logging - 🏥 Health monitoring endpoint - ⚙️ Environment-based configuration +- 📚 Full OpenAPI/Swagger documentation +- ✅ Implements official AdGuard Home API spec ## Requirements @@ -61,52 +63,79 @@ python -m uvicorn src.simpleguardhome.main:app --reload The application will be available at `http://localhost:8000` -## API Endpoints +## API Documentation -### Web Interface -- `GET /` - Main web interface for domain checking and unblocking +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` ### API Endpoints -- `POST /check-domain` - Check if a domain is blocked - - Parameters: `domain` (form data) - - Returns: Blocking status and rule information -- `POST /unblock-domain` - Add a domain to the allowed list - - Parameters: `domain` (form data) - - Returns: Success/failure status +All endpoints follow the official AdGuard Home API specification: -- `GET /health` - Check application and AdGuard Home connection status - - Returns: Health status of the application and AdGuard Home connection +#### Web Interface +- `GET /` - Main web interface for domain checking and unblocking -## Troubleshooting +#### Filtering Endpoints +- `POST /control/filtering/check_host` - Check if a domain is blocked + - Parameters: `name` (query parameter) + - Returns: Detailed filtering status and rules -### Common Issues +- `POST /control/filtering/whitelist/add` - Add a domain to the allowed list + - Parameters: `name` (JSON body) + - Returns: Success status -1. **Connection Failed** - - Ensure AdGuard Home is running - - Verify the host and port in .env are correct - - Check if AdGuard Home's API is accessible +- `GET /control/filtering/status` - Get current filtering configuration + - Returns: Complete filtering status including rules and filters -2. **Authentication Failed** - - Verify username and password in .env - - Ensure AdGuard Home authentication is enabled/disabled as expected +#### System Status +- `GET /control/status` - Check application and AdGuard Home connection status + - Returns: Health status with filtering state -3. **Domain Check Failed** - - Check AdGuard Home logs for filtering issues - - Verify domain format is correct - - Ensure AdGuard Home filtering is enabled +## Response Models -### Checking System Status +The application uses Pydantic models that match the AdGuard Home API specification: -1. Use the health check endpoint: -```bash -curl http://localhost:8000/health +### FilterStatus +```python +{ + "enabled": bool, + "filters": [ + { + "enabled": bool, + "id": int, + "name": str, + "rules_count": int, + "url": str + } + ], + "user_rules": List[str], + "whitelist_filters": List[Filter] +} ``` -2. Check application logs: -- The application uses structured logging -- Look for ERROR level messages for issues -- Connection problems are logged with detailed error information +### DomainCheckResult +```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 +} +``` + +## 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 @@ -134,6 +163,7 @@ simpleguardhome/ - 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` @@ -145,6 +175,9 @@ simpleguardhome/ - 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 ## License diff --git a/adguard_openapi.yaml b/adguard_openapi.yaml new file mode 100644 index 0000000..cbc07a6 --- /dev/null +++ b/adguard_openapi.yaml @@ -0,0 +1,3150 @@ +'openapi': '3.0.3' +'info': + 'title': 'AdGuard Home' + 'description': > + AdGuard Home REST-ish API. Our admin web interface is built on top of this + REST-ish API. + 'version': '0.107' + 'contact': + 'name': 'AdGuard Home' + 'url': 'https://github.com/AdguardTeam/AdGuardHome' + +'servers': +- 'url': '/control' + +'security': +- 'basicAuth': [] + +'tags': +- 'name': 'clients' + 'description': 'Clients list operations' +- 'name': 'dhcp' + 'description': 'Built-in DHCP server controls' +- 'name': 'filtering' + 'description': 'Rule-based filtering' +- 'name': 'global' + 'description': 'AdGuard Home server general settings and controls' +- 'name': 'i18n' + 'description': 'Application localization' +- 'name': 'install' + 'description': 'First-time install configuration handlers' +- 'name': 'log' + 'description': 'AdGuard Home query log' +- 'name': 'mobileconfig' + 'description': 'Apple .mobileconfig' +- 'name': 'parental' + 'description': 'Blocking adult and explicit materials' +- 'name': 'safebrowsing' + 'description': 'Blocking malware/phishing sites' +- 'name': 'safesearch' + 'description': 'Enforce family-friendly results in search engines' +- 'name': 'stats' + 'description': 'AdGuard Home statistics' +- 'name': 'tls' + 'description': 'AdGuard Home HTTPS/DoH/DoQ/DoT settings' + +'paths': + '/status': + 'get': + 'tags': + - 'global' + 'operationId': 'status' + 'summary': 'Get DNS server current status and general settings' + 'responses': + '200': + 'description': 'OK' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ServerStatus' + '/dns_info': + 'get': + 'tags': + - 'global' + 'operationId': 'dnsInfo' + 'summary': 'Get general DNS parameters' + 'responses': + '200': + 'description': 'OK' + 'content': + 'application/json': + 'schema': + 'allOf': + - '$ref': '#/components/schemas/DNSConfig' + - 'type': 'object' + 'properties': + 'default_local_ptr_upstreams': + 'type': 'array' + 'items': + 'type': 'string' + 'example': + - '192.168.168.192' + - '10.0.0.10' + '/dns_config': + 'post': + 'tags': + - 'global' + 'operationId': 'dnsConfig' + 'summary': 'Set general DNS parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DNSConfig' + 'responses': + '200': + 'description': 'OK' + '/protection': + 'post': + 'tags': + - 'global' + 'operationId': 'setProtection' + 'summary': 'Set protection state and duration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/SetProtectionRequest' + 'responses': + '200': + 'description': 'OK' + '/cache_clear': + 'post': + 'tags': + - 'global' + 'operationId': 'cacheClear' + 'summary': 'Clear DNS cache' + 'responses': + '200': + 'description': 'OK' + '/test_upstream_dns': + 'post': + 'tags': + - 'global' + 'operationId': 'testUpstreamDNS' + 'summary': 'Test upstream configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/UpstreamsConfig' + 'description': 'Upstream configuration to be tested' + 'responses': + '200': + 'description': > + Status of testing each requested server, with "OK" meaning that + server works, any other text means an error. + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/UpstreamsConfigResponse' + 'examples': + 'response': + 'value': + '1.1.1.1': 'OK' + '1.0.0.1': 'OK' + '8.8.8.8': 'OK' + '8.8.4.4': 'OK' + '192.168.1.104:53535': > + upstream "192.168.1.104:1234" fails to exchange: couldn't + communicate with upstream: read udp + 192.168.1.100:60675->8.8.8.8:1234: i/o timeout + '/version.json': + 'post': + 'tags': + - 'global' + 'operationId': 'getVersionJson' + 'summary': > + Gets information about the latest available version of AdGuard + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/GetVersionRequest' + 'required': true + 'responses': + '200': + 'description': > + Version info. If response message is empty, UI does not show + a version update message. + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/VersionInfo' + '500': + 'description': 'Cannot write answer' + '502': + 'description': 'Cannot retrieve the version.json file contents' + '/update': + 'post': + 'tags': + - 'global' + 'operationId': 'beginUpdate' + 'summary': 'Begin auto-upgrade procedure' + 'responses': + '200': + 'description': 'OK.' + '500': + 'description': 'Failed' + '/querylog': + 'get': + 'tags': + - 'log' + 'operationId': 'queryLog' + 'summary': 'Get DNS server query log.' + 'parameters': + - 'name': 'older_than' + 'in': 'query' + 'description': 'Filter by older than' + 'schema': + 'type': 'string' + - 'name': 'offset' + 'in': 'query' + 'description': > + Specify the ranking number of the first item on the page. Even + though it is possible to use "offset" and "older_than", we recommend + choosing one of them and sticking to it. + 'schema': + 'type': 'integer' + - 'name': 'limit' + 'in': 'query' + 'description': 'Limit the number of records to be returned' + 'schema': + 'type': 'integer' + - 'name': 'search' + 'in': 'query' + 'description': 'Filter by domain name or client IP' + 'schema': + 'type': 'string' + - 'name': 'response_status' + 'in': 'query' + 'description': 'Filter by response status' + 'schema': + 'type': 'string' + 'enum': + - 'all' + - 'filtered' + - 'blocked' + - 'blocked_safebrowsing' + - 'blocked_parental' + - 'whitelisted' + - 'rewritten' + - 'safe_search' + - 'processed' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/QueryLog' + '/querylog_info': + 'get': + 'deprecated': true + 'description': | + Deprecated: Use `GET /querylog/config` instead. + + NOTE: If `interval` was configured by editing configuration file or new + HTTP API call `PUT /querylog/config/update` and it's not equal to + previous allowed enum values then it will be equal to `90` days for + compatibility reasons. + 'tags': + - 'log' + 'operationId': 'queryLogInfo' + 'summary': 'Get query log parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/QueryLogConfig' + '/querylog_config': + 'post': + 'deprecated': true + 'description': > + Deprecated: Use `PUT /querylog/config/update` instead. + 'tags': + - 'log' + 'operationId': 'queryLogConfig' + 'summary': 'Set query log parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/QueryLogConfig' + 'responses': + '200': + 'description': 'OK.' + '/querylog_clear': + 'post': + 'tags': + - 'log' + 'operationId': 'querylogClear' + 'summary': 'Clear query log' + 'responses': + '200': + 'description': 'OK.' + '/querylog/config': + 'get': + 'tags': + - 'log' + 'operationId': 'getQueryLogConfig' + 'summary': 'Get query log parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/GetQueryLogConfigResponse' + '/querylog/config/update': + 'put': + 'tags': + - 'log' + 'operationId': 'putQueryLogConfig' + 'summary': 'Set query log parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/PutQueryLogConfigUpdateRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/stats': + 'get': + 'tags': + - 'stats' + 'operationId': 'stats' + 'summary': 'Get DNS server statistics' + 'responses': + '200': + 'description': 'Returns statistics data' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Stats' + '/stats_reset': + 'post': + 'tags': + - 'stats' + 'operationId': 'statsReset' + 'summary': 'Reset all statistics to zeroes' + 'responses': + '200': + 'description': 'OK.' + '/stats_info': + 'get': + 'deprecated': true + 'description': | + Deprecated: Use `GET /stats/config` instead. + + NOTE: If `interval` was configured by editing configuration file or new + HTTP API call `PUT /stats/config/update` and it's not equal to + previous allowed enum values then it will be equal to `90` days for + compatibility reasons. + 'tags': + - 'stats' + 'operationId': 'statsInfo' + 'summary': 'Get statistics parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/StatsConfig' + '/stats_config': + 'post': + 'deprecated': true + 'description': > + Deprecated: Use `PUT /stats/config/update` instead. + 'tags': + - 'stats' + 'operationId': 'statsConfig' + 'summary': 'Set statistics parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/StatsConfig' + 'responses': + '200': + 'description': 'OK.' + '/stats/config': + 'get': + 'tags': + - 'stats' + 'operationId': 'getStatsConfig' + 'summary': 'Get statistics parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/GetStatsConfigResponse' + '/stats/config/update': + 'put': + 'tags': + - 'stats' + 'operationId': 'putStatsConfig' + 'summary': 'Set statistics parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/PutStatsConfigUpdateRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/tls/status': + 'get': + 'tags': + - 'tls' + 'operationId': 'tlsStatus' + 'summary': 'Returns TLS configuration and its status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + '/tls/configure': + 'post': + 'tags': + - 'tls' + 'operationId': 'tlsConfigure' + 'summary': 'Updates current TLS configuration' + 'requestBody': + '$ref': '#/components/requestBodies/TlsConfig' + 'responses': + '200': + 'description': 'TLS configuration and its status' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + '400': + 'description': 'Invalid configuration or unavailable port' + '500': + 'description': 'Error occurred while applying configuration' + '/tls/validate': + 'post': + 'tags': + - 'tls' + 'operationId': 'tlsValidate' + 'summary': 'Checks if the current TLS configuration is valid' + 'requestBody': + '$ref': '#/components/requestBodies/TlsConfig' + 'responses': + '200': + 'description': 'TLS configuration and its status' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + '400': + 'description': 'Invalid configuration or unavailable port' + '/dhcp/status': + 'get': + 'tags': + - 'dhcp' + 'operationId': 'dhcpStatus' + 'summary': 'Gets the current DHCP settings and status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpStatus' + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/interfaces': + 'get': + 'tags': + - 'dhcp' + 'operationId': 'dhcpInterfaces' + 'summary': 'Gets the available interfaces' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/NetInterfaces' + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/set_config': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpSetConfig' + 'summary': 'Updates the current DHCP server configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpConfig' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/find_active_dhcp': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'checkActiveDhcp' + 'summary': 'Searches for an active DHCP server on the network' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpFindActiveReq' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpSearchResult' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/add_static_lease': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpAddStaticLease' + 'summary': 'Adds a static lease' + 'requestBody': + '$ref': '#/components/requestBodies/DhcpStaticLease' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/remove_static_lease': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpRemoveStaticLease' + 'summary': 'Removes a static lease' + 'requestBody': + '$ref': '#/components/requestBodies/DhcpStaticLease' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/update_static_lease': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpUpdateStaticLease' + 'description': > + Updates IP address, hostname of the static lease. IP version must be + the same as previous. + 'summary': 'Updates a static lease' + 'requestBody': + '$ref': '#/components/requestBodies/DhcpStaticLease' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/reset': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpReset' + 'summary': 'Reset DHCP configuration' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/reset_leases': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpResetLeases' + 'summary': 'Reset DHCP leases' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/filtering/status': + 'get': + 'tags': + - 'filtering' + 'operationId': 'filteringStatus' + 'summary': 'Get filtering parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterStatus' + '/filtering/config': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringConfig' + 'summary': 'Set filtering parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterConfig' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/filtering/add_url': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringAddURL' + 'summary': 'Add filter URL or an absolute file path' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AddUrlRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/filtering/remove_url': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringRemoveURL' + 'summary': 'Remove filter URL' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RemoveUrlRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/filtering/set_url': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringSetURL' + 'summary': 'Set URL parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterSetUrl' + 'responses': + '200': + 'description': 'OK.' + '/filtering/refresh': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringRefresh' + 'summary': > + Reload filtering rules from URLs. This might be needed if new URL was + just added and you don't want to wait for automatic refresh to kick in. + This API request is ratelimited, so you can call it freely as often as + you like, it wont create unnecessary burden on servers that host the + URL. This should work as intended, a `force` parameter is offered as + last-resort attempt to make filter lists fresh. If you ever find + yourself using `force` to make something work that otherwise wont, this + is a bug and report it accordingly. + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterRefreshRequest' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterRefreshResponse' + '/filtering/set_rules': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringSetRules' + 'summary': 'Set user-defined filter rules' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/SetRulesRequest' + 'description': 'Custom filtering rules.' + 'responses': + '200': + 'description': 'OK.' + '/filtering/check_host': + 'get': + 'tags': + - 'filtering' + 'operationId': 'filteringCheckHost' + 'summary': 'Check if host name is filtered' + 'parameters': + - 'name': 'name' + 'in': 'query' + 'description': 'Filter by host name' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterCheckHostResponse' + '/safebrowsing/enable': + 'post': + 'tags': + - 'safebrowsing' + 'operationId': 'safebrowsingEnable' + 'summary': 'Enable safebrowsing' + 'responses': + '200': + 'description': 'OK.' + '/safebrowsing/disable': + 'post': + 'tags': + - 'safebrowsing' + 'operationId': 'safebrowsingDisable' + 'summary': 'Disable safebrowsing' + 'responses': + '200': + 'description': 'OK.' + '/safebrowsing/status': + 'get': + 'tags': + - 'safebrowsing' + 'operationId': 'safebrowsingStatus' + 'summary': 'Get safebrowsing status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + 'type': 'object' + 'properties': + 'enabled': + 'type': 'boolean' + 'examples': + 'response': + 'value': + 'enabled': false + '/parental/enable': + 'post': + 'tags': + - 'parental' + 'operationId': 'parentalEnable' + 'summary': 'Enable parental filtering' + 'responses': + '200': + 'description': 'OK.' + '/parental/disable': + 'post': + 'tags': + - 'parental' + 'operationId': 'parentalDisable' + 'summary': 'Disable parental filtering' + 'responses': + '200': + 'description': 'OK.' + '/parental/status': + 'get': + 'tags': + - 'parental' + 'operationId': 'parentalStatus' + 'summary': 'Get parental filtering status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + 'type': 'object' + 'properties': + 'enable': + 'type': 'boolean' + 'sensitivity': + 'type': 'integer' + 'examples': + 'response': + 'value': + 'enabled': true + 'sensitivity': 13 + '/safesearch/enable': + 'post': + 'deprecated': true + 'tags': + - 'safesearch' + 'operationId': 'safesearchEnable' + 'summary': 'Enable safesearch' + 'responses': + '200': + 'description': 'OK.' + '/safesearch/disable': + 'post': + 'deprecated': true + 'tags': + - 'safesearch' + 'operationId': 'safesearchDisable' + 'summary': 'Disable safesearch' + 'responses': + '200': + 'description': 'OK.' + '/safesearch/settings': + 'put': + 'tags': + - 'safesearch' + 'operationId': 'safesearchSettings' + 'summary': 'Update safesearch settings' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/SafeSearchConfig' + 'responses': + '200': + 'description': 'OK.' + '/safesearch/status': + 'get': + 'tags': + - 'safesearch' + 'operationId': 'safesearchStatus' + 'summary': 'Get safesearch status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/SafeSearchConfig' + '/clients': + 'get': + 'tags': + - 'clients' + 'operationId': 'clientsStatus' + 'summary': 'Get information about configured clients' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Clients' + '/clients/add': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsAdd' + 'summary': 'Add a new client' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Client' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/clients/delete': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsDelete' + 'summary': 'Remove a client' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientDelete' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/clients/update': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsUpdate' + 'summary': 'Update client information' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientUpdate' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/clients/find': + 'get': + 'deprecated': true + 'description': > + Deprecated: Use `POST /clients/search` instead. + 'tags': + - 'clients' + 'operationId': 'clientsFind' + 'summary': > + Get information about clients by their IP addresses or ClientIDs. + 'parameters': + - 'name': 'ip0' + 'in': 'query' + 'description': > + Filter by IP address or ClientIDs. Parameters with names `ip1`, + `ip2`, and so on are also accepted and interpreted as "ip0 OR ip1 OR + ip2". + + TODO(a.garipov): Replace with a better query API. + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientsFindResponse' + '/clients/search': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsSearch' + 'summary': > + Get information about clients by their IP addresses, CIDRs, MAC addresses, or ClientIDs. + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientsSearchRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientsFindResponse' + '/access/list': + 'get': + 'operationId': 'accessList' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AccessListResponse' + 'summary': 'List (dis)allowed clients, blocked hosts, etc.' + 'tags': + - 'clients' + '/access/set': + 'post': + 'operationId': 'accessSet' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AccessSetRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '400': + 'description': > + Failed to parse JSON or cannot save the list. + '500': + 'description': 'Internal error.' + 'summary': 'Set (dis)allowed clients, blocked hosts, etc.' + 'tags': + - 'clients' + '/blocked_services/services': + 'get': + 'deprecated': true + 'description': > + Deprecated: Use `GET /blocked_services/all` instead. + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesAvailableServices' + 'summary': 'Get available services to use for blocking' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesArray' + '/blocked_services/all': + 'get': + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesAll' + 'summary': 'Get available services to use for blocking' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesAll' + '/blocked_services/list': + 'get': + 'deprecated': true + 'description': > + Deprecated: Use `GET /blocked_services/get` instead. + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesList' + 'summary': 'Get blocked services list' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesArray' + '/blocked_services/set': + 'post': + 'deprecated': true + 'description': > + Deprecated: Use `PUT /blocked_services/update` instead. + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesSet' + 'summary': 'Set blocked services list' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesArray' + 'responses': + '200': + 'description': 'OK.' + '/blocked_services/get': + 'get': + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesSchedule' + 'summary': 'Get blocked services' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesSchedule' + '/blocked_services/update': + 'put': + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesScheduleUpdate' + 'summary': 'Update blocked services' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesSchedule' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/rewrite/list': + 'get': + 'tags': + - 'rewrite' + 'operationId': 'rewriteList' + 'summary': 'Get list of Rewrite rules' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RewriteList' + '/rewrite/add': + 'post': + 'tags': + - 'rewrite' + 'operationId': 'rewriteAdd' + 'summary': 'Add a new Rewrite rule' + 'requestBody': + '$ref': '#/components/requestBodies/RewriteEntry' + 'responses': + '200': + 'description': 'OK.' + '/rewrite/delete': + 'post': + 'tags': + - 'rewrite' + 'operationId': 'rewriteDelete' + 'summary': 'Remove a Rewrite rule' + 'requestBody': + '$ref': '#/components/requestBodies/RewriteEntry' + 'responses': + '200': + 'description': 'OK.' + '/rewrite/update': + 'put': + 'tags': + - 'rewrite' + 'operationId': 'rewriteUpdate' + 'summary': 'Update a Rewrite rule' + 'requestBody': + '$ref': '#/components/requestBodies/RewriteUpdate' + 'responses': + '200': + 'description': 'OK.' + '/i18n/change_language': + 'post': + 'deprecated': true + 'description': > + Deprecated: Use `PUT /control/profile` instead. + 'tags': + - 'i18n' + 'operationId': 'changeLanguage' + 'summary': > + Change current language. Argument must be an ISO 639-1 two-letter code. + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/LanguageSettings' + 'description': > + New language. It must be known to the server and must be an ISO 639-1 + two-letter code. + 'responses': + '200': + 'description': 'OK.' + '/i18n/current_language': + 'get': + 'deprecated': true + 'description': > + Deprecated: Use `GET /control/profile` instead. + 'tags': + - 'i18n' + 'operationId': 'currentLanguage' + 'summary': > + Get currently set language. Result is ISO 639-1 two-letter code. Empty + result means default language. + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/LanguageSettings' + '/install/get_addresses': + 'get': + 'tags': + - 'install' + 'operationId': 'installGetAddresses' + 'summary': 'Gets the network interfaces information.' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AddressesInfo' + '/install/check_config': + 'post': + 'tags': + - 'install' + 'operationId': 'installCheckConfig' + 'summary': 'Checks configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/CheckConfigRequest' + 'description': 'Configuration to be checked' + 'required': true + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/CheckConfigResponse' + '400': + 'description': > + Failed to parse JSON or cannot listen on the specified address. + '/install/configure': + 'post': + 'tags': + - 'install' + 'operationId': 'installConfigure' + 'summary': 'Applies the initial configuration.' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/InitialConfiguration' + 'description': 'Initial configuration JSON' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '400': + 'description': > + Failed to parse initial configuration or cannot listen to the + specified addresses. + '422': + 'description': > + The specified password does not meet the strength requirements. + '500': + 'description': 'Cannot start the DNS server' + '/login': + 'post': + 'tags': + - 'global' + 'operationId': 'login' + 'summary': 'Perform administrator log-in' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Login' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '400': + 'description': > + Invalid username or password. + '429': + 'description': > + Out of login attempts. + '/logout': + 'get': + 'tags': + - 'global' + 'operationId': 'logout' + 'summary': 'Perform administrator log-out' + 'responses': + '302': + 'description': 'OK.' + '/profile/update': + 'put': + 'tags': + - 'global' + 'operationId': 'updateProfile' + 'summary': 'Updates current user info' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ProfileInfo' + 'responses': + '200': + 'description': 'OK' + '/profile': + 'get': + 'tags': + - 'global' + 'operationId': 'getProfile' + 'summary': '' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ProfileInfo' + + '/apple/doh.mobileconfig': + 'get': + 'operationId': 'mobileConfigDoH' + 'parameters': + - 'description': > + Host for which the config is generated. If no host is provided, + `tls.server_name` from the configuration file is used. If + `tls.server_name` is not set, the API returns an error with a 500 + status. + 'example': 'example.org' + 'in': 'query' + 'name': 'host' + 'required': true + 'schema': + 'type': 'string' + - 'description': > + ClientID. + 'example': 'client-1' + 'in': 'query' + 'name': 'client_id' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'DNS over HTTPS plist file.' + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Server configuration error.' + 'summary': 'Get DNS over HTTPS .mobileconfig.' + 'tags': + - 'mobileconfig' + - 'global' + '/apple/dot.mobileconfig': + 'get': + 'operationId': 'mobileConfigDoT' + 'parameters': + - 'description': > + Host for which the config is generated. If no host is provided, + `tls.server_name` from the configuration file is used. If + `tls.server_name` is not set, the API returns an error with a 500 + status. + 'example': 'example.org' + 'in': 'query' + 'name': 'host' + 'required': true + 'schema': + 'type': 'string' + - 'description': > + ClientID. + 'example': 'client-1' + 'in': 'query' + 'name': 'client_id' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'DNS over TLS plist file' + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Server configuration error.' + 'summary': 'Get DNS over TLS .mobileconfig.' + 'tags': + - 'mobileconfig' + - 'global' + +'components': + 'requestBodies': + 'TlsConfig': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + 'description': 'TLS configuration JSON' + 'required': true + 'DhcpStaticLease': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpStaticLease' + 'required': true + 'RewriteEntry': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RewriteEntry' + 'required': true + 'RewriteUpdate': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RewriteUpdate' + 'required': true + 'schemas': + 'ServerStatus': + 'type': 'object' + 'description': 'AdGuard Home server status and configuration' + 'required': + - 'dns_addresses' + - 'dns_port' + - 'http_port' + - 'protection_enabled' + - 'protection_disabled_until' + - 'running' + - 'version' + - 'language' + 'properties': + 'dns_addresses': + 'example': ['127.0.0.1'] + 'items': + 'type': 'string' + 'type': 'array' + 'dns_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 + 'minimum': 1 + 'maximum': 65535 + 'http_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 80 + 'minimum': 1 + 'maximum': 65535 + 'protection_enabled': + 'type': 'boolean' + 'protection_disabled_duration': + 'type': 'integer' + 'format': 'int64' + 'dhcp_available': + 'type': 'boolean' + 'running': + 'type': 'boolean' + 'version': + 'type': 'string' + 'example': 'v0.123.4' + 'language': + 'type': 'string' + 'example': 'en' + 'DNSConfig': + 'type': 'object' + 'description': 'DNS server configuration' + 'properties': + 'bootstrap_dns': + 'type': 'array' + 'description': > + Bootstrap servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - '8.8.8.8:53' + - '1.1.1.1:53' + 'upstream_dns': + 'type': 'array' + 'description': > + Upstream servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - 'tls://1.1.1.1' + - 'tls://1.0.0.1' + 'fallback_dns': + 'type': 'array' + 'description': > + List of fallback DNS servers used when upstream DNS servers are not + responding. Empty value will clear the list. + 'items': + 'type': 'string' + 'example': + - '8.8.8.8' + - '1.1.1.1:53' + 'upstream_dns_file': + 'type': 'string' + 'protection_enabled': + 'type': 'boolean' + 'ratelimit': + 'type': 'integer' + 'ratelimit_subnet_subnet_len_ipv4': + 'description': 'Length of the subnet mask for IPv4 addresses.' + 'type': 'integer' + 'default': 24 + 'minimum': 0 + 'maximum': 32 + 'ratelimit_subnet_subnet_len_ipv6': + 'description': 'Length of the subnet mask for IPv6 addresses.' + 'type': 'integer' + 'default': 56 + 'minimum': 0 + 'maximum': 128 + 'ratelimit_whitelist': + 'type': 'array' + 'description': 'List of IP addresses excluded from rate limiting.' + 'items': + 'type': 'string' + 'blocking_mode': + 'type': 'string' + 'enum': + - 'default' + - 'refused' + - 'nxdomain' + - 'null_ip' + - 'custom_ip' + 'blocking_ipv4': + 'type': 'string' + 'blocking_ipv6': + 'type': 'string' + 'blocked_response_ttl': + 'type': 'integer' + 'minimum': 0 + 'description': 'TTL for blocked responses.' + 'protection_disabled_until': + 'type': 'string' + 'description': 'Protection is pause until this time. Nullable.' + 'example': '2018-11-26T00:02:41+03:00' + 'edns_cs_enabled': + 'type': 'boolean' + 'edns_cs_use_custom': + 'type': 'boolean' + 'edns_cs_custom_ip': + 'type': 'string' + 'disable_ipv6': + 'type': 'boolean' + 'dnssec_enabled': + 'type': 'boolean' + 'cache_size': + 'type': 'integer' + 'cache_ttl_min': + 'type': 'integer' + 'cache_ttl_max': + 'type': 'integer' + 'cache_optimistic': + 'type': 'boolean' + 'upstream_mode': + 'type': 'string' + 'enum': + - const: '' + deprecated: true + description: Use `load_balance` instead. + - const: 'fastest_addr' + - const: 'load_balance' + - const: 'parallel' + 'description': Upstream modes enumeration. + 'use_private_ptr_resolvers': + 'type': 'boolean' + 'resolve_clients': + 'type': 'boolean' + 'local_ptr_upstreams': + 'type': 'array' + 'description': > + Upstream servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - 'tls://1.1.1.1' + - 'tls://1.0.0.1' + 'UpstreamsConfig': + 'type': 'object' + 'description': 'Upstream configuration to be tested' + 'required': + - 'bootstrap_dns' + - 'upstream_dns' + 'properties': + 'bootstrap_dns': + 'type': 'array' + 'description': > + Bootstrap DNS servers, port is optional after colon. + 'items': + 'type': 'string' + 'example': + - '8.8.8.8:53' + - '1.1.1.1:53' + 'upstream_dns': + 'type': 'array' + 'description': > + Upstream DNS servers, port is optional after colon. + 'items': + 'type': 'string' + 'example': + - 'tls://1.1.1.1' + - 'tls://1.0.0.1' + 'fallback_dns': + 'type': 'array' + 'description': > + Fallback DNS servers, port is optional after colon. + 'items': + 'type': 'string' + 'example': + - '8.8.8.8' + - '1.1.1.1:53' + 'private_upstream': + 'type': 'array' + 'description': > + Local PTR resolvers, port is optional after colon. + 'items': + 'type': 'string' + 'example': + - 'tls://1.1.1.1' + - 'tls://1.0.0.1' + 'UpstreamsConfigResponse': + 'type': 'object' + 'description': 'Upstreams configuration response' + 'additionalProperties': + 'type': 'string' + 'Filter': + 'type': 'object' + 'description': 'Filter subscription info' + 'required': + - 'enabled' + - 'id' + - 'name' + - 'rules_count' + - 'url' + 'properties': + 'enabled': + 'type': 'boolean' + 'id': + 'example': 1234 + 'format': 'int64' + 'type': 'integer' + 'last_updated': + 'example': '2018-10-30T12:18:57+03:00' + 'format': 'date-time' + 'type': 'string' + 'name': + 'example': 'AdGuard Simplified Domain Names filter' + 'type': 'string' + 'rules_count': + 'example': 5912 + 'format': 'uint32' + 'type': 'integer' + 'url': + 'type': 'string' + 'example': > + https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt + 'FilterStatus': + 'type': 'object' + 'description': 'Filtering settings' + 'properties': + 'enabled': + 'type': 'boolean' + 'interval': + 'type': 'integer' + 'filters': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/Filter' + 'whitelist_filters': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/Filter' + 'user_rules': + 'type': 'array' + 'items': + 'type': 'string' + 'FilterConfig': + 'type': 'object' + 'description': 'Filtering settings' + 'properties': + 'enabled': + 'type': 'boolean' + 'interval': + 'type': 'integer' + 'FilterSetUrl': + 'type': 'object' + 'description': 'Filtering URL settings' + 'properties': + 'data': + '$ref': '#/components/schemas/FilterSetUrlData' + 'url': + 'type': 'string' + 'whitelist': + 'type': 'boolean' + 'FilterSetUrlData': + 'type': 'object' + 'description': 'Filter update data' + 'required': + - 'enabled' + - 'name' + - 'url' + 'properties': + 'enabled': + 'type': 'boolean' + 'name': + 'example': 'AdGuard Simplified Domain Names filter' + 'type': 'string' + 'url': + 'type': 'string' + 'example': > + https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt + 'FilterRefreshRequest': + 'type': 'object' + 'description': 'Refresh Filters request data' + 'properties': + 'whitelist': + 'type': 'boolean' + 'FilterCheckHostResponse': + 'type': 'object' + 'description': 'Check Host Result' + 'properties': + 'reason': + 'type': 'string' + 'description': 'Request filtering status.' + 'enum': + - 'NotFilteredNotFound' + - 'NotFilteredWhiteList' + - 'NotFilteredError' + - 'FilteredBlackList' + - 'FilteredSafeBrowsing' + - 'FilteredParental' + - 'FilteredInvalid' + - 'FilteredSafeSearch' + - 'FilteredBlockedService' + - 'Rewrite' + - 'RewriteEtcHosts' + - 'RewriteRule' + 'filter_id': + 'deprecated': true + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. + + Deprecated: use `rules[*].filter_list_id` instead. + 'type': 'integer' + 'rule': + 'deprecated': true + 'type': 'string' + 'example': '||example.org^' + 'description': > + Filtering rule applied to the request (if any). + + Deprecated: use `rules[*].text` instead. + 'rules': + 'description': 'Applied rules.' + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ResultRule' + 'service_name': + 'type': 'string' + 'description': 'Set if reason=FilteredBlockedService' + 'cname': + 'type': 'string' + 'description': 'Set if reason=Rewrite' + 'ip_addrs': + 'type': 'array' + 'items': + 'type': 'string' + 'description': 'Set if reason=Rewrite' + 'FilterRefreshResponse': + 'type': 'object' + 'description': '/filtering/refresh response data' + 'properties': + 'updated': + 'type': 'integer' + 'SetRulesRequest': + 'description': 'Custom filtering rules setting request.' + 'example': + 'rules': + - '||example.com^' + - '# comment' + - '@@||www.example.com^' + 'properties': + 'rules': + 'items': + 'type': 'string' + 'type': 'array' + 'type': 'object' + 'GetVersionRequest': + 'type': 'object' + 'description': '/version.json request data' + 'properties': + 'recheck_now': + 'description': > + If false, server will check for a new version data only once in + several hours. + 'type': 'boolean' + 'VersionInfo': + 'type': 'object' + 'description': > + Information about the latest available version of AdGuard Home. + 'required': + - 'disabled' + 'properties': + 'disabled': + 'type': 'boolean' + 'description': > + If true then other fields doesn't appear. + 'new_version': + 'type': 'string' + 'example': 'v0.9' + 'announcement': + 'type': 'string' + 'example': 'AdGuard Home v0.9 is now available!' + 'announcement_url': + 'type': 'string' + 'example': > + https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9 + 'can_autoupdate': + 'type': 'boolean' + 'Stats': + 'type': 'object' + 'description': 'Server statistics data' + 'properties': + 'time_units': + 'type': 'string' + 'enum': + - 'hours' + - 'days' + 'description': 'Time units' + 'example': 'hours' + 'num_dns_queries': + 'type': 'integer' + 'description': 'Total number of DNS queries' + 'example': 123 + 'num_blocked_filtering': + 'type': 'integer' + 'description': 'Number of requests blocked by filtering rules' + 'example': 50 + 'num_replaced_safebrowsing': + 'type': 'integer' + 'description': 'Number of requests blocked by safebrowsing module' + 'example': 5 + 'num_replaced_safesearch': + 'type': 'integer' + 'description': 'Number of requests blocked by safesearch module' + 'example': 5 + 'num_replaced_parental': + 'type': 'integer' + 'description': 'Number of blocked adult websites' + 'example': 15 + 'avg_processing_time': + 'type': 'number' + 'format': 'float' + 'description': 'Average time in seconds on processing a DNS request' + 'example': 0.34 + 'top_queried_domains': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'top_clients': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'top_blocked_domains': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'top_upstreams_responses': + 'type': 'array' + 'description': 'Total number of responses from each upstream.' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'maxItems': 100 + 'top_upstreams_avg_time': + 'type': 'array' + 'description': > + Average processing time in seconds of requests from each upstream. + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'maxItems': 100 + 'dns_queries': + 'type': 'array' + 'items': + 'type': 'integer' + 'blocked_filtering': + 'type': 'array' + 'items': + 'type': 'integer' + 'replaced_safebrowsing': + 'type': 'array' + 'items': + 'type': 'integer' + 'replaced_parental': + 'type': 'array' + 'items': + 'type': 'integer' + 'TopArrayEntry': + 'type': 'object' + 'description': > + Represent the number of hits or time duration per key (url, domain, or + client IP). + 'properties': + 'domain_or_ip': + 'type': 'number' + 'additionalProperties': + 'type': 'number' + 'StatsConfig': + 'type': 'object' + 'description': 'Statistics configuration' + 'properties': + 'interval': + 'description': > + Time period to keep the data. `0` means that the statistics is + disabled. + 'enum': + - 0 + - 1 + - 7 + - 30 + - 90 + 'type': 'integer' + 'GetStatsConfigResponse': + 'type': 'object' + 'description': 'Statistics configuration' + 'required': + - 'enabled' + - 'interval' + - 'ignored' + 'properties': + 'enabled': + 'description': 'Are statistics enabled' + 'type': 'boolean' + 'interval': + 'description': 'Statistics rotation interval in milliseconds' + 'type': 'number' + 'ignored': + 'description': 'List of host names, which should not be counted' + 'type': 'array' + 'items': + 'type': 'string' + 'PutStatsConfigUpdateRequest': + '$ref': '#/components/schemas/GetStatsConfigResponse' + 'DhcpConfig': + 'type': 'object' + 'properties': + 'enabled': + 'type': 'boolean' + 'interface_name': + 'type': 'string' + 'v4': + '$ref': '#/components/schemas/DhcpConfigV4' + 'v6': + '$ref': '#/components/schemas/DhcpConfigV6' + 'DhcpConfigV4': + 'type': 'object' + 'properties': + 'gateway_ip': + 'type': 'string' + 'example': '192.168.1.1' + 'subnet_mask': + 'type': 'string' + 'example': '255.255.255.0' + 'range_start': + 'type': 'string' + 'example': '192.168.1.2' + 'range_end': + 'type': 'string' + 'example': '192.168.10.50' + 'lease_duration': + 'type': 'integer' + 'DhcpConfigV6': + 'type': 'object' + 'properties': + 'range_start': + 'type': 'string' + 'lease_duration': + 'type': 'integer' + 'DhcpLease': + 'type': 'object' + 'description': 'DHCP lease information' + 'required': + - 'mac' + - 'ip' + - 'hostname' + - 'expires' + 'properties': + 'mac': + 'type': 'string' + 'example': '00:11:09:b3:b3:b8' + 'ip': + 'type': 'string' + 'example': '192.168.1.22' + 'hostname': + 'type': 'string' + 'example': 'dell' + 'expires': + 'type': 'string' + 'example': '2017-07-21T17:32:28Z' + 'DhcpStaticLease': + 'type': 'object' + 'description': 'DHCP static lease information' + 'required': + - 'mac' + - 'ip' + - 'hostname' + 'properties': + 'mac': + 'type': 'string' + 'example': '00:11:09:b3:b3:b8' + 'ip': + 'type': 'string' + 'example': '192.168.1.22' + 'hostname': + 'type': 'string' + 'example': 'dell' + 'DhcpStatus': + 'type': 'object' + 'description': 'Built-in DHCP server configuration and status' + 'required': + - 'config' + - 'leases' + 'properties': + 'enabled': + 'type': 'boolean' + 'interface_name': + 'type': 'string' + 'v4': + '$ref': '#/components/schemas/DhcpConfigV4' + 'v6': + '$ref': '#/components/schemas/DhcpConfigV6' + 'leases': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/DhcpLease' + 'static_leases': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/DhcpStaticLease' + 'NetInterfaces': + 'type': 'object' + 'description': > + Network interfaces dictionary, keys are interface names. + 'additionalProperties': + '$ref': '#/components/schemas/NetInterface' + + 'DhcpFindActiveReq': + 'description': > + Request for checking for other DHCP servers in the network. + 'properties': + 'interface': + 'description': 'The name of the network interface' + 'example': 'eth0' + 'type': 'string' + 'type': 'object' + + 'DhcpSearchResult': + 'type': 'object' + 'description': > + Information about a DHCP server discovered in the current network. + 'properties': + 'v4': + '$ref': '#/components/schemas/DhcpSearchV4' + 'v6': + '$ref': '#/components/schemas/DhcpSearchV6' + + 'DhcpSearchV4': + 'type': 'object' + 'properties': + 'other_server': + '$ref': '#/components/schemas/DhcpSearchResultOtherServer' + 'static_ip': + '$ref': '#/components/schemas/DhcpSearchResultStaticIP' + + 'DhcpSearchV6': + 'type': 'object' + 'properties': + 'other_server': + '$ref': '#/components/schemas/DhcpSearchResultOtherServer' + + 'DhcpSearchResultOtherServer': + 'type': 'object' + 'properties': + 'found': + 'type': 'string' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': > + The result of searching the other DHCP server. + 'example': 'no' + 'error': + 'type': 'string' + 'description': 'Set if found=error' + 'example': '' + + 'DhcpSearchResultStaticIP': + 'type': 'object' + 'properties': + 'static': + 'type': 'string' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': > + The result of determining static IP address. + 'example': 'yes' + 'ip': + 'type': 'string' + 'description': 'Set if static=no' + 'example': '' + + 'DnsAnswer': + 'type': 'object' + 'description': 'DNS answer section' + 'properties': + 'ttl': + 'example': 55 + 'format': 'uint32' + 'type': 'integer' + 'type': + 'type': 'string' + 'example': 'A' + 'value': + 'type': 'string' + 'example': '217.69.139.201' + 'DnsQuestion': + 'type': 'object' + 'description': 'DNS question section' + 'properties': + 'class': + 'type': 'string' + 'example': 'IN' + 'name': + 'type': 'string' + 'example': 'xn--d1abbgf6aiiy.xn--p1ai' + 'unicode_name': + 'type': 'string' + 'example': 'президент.рф' + 'type': + 'type': 'string' + 'example': 'A' + 'AddUrlRequest': + 'type': 'object' + 'description': '/add_url request data' + 'properties': + 'name': + 'type': 'string' + 'url': + 'description': > + URL or an absolute path to the file containing filtering rules. + 'type': 'string' + 'example': 'https://filters.adtidy.org/windows/filters/15.txt' + 'whitelist': + 'type': 'boolean' + 'RemoveUrlRequest': + 'type': 'object' + 'description': '/remove_url request data' + 'properties': + 'url': + 'description': 'Previously added URL containing filtering rules' + 'type': 'string' + 'example': 'https://filters.adtidy.org/windows/filters/15.txt' + 'whitelist': + 'type': 'boolean' + 'QueryLogItem': + 'type': 'object' + 'description': 'Query log item' + 'properties': + 'answer': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/DnsAnswer' + 'original_answer': + 'type': 'array' + 'description': 'Answer from upstream server (optional)' + 'items': + '$ref': '#/components/schemas/DnsAnswer' + 'cached': + 'type': 'boolean' + 'description': > + Defines if the response has been served from cache. + 'upstream': + 'type': 'string' + 'description': > + Upstream URL starting with tcp://, tls://, https://, or with an IP + address. + 'answer_dnssec': + 'description': > + If true, the response had the Authenticated Data (AD) flag set. + 'type': 'boolean' + 'client': + 'description': > + The client's IP address. + 'example': '192.168.0.1' + 'type': 'string' + 'client_id': + 'description': > + The ClientID, if provided in DoH, DoQ, or DoT. + 'example': 'cli123' + 'type': 'string' + 'client_info': + '$ref': '#/components/schemas/QueryLogItemClient' + 'client_proto': + 'enum': + - 'dot' + - 'doh' + - 'doq' + - 'dnscrypt' + - '' + 'ecs': + 'type': 'string' + 'example': '192.168.0.0/16' + 'description': > + The IP network defined by an EDNS Client-Subnet option in the + request message if any. + 'elapsedMs': + 'type': 'string' + 'example': '54.023928' + 'question': + '$ref': '#/components/schemas/DnsQuestion' + 'filterId': + 'deprecated': true + 'type': 'integer' + 'example': 123123 + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. + + Deprecated: use `rules[*].filter_list_id` instead. + 'rule': + 'deprecated': true + 'type': 'string' + 'example': '||example.org^' + 'description': > + Filtering rule applied to the request (if any). + + Deprecated: use `rules[*].text` instead. + 'rules': + 'description': 'Applied rules.' + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ResultRule' + 'reason': + 'type': 'string' + 'description': 'Request filtering status.' + 'enum': + - 'NotFilteredNotFound' + - 'NotFilteredWhiteList' + - 'NotFilteredError' + - 'FilteredBlackList' + - 'FilteredSafeBrowsing' + - 'FilteredParental' + - 'FilteredInvalid' + - 'FilteredSafeSearch' + - 'FilteredBlockedService' + - 'Rewrite' + - 'RewriteEtcHosts' + - 'RewriteRule' + 'service_name': + 'type': 'string' + 'description': 'Set if reason=FilteredBlockedService' + 'status': + 'type': 'string' + 'description': 'DNS response status' + 'example': 'NOERROR' + 'time': + 'type': 'string' + 'description': 'DNS request processing start time' + 'example': '2018-11-26T00:02:41+03:00' + 'QueryLogItemClient': + 'description': > + Client information for a query log item. + 'properties': + 'disallowed': + 'type': 'boolean' + 'description': > + Whether the client's IP is blocked or not. + 'disallowed_rule': + 'type': 'string' + 'description': > + The rule due to which the client is allowed or blocked. + 'name': + 'description': > + Persistent client's name or runtime client's hostname. May be + empty. + 'type': 'string' + 'whois': + '$ref': '#/components/schemas/QueryLogItemClientWhois' + 'required': + - 'disallowed' + - 'disallowed_rule' + - 'name' + - 'whois' + 'type': 'object' + 'QueryLogItemClientWhois': + 'description': > + Client WHOIS information, if any. + 'properties': + 'city': + 'description': > + City, if any. + 'type': 'string' + 'country': + 'description': > + Country, if any. + 'type': 'string' + 'orgname': + 'description': > + Organization name, if any. + 'type': 'string' + 'type': 'object' + 'QueryLog': + 'type': 'object' + 'description': 'Query log' + 'properties': + 'oldest': + 'type': 'string' + 'example': '2018-11-26T00:02:41+03:00' + 'data': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/QueryLogItem' + 'QueryLogConfig': + 'type': 'object' + 'description': 'Query log configuration' + 'properties': + 'enabled': + 'type': 'boolean' + 'description': 'Is query log enabled' + 'interval': + 'description': > + Time period for query log rotation. + 'type': 'number' + 'enum': + - 0.25 + - 1 + - 7 + - 30 + - 90 + 'anonymize_client_ip': + 'type': 'boolean' + 'description': "Anonymize clients' IP addresses" + 'GetQueryLogConfigResponse': + 'type': 'object' + 'description': 'Query log configuration' + 'required': + - 'enabled' + - 'interval' + - 'anonymize_client_ip' + - 'ignored' + 'properties': + 'enabled': + 'type': 'boolean' + 'description': 'Is query log enabled' + 'interval': + 'description': > + Time period for query log rotation in milliseconds. + 'type': 'number' + 'anonymize_client_ip': + 'type': 'boolean' + 'description': "Anonymize clients' IP addresses" + 'ignored': + 'description': 'List of host names, which should not be written to log' + 'type': 'array' + 'items': + 'type': 'string' + 'PutQueryLogConfigUpdateRequest': + '$ref': '#/components/schemas/GetQueryLogConfigResponse' + 'ResultRule': + 'description': 'Applied rule.' + 'properties': + 'filter_list_id': + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. + 'example': 123123 + 'format': 'int64' + 'type': 'integer' + 'text': + 'description': > + The text of the filtering rule applied to the request (if any). + 'example': '||example.org^' + 'type': 'string' + 'type': 'object' + 'TlsConfig': + 'type': 'object' + 'description': 'TLS configuration settings and status' + 'properties': + 'enabled': + 'type': 'boolean' + 'example': true + 'description': 'enabled is the encryption (DoT/DoH/HTTPS) status' + 'server_name': + 'type': 'string' + 'example': 'example.org' + 'description': 'server_name is the hostname of your HTTPS/TLS server' + 'force_https': + 'type': 'boolean' + 'example': true + 'description': 'if true, forces HTTP->HTTPS redirect' + 'port_https': + 'type': 'integer' + 'format': 'uint16' + 'example': 443 + 'description': 'HTTPS port. If 0, HTTPS will be disabled.' + 'port_dns_over_tls': + 'type': 'integer' + 'format': 'uint16' + 'example': 853 + 'description': 'DNS-over-TLS port. If 0, DoT will be disabled.' + 'port_dns_over_quic': + 'type': 'integer' + 'format': 'uint16' + 'example': 784 + 'description': 'DNS-over-QUIC port. If 0, DoQ will be disabled.' + 'certificate_chain': + 'type': 'string' + 'description': 'Base64 string with PEM-encoded certificates chain' + 'private_key': + 'type': 'string' + 'description': 'Base64 string with PEM-encoded private key' + 'private_key_saved': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if the user has previously saved a private key as + a string. This is used so that the server and the client don't + have to send the private key between each other every time, + which might lead to security issues. + 'certificate_path': + 'type': 'string' + 'description': 'Path to certificate file' + 'private_key_path': + 'type': 'string' + 'description': 'Path to private key file' + 'valid_cert': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if the specified certificates chain is a valid chain of + X509 certificates. + 'valid_chain': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if the specified certificates chain is verified and + issued by a known CA. + 'subject': + 'type': 'string' + 'example': 'CN=example.org' + 'description': 'The subject of the first certificate in the chain.' + 'issuer': + 'type': 'string' + 'example': "CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US" + 'description': 'The issuer of the first certificate in the chain.' + 'not_before': + 'type': 'string' + 'example': '2019-01-31T10:47:32Z' + 'description': > + The NotBefore field of the first certificate in the chain. + 'not_after': + 'type': 'string' + 'example': '2019-05-01T10:47:32Z' + 'description': > + The NotAfter field of the first certificate in the chain. + 'dns_names': + 'type': 'array' + 'items': + 'type': 'string' + 'description': > + The value of SubjectAltNames field of the first certificate in the + chain. + 'example': + - '*.example.org' + 'valid_key': + 'type': 'boolean' + 'example': true + 'description': 'Set to true if the key is a valid private key.' + 'key_type': + 'type': 'string' + 'example': 'RSA' + 'enum': + - 'RSA' + - 'ECDSA' + 'description': 'Key type.' + 'warning_validation': + 'type': 'string' + 'example': 'You have specified an empty certificate' + 'description': > + A validation warning message with the issue description. + 'valid_pair': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if both certificate and private key are correct. + 'serve_plain_dns': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if plain DNS is allowed for incoming requests. + 'NetInterface': + 'type': 'object' + 'description': 'Network interface info' + 'required': + - 'flags' + - 'gateway_ip' + - 'hardware_address' + - 'ipv4_addresses' + - 'ipv6_addresses' + - 'name' + 'properties': + 'flags': + 'type': 'string' + 'description': > + Flags could be any combination of the following values, divided by + the "|" character: "up", "broadcast", "loopback", "pointtopoint" and + "multicast". + 'example': 'up|broadcast|multicast' + 'gateway_ip': + 'type': 'string' + 'description': 'The IP address of the gateway.' + 'example': '192.0.2.0' + 'hardware_address': + 'type': 'string' + 'example': '52:54:00:11:09:ba' + 'ipv4_addresses': + 'type': 'array' + 'description': > + The addresses of the interface of v4 family. + 'items': + 'type': 'string' + 'ipv6_addresses': + 'type': 'array' + 'description': > + The addresses of the interface of v6 family. + 'items': + 'type': 'string' + 'name': + 'type': 'string' + 'example': 'eth0' + 'AddressInfo': + 'type': 'object' + 'description': 'Port information' + 'required': + - 'ip' + - 'port' + 'properties': + 'ip': + 'type': 'string' + 'example': '127.0.0.1' + 'port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 + 'AddressesInfo': + 'type': 'object' + 'description': 'AdGuard Home addresses configuration' + 'required': + - 'dns_port' + - 'interfaces' + - 'version' + - 'web_port' + 'properties': + 'dns_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 + 'interfaces': + '$ref': '#/components/schemas/NetInterfaces' + 'version': + 'type': 'string' + 'example': 'v0.123.4' + 'web_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 80 + 'SetProtectionRequest': + 'type': 'object' + 'description': 'Protection state configuration' + 'properties': + 'enabled': + 'type': 'boolean' + 'duration': + 'type': 'integer' + 'format': 'uint64' + 'description': 'Duration of a pause, in milliseconds. Enabled should be false.' + 'required': + - 'enabled' + 'ProfileInfo': + 'type': 'object' + 'description': 'Information about the current user' + 'properties': + 'name': + 'type': 'string' + 'language': + 'type': 'string' + 'theme': + 'type': 'string' + 'description': 'Interface theme' + 'enum': + - 'auto' + - 'dark' + - 'light' + 'required': + - 'name' + - 'language' + - 'theme' + 'SafeSearchConfig': + 'type': 'object' + 'description': 'Safe search settings.' + 'properties': + 'enabled': + 'type': 'boolean' + 'bing': + 'type': 'boolean' + 'duckduckgo': + 'type': 'boolean' + 'ecosia': + 'type': 'boolean' + 'google': + 'type': 'boolean' + 'pixabay': + 'type': 'boolean' + 'yandex': + 'type': 'boolean' + 'youtube': + 'type': 'boolean' + 'Schedule': + 'type': 'object' + 'description': > + Sets periods of inactivity for filtering blocked services. The + schedule contains 7 days (Sunday to Saturday) and a time zone. + 'properties': + 'time_zone': + 'description': > + Time zone name according to IANA time zone database. For example + `Europe/Brussels`. `Local` represents the system's local time + zone. + 'type': 'string' + 'sun': + '$ref': '#/components/schemas/DayRange' + 'mon': + '$ref': '#/components/schemas/DayRange' + 'tue': + '$ref': '#/components/schemas/DayRange' + 'wed': + '$ref': '#/components/schemas/DayRange' + 'thu': + '$ref': '#/components/schemas/DayRange' + 'fri': + '$ref': '#/components/schemas/DayRange' + 'sat': + '$ref': '#/components/schemas/DayRange' + 'DayRange': + 'type': 'object' + 'description': > + The single interval within a day. It begins at the `start` and ends + before the `end`. + 'properties': + 'start': + 'type': 'number' + 'description': > + The number of milliseconds elapsed from the start of a day. It + must be less than `end` and is expected to be rounded to minutes. + So the maximum value is `86340000` (23 hours and 59 minutes). + 'minimum': 0 + 'maximum': 86340000 + 'end': + 'type': 'number' + 'description': > + The number of milliseconds elapsed from the start of a day. It is + expected to be rounded to minutes. The maximum value is `86400000` + (24 hours). + 'minimum': 0 + 'maximum': 86400000 + 'Client': + 'type': 'object' + 'description': 'Client information.' + 'properties': + 'name': + 'type': 'string' + 'description': 'Name' + 'example': 'localhost' + 'ids': + 'type': 'array' + 'description': 'IP, CIDR, MAC, or ClientID.' + 'items': + 'type': 'string' + 'use_global_settings': + 'type': 'boolean' + 'filtering_enabled': + 'type': 'boolean' + 'parental_enabled': + 'type': 'boolean' + 'safebrowsing_enabled': + 'type': 'boolean' + 'safesearch_enabled': + 'deprecated': true + 'type': 'boolean' + 'safe_search': + '$ref': '#/components/schemas/SafeSearchConfig' + 'use_global_blocked_services': + 'type': 'boolean' + 'blocked_services_schedule': + '$ref': '#/components/schemas/Schedule' + 'blocked_services': + 'type': 'array' + 'items': + 'type': 'string' + 'upstreams': + 'type': 'array' + 'items': + 'type': 'string' + 'tags': + 'items': + 'type': 'string' + 'type': 'array' + 'ignore_querylog': + 'description': | + NOTE: If `ignore_querylog` is not set in HTTP API `GET /clients/add` + request then default value (false) will be used. + + If `ignore_querylog` is not set in HTTP API `GET /clients/update` + request then the existing value will not be changed. + + This behaviour can be changed in the future versions. + 'type': 'boolean' + 'ignore_statistics': + 'description': | + NOTE: If `ignore_statistics` is not set in HTTP API `GET + /clients/add` request then default value (false) will be used. + + If `ignore_statistics` is not set in HTTP API `GET /clients/update` + request then the existing value will not be changed. + + This behaviour can be changed in the future versions. + 'type': 'boolean' + 'upstreams_cache_enabled': + 'description': | + NOTE: If `upstreams_cache_enabled` is not set in HTTP API + `GET /clients/add` request then default value (false) will be used. + + If `upstreams_cache_enabled` is not set in HTTP API + `GET /clients/update` request then the existing value will not be + changed. + + This behaviour can be changed in the future versions. + 'type': 'boolean' + 'upstreams_cache_size': + 'description': | + NOTE: If `upstreams_cache_enabled` is not set in HTTP API + `GET /clients/update` request then the existing value will not be + changed. + + This behaviour can be changed in the future versions. + 'type': 'integer' + 'ClientAuto': + 'type': 'object' + 'description': 'Auto-Client information' + 'properties': + 'ip': + 'type': 'string' + 'description': 'IP address' + 'example': '127.0.0.1' + 'name': + 'type': 'string' + 'description': 'Name' + 'example': 'localhost' + 'source': + 'type': 'string' + 'description': 'The source of this information' + 'example': 'etc/hosts' + 'whois_info': + '$ref': '#/components/schemas/WhoisInfo' + 'ClientUpdate': + 'type': 'object' + 'description': 'Client update request' + 'properties': + 'name': + 'type': 'string' + 'data': + '$ref': '#/components/schemas/Client' + 'ClientDelete': + 'type': 'object' + 'description': 'Client delete request' + 'properties': + 'name': + 'type': 'string' + 'ClientsSearchRequest': + 'type': 'object' + 'description': 'Client search request' + 'properties': + 'clients': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ClientsSearchRequestItem' + 'ClientsSearchRequestItem': + 'type': 'object' + 'properties': + 'id': + 'type': 'string' + 'description': 'Client IP address, CIDR, MAC address, or ClientID' + 'ClientsFindResponse': + 'type': 'array' + 'description': 'Client search results.' + 'items': + '$ref': '#/components/schemas/ClientsFindEntry' + 'example': + - 'cli42': + 'name': 'Client 42' + 'ids': ['cli42'] + 'use_global_settings': true + 'filtering_enabled': true + 'parental_enabled': true + 'safebrowsing_enabled': true + 'safesearch_enabled': true + 'safe_search': {} + 'use_global_blocked_services': true + 'blocked_services': null + 'upstreams': null + 'whois_info': {} + 'disallowed': false + 'disallowed_rule': '' + 'ignore_querylog': false + 'ignore_statistics': false + - '1.2.3.4': + 'name': 'Client 1-2-3-4' + 'ids': ['1.2.3.4'] + 'use_global_settings': true + 'filtering_enabled': true + 'parental_enabled': true + 'safebrowsing_enabled': true + 'safesearch_enabled': true + 'safe_search': {} + 'use_global_blocked_services': true + 'blocked_services': null + 'upstreams': null + 'whois_info': {} + 'disallowed': false + 'disallowed_rule': '' + 'ignore_querylog': false + 'ignore_statistics': false + 'AccessListResponse': + '$ref': '#/components/schemas/AccessList' + 'AccessSetRequest': + '$ref': '#/components/schemas/AccessList' + 'AccessList': + 'description': > + Client and host access list. Each of the lists should contain only + unique elements. In addition, allowed and disallowed lists cannot + contain the same elements. + 'properties': + 'allowed_clients': + 'description': > + The allowlist of clients: IP addresses, CIDRs, or ClientIDs. + 'items': + 'type': 'string' + 'type': 'array' + 'disallowed_clients': + 'description': > + The blocklist of clients: IP addresses, CIDRs, or ClientIDs. + 'items': + 'type': 'string' + 'type': 'array' + 'blocked_hosts': + 'description': 'The blocklist of hosts.' + 'items': + 'type': 'string' + 'type': 'array' + 'type': 'object' + 'ClientsFindEntry': + 'type': 'object' + 'additionalProperties': + '$ref': '#/components/schemas/ClientFindSubEntry' + 'ClientFindSubEntry': + 'type': 'object' + 'description': 'Client information.' + 'properties': + 'name': + 'type': 'string' + 'description': 'Name' + 'example': 'localhost' + 'ids': + 'type': 'array' + 'description': 'IP, CIDR, MAC, or ClientID.' + 'items': + 'type': 'string' + 'use_global_settings': + 'type': 'boolean' + 'filtering_enabled': + 'type': 'boolean' + 'parental_enabled': + 'type': 'boolean' + 'safebrowsing_enabled': + 'type': 'boolean' + 'safesearch_enabled': + 'deprecated': true + 'type': 'boolean' + 'safe_search': + '$ref': '#/components/schemas/SafeSearchConfig' + 'use_global_blocked_services': + 'type': 'boolean' + 'blocked_services': + 'type': 'array' + 'items': + 'type': 'string' + 'upstreams': + 'type': 'array' + 'items': + 'type': 'string' + 'whois_info': + '$ref': '#/components/schemas/WhoisInfo' + 'disallowed': + 'type': 'boolean' + 'description': > + Whether the client's IP is blocked or not. + 'disallowed_rule': + 'type': 'string' + 'description': > + The rule due to which the client is disallowed. If disallowed is + set to true, and this string is empty, then the client IP is + disallowed by the "allowed IP list", that is it is not included in + the allowed list. + 'ignore_querylog': + 'type': 'boolean' + 'ignore_statistics': + 'type': 'boolean' + 'WhoisInfo': + 'type': 'object' + 'additionalProperties': + 'type': 'string' + + 'Clients': + 'type': 'object' + 'properties': + 'clients': + '$ref': '#/components/schemas/ClientsArray' + 'auto_clients': + '$ref': '#/components/schemas/ClientsAutoArray' + 'supported_tags': + 'items': + 'type': 'string' + 'type': 'array' + 'ClientsArray': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/Client' + 'description': 'Clients array' + 'ClientsAutoArray': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ClientAuto' + 'description': 'Auto-Clients array' + 'RewriteList': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/RewriteEntry' + 'description': 'Rewrite rules array' + 'RewriteUpdate': + 'type': 'object' + 'description': 'Rewrite rule update object' + 'properties': + 'target': + '$ref': '#/components/schemas/RewriteEntry' + 'update': + '$ref': '#/components/schemas/RewriteEntry' + 'RewriteEntry': + 'type': 'object' + 'description': 'Rewrite rule' + 'properties': + 'domain': + 'type': 'string' + 'description': 'Domain name' + 'example': 'example.org' + 'answer': + 'type': 'string' + 'description': 'value of A, AAAA or CNAME DNS record' + 'example': '127.0.0.1' + 'BlockedServicesArray': + 'type': 'array' + 'items': + 'type': 'string' + 'BlockedServicesAll': + 'properties': + 'blocked_services': + 'items': + '$ref': '#/components/schemas/BlockedService' + 'type': 'array' + 'required': + - 'blocked_services' + 'type': 'object' + 'BlockedService': + 'properties': + 'icon_svg': + 'description': > + The SVG icon as a Base64-encoded string to make it easier to embed + it into a data URL. + 'type': 'string' + 'id': + 'description': > + The ID of this service. + 'type': 'string' + 'name': + 'description': > + The human-readable name of this service. + 'type': 'string' + 'rules': + 'description': > + The array of the filtering rules. + 'items': + 'type': 'string' + 'type': 'array' + 'required': + - 'icon_svg' + - 'id' + - 'name' + - 'rules' + 'type': 'object' + 'BlockedServicesSchedule': + 'type': 'object' + 'properties': + 'schedule': + '$ref': '#/components/schemas/Schedule' + 'ids': + 'description': > + The names of the blocked services. + 'type': 'array' + 'items': + 'type': 'string' + 'CheckConfigRequest': + 'type': 'object' + 'description': 'Configuration to be checked' + 'properties': + 'dns': + '$ref': '#/components/schemas/CheckConfigRequestInfo' + 'web': + '$ref': '#/components/schemas/CheckConfigRequestInfo' + 'set_static_ip': + 'type': 'boolean' + 'example': false + 'CheckConfigRequestInfo': + 'type': 'object' + 'properties': + 'ip': + 'type': 'string' + 'example': '127.0.0.1' + 'port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 + 'autofix': + 'type': 'boolean' + 'example': false + 'CheckConfigResponse': + 'type': 'object' + 'required': + - 'dns' + - 'web' + - 'static_ip' + 'properties': + 'dns': + '$ref': '#/components/schemas/CheckConfigResponseInfo' + 'web': + '$ref': '#/components/schemas/CheckConfigResponseInfo' + 'static_ip': + '$ref': '#/components/schemas/CheckConfigStaticIpInfo' + 'CheckConfigResponseInfo': + 'type': 'object' + 'required': + - 'status' + - 'can_autofix' + 'properties': + 'status': + 'type': 'string' + 'default': '' + 'can_autofix': + 'type': 'boolean' + 'example': false + 'CheckConfigStaticIpInfoStatic': + 'type': 'string' + 'example': 'no' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': 'Can be: yes, no, error' + 'CheckConfigStaticIpInfo': + 'type': 'object' + 'properties': + 'static': + '$ref': '#/components/schemas/CheckConfigStaticIpInfoStatic' + 'ip': + 'type': 'string' + 'default': '' + 'example': '192.168.1.1' + 'description': 'Current dynamic IP address. Set if static=no' + 'error': + 'type': 'string' + 'default': '' + 'description': 'Error text. Set if static=error' + 'InitialConfiguration': + 'type': 'object' + 'description': > + AdGuard Home initial configuration for the first-install wizard. + 'required': + - 'dns' + - 'web' + - 'username' + - 'password' + 'properties': + 'dns': + '$ref': '#/components/schemas/AddressInfo' + 'web': + '$ref': '#/components/schemas/AddressInfo' + 'username': + 'type': 'string' + 'description': 'Basic auth username' + 'example': 'admin' + 'password': + 'type': 'string' + 'description': 'Basic auth password' + 'example': 'password' + 'Login': + 'type': 'object' + 'description': 'Login request data' + 'properties': + 'name': + 'type': 'string' + 'description': 'User name' + 'password': + 'type': 'string' + 'description': 'Password' + 'Error': + 'description': 'A generic JSON error response.' + 'properties': + 'message': + 'description': 'The error message, an opaque string.' + 'type': 'string' + 'type': 'object' + 'LanguageSettings': + 'description': 'Language settings object.' + 'properties': + 'language': + 'description': 'The current language or the language to set.' + 'type': 'string' + 'required': + - 'language' + 'type': 'object' + 'securitySchemes': + 'basicAuth': + 'type': 'http' + 'scheme': 'basic' diff --git a/requirements.txt b/requirements.txt index 62a31df..4c220fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ pydantic-settings==2.7.1 pytest>=7.4.4 pytest-asyncio>=0.25.2 python-multipart==0.0.20 -jinja2==3.1.5 \ No newline at end of file +jinja2==3.1.5 +slowapi==0.1.9 \ No newline at end of file diff --git a/src/simpleguardhome/adguard.py b/src/simpleguardhome/adguard.py index 3104547..aaf9e75 100644 --- a/src/simpleguardhome/adguard.py +++ b/src/simpleguardhome/adguard.py @@ -1,4 +1,5 @@ from typing import Dict, List, Optional +from pydantic import BaseModel, Field import httpx import logging from .config import settings @@ -19,13 +20,66 @@ class AdGuardAPIError(AdGuardError): """Raised when AdGuard Home API returns an error.""" pass +# Response models matching AdGuard Home API spec +class Filter(BaseModel): + """Filter subscription info according to AdGuard spec.""" + enabled: bool + id: int = Field(..., description="Filter ID", example=1234) + name: str = Field(..., example="AdGuard Simplified Domain Names filter") + rules_count: int = Field(..., description="Number of rules in filter", example=5912) + url: str = Field(..., example="https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt") + last_updated: Optional[str] = Field(None, example="2018-10-30T12:18:57+03:00") + +class FilterStatus(BaseModel): + """Filtering settings according to AdGuard spec.""" + enabled: bool = Field(..., description="Whether filtering is enabled") + interval: Optional[int] = Field(None, description="Update interval in hours") + filters: List[Filter] = Field(default_factory=list) + whitelist_filters: List[Filter] = Field(default_factory=list) + user_rules: List[str] = Field(default_factory=list) + +class DnsAnswer(BaseModel): + """DNS answer section according to AdGuard spec.""" + ttl: int = Field(..., description="Time to live") + type: str = Field(..., description="Record type", example="A") + value: str = Field(..., description="Record value", example="217.69.139.201") + +class DomainCheckResult(BaseModel): + """Response model for check_host endpoint according to AdGuard spec.""" + reason: str = Field( + ..., + description="Request filtering status", + enum=[ + "NotFilteredNotFound", + "NotFilteredWhiteList", + "NotFilteredError", + "FilteredBlackList", + "FilteredSafeBrowsing", + "FilteredParental", + "FilteredInvalid", + "FilteredSafeSearch", + "FilteredBlockedService", + "Rewrite", + "RewriteEtcHosts", + "RewriteRule" + ] + ) + filter_id: Optional[int] = Field(None, description="ID of the filter list containing the rule") + rule: Optional[str] = Field(None, description="Applied filtering rule") + service_name: Optional[str] = Field(None, description="Blocked service name if applicable") + cname: Optional[str] = Field(None, description="CNAME value if rewritten") + ip_addrs: Optional[List[str]] = Field(None, description="IP addresses if rewritten") + class AdGuardClient: - """Client for interacting with AdGuard Home API.""" + """Client for interacting with AdGuard Home API according to OpenAPI spec.""" def __init__(self): """Initialize the AdGuard Home API client.""" - self.base_url = settings.adguard_base_url - self.client = httpx.AsyncClient(timeout=10.0) # 10 second timeout + self.base_url = f"{settings.adguard_base_url}/control" + self.client = httpx.AsyncClient( + timeout=10.0, + limits=httpx.Limits(max_keepalive_connections=5, max_connections=10) + ) self._session_cookie = None self._auth = None if settings.ADGUARD_USERNAME and settings.ADGUARD_PASSWORD: @@ -49,14 +103,13 @@ class AdGuardClient: logger.warning("No credentials configured, skipping authentication") return False - url = f"{self.base_url}/control/login" + url = f"{self.base_url}/login" try: logger.info("Authenticating with AdGuard Home") response = await self.client.post(url, json=self._auth) response.raise_for_status() - # Extract and store session cookie cookies = response.cookies if 'agh_session' in cookies: self._session_cookie = cookies['agh_session'] @@ -81,14 +134,14 @@ class AdGuardClient: if not self._session_cookie: await self.login() - async def check_domain(self, domain: str) -> Dict: + async def check_domain(self, domain: str) -> DomainCheckResult: """Check if a domain is blocked by AdGuard Home. Args: domain: The domain to check Returns: - Dict containing the filtering status + DomainCheckResult according to AdGuard spec Raises: AdGuardConnectionError: If connection to AdGuard Home fails @@ -106,7 +159,6 @@ class AdGuardClient: logger.info(f"Checking domain: {domain}") response = await self.client.get(url, params=params, headers=headers) - # Handle unauthorized response by attempting reauth if response.status_code == 401: logger.info("Session expired, attempting reauth") await self.login() @@ -117,7 +169,7 @@ class AdGuardClient: response.raise_for_status() result = response.json() logger.info(f"Domain check result for {domain}: {result}") - return result + return DomainCheckResult(**result) except httpx.ConnectError as e: logger.error(f"Connection error while checking domain {domain}: {str(e)}") @@ -129,11 +181,11 @@ class AdGuardClient: logger.error(f"Unexpected error while checking domain {domain}: {str(e)}") raise AdGuardError(f"Unexpected error: {str(e)}") - async def get_filter_status(self) -> Dict: + async def get_filter_status(self) -> FilterStatus: """Get the current filtering status. Returns: - Dict containing the filtering status + FilterStatus according to AdGuard spec Raises: AdGuardConnectionError: If connection to AdGuard Home fails @@ -150,7 +202,6 @@ class AdGuardClient: logger.info("Getting filter status") response = await self.client.get(url, headers=headers) - # Handle unauthorized response by attempting reauth if response.status_code == 401: logger.info("Session expired, attempting reauth") await self.login() @@ -161,7 +212,7 @@ class AdGuardClient: response.raise_for_status() result = response.json() logger.info("Successfully retrieved filter status") - return result + return FilterStatus(**result) except httpx.ConnectError as e: logger.error(f"Connection error while getting filter status: {str(e)}") @@ -174,7 +225,7 @@ class AdGuardClient: raise AdGuardError(f"Unexpected error: {str(e)}") async def add_allowed_domain(self, domain: str) -> bool: - """Add a domain to the allowed list. + """Add a domain to the allowed list according to AdGuard spec. Args: domain: The domain to allow @@ -198,7 +249,6 @@ class AdGuardClient: logger.info(f"Adding domain to whitelist: {domain}") response = await self.client.post(url, json=data, headers=headers) - # Handle unauthorized response by attempting reauth if response.status_code == 401: logger.info("Session expired, attempting reauth") await self.login() diff --git a/src/simpleguardhome/main.py b/src/simpleguardhome/main.py index 9c393ef..a30bfbb 100644 --- a/src/simpleguardhome/main.py +++ b/src/simpleguardhome/main.py @@ -1,25 +1,76 @@ -from fastapi import FastAPI, Request, Form, HTTPException +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 typing import Dict from . import adguard from .config import settings -from .adguard import AdGuardError, AdGuardConnectionError, AdGuardAPIError +from .adguard import ( + AdGuardError, + AdGuardConnectionError, + AdGuardAPIError, + DomainCheckResult, + FilterStatus +) +from pydantic import BaseModel # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -app = FastAPI(title="SimpleGuardHome") +# Initialize rate limiter +app = FastAPI( + title="SimpleGuardHome", + description="AdGuard Home REST API interface", + version="1.0.0", + openapi_url="/api/openapi.json", + docs_url="/api/docs", + redoc_url="/api/redoc" +) + +# Add CORS middleware with security headers +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["X-Request-ID"] +) # Setup templates directory templates_path = Path(__file__).parent / "templates" templates = Jinja2Templates(directory=str(templates_path)) +# Response models matching AdGuard spec +class HealthResponse(BaseModel): + """Health check response model.""" + status: str + adguard_connection: str + filtering_enabled: bool = False + error: str = None + +class DomainResponse(BaseModel): + """Domain check response model matching AdGuard spec.""" + success: bool + domain: str + filtered: bool = False + reason: str = None + rule: str = None + filter_list_id: int = None + service_name: str = None + cname: str = None + ip_addrs: list[str] = None + message: str = None + +class ErrorResponse(BaseModel): + """Error response model matching AdGuard spec.""" + message: str + @app.get("/", response_class=HTMLResponse) async def home(request: Request): """Render the home page.""" @@ -28,7 +79,16 @@ async def home(request: Request): {"request": request} ) -@app.get("/health") +@app.get( + "/control/status", + response_model=HealthResponse, + responses={ + 200: {"description": "OK"}, + 503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}, + 500: {"description": "Internal server error", "model": ErrorResponse} + }, + tags=["health"] +) async def health_check() -> Dict: """Check the health of the application and AdGuard Home connection.""" try: @@ -37,45 +97,59 @@ async def health_check() -> Dict: return { "status": "healthy", "adguard_connection": "connected", - "filtering_enabled": status.get("enabled", False) + "filtering_enabled": status.enabled if status else False } except AdGuardConnectionError: - return { - "status": "degraded", - "adguard_connection": "failed", - "error": "Could not connect to AdGuard Home" - } + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "status": "degraded", + "adguard_connection": "failed", + "error": "Could not connect to AdGuard Home" + } + ) except Exception as e: logger.error(f"Health check failed: {str(e)}") - return { - "status": "error", - "error": "An internal error has occurred. Please try again later." - } + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "status": "error", + "error": "An internal error has occurred. Please try again later." + } + ) @app.exception_handler(AdGuardError) async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSONResponse: - """Handle AdGuard-related exceptions.""" + """Handle AdGuard-related exceptions according to spec.""" if isinstance(exc, AdGuardConnectionError): - status_code = 503 # Service Unavailable + status_code = status.HTTP_503_SERVICE_UNAVAILABLE elif isinstance(exc, AdGuardAPIError): - status_code = 502 # Bad Gateway + status_code = status.HTTP_502_BAD_GATEWAY else: - status_code = 500 # Internal Server Error + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return JSONResponse( status_code=status_code, - content={ - "success": False, - "error": exc.__class__.__name__, - "detail": str(exc) - } + content={"message": str(exc)} ) -@app.post("/check-domain") +@app.post( + "/control/filtering/check_host", + response_model=DomainResponse, + responses={ + 200: {"description": "OK"}, + 400: {"description": "Bad Request", "model": ErrorResponse}, + 503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse} + }, + tags=["filtering"] +) async def check_domain(domain: str = Form(...)) -> Dict: """Check if a domain is blocked by AdGuard Home.""" if not domain: - raise HTTPException(status_code=400, detail="Domain is required") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Domain is required" + ) logger.info(f"Checking domain: {domain}") try: @@ -84,9 +158,13 @@ async def check_domain(domain: str = Form(...)) -> Dict: response = { "success": True, "domain": domain, - "blocked": result.get("filtered", False), - "rule": result.get("rule", ""), - "filter_list": result.get("filter_list", "") + "filtered": result.reason.startswith("Filtered"), + "reason": result.reason, + "rule": result.rule, + "filter_list_id": result.filter_id, + "service_name": result.service_name, + "cname": result.cname, + "ip_addrs": result.ip_addrs } logger.info(f"Domain check result: {response}") return response @@ -94,11 +172,23 @@ async def check_domain(domain: str = Form(...)) -> Dict: logger.error(f"Error checking domain {domain}: {str(e)}") raise -@app.post("/unblock-domain") +@app.post( + "/control/filtering/whitelist/add", + response_model=DomainResponse, + responses={ + 200: {"description": "OK"}, + 400: {"description": "Bad Request", "model": ErrorResponse}, + 503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse} + }, + tags=["filtering"] +) async def unblock_domain(domain: str = Form(...)) -> Dict: """Add a domain to the allowed list.""" if not domain: - raise HTTPException(status_code=400, detail="Domain is required") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Domain is required" + ) logger.info(f"Unblocking domain: {domain}") try: @@ -115,6 +205,24 @@ async def unblock_domain(domain: str = Form(...)) -> Dict: logger.error(f"Error unblocking domain {domain}: {str(e)}") raise +@app.get( + "/control/filtering/status", + response_model=FilterStatus, + responses={ + 200: {"description": "OK"}, + 503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse} + }, + tags=["filtering"] +) +async def get_filtering_status() -> FilterStatus: + """Get the current filtering status.""" + try: + async with adguard.AdGuardClient() as client: + return await client.get_filter_status() + except Exception as e: + logger.error(f"Error getting filter status: {str(e)}") + raise + def start(): """Start the application using uvicorn.""" import uvicorn