Merge branch 'master' into fix/576
This commit is contained in:
commit
d351ed82c1
|
@ -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
|
||||
|
|
|
@ -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
95
app.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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."
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>:
|
||||
{subject}
|
||||
</li>
|
||||
}
|
||||
{issuer &&
|
||||
<li>
|
||||
<Trans>encryption_issuer</Trans>:
|
||||
{issuer}
|
||||
</li>
|
||||
}
|
||||
{not_after && not_after !== EMPTY_DATE &&
|
||||
<li>
|
||||
<Trans>encryption_expire</Trans>:
|
||||
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
|
||||
</li>
|
||||
}
|
||||
{dns_names &&
|
||||
<li>
|
||||
<Trans>encryption_hostnames</Trans>:
|
||||
{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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer__column">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.update {
|
||||
.topline {
|
||||
position: relative;
|
||||
z-index: 102;
|
||||
margin-bottom: 0;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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);
|
|
@ -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}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
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 (
|
||||
const Greeting = () => (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<h1 className="setup__title">
|
||||
|
@ -16,8 +14,6 @@ class Greeting extends Component {
|
|||
</div>
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default withNamespaces()(Greeting);
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
||||
|
|
53
config.go
53
config.go
|
@ -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"`
|
||||
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"},
|
||||
|
|
384
control.go
384
control.go
|
@ -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
13
dns.go
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
8
go.mod
|
@ -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
30
go.sum
|
@ -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=
|
||||
|
|
73
helpers.go
73
helpers.go
|
@ -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)))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
@ -1064,3 +1170,146 @@ definitions:
|
|||
description: "Query log"
|
||||
items:
|
||||
$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"
|
15
release.sh
15
release.sh
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue