Merge branch 'master' into fix/576

This commit is contained in:
Aleksey Dmitrevskiy 2019-02-25 17:07:02 +03:00
commit d351ed82c1
46 changed files with 3577 additions and 2878 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@
/querylog.json.1
/scripts/translations/node_modules
/scripts/translations/oneskyapp.json
/a_main-packr.go
# Test output
dnsfilter/tests/top-1m.csv

View File

@ -214,6 +214,14 @@ Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possib
* `range_start` - start IP address of the controlled range.
* `range_end` - end IP address of the controlled range.
* `lease_duration` - lease duration in seconds. If 0, using default duration (2 hours).
* `tls` - HTTPS/DOH/DOT settings.
* `enabled` - encryption (DOT/DOH/HTTPS) status.
* `server_name` - the hostname of your HTTPS/TLS server.
* `force_https` - if true, forces HTTP->HTTPS redirect.
* `port_https` - HTTPS port. If 0, HTTPS will be disabled.
* `port_dns_over_tls` - DNS-over-TLS port. If 0, DOT will be disabled.
* `certificate_chain` - PEM-encoded certificates chain.
* `private_key` - PEM-encoded private key.
* `user_rules` — User-specified filtering rules.
* `log_file` — Path to the log file. If empty, writes to stdout, if `syslog` -- system log (or eventlog on Windows).
* `verbose` — Enable our disables debug verbose output.

95
app.go
View File

@ -1,6 +1,7 @@
package main
import (
"crypto/tls"
"fmt"
stdlog "log"
"net"
@ -10,6 +11,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"sync"
"syscall"
"time"
@ -21,6 +23,11 @@ import (
// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"
var httpServer *http.Server
var httpsServer struct {
server *http.Server
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
sync.Mutex // protects config.TLS
}
const (
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
@ -159,12 +166,63 @@ func run(args options) {
registerInstallHandlers()
}
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
// for https, we have a separate goroutine loop
go func() {
for { // this is an endless loop
httpsServer.cond.L.Lock()
// this mechanism doesn't let us through until all conditions are ment
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
httpsServer.cond.Wait()
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
// validate current TLS config and update warnings (it could have been loaded from file)
data := validateCertificates(config.TLS)
if !data.usable {
log.Fatal(data.WarningValidation)
os.Exit(1)
}
config.Lock()
config.TLS = data // update warnings
config.Unlock()
// prepare certs for HTTPS server
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
certchain := make([]byte, len(config.TLS.CertificateChain))
copy(certchain, []byte(config.TLS.CertificateChain))
privatekey := make([]byte, len(config.TLS.PrivateKey))
copy(privatekey, []byte(config.TLS.PrivateKey))
cert, err := tls.X509KeyPair(certchain, privatekey)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
httpsServer.cond.L.Unlock()
// prepare HTTPS server
httpsServer.server = &http.Server{
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
printHTTPAddresses("https")
err = httpsServer.server.ListenAndServeTLS("", "")
if err != http.ErrServerClosed {
log.Fatal(err)
os.Exit(1)
}
}
}()
// this loop is used as an ability to change listening host and/or port
for {
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
printHTTPAddresses("http")
// we need to have new instance, because after Shutdown() the Server is not usable
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
httpServer = &http.Server{
Addr: address,
}
@ -336,3 +394,34 @@ func loadOptions() options {
return o
}
// prints IP addresses which user can use to open the admin interface
// proto is either "http" or "https"
func printHTTPAddresses(proto string) {
var address string
if proto == "https" && config.TLS.ServerName != "" {
if config.TLS.PortHTTPS == 443 {
log.Printf("Go to https://%s", config.TLS.ServerName)
} else {
log.Printf("Go to https://%s:%d", config.TLS.ServerName, config.TLS.PortHTTPS)
}
} else if config.BindHost == "0.0.0.0" {
log.Println("AdGuard Home is available on the following addresses:")
ifaces, err := getValidNetInterfacesForWeb()
if err != nil {
// That's weird, but we'll ignore it
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
log.Printf("Go to %s://%s", proto, address)
return
}
for _, iface := range ifaces {
address = net.JoinHostPort(iface.Addresses[0], strconv.Itoa(config.BindPort))
log.Printf("Go to %s://%s", proto, address)
}
} else {
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
log.Printf("Go to %s://%s", proto, address)
}
}

3930
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

9
client/package.json vendored
View File

@ -16,7 +16,7 @@
"file-saver": "^1.3.8",
"i18next": "^12.0.0",
"i18next-browser-languagedetector": "^2.2.3",
"lodash": "^4.17.10",
"lodash": "^4.17.11",
"nanoid": "^1.2.3",
"prop-types": "^15.6.1",
"react": "^16.4.0",
@ -33,14 +33,12 @@
"redux-actions": "^2.4.0",
"redux-form": "^7.4.2",
"redux-thunk": "^2.3.0",
"svg-url-loader": "^2.3.2",
"whatwg-fetch": "2.0.3"
"svg-url-loader": "^2.3.2"
},
"devDependencies": {
"autoprefixer": "^8.6.3",
"babel-core": "6.26.0",
"babel-eslint": "^8.2.3",
"babel-jest": "20.0.3",
"babel-loader": "7.1.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
@ -60,7 +58,6 @@
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "1.1.5",
"html-webpack-plugin": "^3.2.0",
"jest": "20.0.4",
"postcss-flexbugs-fixes": "3.2.0",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.5",
@ -68,7 +65,7 @@
"postcss-preset-env": "^5.1.0",
"postcss-svg": "^2.4.0",
"style-loader": "^0.21.0",
"stylelint": "9.2.1",
"stylelint": "^9.10.1",
"stylelint-webpack-plugin": "0.10.4",
"uglifyjs-webpack-plugin": "^1.2.7",
"url-loader": "^1.0.1",

View File

@ -184,14 +184,14 @@
"install_devices_router": "Router",
"install_devices_router_desc": "This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.",
"install_devices_address": "AdGuard Home DNS server is listening to the following addresses",
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.",
"install_devices_router_list_2": "Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http:\/\/192.168.0.1\/ or http:\/\/192.168.1.1\/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer\/phone.",
"install_devices_router_list_2": "Find the DHCP\/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
"install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",
"install_devices_windows_list_1": "Open Control Panel through Start menu or Windows search.",
"install_devices_windows_list_2": "Go to Network and Internet category and then to Network and Sharing Center.",
"install_devices_windows_list_3": "On the left side of the screen find Change adapter settings and click on it.",
"install_devices_windows_list_4": "Select your active connection, right-click on it and choose Properties.",
"install_devices_windows_list_5": "Find Internet Protocol Version 4 (TCP/IP) in the list, select it and then click on Properties again.",
"install_devices_windows_list_5": "Find Internet Protocol Version 4 (TCP\/IP) in the list, select it and then click on Properties again.",
"install_devices_windows_list_6": "Choose Use the following DNS server addresses and enter your AdGuard Home server addresses.",
"install_devices_macos_list_1": "Click on Apple icon and go to System Preferences.",
"install_devices_macos_list_2": "Click on Network.",
@ -209,6 +209,42 @@
"get_started": "Get Started",
"next": "Next",
"open_dashboard": "Open Dashboard",
"install_saved": "All settings saved",
"form_error_password": "Password mismatched"
"install_saved": "Saved successfully",
"encryption_title": "Encryption",
"encryption_desc": "Encryption (HTTPS/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need yo enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port",
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '\/dns-query' location.",
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
"encryption_certificates": "Certificates",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy/paste your PEM-encoded cerificates here.",
"encryption_status": "Status",
"encryption_expire": "Expires",
"encryption_key": "Private key",
"encryption_key_input": "Copy/paste your PEM-encoded private key for your cerficate here.",
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Certificate chain is valid",
"encryption_chain_invalid": "Certificate chain is invalid",
"encryption_key_valid": "This is a valid {{type}} private key",
"encryption_key_invalid": "This is an invalid {{type}} private key",
"encryption_subject": "Subject",
"encryption_issuer": "Issuer",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Are you sure you want to reset encryption settings?",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
"form_error_port_range": "Enter port value in the range of 80-65535",
"form_error_port_unsafe": "This is an unsafe port",
"form_error_equal": "Shouldn't be equal",
"form_error_password": "Password mismatched",
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info."
}

View File

@ -1,15 +1,15 @@
{
"url_added_successfully": "\u7db2\u5740\u5df2\u88ab\u6210\u529f\u5730\u52a0\u5165",
"url_added_successfully": "\u7db2\u5740\u88ab\u6210\u529f\u5730\u52a0\u5165",
"check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"save_config": "\u5132\u5b58\u914d\u7f6e",
"enabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u5df2\u88ab\u555f\u7528",
"disabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u5df2\u88ab\u7981\u7528",
"enabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u555f\u7528",
"disabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u7981\u7528",
"dhcp_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff08\u5be6\u9a57\u6027\u7684\uff01\uff09",
"dhcp_description": "\u5982\u679c\u60a8\u7684\u8def\u7531\u5668\u672a\u63d0\u4f9b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u8a2d\u5b9a\uff0c\u60a8\u53ef\u4f7f\u7528AdGuard\u81ea\u8eab\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u3002",
"dhcp_enable": "\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_disable": "\u7981\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_not_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u7121\u6709\u6548\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684\u3002",
"dhcp_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u67d0\u4e9b\u6709\u6548\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
"dhcp_not_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u7121\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684\u3002",
"dhcp_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u67d0\u4e9b\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
"dhcp_leases": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_leases_not_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_config_saved": "\u5df2\u5132\u5b58\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u914d\u7f6e",
@ -28,6 +28,7 @@
"dhcp_ip_addresses": "IP \u4f4d\u5740",
"dhcp_table_hostname": "\u4e3b\u6a5f\u540d\u7a31",
"dhcp_table_expires": "\u5230\u671f",
"dhcp_warning": "\u5982\u679c\u60a8\u60f3\u8981\u555f\u7528\u5167\u5efa\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u78ba\u4fdd\u7121\u5176\u5b83\u73fe\u884c\u7684DHCP\u4f3a\u670d\u5668\u3002\u5426\u5247\uff0c\u5b83\u53ef\u80fd\u6703\u7834\u58de\u4f9b\u5df2\u9023\u7dda\u7684\u88dd\u7f6e\u4e4b\u7db2\u969b\u7db2\u8def\uff01",
"back": "\u8fd4\u56de",
"dashboard": "\u5100\u8868\u677f",
"settings": "\u8a2d\u5b9a",
@ -36,8 +37,8 @@
"faq": "\u5e38\u898b\u554f\u7b54\u96c6",
"version": "\u7248\u672c",
"address": "\u4f4d\u5740",
"on": "\u958b\u555f",
"off": "\u95dc\u9589",
"on": "\u958b\u8457",
"off": "\u95dc\u8457",
"copyright": "\u7248\u6b0a",
"homepage": "\u9996\u9801",
"report_an_issue": "\u5831\u544a\u554f\u984c",
@ -73,7 +74,7 @@
"use_adguard_parental": "\u4f7f\u7528AdGuard\u5bb6\u9577\u76e3\u63a7\u4e4b\u7db2\u8def\u670d\u52d9",
"use_adguard_parental_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u5305\u542b\u6210\u4eba\u8cc7\u6599\u3002\u5b83\u4f7f\u7528\u5982\u540c\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u4e00\u6a23\u4e4b\u53cb\u597d\u7684\u96b1\u79c1\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u3002",
"enforce_safe_search": "\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b",
"enforce_save_search_hint": "AdGuard Home\u53ef\u5728\u4ee5\u4e0b\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u548cYandex\u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
"enforce_save_search_hint": "AdGuard Home\u53ef\u5728\u4e0b\u5217\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u548cYandex\u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
"no_servers_specified": "\u7121\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u4f3a\u670d\u5668",
"no_settings": "\u7121\u8a2d\u5b9a",
"general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a",
@ -115,6 +116,7 @@
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
"example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3",
"example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3",
"example_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u914d\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09",
"example_upstream_dot": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS <\/a>",
@ -129,7 +131,7 @@
"time_table_header": "\u6642\u9593",
"domain_name_table_header": "\u57df\u540d",
"type_table_header": "\u985e\u578b",
"response_table_header": "\u53cd\u61c9",
"response_table_header": "\u56de\u61c9",
"client_table_header": "\u7528\u6236\u7aef",
"empty_response_status": "\u7a7a\u767d\u7684",
"show_all_filter_type": "\u986f\u793a\u5168\u90e8",
@ -147,14 +149,66 @@
"of_table_footer_text": "\u4e4b",
"rows_table_footer_text": "\u5217",
"updated_custom_filtering_toast": "\u5df2\u66f4\u65b0\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
"rule_removed_from_custom_filtering_toast": "\u898f\u5247\u5df2\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u898f\u5247\u5df2\u88ab\u52a0\u81f3\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d",
"query_log_disabled_toast": "\u67e5\u8a62\u8a18\u9304\u5df2\u88ab\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8a62\u8a18\u9304\u5df2\u88ab\u555f\u7528",
"rule_removed_from_custom_filtering_toast": "\u898f\u5247\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u898f\u5247\u88ab\u52a0\u81f3\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d",
"query_log_disabled_toast": "\u67e5\u8a62\u8a18\u9304\u88ab\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8a62\u8a18\u9304\u88ab\u555f\u7528",
"source_label": "\u4f86\u6e90",
"found_in_known_domain_db": "\u5728\u5df2\u77e5\u7684\u57df\u540d\u8cc7\u6599\u5eab\u4e2d\u88ab\u767c\u73fe\u3002",
"category_label": "\u985e\u5225",
"rule_label": "\u898f\u5247",
"filter_label": "\u904e\u6ffe\u5668",
"unknown_filter": "\u672a\u77e5\u7684\u904e\u6ffe\u5668 {{filterId}}"
"unknown_filter": "\u672a\u77e5\u7684\u904e\u6ffe\u5668 {{filterId}}",
"install_welcome_title": "\u6b61\u8fce\u81f3AdGuard Home\uff01",
"install_welcome_desc": "AdGuard Home\u662f\u5168\u7db2\u8def\u7bc4\u570d\u5ee3\u544a\u548c\u8ffd\u8e64\u5668\u5c01\u9396\u7684DNS\u4f3a\u670d\u5668\u3002\u5b83\u7684\u76ee\u7684\u70ba\u8b93\u60a8\u63a7\u5236\u60a8\u6574\u500b\u7684\u7db2\u8def\u548c\u6240\u6709\u60a8\u7684\u88dd\u7f6e\uff0c\u4e14\u4e0d\u9700\u8981\u4f7f\u7528\u7528\u6236\u7aef\u7a0b\u5f0f\u3002",
"install_settings_title": "\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762",
"install_settings_listen": "\u76e3\u807d\u4ecb\u9762",
"install_settings_port": "\u9023\u63a5\u57e0",
"install_settings_interface_link": "\u60a8\u7684AdGuard Home\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5c07\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u70ba\u53ef\u7528\u7684\uff1a",
"form_error_port": "\u8f38\u5165\u6709\u6548\u7684\u9023\u63a5\u57e0\u503c",
"install_settings_dns": "DNS \u4f3a\u670d\u5668",
"install_settings_dns_desc": "\u60a8\u5c07\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u6216\u8def\u7531\u5668\u4ee5\u4f7f\u7528\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u4e4bDNS\u4f3a\u670d\u5668\uff1a",
"install_settings_all_interfaces": "\u6240\u6709\u7684\u4ecb\u9762",
"install_auth_title": "\u9a57\u8b49",
"install_auth_desc": "\u88ab\u975e\u5e38\u5efa\u8b70\u914d\u7f6e\u5c6c\u65bc\u60a8\u7684AdGuard Home\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u4e4b\u5bc6\u78bc\u9a57\u8b49\u3002\u5373\u4f7f\u5b83\u50c5\u5728\u60a8\u7684\u5340\u57df\u7db2\u8def\u4e2d\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u8b93\u5b83\u53d7\u4fdd\u8b77\u514d\u65bc\u4e0d\u53d7\u9650\u5236\u7684\u5b58\u53d6\u70ba\u4ecd\u7136\u91cd\u8981\u7684\u3002",
"install_auth_username": "\u7528\u6236\u540d",
"install_auth_password": "\u5bc6\u78bc",
"install_auth_confirm": "\u78ba\u8a8d\u5bc6\u78bc",
"install_auth_username_enter": "\u8f38\u5165\u7528\u6236\u540d",
"install_auth_password_enter": "\u8f38\u5165\u5bc6\u78bc",
"install_step": "\u6b65\u9a5f",
"install_devices_title": "\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e",
"install_devices_desc": "\u70ba\u4f7fAdGuard Home\u958b\u59cb\u904b\u4f5c\uff0c\u60a8\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u4ee5\u4f7f\u7528\u5b83\u3002",
"install_submit_title": "\u606d\u559c\uff01",
"install_submit_desc": "\u8a72\u8a2d\u7f6e\u7a0b\u5e8f\u88ab\u5b8c\u6210\uff0c\u4e14\u60a8\u6e96\u5099\u597d\u958b\u59cb\u4f7f\u7528AdGuard Home\u3002",
"install_devices_router": "\u8def\u7531\u5668",
"install_devices_router_desc": "\u8a72\u8a2d\u7f6e\u5c07\u81ea\u52d5\u5730\u6db5\u84cb\u88ab\u9023\u7dda\u81f3\u60a8\u7684\u5bb6\u5ead\u8def\u7531\u5668\u4e4b\u6240\u6709\u7684\u88dd\u7f6e\uff0c\u4e14\u60a8\u5c07\u7121\u9700\u624b\u52d5\u5730\u914d\u7f6e\u5b83\u5011\u6bcf\u500b\u3002",
"install_devices_address": "AdGuard Home DNS\u4f3a\u670d\u5668\u6b63\u5728\u76e3\u807d\u4e0b\u5217\u7684\u4f4d\u5740",
"install_devices_router_list_1": "\u958b\u555f\u95dc\u65bc\u60a8\u7684\u8def\u7531\u5668\u4e4b\u504f\u597d\u8a2d\u5b9a\u3002\u901a\u5e38\u5730\uff0c\u60a8\u53ef\u900f\u904e\u7db2\u5740\uff08\u5982 http:\/\/192.168.0.1\/ \u6216 http:\/\/192.168.1.1\/\uff09\u5f9e\u60a8\u7684\u700f\u89bd\u5668\u4e2d\u5b58\u53d6\u5b83\u3002\u60a8\u53ef\u80fd\u88ab\u8981\u6c42\u8f38\u5165\u8a72\u5bc6\u78bc\u3002\u5982\u679c\u60a8\u4e0d\u8a18\u5f97\u5b83\uff0c\u60a8\u7d93\u5e38\u53ef\u900f\u904e\u6309\u58d3\u65bc\u8a72\u8def\u7531\u5668\u672c\u8eab\u4e0a\u7684\u6309\u9215\u4f86\u91cd\u7f6e\u5bc6\u78bc\u3002\u67d0\u4e9b\u8def\u7531\u5668\u9700\u8981\u7279\u5b9a\u7684\u61c9\u7528\u7a0b\u5f0f\uff0c\u65e2\u7136\u5982\u6b64\u5176\u61c9\u5df2\u88ab\u5b89\u88dd\u65bc\u60a8\u7684\u96fb\u8166\/\u624b\u6a5f\u4e0a\u3002",
"install_devices_router_list_2": "\u627e\u5230DHCP\/DNS\u8a2d\u5b9a\u3002\u5c0b\u627e\u7dca\u9130\u8457\u5141\u8a31\u5169\u7d44\u6216\u4e09\u7d44\u6578\u5b57\u96c6\u7684\u6b04\u4f4d\u4e4bDNS\u5b57\u6bcd\uff0c\u6bcf\u7d44\u88ab\u62c6\u6210\u56db\u500b\u542b\u6709\u4e00\u81f3\u4e09\u500b\u6578\u5b57\u7684\u7fa4\u96c6\u3002",
"install_devices_router_list_3": "\u5728\u90a3\u88e1\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_windows_list_1": "\u901a\u904e\u958b\u59cb\u529f\u80fd\u8868\u6216Windows \u641c\u5c0b\uff0c\u958b\u555f\u63a7\u5236\u53f0\u3002",
"install_devices_windows_list_2": "\u53bb\u7db2\u8def\u548c\u7db2\u969b\u7db2\u8def\u985e\u5225\uff0c\u7136\u5f8c\u53bb\u7db2\u8def\u548c\u5171\u7528\u4e2d\u5fc3\u3002",
"install_devices_windows_list_3": "\u65bc\u756b\u9762\u4e4b\u5de6\u5074\u4e0a\u627e\u5230\u8b8a\u66f4\u4ecb\u9762\u5361\u8a2d\u5b9a\u4e26\u65bc\u5b83\u4e0a\u9ede\u64ca\u3002",
"install_devices_windows_list_4": "\u9078\u64c7\u60a8\u73fe\u884c\u7684\u9023\u7dda\uff0c\u65bc\u5b83\u4e0a\u9ede\u64ca\u6ed1\u9f20\u53f3\u9375\uff0c\u7136\u5f8c\u9078\u64c7\u5167\u5bb9\u3002",
"install_devices_windows_list_5": "\u5728\u6e05\u55ae\u4e2d\u627e\u5230\u7db2\u969b\u7db2\u8def\u901a\u8a0a\u5354\u5b9a\u7b2c 4 \u7248\uff08TCP\/IPv4\uff09\uff0c\u9078\u64c7\u5b83\uff0c\u7136\u5f8c\u518d\u6b21\u65bc\u5167\u5bb9\u4e0a\u9ede\u64ca\u3002",
"install_devices_windows_list_6": "\u9078\u64c7\u4f7f\u7528\u4e0b\u5217\u7684DNS\u4f3a\u670d\u5668\u4f4d\u5740\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_macos_list_1": "\u65bcApple\u5716\u50cf\u4e0a\u9ede\u64ca\uff0c\u7136\u5f8c\u53bb\u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a\u3002",
"install_devices_macos_list_2": "\u65bc\u7db2\u8def\u4e0a\u9ede\u64ca\u3002",
"install_devices_macos_list_3": "\u9078\u64c7\u5728\u60a8\u7684\u6e05\u55ae\u4e2d\u4e4b\u9996\u8981\u7684\u9023\u7dda\uff0c\u7136\u5f8c\u9ede\u64ca\u9032\u968e\u7684\u3002",
"install_devices_macos_list_4": "\u9078\u64c7\u8a72DNS\u5206\u9801\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_android_list_1": "\u5f9eAndroid\u9078\u55ae\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
"install_devices_android_list_2": "\u65bc\u8a72\u9078\u55ae\u4e0a\u8f15\u89f8Wi-Fi\u3002\u6b63\u5728\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684\u7db2\u8def\u4e4b\u756b\u9762\u5c07\u88ab\u986f\u793a\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u9023\u7dda\u8a2d\u5b9a\u81ea\u8a02\u7684DNS\uff09\u3002",
"install_devices_android_list_3": "\u9577\u6309\u60a8\u6240\u9023\u7dda\u81f3\u7684\u7db2\u8def\uff0c\u7136\u5f8c\u8f15\u89f8\u4fee\u6539\u7db2\u8def\u3002",
"install_devices_android_list_4": "\u65bc\u67d0\u4e9b\u88dd\u7f6e\u4e0a\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u6aa2\u67e5\u95dc\u65bc\u9032\u968e\u7684\u65b9\u6846\u4ee5\u67e5\u770b\u9032\u4e00\u6b65\u7684\u8a2d\u5b9a\u3002\u70ba\u4e86\u8abf\u6574\u60a8\u7684Android DNS\u8a2d\u5b9a\uff0c\u60a8\u5c07\u9700\u8981\u628aIP \u8a2d\u5b9a\u5f9eDHCP\u8f49\u63db\u6210\u975c\u614b\u3002",
"install_devices_android_list_5": "\u4f7f\u8a2d\u5b9aDNS 1\u548cDNS 2\u503c\u66f4\u6539\u6210\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"install_devices_ios_list_1": "\u5f9e\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
"install_devices_ios_list_2": "\u5728\u5de6\u5074\u7684\u9078\u55ae\u4e2d\u9078\u64c7Wi-Fi\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u7db2\u8def\u914d\u7f6eDNS\uff09\u3002",
"install_devices_ios_list_3": "\u65bc\u76ee\u524d\u73fe\u884c\u7684\u7db2\u8def\u4e4b\u540d\u7a31\u4e0a\u8f15\u89f8\u3002",
"install_devices_ios_list_4": "\u5728\u8a72DNS\u6b04\u4f4d\u4e2d\uff0c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
"get_started": "\u958b\u59cb\u5427",
"next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
"install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58",
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc"
}

View File

@ -0,0 +1,73 @@
import { createAction } from 'redux-actions';
import Api from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
import { redirectToCurrentProtocol } from '../helpers/helpers';
const apiClient = new Api();
export const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST');
export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE');
export const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS');
export const getTlsStatus = () => async (dispatch) => {
dispatch(getTlsStatusRequest());
try {
const status = await apiClient.getTlsStatus();
status.certificate_chain = atob(status.certificate_chain);
status.private_key = atob(status.private_key);
dispatch(getTlsStatusSuccess(status));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTlsStatusFailure());
}
};
export const setTlsConfigRequest = createAction('SET_TLS_CONFIG_REQUEST');
export const setTlsConfigFailure = createAction('SET_TLS_CONFIG_FAILURE');
export const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS');
export const setTlsConfig = config => async (dispatch, getState) => {
dispatch(setTlsConfigRequest());
try {
const { httpPort } = getState().dashboard;
const values = { ...config };
values.certificate_chain = btoa(values.certificate_chain);
values.private_key = btoa(values.private_key);
values.port_https = values.port_https || 0;
values.port_dns_over_tls = values.port_dns_over_tls || 0;
const response = await apiClient.setTlsConfig(values);
response.certificate_chain = atob(response.certificate_chain);
response.private_key = atob(response.private_key);
dispatch(setTlsConfigSuccess(response));
dispatch(addSuccessToast('encryption_config_saved'));
redirectToCurrentProtocol(response, httpPort);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setTlsConfigFailure());
}
};
export const validateTlsConfigRequest = createAction('VALIDATE_TLS_CONFIG_REQUEST');
export const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE');
export const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS');
export const validateTlsConfig = config => async (dispatch) => {
dispatch(validateTlsConfigRequest());
try {
const values = { ...config };
values.certificate_chain = btoa(values.certificate_chain);
values.private_key = btoa(values.private_key);
values.port_https = values.port_https || 0;
values.port_dns_over_tls = values.port_dns_over_tls || 0;
const response = await apiClient.validateTlsConfig(values);
response.certificate_chain = atob(response.certificate_chain);
response.private_key = atob(response.private_key);
dispatch(validateTlsConfigSuccess(response));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(validateTlsConfigFailure());
}
};

View File

@ -15,7 +15,11 @@ export default class Api {
return response.data;
} catch (error) {
console.error(error);
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
const errorPath = `${this.baseUrl}/${path}`;
if (error.response) {
throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);
}
throw new Error(`${errorPath} | ${error.message ? error.message : error}`);
}
}
@ -354,4 +358,32 @@ export default class Api {
};
return this.makeRequest(path, method, parameters);
}
// DNS-over-HTTPS and DNS-over-TLS
TLS_STATUS = { path: 'tls/status', method: 'GET' };
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
TLS_VALIDATE = { path: 'tls/validate', method: 'POST' };
getTlsStatus() {
const { path, method } = this.TLS_STATUS;
return this.makeRequest(path, method);
}
setTlsConfig(config) {
const { path, method } = this.TLS_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
validateTlsConfig(config) {
const { path, method } = this.TLS_VALIDATE;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
}

View File

@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import LoadingBar from 'react-redux-loading-bar';
import 'react-table/react-table.css';
@ -16,7 +17,8 @@ import Logs from '../../containers/Logs';
import Footer from '../ui/Footer';
import Toasts from '../Toasts';
import Status from '../ui/Status';
import Update from '../ui/Update';
import UpdateTopline from '../ui/UpdateTopline';
import EncryptionTopline from '../ui/EncryptionTopline';
import i18n from '../../i18n';
class App extends Component {
@ -50,7 +52,7 @@ class App extends Component {
}
render() {
const { dashboard } = this.props;
const { dashboard, encryption } = this.props;
const updateAvailable =
!dashboard.processingVersions &&
dashboard.isCoreRunning &&
@ -60,11 +62,14 @@ class App extends Component {
<HashRouter hashType='noslash'>
<Fragment>
{updateAvailable &&
<Update
announcement={dashboard.announcement}
announcementUrl={dashboard.announcementUrl}
<UpdateTopline
url={dashboard.announcementUrl}
version={dashboard.version}
/>
}
{!encryption.processing &&
<EncryptionTopline notAfter={encryption.not_after} />
}
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap">
@ -100,6 +105,7 @@ App.propTypes = {
error: PropTypes.string,
getVersion: PropTypes.func,
changeLanguage: PropTypes.func,
encryption: PropTypes.object,
};
export default App;
export default withNamespaces()(App);

View File

@ -1,6 +1,5 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import 'whatwg-fetch';
import { Trans, withNamespaces } from 'react-i18next';
import Statistics from './Statistics';

View File

@ -12,7 +12,6 @@ import './Header.css';
class Header extends Component {
state = {
isMenuOpen: false,
isDropdownOpen: false,
};
toggleMenuOpen = () => {
@ -25,6 +24,7 @@ class Header extends Component {
render() {
const { dashboard } = this.props;
const { isMenuOpen } = this.state;
const badgeClass = classnames({
'badge dns-status': true,
'badge-success': dashboard.protectionEnabled,
@ -52,7 +52,7 @@ class Header extends Component {
</div>
<Menu
location={this.props.location}
isMenuOpen={this.state.isMenuOpen}
isMenuOpen={isMenuOpen}
toggleMenuOpen={this.toggleMenuOpen}
closeMenu={this.closeMenu}
/>

View File

@ -1,48 +1,10 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import { withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import { R_IPV4 } from '../../../helpers/constants';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
const isPositive = (value) => {
if ((value || value === 0) && (value <= 0)) {
return <Trans>form_error_positive</Trans>;
}
return false;
};
const toNumber = value => value && parseInt(value, 10);
const renderField = ({
input, className, placeholder, type, disabled, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
const Form = (props) => {
const {
@ -57,7 +19,7 @@ const Form = (props) => {
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="gateway_ip"
@ -68,7 +30,7 @@ const Form = (props) => {
validate={[ipv4, required]}
/>
</div>
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="subnet_mask"
@ -81,7 +43,7 @@ const Form = (props) => {
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
@ -108,7 +70,7 @@ const Form = (props) => {
</div>
</div>
</div>
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="lease_duration"

View File

@ -63,7 +63,7 @@ let Interface = (props) => {
{!processing && interfaces &&
<div className="row">
<div className="col-sm-12 col-md-6">
<div className="form__group form__group--dhcp">
<div className="form__group form__group--settings">
<label>{t('dhcp_interface_select')}</label>
<Field
name="interface_name"

View File

@ -0,0 +1,364 @@
import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import format from 'date-fns/format';
import { renderField, renderSelectField, toNumber, port, isSafePort } from '../../../helpers/form';
import { EMPTY_DATE } from '../../../helpers/constants';
import i18n from '../../../i18n';
const validate = (values) => {
const errors = {};
if (values.port_dns_over_tls && values.port_https) {
if (values.port_dns_over_tls === values.port_https) {
errors.port_dns_over_tls = i18n.t('form_error_equal');
errors.port_https = i18n.t('form_error_equal');
}
}
return errors;
};
const clearFields = (change, setTlsConfig, t) => {
const fields = {
private_key: '',
certificate_chain: '',
port_https: 443,
port_dns_over_tls: 853,
server_name: '',
force_https: false,
enabled: false,
};
// eslint-disable-next-line no-alert
if (window.confirm(t('encryption_reset'))) {
Object.keys(fields).forEach(field => change(field, fields[field]));
setTlsConfig(fields);
}
};
let Form = (props) => {
const {
t,
handleSubmit,
handleChange,
isEnabled,
certificateChain,
privateKey,
change,
invalid,
submitting,
processingConfig,
processingValidate,
not_after,
valid_chain,
valid_key,
valid_cert,
dns_names,
key_type,
issuer,
subject,
warning_validation,
setTlsConfig,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('encryption_enable')}
onChange={handleChange}
/>
</div>
<div className="form__desc">
<Trans>encryption_enable_desc</Trans>
</div>
<hr/>
</div>
<div className="col-12">
<label className="form__label" htmlFor="server_name">
<Trans>encryption_server</Trans>
</label>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<Field
id="server_name"
name="server_name"
component={renderField}
type="text"
className="form-control"
placeholder={t('encryption_server_enter')}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_server_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<Field
name="force_https"
type="checkbox"
component={renderSelectField}
placeholder={t('encryption_redirect')}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_redirect_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_https">
<Trans>encryption_https</Trans>
</label>
<Field
id="port_https"
name="port_https"
component={renderField}
type="number"
className="form-control"
placeholder={t('encryption_https')}
validate={[port, isSafePort]}
normalize={toNumber}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_https_desc</Trans>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_dns_over_tls">
<Trans>encryption_dot</Trans>
</label>
<Field
id="port_dns_over_tls"
name="port_dns_over_tls"
component={renderField}
type="number"
className="form-control"
placeholder={t('encryption_dot')}
validate={[port]}
normalize={toNumber}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_dot_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label form__label--bold" htmlFor="certificate_chain">
<Trans>encryption_certificates</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans
values={{ link: 'letsencrypt.org' }}
components={[<a href="https://letsencrypt.org/" key="0">link</a>]}
>
encryption_certificates_desc
</Trans>
</div>
<Field
id="certificate_chain"
name="certificate_chain"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('encryption_certificates_input')}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__status">
{certificateChain &&
<Fragment>
<div className="form__label form__label--bold">
<Trans>encryption_status</Trans>:
</div>
<ul className="encryption__list">
<li className={valid_chain ? 'text-success' : 'text-danger'}>
{valid_chain ?
<Trans>encryption_chain_valid</Trans>
: <Trans>encryption_chain_invalid</Trans>
}
</li>
{valid_cert &&
<Fragment>
{subject &&
<li>
<Trans>encryption_subject</Trans>:&nbsp;
{subject}
</li>
}
{issuer &&
<li>
<Trans>encryption_issuer</Trans>:&nbsp;
{issuer}
</li>
}
{not_after && not_after !== EMPTY_DATE &&
<li>
<Trans>encryption_expire</Trans>:&nbsp;
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
</li>
}
{dns_names &&
<li>
<Trans>encryption_hostnames</Trans>:&nbsp;
{dns_names}
</li>
}
</Fragment>
}
</ul>
</Fragment>
}
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label form__label--bold" htmlFor="private_key">
<Trans>encryption_key</Trans>
</label>
<Field
id="private_key"
name="private_key"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder="Copy/paste your PEM-encoded private key for your cerficate here."
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__status">
{privateKey &&
<Fragment>
<div className="form__label form__label--bold">
<Trans>encryption_status</Trans>:
</div>
<ul className="encryption__list">
<li className={valid_key ? 'text-success' : 'text-danger'}>
{valid_key ?
<Trans values={{ type: key_type }}>
encryption_key_valid
</Trans>
: <Trans values={{ type: key_type }}>
encryption_key_invalid
</Trans>
}
</li>
</ul>
</Fragment>
}
</div>
</div>
</div>
{warning_validation &&
<div className="col-12">
<p className="text-danger">
{warning_validation}
</p>
</div>
}
</div>
<div className="btn-list mt-2">
<button
type="submit"
className="btn btn-success btn-standart"
disabled={
invalid
|| submitting
|| processingConfig
|| processingValidate
|| (isEnabled && (!privateKey || !certificateChain))
|| (privateKey && !valid_key)
|| (certificateChain && !valid_cert)
}
>
<Trans>save_config</Trans>
</button>
<button
type="button"
className="btn btn-secondary btn-standart"
disabled={submitting || processingConfig}
onClick={() => clearFields(change, setTlsConfig, t)}
>
<Trans>reset_settings</Trans>
</button>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isEnabled: PropTypes.bool.isRequired,
certificateChain: PropTypes.string.isRequired,
privateKey: PropTypes.string.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object.isRequired,
processingConfig: PropTypes.bool.isRequired,
processingValidate: PropTypes.bool.isRequired,
status_key: PropTypes.string,
not_after: PropTypes.string,
warning_validation: PropTypes.string,
valid_chain: PropTypes.bool,
valid_key: PropTypes.bool,
valid_cert: PropTypes.bool,
dns_names: PropTypes.string,
key_type: PropTypes.string,
issuer: PropTypes.string,
subject: PropTypes.string,
t: PropTypes.func.isRequired,
setTlsConfig: PropTypes.func.isRequired,
};
const selector = formValueSelector('encryptionForm');
Form = connect((state) => {
const isEnabled = selector(state, 'enabled');
const certificateChain = selector(state, 'certificate_chain');
const privateKey = selector(state, 'private_key');
return {
isEnabled,
certificateChain,
privateKey,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({
form: 'encryptionForm',
validate,
}),
])(Form);

View File

@ -0,0 +1,72 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import debounce from 'lodash/debounce';
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
import Form from './Form';
import Card from '../../ui/Card';
class Encryption extends Component {
componentDidMount() {
this.props.validateTlsConfig(this.props.encryption);
}
handleFormSubmit = (values) => {
this.props.setTlsConfig(values);
};
handleFormChange = debounce((values) => {
this.props.validateTlsConfig(values);
}, DEBOUNCE_TIMEOUT);
render() {
const { encryption, t } = this.props;
const {
enabled,
server_name,
force_https,
port_https,
port_dns_over_tls,
certificate_chain,
private_key,
} = encryption;
return (
<div className="encryption">
{encryption &&
<Card
title={t('encryption_title')}
subtitle={t('encryption_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={{
enabled,
server_name,
force_https,
port_https,
port_dns_over_tls,
certificate_chain,
private_key,
}}
onSubmit={this.handleFormSubmit}
onChange={this.handleFormChange}
setTlsConfig={this.props.setTlsConfig}
{...this.props.encryption}
/>
</Card>
}
</div>
);
}
}
Encryption.propTypes = {
setTlsConfig: PropTypes.func.isRequired,
validateTlsConfig: PropTypes.func.isRequired,
encryption: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Encryption);

View File

@ -7,8 +7,8 @@
margin-bottom: 0;
}
.form__group--dhcp:last-child {
margin-bottom: 15px;
.form__group--settings:last-child {
margin-bottom: 20px;
}
.btn-standard {
@ -48,3 +48,31 @@
.dhcp {
min-height: 450px;
}
.form__desc {
margin-top: 10px;
font-size: 13px;
color: rgba(74, 74, 74, 0.7);
}
.form__desc--top {
margin: 0 0 8px;
}
.form__label--bold {
font-weight: 700;
}
.form__status {
margin-top: 10px;
font-size: 14px;
line-height: 1.7;
}
.encryption__list {
padding-left: 0;
}
.encryption__list li {
list-style: inside;
}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
import Upstream from './Upstream';
import Dhcp from './Dhcp';
import Encryption from './Encryption';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
@ -37,6 +38,7 @@ class Settings extends Component {
this.props.initSettings(this.settings);
this.props.getDhcpStatus();
this.props.getDhcpInterfaces();
this.props.getTlsStatus();
}
handleUpstreamChange = (value) => {
@ -95,6 +97,11 @@ class Settings extends Component {
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
/>
<Encryption
encryption={this.props.encryption}
setTlsConfig={this.props.setTlsConfig}
validateTlsConfig={this.props.validateTlsConfig}
/>
<Dhcp
dhcp={this.props.dhcp}
toggleDhcp={this.props.toggleDhcp}

View File

@ -22,6 +22,11 @@
font-weight: 600;
}
.checkbox--form .checkbox__label:before {
top: 2px;
margin-right: 10px;
}
.checkbox__label {
position: relative;
display: flex;
@ -68,14 +73,19 @@
opacity: 0;
}
.checkbox__input:checked+.checkbox__label:before {
.checkbox__input:checked + .checkbox__label:before {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMi4zIDkuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxwYXRoIGQ9Ik0xMS44IDAuNUw1LjMgOC41IDAuNSA0LjIiLz48L3N2Zz4=);
}
.checkbox__input:focus+.checkbox__label:before {
.checkbox__input:focus + .checkbox__label:before {
box-shadow: 0 0 1px 1px rgba(74, 74, 74, 0.32);
}
.checkbox__input:disabled + .checkbox__label {
opacity: 0.6;
cursor: default;
}
.checkbox__label-text {
max-width: 515px;
line-height: 1.5;

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import isAfter from 'date-fns/is_after';
import addDays from 'date-fns/add_days';
import Topline from './Topline';
import { EMPTY_DATE } from '../../helpers/constants';
const EncryptionTopline = (props) => {
if (props.notAfter === EMPTY_DATE) {
return false;
}
const isAboutExpire = isAfter(addDays(Date.now(), 30), props.notAfter);
const isExpired = isAfter(Date.now(), props.notAfter);
if (isExpired) {
return (
<Topline type="danger">
<Trans components={[<a href="#settings" key="0">link</a>]}>
topline_expired_certificate
</Trans>
</Topline>
);
} else if (isAboutExpire) {
return (
<Topline type="warning">
<Trans components={[<a href="#settings" key="0">link</a>]}>
topline_expiring_certificate
</Trans>
</Topline>
);
}
return false;
};
EncryptionTopline.propTypes = {
notAfter: PropTypes.string.isRequired,
};
export default withNamespaces()(EncryptionTopline);

View File

@ -23,7 +23,7 @@ class Footer extends Component {
<div className="footer__row">
<div className="footer__column">
<div className="footer__copyright">
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
<Trans>copyright</Trans> &copy; {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
</div>
</div>
<div className="footer__column">

View File

@ -1,4 +1,4 @@
.update {
.topline {
position: relative;
z-index: 102;
margin-bottom: 0;

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Topline.css';
const Topline = props => (
<div className={`alert alert-${props.type} topline`}>
<div className="container">
{props.children}
</div>
</div>
);
Topline.propTypes = {
children: PropTypes.node.isRequired,
type: PropTypes.string.isRequired,
};
export default Topline;

View File

@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Update.css';
const Update = props => (
<div className="alert alert-info update">
<div className="container">
{props.announcement} <a href={props.announcementUrl} target="_blank" rel="noopener noreferrer">Click here</a> for more info.
</div>
</div>
);
Update.propTypes = {
announcement: PropTypes.string.isRequired,
announcementUrl: PropTypes.string.isRequired,
};
export default Update;

View File

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import Topline from './Topline';
const UpdateTopline = props => (
<Topline type="info">
<Trans
values={{ version: props.version }}
components={[
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
Click here
</a>,
]}
>
update_announcement
</Trans>
</Topline>
);
UpdateTopline.propTypes = {
version: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};
export default withNamespaces()(UpdateTopline);

View File

@ -3,8 +3,8 @@ import * as actionCreators from '../actions';
import App from '../components/App';
const mapStateToProps = (state) => {
const { dashboard } = state;
const props = { dashboard };
const { dashboard, encryption } = state;
const props = { dashboard, encryption };
return props;
};

View File

@ -12,11 +12,26 @@ import {
setDhcpConfig,
findActiveDhcp,
} from '../actions';
import {
getTlsStatus,
setTlsConfig,
validateTlsConfig,
} from '../actions/encryption';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const { settings, dashboard, dhcp } = state;
const props = { settings, dashboard, dhcp };
const {
settings,
dashboard,
dhcp,
encryption,
} = state;
const props = {
settings,
dashboard,
dhcp,
encryption,
};
return props;
};
@ -32,6 +47,9 @@ const mapDispatchToProps = {
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
getTlsStatus,
setTlsConfig,
validateTlsConfig,
};
export default connect(

View File

@ -73,3 +73,77 @@ export const SETTINGS_NAMES = {
export const STANDARD_DNS_PORT = 53;
export const STANDARD_WEB_PORT = 80;
export const STANDARD_HTTPS_PORT = 443;
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
export const DEBOUNCE_TIMEOUT = 300;
export const CHECK_TIMEOUT = 1000;
export const STOP_TIMEOUT = 10000;
export const UNSAFE_PORTS = [
1,
7,
9,
11,
13,
15,
17,
19,
20,
21,
22,
23,
25,
37,
42,
43,
53,
77,
79,
87,
95,
101,
102,
103,
104,
109,
110,
111,
113,
115,
117,
119,
123,
135,
139,
143,
179,
389,
465,
512,
513,
514,
515,
526,
530,
531,
532,
540,
556,
563,
587,
601,
636,
993,
995,
2049,
3659,
4045,
6000,
6665,
6666,
6667,
6668,
6669,
];

View File

@ -0,0 +1,79 @@
import React, { Fragment } from 'react';
import { Trans } from 'react-i18next';
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
export const renderField = ({
input, id, className, placeholder, type, disabled, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
id={id}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const renderSelectField = ({
input, placeholder, disabled, meta: { touched, error },
}) => (
<Fragment>
<label className="checkbox checkbox--form">
<span className="checkbox__marker"/>
<input
{...input}
type="checkbox"
className="checkbox__input"
disabled={disabled}
/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-title">{placeholder}</span>
</span>
</span>
</label>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
export const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
export const isPositive = (value) => {
if ((value || value === 0) && (value <= 0)) {
return <Trans>form_error_positive</Trans>;
}
return false;
};
export const port = (value) => {
if ((value || value === 0) && (value < 80 || value > 65535)) {
return <Trans>form_error_port_range</Trans>;
}
return false;
};
export const isSafePort = (value) => {
if (UNSAFE_PORTS.includes(value)) {
return <Trans>form_error_port_unsafe</Trans>;
}
return false;
};
export const toNumber = value => value && parseInt(value, 10);

View File

@ -3,8 +3,15 @@ import dateFormat from 'date-fns/format';
import subHours from 'date-fns/sub_hours';
import addHours from 'date-fns/add_hours';
import round from 'lodash/round';
import axios from 'axios';
import { STATS_NAMES, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from './constants';
import {
STATS_NAMES,
STANDARD_DNS_PORT,
STANDARD_WEB_PORT,
STANDARD_HTTPS_PORT,
CHECK_TIMEOUT,
} from './constants';
export const formatTime = (time) => {
const parsedTime = dateParse(time);
@ -140,3 +147,57 @@ export const getWebAddress = (ip, port = '') => {
return address;
};
export const checkRedirect = (url, attempts) => {
let count = attempts || 1;
if (count > 10) {
window.location.replace(url);
return false;
}
const rmTimeout = t => t && clearTimeout(t);
const setRecursiveTimeout = (time, ...args) => setTimeout(
checkRedirect,
time,
...args,
);
let timeout;
axios.get(url)
.then((response) => {
rmTimeout(timeout);
if (response) {
window.location.replace(url);
return;
}
timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
})
.catch((error) => {
rmTimeout(timeout);
if (error.response) {
window.location.replace(url);
return;
}
timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
});
return false;
};
export const redirectToCurrentProtocol = (values, httpPort = 80) => {
const {
protocol, hostname, hash, port,
} = window.location;
const { enabled, port_https } = values;
const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : '';
if (protocol !== 'https:' && enabled && port_https) {
checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
} else if (protocol === 'https:' && enabled && port_https && port_https !== parseInt(port, 10)) {
checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
} else if (protocol === 'https:' && (!enabled || !port_https)) {
window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
}
};

View File

@ -1,23 +1,19 @@
import React, { Component } from 'react';
import React from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import Controls from './Controls';
class Greeting extends Component {
render() {
return (
<div className="setup__step">
<div className="setup__group">
<h1 className="setup__title">
<Trans>install_welcome_title</Trans>
</h1>
<p className="setup__desc text-center">
<Trans>install_welcome_desc</Trans>
</p>
</div>
<Controls />
</div>
);
}
}
const Greeting = () => (
<div className="setup__step">
<div className="setup__group">
<h1 className="setup__title">
<Trans>install_welcome_title</Trans>
</h1>
<p className="setup__desc text-center">
<Trans>install_welcome_desc</Trans>
</p>
</div>
<Controls />
</div>
);
export default withNamespaces()(Greeting);

View File

@ -15,7 +15,7 @@
padding: 30px 20px;
line-height: 1.6;
background-color: #fff;
box-shadow: 0 1px 4px rgba(74, 74, 74, .36);
box-shadow: 0 1px 4px rgba(74, 74, 74, 0.36);
border-radius: 3px;
}
@ -92,7 +92,7 @@
line-height: 20px;
color: #fff;
text-align: center;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
transition: width 0.6s ease;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}

View File

@ -0,0 +1,81 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/encryption';
const encryption = handleActions({
[actions.getTlsStatusRequest]: state => ({ ...state, processing: true }),
[actions.getTlsStatusFailure]: state => ({ ...state, processing: false }),
[actions.getTlsStatusSuccess]: (state, { payload }) => {
const newState = {
...state,
...payload,
processing: false,
};
return newState;
},
[actions.setTlsConfigRequest]: state => ({ ...state, processingConfig: true }),
[actions.setTlsConfigFailure]: state => ({ ...state, processingConfig: false }),
[actions.setTlsConfigSuccess]: (state, { payload }) => {
const newState = {
...state,
...payload,
processingConfig: false,
};
return newState;
},
[actions.validateTlsConfigRequest]: state => ({ ...state, processingValidate: true }),
[actions.validateTlsConfigFailure]: state => ({ ...state, processingValidate: false }),
[actions.validateTlsConfigSuccess]: (state, { payload }) => {
const {
issuer = '',
key_type = '',
not_after = '',
not_before = '',
subject = '',
warning_validation = '',
dns_names = '',
...values
} = payload;
const newState = {
...state,
...values,
issuer,
key_type,
not_after,
not_before,
subject,
warning_validation,
dns_names,
processingValidate: false,
};
return newState;
},
}, {
processing: true,
processingConfig: false,
processingValidate: false,
enabled: false,
dns_names: null,
force_https: false,
issuer: '',
key_type: '',
not_after: '',
not_before: '',
port_dns_over_tls: '',
port_https: '',
subject: '',
valid_chain: false,
valid_key: false,
valid_cert: false,
status_cert: '',
status_key: '',
certificate_chain: '',
private_key: '',
server_name: '',
warning_validation: '',
});
export default encryption;

View File

@ -6,6 +6,7 @@ import versionCompare from '../helpers/versionCompare';
import * as actions from '../actions';
import toasts from './toasts';
import encryption from './encryption';
const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@ -52,6 +53,7 @@ const dashboard = handleActions({
upstream_dns: upstreamDns,
protection_enabled: protectionEnabled,
language,
http_port: httpPort,
} = payload;
const newState = {
...state,
@ -64,6 +66,7 @@ const dashboard = handleActions({
upstreamDns: upstreamDns.join('\n'),
protectionEnabled,
language,
httpPort,
};
return newState;
},
@ -117,13 +120,13 @@ const dashboard = handleActions({
if (versionCompare(currentVersion, payload.version) === -1) {
const {
announcement,
version,
announcement_url: announcementUrl,
} = payload;
const newState = {
...state,
announcement,
version,
announcementUrl,
isUpdateAvailable: true,
};
@ -171,6 +174,7 @@ const dashboard = handleActions({
upstreamDns: [],
protectionEnabled: false,
processingProtection: false,
httpPort: 80,
});
const queryLogs = handleActions({
@ -309,6 +313,7 @@ export default combineReducers({
filtering,
toasts,
dhcp,
encryption,
loadingBar: loadingBarReducer,
form: formReducer,
});

View File

@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
@ -31,12 +32,13 @@ type configuration struct {
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
BindHost string `yaml:"bind_host"`
BindPort int `yaml:"bind_port"`
AuthName string `yaml:"auth_name"`
AuthPass string `yaml:"auth_pass"`
Language string `yaml:"language"` // two-letter ISO 639-1 language code
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
Language string `yaml:"language"` // two-letter ISO 639-1 language code
DNS dnsConfig `yaml:"dns"`
TLS tlsConfig `yaml:"tls"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
@ -60,6 +62,43 @@ type dnsConfig struct {
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
type tlsConfigSettings struct {
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
ForceHTTPS bool `yaml:"force_https" json:"force_https,omitempty"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
PortHTTPS int `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
}
// field ordering is not important -- these are for API and are recalculated on each run
type tlsConfigStatus struct {
ValidCert bool `yaml:"-" json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
ValidChain bool `yaml:"-" json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
Subject string `yaml:"-" json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
Issuer string `yaml:"-" json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
NotBefore time.Time `yaml:"-" json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
NotAfter time.Time `yaml:"-" json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
DNSNames []string `yaml:"-" json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
// key status
ValidKey bool `yaml:"-" json:"valid_key"` // ValidKey is true if the key is a valid private key
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
// is usable? set by validator
usable bool
// warnings
WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
}
// field ordering is important -- yaml fields will mirror ordering from here
type tlsConfig struct {
tlsConfigSettings `yaml:",inline" json:",inline"`
tlsConfigStatus `yaml:"-" json:",inline"`
}
// initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{
ourConfigFilename: "AdGuardHome.yaml",
@ -79,6 +118,12 @@ var config = configuration{
},
UpstreamDNS: defaultDNS,
},
TLS: tlsConfig{
tlsConfigSettings: tlsConfigSettings{
PortHTTPS: 443,
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
},
},
Filters: []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},

View File

@ -3,12 +3,21 @@ package main
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"reflect"
"sort"
"strconv"
"strings"
@ -17,6 +26,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
"github.com/joomcode/errorx"
"github.com/miekg/dns"
govalidator "gopkg.in/asaskevich/govalidator.v4"
)
@ -68,9 +78,7 @@ func writeAllConfigsAndReloadDNS() error {
func httpUpdateConfigReloadDNSReturnOK(w http.ResponseWriter, r *http.Request) {
err := writeAllConfigsAndReloadDNS()
if err != nil {
errorText := fmt.Sprintf("Couldn't write config file: %s", err)
log.Println(errorText)
http.Error(w, errorText, http.StatusInternalServerError)
httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
return
}
returnOK(w)
@ -78,7 +86,8 @@ func httpUpdateConfigReloadDNSReturnOK(w http.ResponseWriter, r *http.Request) {
func handleStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"dns_address": config.BindHost,
"dns_address": config.DNS.BindHost,
"http_port": config.BindPort,
"dns_port": config.DNS.Port,
"protection_enabled": config.DNS.ProtectionEnabled,
"querylog_enabled": config.DNS.QueryLogEnabled,
@ -400,7 +409,7 @@ func handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
func checkDNS(input string) error {
log.Printf("Checking if DNS %s works...", input)
u, err := upstream.AddressToUpstream(input, "", dnsforward.DefaultTimeout)
u, err := upstream.AddressToUpstream(input, upstream.Options{Timeout: dnsforward.DefaultTimeout})
if err != nil {
return fmt.Errorf("failed to choose upstream for %s: %s", input, err)
}
@ -900,17 +909,6 @@ type firstRunData struct {
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := firstRunData{}
ifaces, err := getValidNetInterfaces()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
}
if len(ifaces) == 0 {
httpError(w, http.StatusServiceUnavailable, "Couldn't find any legible interface, plase try again later")
return
}
// fill out the fields
// find out if port 80 is available -- if not, fall back to 3000
if checkPortAvailable("", 80) == nil {
@ -925,41 +923,15 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data.DNS.Warning = "Port 53 is not available for binding -- this will make DNS clients unable to contact AdGuard Home."
}
ifaces, err := getValidNetInterfacesForWeb()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
}
data.Interfaces = make(map[string]interface{})
for _, iface := range ifaces {
addrs, e := iface.Addrs()
if e != nil {
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %s: %s", iface.Name, err)
return
}
jsonIface := netInterface{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(),
}
if iface.Flags != 0 {
jsonIface.Flags = iface.Flags.String()
}
// we don't want link-local addresses in json, so skip them
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
httpError(w, http.StatusInternalServerError, "SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
return
}
// ignore link-local
if ipnet.IP.IsLinkLocalUnicast() {
continue
}
jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String())
}
if len(jsonIface.Addresses) != 0 {
data.Interfaces[iface.Name] = jsonIface
}
data.Interfaces[iface.Name] = iface
}
w.Header().Set("Content-Type", "application/json")
@ -974,7 +946,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
newSettings := firstRunData{}
err := json.NewDecoder(r.Body).Decode(&newSettings)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
httpError(w, http.StatusBadRequest, "Failed to parse new config json: %s", err)
return
}
@ -1025,6 +997,312 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
}
}
// ---
// TLS
// ---
func handleTLSStatus(w http.ResponseWriter, r *http.Request) {
marshalTLS(w, config.TLS)
}
func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
data = validateCertificates(data)
marshalTLS(w, data)
}
func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
restartHTTPS := false
data = validateCertificates(data)
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
log.Printf("tls config settings have changed, will restart HTTPS server")
restartHTTPS = true
}
config.TLS = data
err = writeAllConfigsAndReloadDNS()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
return
}
marshalTLS(w, data)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTPS {
go func() {
time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server
httpsServer.cond.L.Lock()
httpsServer.cond.Broadcast()
if httpsServer.server != nil {
httpsServer.server.Shutdown(context.TODO())
}
httpsServer.cond.L.Unlock()
}()
}
}
func validateCertificates(data tlsConfig) tlsConfig {
var err error
// clear out status for certificates
data.tlsConfigStatus = tlsConfigStatus{}
// check only public certificate separately from the key
if data.CertificateChain != "" {
log.Tracef("got certificate: %s", data.CertificateChain)
// now do a more extended validation
var certs []*pem.Block // PEM-encoded certificates
var skippedBytes []string // skipped bytes
pemblock := []byte(data.CertificateChain)
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
if decoded.Type == "CERTIFICATE" {
certs = append(certs, decoded)
} else {
skippedBytes = append(skippedBytes, decoded.Type)
}
}
var parsedCerts []*x509.Certificate
for _, cert := range certs {
parsed, err := x509.ParseCertificate(cert.Bytes)
if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
return data
}
parsedCerts = append(parsedCerts, parsed)
}
if len(parsedCerts) == 0 {
data.WarningValidation = fmt.Sprintf("You have specified an empty certificate")
return data
}
data.ValidCert = true
// spew.Dump(parsedCerts)
opts := x509.VerifyOptions{
DNSName: data.ServerName,
}
log.Printf("number of certs - %d", len(parsedCerts))
if len(parsedCerts) > 1 {
// set up an intermediate
pool := x509.NewCertPool()
for _, cert := range parsedCerts[1:] {
log.Printf("got an intermediate cert")
pool.AddCert(cert)
}
opts.Intermediates = pool
}
// TODO: save it as a warning rather than error it out -- shouldn't be a big problem
mainCert := parsedCerts[0]
_, err := mainCert.Verify(opts)
if err != nil {
// let self-signed certs through
data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
} else {
data.ValidChain = true
}
// spew.Dump(chains)
// update status
if mainCert != nil {
notAfter := mainCert.NotAfter
data.Subject = mainCert.Subject.String()
data.Issuer = mainCert.Issuer.String()
data.NotAfter = notAfter
data.NotBefore = mainCert.NotBefore
data.DNSNames = mainCert.DNSNames
}
}
// validate private key (right now the only validation possible is just parsing it)
if data.PrivateKey != "" {
// now do a more extended validation
var key *pem.Block // PEM-encoded certificates
var skippedBytes []string // skipped bytes
// go through all pem blocks, but take first valid pem block and drop the rest
pemblock := []byte(data.PrivateKey)
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") {
key = decoded
break
} else {
skippedBytes = append(skippedBytes, decoded.Type)
}
}
if key == nil {
data.WarningValidation = "No valid keys were found"
return data
}
// parse the decoded key
_, keytype, err := parsePrivateKey(key.Bytes)
if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
return data
}
data.ValidKey = true
data.KeyType = keytype
}
// if both are set, validate both in unison
if data.PrivateKey != "" && data.CertificateChain != "" {
_, err = tls.X509KeyPair([]byte(data.CertificateChain), []byte(data.PrivateKey))
if err != nil {
data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
return data
}
data.usable = true
}
return data
}
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, "RSA", nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, "RSA", nil
case *ecdsa.PrivateKey:
return key, "ECDSA", nil
default:
return nil, "", errors.New("tls: found unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, "ECDSA", nil
}
return nil, "", errors.New("tls: failed to parse private key")
}
// unmarshalTLS handles base64-encoded certificates transparently
func unmarshalTLS(r *http.Request) (tlsConfig, error) {
data := tlsConfig{}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
return data, errorx.Decorate(err, "Failed to parse new TLS config json")
}
if data.CertificateChain != "" {
certPEM, err := base64.StdEncoding.DecodeString(data.CertificateChain)
if err != nil {
return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
}
data.CertificateChain = string(certPEM)
}
if data.PrivateKey != "" {
keyPEM, err := base64.StdEncoding.DecodeString(data.PrivateKey)
if err != nil {
return data, errorx.Decorate(err, "Failed to base64-decode private key")
}
data.PrivateKey = string(keyPEM)
}
return data, nil
}
func marshalTLS(w http.ResponseWriter, data tlsConfig) {
w.Header().Set("Content-Type", "application/json")
if data.CertificateChain != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
data.CertificateChain = encoded
}
if data.PrivateKey != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey))
data.PrivateKey = encoded
}
err := json.NewEncoder(w).Encode(data)
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to marshal json with TLS status: %s", err)
return
}
}
// --------------
// DNS-over-HTTPS
// --------------
func handleDOH(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
httpError(w, http.StatusNotFound, "Not Found")
return
}
if !isRunning() {
httpError(w, http.StatusInternalServerError, "DNS server is not running")
return
}
dnsServer.ServeHTTP(w, r)
}
// ------------------------
// registration of handlers
// ------------------------
func registerInstallHandlers() {
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
@ -1068,4 +1346,10 @@ func registerControlHandlers() {
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
http.HandleFunc("/control/tls/status", postInstall(optionalAuth(ensureGET(handleTLSStatus))))
http.HandleFunc("/control/tls/configure", postInstall(optionalAuth(ensurePOST(handleTLSConfigure))))
http.HandleFunc("/control/tls/validate", postInstall(optionalAuth(ensurePOST(handleTLSValidate))))
http.HandleFunc("/dns-query", postInstall(handleDOH))
}

13
dns.go
View File

@ -51,8 +51,19 @@ func generateServerConfig() dnsforward.ServerConfig {
Filters: filters,
}
if config.TLS.Enabled {
newconfig.TLSConfig = config.TLS.TLSConfig
if config.TLS.PortDNSOverTLS != 0 {
newconfig.TLSListenAddr = &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.TLS.PortDNSOverTLS}
}
}
for _, u := range config.DNS.UpstreamDNS {
dnsUpstream, err := upstream.AddressToUpstream(u, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
opts := upstream.Options{
Timeout: dnsforward.DefaultTimeout,
Bootstrap: []string{config.DNS.BootstrapDNS},
}
dnsUpstream, err := upstream.AddressToUpstream(u, opts)
if err != nil {
log.Printf("Couldn't get upstream: %s", err)
// continue, just ignore the upstream

View File

@ -1,9 +1,11 @@
package dnsforward
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
@ -55,6 +57,7 @@ func NewServer(baseDir string) *Server {
}
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
// The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
@ -68,6 +71,13 @@ type FilteringConfig struct {
dnsfilter.Config `yaml:",inline"`
}
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
type TLSConfig struct {
TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"`
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
PrivateKey string `yaml:"private_key" json:"private_key"` // PEM-encoded private key
}
// ServerConfig represents server configuration.
// The zero ServerConfig is empty and ready for use.
type ServerConfig struct {
@ -77,6 +87,7 @@ type ServerConfig struct {
Filters []dnsfilter.Filter // A list of filters to use
FilteringConfig
TLSConfig
}
// if any of ServerConfig values are zero, then default values from below are used
@ -91,7 +102,7 @@ func init() {
defaultUpstreams := make([]upstream.Upstream, 0)
for _, addr := range defaultDNS {
u, err := upstream.AddressToUpstream(addr, "", DefaultTimeout)
u, err := upstream.AddressToUpstream(addr, upstream.Options{Timeout: DefaultTimeout})
if err == nil {
defaultUpstreams = append(defaultUpstreams, u)
}
@ -154,6 +165,15 @@ func (s *Server) startInternal(config *ServerConfig) error {
Handler: s.handleDNSRequest,
}
if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {
proxyConfig.TLSListenAddr = s.TLSListenAddr
keypair, err := tls.X509KeyPair([]byte(s.CertificateChain), []byte(s.PrivateKey))
if err != nil {
return errorx.Decorate(err, "Failed to parse TLS keypair")
}
proxyConfig.TLSConfig = &tls.Config{Certificates: []tls.Certificate{keypair}}
}
if proxyConfig.UDPListenAddr == nil {
proxyConfig.UDPListenAddr = defaultValues.UDPListenAddr
}
@ -240,24 +260,38 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
return nil
}
// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.RLock()
s.dnsProxy.ServeHTTP(w, r)
s.RUnlock()
}
// GetQueryLog returns a map with the current query log ready to be converted to a JSON
func (s *Server) GetQueryLog() []map[string]interface{} {
s.RLock()
defer s.RUnlock()
return s.queryLog.getQueryLog()
}
// GetStatsTop returns the current stop stats
func (s *Server) GetStatsTop() *StatsTop {
s.RLock()
defer s.RUnlock()
return s.queryLog.runningTop.getStatsTop()
}
// PurgeStats purges current server stats
func (s *Server) PurgeStats() {
// TODO: Locks?
s.Lock()
defer s.Unlock()
s.stats.purgeStats()
}
// GetAggregatedStats returns aggregated stats data for the 24 hours
func (s *Server) GetAggregatedStats() map[string]interface{} {
s.RLock()
defer s.RUnlock()
return s.stats.getAggregatedStats()
}
@ -267,6 +301,8 @@ func (s *Server) GetAggregatedStats() map[string]interface{} {
// end is end of the time range
// returns nil if time unit is not supported
func (s *Server) GetStatsHistory(timeUnit time.Duration, startTime time.Time, endTime time.Time) (map[string]interface{}, error) {
s.RLock()
defer s.RUnlock()
return s.stats.getStatsHistory(timeUnit, startTime, endTime)
}
@ -350,9 +386,9 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
return s.genBlockedHost(m, safeBrowsingBlockHost, d.Upstream)
return s.genBlockedHost(m, safeBrowsingBlockHost, d)
case dnsfilter.FilteredParental:
return s.genBlockedHost(m, parentalBlockHost, d.Upstream)
return s.genBlockedHost(m, parentalBlockHost, d)
default:
if result.IP != nil {
return s.genARecord(m, result.IP)
@ -381,22 +417,30 @@ func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
return &resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, upstream upstream.Upstream) *dns.Msg {
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
// look up the hostname, TODO: cache
replReq := dns.Msg{}
replReq.SetQuestion(dns.Fqdn(newAddr), request.Question[0].Qtype)
replReq.RecursionDesired = true
reply, err := upstream.Exchange(&replReq)
newContext := &proxy.DNSContext{
Proto: d.Proto,
Addr: d.Addr,
StartTime: time.Now(),
Req: &replReq,
}
err := s.dnsProxy.Resolve(newContext)
if err != nil {
log.Printf("Couldn't look up replacement host '%s' on upstream %s: %s", newAddr, upstream.Address(), err)
log.Printf("Couldn't look up replacement host '%s': %s", newAddr, err)
return s.genServerFailure(request)
}
resp := dns.Msg{}
resp.SetReply(request)
resp.Authoritative, resp.RecursionAvailable = true, true
if reply != nil {
for _, answer := range reply.Answer {
if newContext.Res != nil {
for _, answer := range newContext.Res.Answer {
answer.Header().Name = request.Question[0].Name
resp.Answer = append(resp.Answer, answer)
}

View File

@ -1,17 +1,34 @@
package dnsforward
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"sync"
"testing"
"time"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/stretchr/testify/assert"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/miekg/dns"
)
const (
tlsServerName = "testdns.adguard.com"
dataDir = "testData"
testMessagesCount = 10
)
func TestServer(t *testing.T) {
s := createTestServer(t)
defer removeDataDir(t)
@ -22,7 +39,7 @@ func TestServer(t *testing.T) {
// message over UDP
req := createGoogleATestMessage()
addr := s.dnsProxy.Addr("udp")
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
client := dns.Client{Net: "udp"}
reply, _, err := client.Exchange(req, addr.String())
if err != nil {
@ -63,6 +80,69 @@ func TestServer(t *testing.T) {
}
}
func TestDotServer(t *testing.T) {
// Prepare the proxy server
_, certPem, keyPem := createServerTLSConfig(t)
s := createTestServer(t)
defer removeDataDir(t)
s.TLSConfig = TLSConfig{
TLSListenAddr: &net.TCPAddr{Port: 0},
CertificateChain: string(certPem),
PrivateKey: string(keyPem),
}
// Starting the server
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
// Add our self-signed generated config to roots
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(certPem)
tlsConfig := &tls.Config{ServerName: tlsServerName, RootCAs: roots}
// Create a DNS-over-TLS client connection
addr := s.dnsProxy.Addr(proxy.ProtoTLS)
conn, err := dns.DialWithTLS("tcp-tls", addr.String(), tlsConfig)
if err != nil {
t.Fatalf("cannot connect to the proxy: %s", err)
}
sendTestMessages(t, conn)
// Stop the proxy
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestServerRace(t *testing.T) {
s := createTestServer(t)
defer removeDataDir(t)
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
// message over UDP
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
conn, err := dns.Dial("udp", addr.String())
if err != nil {
t.Fatalf("cannot connect to the proxy: %s", err)
}
sendTestMessagesAsync(t, conn)
// Stop the proxy
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestSafeSearch(t *testing.T) {
s := createTestServer(t)
s.SafeSearchEnabled = true
@ -141,7 +221,7 @@ func TestInvalidRequest(t *testing.T) {
}
// server is running, send a message
addr := s.dnsProxy.Addr("udp")
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
@ -175,7 +255,7 @@ func TestBlockedRequest(t *testing.T) {
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
addr := s.dnsProxy.Addr("udp")
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
//
// NXDomain blocking
@ -216,7 +296,7 @@ func TestBlockedByHosts(t *testing.T) {
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
addr := s.dnsProxy.Addr("udp")
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
//
// Hosts blocking
@ -264,7 +344,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
addr := s.dnsProxy.Addr("udp")
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
//
// Safebrowsing blocking
@ -320,6 +400,7 @@ func createTestServer(t *testing.T) *Server {
s := NewServer(createDataDir(t))
s.UDPListenAddr = &net.UDPAddr{Port: 0}
s.TCPListenAddr = &net.TCPAddr{Port: 0}
s.QueryLogEnabled = true
s.FilteringConfig.FilteringEnabled = true
s.FilteringConfig.ProtectionEnabled = true
@ -335,20 +416,111 @@ func createTestServer(t *testing.T) *Server {
return s
}
func createDataDir(t *testing.T) string {
dir := "testData"
err := os.MkdirAll(dir, 0755)
func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Cannot create %s: %s", dir, err)
t.Fatalf("cannot generate RSA key: %s", err)
}
return dir
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
t.Fatalf("failed to generate serial number: %s", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(5 * 365 * time.Hour * 24)
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"AdGuard Tests"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
template.DNSNames = append(template.DNSNames, tlsServerName)
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(privateKey), privateKey)
if err != nil {
t.Fatalf("failed to create certificate: %s", err)
}
certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
cert, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
t.Fatalf("failed to create certificate: %s", err)
}
return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName}, certPem, keyPem
}
func createDataDir(t *testing.T) string {
err := os.MkdirAll(dataDir, 0755)
if err != nil {
t.Fatalf("Cannot create %s: %s", dataDir, err)
}
return dataDir
}
func removeDataDir(t *testing.T) {
dir := "testData"
err := os.RemoveAll(dir)
err := os.RemoveAll(dataDir)
if err != nil {
t.Fatalf("Cannot remove %s: %s", dir, err)
t.Fatalf("Cannot remove %s: %s", dataDir, err)
}
}
func sendTestMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup) {
defer func() {
g.Done()
}()
req := createTestMessage()
err := conn.WriteMsg(req)
if err != nil {
t.Fatalf("cannot write message: %s", err)
}
res, err := conn.ReadMsg()
if err != nil {
t.Fatalf("cannot read response to message: %s", err)
}
assertResponse(t, res)
}
// sendTestMessagesAsync sends messages in parallel
// so that we could find race issues
func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) {
g := &sync.WaitGroup{}
g.Add(testMessagesCount)
for i := 0; i < testMessagesCount; i++ {
go sendTestMessageAsync(t, conn, g)
}
g.Wait()
}
func sendTestMessages(t *testing.T, conn *dns.Conn) {
for i := 0; i < 10; i++ {
req := createTestMessage()
err := conn.WriteMsg(req)
if err != nil {
t.Fatalf("cannot write message #%d: %s", i, err)
}
res, err := conn.ReadMsg()
if err != nil {
t.Fatalf("cannot read response to message #%d: %s", i, err)
}
assertResponse(t, res)
}
}
@ -391,3 +563,14 @@ func assertResponse(t *testing.T, reply *dns.Msg, ip string) {
t.Fatalf("DNS server returned wrong answer type instead of A: %v", reply.Answer[0])
}
}
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}

View File

@ -166,7 +166,7 @@ func (filter *filter) update(force bool) (bool, error) {
return false, nil
}
log.Printf("Downloading update for filter %d from %s", filter.ID, filter.URL)
log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL)
resp, err := client.Get(filter.URL)
if resp != nil && resp.Body != nil {
@ -203,7 +203,7 @@ func (filter *filter) update(force bool) (bool, error) {
// Check if the filter has been really changed
if reflect.DeepEqual(filter.Rules, rules) {
log.Printf("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
return false, nil
}

8
go.mod
View File

@ -1,13 +1,13 @@
module github.com/AdguardTeam/AdGuardHome
require (
github.com/AdguardTeam/dnsproxy v0.9.10
github.com/AdguardTeam/dnsproxy v0.11.1
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-test/deep v1.0.1
github.com/gobuffalo/packr v1.19.0
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24
github.com/joomcode/errorx v0.1.0
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b
@ -17,8 +17,8 @@ require (
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
github.com/stretchr/testify v1.2.2
go.uber.org/goleak v0.10.0
golang.org/x/net v0.0.0-20181220203305-927f97764cc3
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
golang.org/x/sys v0.0.0-20190122071731-054c452bb702
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
gopkg.in/yaml.v2 v2.2.1
)

30
go.sum
View File

@ -1,13 +1,13 @@
github.com/AdguardTeam/dnsproxy v0.9.10 h1:q364WlTvC+CS8kJbMy7TCyt4Niqixxw584MQJtCGhJU=
github.com/AdguardTeam/dnsproxy v0.9.10/go.mod h1:IqBhopgNpzB168kMurbjXf86dn50geasBIuGVxY63j0=
github.com/AdguardTeam/dnsproxy v0.11.1 h1:qO5VH0GYF9vdksQRG8frEfJ+CJjsPBwuct8FH6Mij7o=
github.com/AdguardTeam/dnsproxy v0.11.1/go.mod h1:lEi2srAWwfSQWoy8GeZR6lwS+FSMoiZid8bQPreOLb0=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ameshkov/dnscrypt v1.0.4 h1:vtwHm5m4R2dhcCx23wiI+gNBoy7qm4h7+kZ4Pucw/vE=
github.com/ameshkov/dnscrypt v1.0.4/go.mod h1:hVW52S6r0QvUpIwsyfZ1ifYYpfGu5pewD3pl7afMJcQ=
github.com/ameshkov/dnscrypt v1.0.6 h1:55wfnNF8c4E3JXDNlwPl2Pbs7UPPIh+kI6KK3THqYS0=
github.com/ameshkov/dnscrypt v1.0.6/go.mod h1:ZvT9LaNaJfDNXKIbkYFf24HUgHuQR6MNT6nwVvN4jMQ=
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
@ -26,13 +26,11 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSq
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 h1:FMAReGTEDNr4AdbScv/PqzjMQUpkkVHiF/t8sDHQQVQ=
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4/go.mod h1:H6Ev6svFxUVPFThxLtdnFfcE9e3GWufpfmcVFpqV6HM=
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24 h1:yyDtaSMcAZdm1I6uL8YLghpWiJljfBHs8NC/P86PYQk=
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24/go.mod h1:H6Ev6svFxUVPFThxLtdnFfcE9e3GWufpfmcVFpqV6HM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff h1:6NvhExg4omUC9NfA+l4Oq3ibNNeJUdiAF3iBVB0PlDk=
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff/go.mod h1:ddfPX8Z28YMjiqoaJhNBzWHapTHXejnB5cDCUWDwriw=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc=
@ -67,21 +65,21 @@ go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190122013713-64072686203f h1:u1CmMhe3a44hy8VIgpInORnI01UVaUYheqR7x9BxT3c=
golang.org/x/crypto v0.0.0-20190122013713-64072686203f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 h1:gT0Y6H7hbVPUtvtk0YGxMXPgN+p8fYlqWkgJeUCZcaQ=
golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190122071731-054c452bb702 h1:Lk4tbZFnlyPgV+sLgTw5yGfzrlOn9kx4vSombi2FFlY=
golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477 h1:5xUJw+lg4zao9W4HIDzlFbMYgSgtvNVHh00MEHvbGpQ=
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477/go.mod h1:QDV1vrFSrowdoOba0UM8VJPUZONT7dnfdLsM+GG53Z8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -8,12 +8,15 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/joomcode/errorx"
)
// ----------------------------------
@ -137,12 +140,32 @@ func preInstallHandler(handler http.Handler) http.Handler {
}
// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise
// it also enforces HTTPS if it is enabled and configured
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if config.firstRun && !strings.HasPrefix(r.URL.Path, "/install.") {
http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
return
}
// enforce https?
if config.TLS.ForceHTTPS && r.TLS == nil && config.TLS.Enabled && config.TLS.PortHTTPS != 0 && httpsServer.server != nil {
// yes, and we want host from host:port
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// no port in host
host = r.Host
}
// construct new URL to redirect to
newURL := url.URL{
Scheme: "https",
Host: net.JoinHostPort(host, strconv.Itoa(config.TLS.PortHTTPS)),
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
handler(w, r)
}
}
@ -216,6 +239,56 @@ func getValidNetInterfaces() ([]net.Interface, error) {
return netIfaces, nil
}
// getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only
// we do not return link-local addresses here
func getValidNetInterfacesForWeb() ([]netInterface, error) {
ifaces, err := getValidNetInterfaces()
if err != nil {
return nil, errorx.Decorate(err, "Couldn't get interfaces")
}
if len(ifaces) == 0 {
return nil, errors.New("couldn't find any legible interface")
}
var netInterfaces []netInterface
for _, iface := range ifaces {
addrs, e := iface.Addrs()
if e != nil {
return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
}
netIface := netInterface{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(),
}
if iface.Flags != 0 {
netIface.Flags = iface.Flags.String()
}
// we don't want link-local addresses in json, so skip them
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
return nil, fmt.Errorf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
}
// ignore link-local
if ipnet.IP.IsLinkLocalUnicast() {
continue
}
netIface.Addresses = append(netIface.Addresses, ipnet.IP.String())
}
if len(netIface.Addresses) != 0 {
netInterfaces = append(netInterfaces, netIface)
}
}
return netInterfaces, nil
}
// checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily
func checkPortAvailable(host string, port int) error {
ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))

25
helpers_test.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"testing"
"github.com/hmage/golibs/log"
)
func TestGetValidNetInterfacesForWeb(t *testing.T) {
ifaces, err := getValidNetInterfacesForWeb()
if err != nil {
t.Fatalf("Cannot get net interfaces: %s", err)
}
if len(ifaces) == 0 {
t.Fatalf("No net interfaces found")
}
for _, iface := range ifaces {
if len(iface.Addresses) == 0 {
t.Fatalf("No addresses found for %s", iface.Name)
}
log.Printf("%v", iface)
}
}

View File

@ -2,7 +2,7 @@ swagger: '2.0'
info:
title: 'AdGuard Home'
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
version: 0.92.0
version: 0.93.0
schemes:
- http
basePath: /control
@ -12,6 +12,9 @@ tags:
-
name: global
description: 'AdGuard Home server general settings and controls'
-
name: tls
description: 'AdGuard Home HTTPS/DOH/DOT settings'
-
name: log
description: 'AdGuard Home query log'
@ -36,6 +39,9 @@ tags:
-
name: dhcp
description: 'Built-in DHCP server controls'
-
name: install
description: 'First-time install configuration handlers'
paths:
# API TO-DO LIST
@ -267,6 +273,70 @@ paths:
200:
description: OK
# --------------------------------------------------
# TLS server methods
# --------------------------------------------------
/tls/status:
get:
tags:
- tls
operationId: tlsStatus
summary: "Returns TLS configuration and its status"
responses:
200:
description: OK
schema:
$ref: "#/definitions/TlsConfig"
/tls/configure:
post:
tags:
- tls
operationId: tlsConfigure
summary: "Updates current TLS configuration"
consumes:
- application/json
parameters:
- in: "body"
name: "body"
description: "TLS configuration JSON"
required: true
schema:
$ref: "#/definitions/TlsConfig"
responses:
200:
description: "TLS configuration and its status"
schema:
$ref: "#/definitions/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"
consumes:
- application/json
parameters:
- in: "body"
name: "body"
description: "TLS configuration JSON"
required: true
schema:
$ref: "#/definitions/TlsConfig"
responses:
200:
description: "TLS configuration and its status"
schema:
$ref: "#/definitions/TlsConfig"
400:
description: "Invalid configuration or unavailable port"
# --------------------------------------------------
# DHCP server methods
# --------------------------------------------------
@ -646,6 +716,42 @@ paths:
text/plain:
en
# --------------------------------------------------
# First-time install configuration methods
# --------------------------------------------------
/install/get_addresses:
get:
tags:
- install
operationId: installGetAddresses
summary: "Gets the network interfaces information."
responses:
200:
description: OK
schema:
$ref: "#/definitions/AddressesInfo"
/install/configure:
post:
tags:
- install
operationId: installConfigure
summary: "Applies the initial configuration."
parameters:
- in: "body"
name: "body"
description: "Initial configuration JSON"
required: true
schema:
$ref: "#/definitions/InitialConfiguration"
responses:
200:
description: OK
400:
description: "Failed to parse initial configuration or cannot listen to the specified addresses"
500:
description: "Cannot start the DNS server"
definitions:
ServerStatus:
type: "object"
@ -1063,4 +1169,147 @@ definitions:
type: "array"
description: "Query log"
items:
$ref: "#/definitions/QueryLogItem"
$ref: "#/definitions/QueryLogItem"
TlsConfig:
type: "object"
description: "TLS configuration settings and status"
properties:
# TLS configuration
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: "int32"
example: 443
description: "HTTPS port. If 0, HTTPS will be disabled."
port_dns_over_tls:
type: "integer"
format: "int32"
example: 853
description: "DNS-over-TLS port. If 0, DOT 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"
# Below goes validation fields
valid_cert:
type: "boolean"
example: "true"
description: "valid_cert is true if the specified certificates chain is a valid chain of X509 certificates"
valid_chain:
type: "boolean"
example: "true"
description: "valid_chain is true if the specified certificates chain is verified and issued by a known CA"
subject:
type: "string"
example: "CN=example.org"
description: "subject is 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: "issuer is the issuer of the first certificate in the chain"
not_before:
type: "string"
example: "2019-01-31T10:47:32Z"
description: "not_before is the NotBefore field of the first certificate in the chain"
not_after:
type: "string"
example: "2019-05-01T10:47:32Z"
description: "not_after is the NotAfter field of the first certificate in the chain"
dns_names:
type: "array"
items:
type: "string"
description: "dns_names is the value of SubjectAltNames field of the first certificate in the chain"
example:
- "*.example.org"
valid_key:
type: "boolean"
example: "true"
description: "valid_key is true if the key is a valid private key"
key_type:
type: "string"
example: "RSA"
description: "key_type is either RSA or ECDSA"
warning_validation:
type: "string"
example: "You have specified an empty certificate"
description: "warning_validation is a validation warning message with the issue description"
NetInterface:
type: "object"
description: "Network interface info"
properties:
flags:
type: "string"
example: "up|broadcast|multicast"
hardware_address:
type: "string"
example: "52:54:00:11:09:ba"
mtu:
type: "integer"
format: "int32"
example: 1500
name:
type: "string"
example: "eth0"
ip_addresses:
type: "array"
items:
type: "string"
example:
- "127.0.0.1"
AddressInfo:
type: "object"
description: "Port information"
properties:
ip:
type: "string"
example: "127.0.01"
port:
type: "integer"
format: "int32"
example: 53
warning:
type: "string"
example: "Cannot bind to this port"
AddressesInfo:
type: "object"
description: "AdGuard Home addresses configuration"
properties:
dns:
$ref: "#/definitions/AddressInfo"
web:
$ref: "#/definitions/AddressInfo"
interfaces:
type: "object"
description: "Network interfaces dictionary (key is the interface name)"
additionalProperties:
$ref: "#/definitions/NetInterface"
InitialConfiguration:
type: "object"
description: "AdGuard Home initial configuration (for the first-install wizard)"
properties:
dns:
$ref: "#/definitions/AddressInfo"
web:
$ref: "#/definitions/AddressInfo"
username:
type: "string"
description: "Basic auth username"
example: "admin"
password:
type: "string"
description: "Basic auth password"
example: "password"

View File

@ -9,20 +9,23 @@ version=`git describe --abbrev=4 --dirty --always --tags`
f() {
make cleanfast; CGO_DISABLED=1 make
if [[ $GOOS == darwin ]]; then
rm -f dist/AdGuardHome_"$version"_MacOS.zip
zip dist/AdGuardHome_"$version"_MacOS.zip AdGuardHome README.md LICENSE.txt
elif [[ $GOOS == windows ]]; then
rm -f dist/AdGuardHome_"$version"_Windows.zip
zip dist/AdGuardHome_"$version"_Windows.zip AdGuardHome.exe README.md LICENSE.txt
else
tar zcvf dist/AdGuardHome_"$version"_"$GOOS"_"$GOARCH".tar.gz AdGuardHome README.md LICENSE.txt
rm -rf dist/AdguardHome
mkdir -p dist/AdGuardHome
cp -pv {AdGuardHome,LICENSE.txt,README.md} dist/AdGuardHome/
pushd dist
tar zcvf AdGuardHome_"$version"_"$GOOS"_"$GOARCH".tar.gz AdGuardHome/{AdGuardHome,LICENSE.txt,README.md}
popd
rm -rf dist/AdguardHome
fi
}
# Clean and rebuild both static and binary
make clean
make
# Prepare the dist folder
rm -rf dist
mkdir -p dist
# Prepare releases