diff --git a/Makefile b/Makefile index 9e9ae505..9cc18d7a 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ client/node_modules: client/package.json client/package-lock.json $(STATIC): $(JSFILES) client/node_modules npm --prefix client run build-prod -$(TARGET): $(STATIC) *.go dnsfilter/*.go dnsforward/*.go +$(TARGET): $(STATIC) *.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go go get -d . GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/... PATH=$(GOPATH)/bin:$(PATH) packr -z diff --git a/app.go b/app.go index bbe36359..9439eb8a 100644 --- a/app.go +++ b/app.go @@ -3,7 +3,6 @@ package main import ( "bufio" "fmt" - "log" "net" "net/http" "os" @@ -14,6 +13,7 @@ import ( "time" "github.com/gobuffalo/packr" + "github.com/hmage/golibs/log" "golang.org/x/crypto/ssh/terminal" ) @@ -44,27 +44,30 @@ func main() { // config can be specified, which reads options from there, but other command line flags have to override config values // therefore, we must do it manually instead of using a lib { + var printHelp func() var configFilename *string var bindHost *string var bindPort *int var opts = []struct { - longName string - shortName string - description string - callback func(value string) + longName string + shortName string + description string + callbackWithValue func(value string) + callbackNoValue func() }{ - {"config", "c", "path to config file", func(value string) { configFilename = &value }}, - {"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }}, + {"config", "c", "path to config file", func(value string) { configFilename = &value }, nil}, + {"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }, nil}, {"port", "p", "port to serve HTTP pages on", func(value string) { v, err := strconv.Atoi(value) if err != nil { panic("Got port that is not a number") } bindPort = &v - }}, - {"help", "h", "print this help", nil}, + }, nil}, + {"verbose", "v", "enable verbose output", nil, func() { log.Verbose = true }}, + {"help", "h", "print this help", nil, func() { printHelp(); os.Exit(64) }}, } - printHelp := func() { + printHelp = func() { fmt.Printf("Usage:\n\n") fmt.Printf("%s [options]\n\n", os.Args[0]) fmt.Printf("Options:\n") @@ -74,30 +77,19 @@ func main() { } for i := 1; i < len(os.Args); i++ { v := os.Args[i] - // short-circuit for help - if v == "--help" || v == "-h" { - printHelp() - os.Exit(64) - } knownParam := false for _, opt := range opts { - if v == "--"+opt.longName { - if i+1 > len(os.Args) { - log.Printf("ERROR: Got %s without argument\n", v) - os.Exit(64) + if v == "--"+opt.longName || v == "-"+opt.shortName { + if opt.callbackWithValue != nil { + if i+1 > len(os.Args) { + log.Printf("ERROR: Got %s without argument\n", v) + os.Exit(64) + } + i++ + opt.callbackWithValue(os.Args[i]) + } else if opt.callbackNoValue != nil { + opt.callbackNoValue() } - i++ - opt.callback(os.Args[i]) - knownParam = true - break - } - if v == "-"+opt.shortName { - if i+1 > len(os.Args) { - log.Printf("ERROR: Got %s without argument\n", v) - os.Exit(64) - } - i++ - opt.callback(os.Args[i]) knownParam = true break } @@ -192,6 +184,11 @@ func main() { log.Fatal(err) } + err = startDHCPServer() + if err != nil { + log.Fatal(err) + } + URL := fmt.Sprintf("http://%s", address) log.Println("Go to " + URL) log.Fatal(http.ListenAndServe(address, nil)) diff --git a/client/package-lock.json b/client/package-lock.json index fc6ea4f1..585d2b4c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4126,6 +4126,11 @@ "next-tick": "1" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", @@ -6588,7 +6593,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -6638,7 +6643,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -7387,8 +7392,7 @@ "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, "is-regex": { "version": "1.0.4", @@ -13202,6 +13206,21 @@ "reduce-reducers": "^0.1.0" } }, + "redux-form": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.4.2.tgz", + "integrity": "sha512-QxC36s4Lelx5Cr8dbpxqvl23dwYOydeAX8c6YPmgkz/Dhj053C16S2qoyZN6LO6HJ2oUF00rKsAyE94GwOUhFA==", + "requires": { + "es6-error": "^4.1.1", + "hoist-non-react-statics": "^2.5.4", + "invariant": "^2.2.4", + "is-promise": "^2.1.0", + "lodash": "^4.17.10", + "lodash-es": "^4.17.10", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4" + } + }, "redux-thunk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", @@ -15003,7 +15022,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, diff --git a/client/package.json b/client/package.json index 7d200e80..6b518d60 100644 --- a/client/package.json +++ b/client/package.json @@ -31,6 +31,7 @@ "react-transition-group": "^2.4.0", "redux": "^4.0.0", "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" diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index f6045521..b248ed6c 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -1,4 +1,30 @@ { + "check_dhcp_servers": "Check for DHCP servers", + "save_config": "Save config", + "enabled_dhcp": "DHCP server enabled", + "disabled_dhcp": "DHCP server disabled", + "dhcp_title": "DHCP server (experimental!)", + "dhcp_description": "If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.", + "dhcp_enable": "Enable DHCP server", + "dhcp_disable": "Disable DHCP server", + "dhcp_not_found": "No active DHCP servers found on the network. It is safe to enable the built-in DHCP server.", + "dhcp_found": "Found active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.", + "dhcp_leases": "DHCP leases", + "dhcp_leases_not_found": "No DHCP leases found", + "dhcp_config_saved": "Saved DHCP server config", + "form_error_required": "Required field", + "form_error_ip_format": "Invalid IPv4 format", + "form_error_positive": "Must be greater than 0", + "dhcp_form_gateway_input": "Gateway IP", + "dhcp_form_subnet_input": "Subnet mask", + "dhcp_form_range_title": "Range of IP addresses", + "dhcp_form_range_start": "Range start", + "dhcp_form_range_end": "Range end", + "dhcp_form_lease_title": "DHCP lease time (in seconds)", + "dhcp_form_lease_input": "Lease duration", + "dhcp_interface_select": "Select DHCP interface", + "dhcp_hardware_address": "Hardware address", + "dhcp_ip_addresses": "IP addresses", "back": "Back", "dashboard": "Dashboard", "settings": "Settings", @@ -89,7 +115,7 @@ "example_upstream_regular": "regular DNS (over UDP)", "example_upstream_dot": "encrypted DNS-over-TLS<\/a>", "example_upstream_doh": "encrypted DNS-over-HTTPS<\/a>", - "example_upstream_sdns": "you can use DNS Stamps for DNSCrypt or DNS-over-HTTPS resolvers", + "example_upstream_sdns": "you can use DNS Stamps<\/a> for DNSCrypt<\/a> or DNS-over-HTTPS<\/a> resolvers", "example_upstream_tcp": "regular DNS (over TCP)", "all_filters_up_to_date_toast": "All filters are already up-to-date", "updated_upstream_dns_toast": "Updated the upstream DNS servers", diff --git a/client/src/__locales/ja.json b/client/src/__locales/ja.json index ab4b766c..5af7af43 100644 --- a/client/src/__locales/ja.json +++ b/client/src/__locales/ja.json @@ -1,10 +1,32 @@ { + "refresh_status": "\u30b9\u30c6\u30fc\u30bf\u30b9\u3092\u6700\u65b0\u306b\u3059\u308b", + "save_config": "\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3059\u308b", + "enabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f", + "disabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f", + "dhcp_title": "DHCP\u30b5\u30fc\u30d0", + "dhcp_description": "\u3042\u306a\u305f\u306e\u30eb\u30fc\u30bf\u304cDHCP\u306e\u8a2d\u5b9a\u3092\u63d0\u4f9b\u3057\u3066\u3044\u306a\u3044\u306e\u306a\u3089\u3001AdGuard\u306b\u5185\u8535\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u5229\u7528\u3067\u304d\u307e\u3059\u3002", + "dhcp_enable": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b", + "dhcp_disable": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3059\u308b", + "dhcp_not_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u5185\u8535\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u5b89\u5168\u3067\u3059\u3002", + "dhcp_leases": "DHCP\u5272\u5f53", + "dhcp_leases_not_found": "DHCP\u5272\u5f53\u306f\u3042\u308a\u307e\u305b\u3093", + "dhcp_config_saved": "DHCP\u30b5\u30fc\u30d0\u306e\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3057\u305f", + "form_error_required": "\u5fc5\u9808\u9805\u76ee\u3067\u3059", + "form_error_ip_format": "IPv4\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "form_error_positive": "0\u3088\u308a\u5927\u304d\u3044\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "dhcp_form_gateway_input": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4IP", + "dhcp_form_subnet_input": "\u30b5\u30d6\u30cd\u30c3\u30c8\u30de\u30b9\u30af", + "dhcp_form_range_title": "IP\u30a2\u30c9\u30ec\u30b9\u306e\u7bc4\u56f2", + "dhcp_form_range_start": "\u7bc4\u56f2\u306e\u958b\u59cb", + "dhcp_form_range_end": "\u7bc4\u56f2\u306e\u7d42\u4e86", + "dhcp_form_lease_title": "DHCP\u5272\u5f53\u6642\u9593\uff08\u79d2\u5358\u4f4d\uff09", + "dhcp_form_lease_input": "\u5272\u5f53\u671f\u9593", "back": "\u623b\u308b", "dashboard": "\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9", "settings": "\u8a2d\u5b9a", "filters": "\u30d5\u30a3\u30eb\u30bf", "query_log": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0", - "faq": "FAQ", + "faq": "\u3088\u304f\u3042\u308b\u8cea\u554f", "version": "\u30d0\u30fc\u30b8\u30e7\u30f3", "address": "\u30a2\u30c9\u30ec\u30b9", "on": "\u30aa\u30f3", @@ -16,18 +38,18 @@ "enabled_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f", "disable_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3059\u308b", "disabled_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f", - "refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u66f4\u65b0\u3059\u308b", + "refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u6700\u65b0\u306b\u3059\u308b", "dns_query": "DNS\u30af\u30a8\u30ea", - "blocked_by": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30af\u30a8\u30ea\u30d5\u30a3\u30eb\u30bf", + "blocked_by": "\u30d5\u30a3\u30eb\u30bf\u306b\u3088\u308a\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30af\u30a8\u30ea", "stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0", "stats_adult": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8", "stats_query_domain": "\u6700\u3082\u554f\u5408\u305b\u3055\u308c\u305f\u30c9\u30e1\u30a4\u30f3", "for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u4ee5\u5185", - "no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u60c5\u5831\u306f\u3042\u308a\u307e\u305b\u3093", "requests_count": "\u30ea\u30af\u30a8\u30b9\u30c8\u6570", "top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30c9\u30e1\u30a4\u30f3", "top_clients": "\u30c8\u30c3\u30d7\u30af\u30e9\u30a4\u30a2\u30f3\u30c8", - "no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u60c5\u5831\u306f\u3042\u308a\u307e\u305b\u3093", "general_statistics": "\u5168\u822c\u7684\u306a\u7d71\u8a08", "number_of_dns_query_24_hours": "\u904e\u53bb24\u6642\u9593\u306b\u51e6\u7406\u3055\u308c\u305fDNS\u30af\u30a8\u30ea\u306e\u6570", "number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570", @@ -39,18 +61,18 @@ "average_processing_time_hint": "DNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u51e6\u7406\u306b\u304b\u304b\u308b\u5e73\u5747\u6642\u9593\uff08\u30df\u30ea\u79d2\u5358\u4f4d\uff09", "block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b", "filters_block_toggle_hint": "\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30af\u3059\u308b\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", - "use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b", - "use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u30fc\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002", - "use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebWeb\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b", - "use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002", + "use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b", + "use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002", + "use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b", + "use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002", "enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u5f37\u5236\u3059\u308b", "enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u5f37\u5236\u3067\u304d\u307e\u3059\u3002", - "no_servers_specified": "\u30b5\u30fc\u30d0\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "no_servers_specified": "\u30b5\u30fc\u30d0\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "no_settings": "\u8a2d\u5b9a\u306a\u3057", "general_settings": "\u4e00\u822c\u8a2d\u5b9a", - "upstream_dns": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc", - "upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u672a\u5165\u529b\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306fCloudflare DNS<\/a>\u3092\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u3068\u3057\u3066\u4f7f\u7528\u3057\u307e\u3059\u3002DNS over TLS\u30b5\u30fc\u30d0\u30fc\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "test_upstream_btn": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u30b5\u30fc\u30d0\u30fc\u3092\u30c6\u30b9\u30c8\u3059\u308b", + "upstream_dns": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0", + "upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u672a\u5165\u529b\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f\u4e0a\u6d41\u3068\u3057\u3066Cloudflare DNS<\/a>\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002DNS over TLS\u30b5\u30fc\u30d0\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "test_upstream_btn": "\u4e0a\u6d41\u30b5\u30fc\u30d0\u3092\u30c6\u30b9\u30c8\u3059\u308b", "apply_btn": "\u9069\u7528\u3059\u308b", "disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f", "enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f", @@ -66,7 +88,7 @@ "rules_count_table_header": "\u30eb\u30fc\u30eb\u6570", "last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u6642\u523b", "actions_table_header": "\u64cd\u4f5c", - "delete_table_action": "\u524a\u9664", + "delete_table_action": "\u524a\u9664\u3059\u308b", "filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8", "filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068hosts\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u307e\u3059\u3002", "no_filters_added": "\u30d5\u30a3\u30eb\u30bf\u306f\u8ffd\u52a0\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", @@ -82,17 +104,18 @@ "examples_title": "\u4f8b", "example_meaning_filter_block": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b", "example_meaning_filter_whitelist": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306e\u30d6\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3059\u308b", - "example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3092\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3057\u305f\u3002", + "example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3092\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3059\u3002", "example_comment": "! \u3053\u3053\u306b\u306f\u30b3\u30e1\u30f3\u30c8\u304c\u5165\u308a\u307e\u3059", "example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8\u3067\u3059", "example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8\u3067\u3059", "example_upstream_regular": "\u901a\u5e38\u306eDNS\uff08UDP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09", "example_upstream_dot": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b DNS-over-TLS<\/a>", "example_upstream_doh": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b DNS-over-HTTPS<\/a>", + "example_upstream_sdns": "DNSCrypt<\/a> \u307e\u305f\u306f DNS-over-HTTPS<\/a> \u30ea\u30be\u30eb\u30d0\u306e\u305f\u3081\u306b DNS Stamps<\/a> \u3092\u4f7f\u3048\u307e\u3059", "example_upstream_tcp": "\u901a\u5e38\u306eDNS\uff08TCP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09", "all_filters_up_to_date_toast": "\u3059\u3079\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u6700\u65b0\u3067\u3059", - "updated_upstream_dns_toast": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f", - "dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059", + "updated_upstream_dns_toast": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f", + "dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059", "dns_test_not_ok_toast": "\u30b5\u30fc\u30d0 \"{{key}}\": \u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b63\u3057\u304f\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", "unblock_btn": "\u30d6\u30ed\u30c3\u30af\u89e3\u9664", "block_btn": "\u30d6\u30ed\u30c3\u30af", @@ -104,7 +127,7 @@ "empty_response_status": "\u672a\u5b9a\u7fa9", "show_all_filter_type": "\u3059\u3079\u3066\u8868\u793a", "show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u3055\u308c\u305f\u30ed\u30b0\u3092\u8868\u793a", - "no_logs_found": "\u30ed\u30b0\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "no_logs_found": "\u30ed\u30b0\u306f\u3042\u308a\u307e\u305b\u3093", "disabled_log_btn": "\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3059\u308b", "download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b", "refresh_btn": "\u6700\u65b0\u306b\u3059\u308b", @@ -125,5 +148,6 @@ "found_in_known_domain_db": "\u65e2\u77e5\u306e\u30c9\u30e1\u30a4\u30f3\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002", "category_label": "\u30ab\u30c6\u30b4\u30ea", "rule_label": "\u30eb\u30fc\u30eb", - "filter_label": "\u30d5\u30a3\u30eb\u30bf" + "filter_label": "\u30d5\u30a3\u30eb\u30bf", + "unknown_filter": "\u4e0d\u660e\u306a\u30d5\u30a3\u30eb\u30bf {{filterId}}" } \ No newline at end of file diff --git a/client/src/__locales/pt-br.json b/client/src/__locales/pt-br.json index 91705df6..1467b370 100644 --- a/client/src/__locales/pt-br.json +++ b/client/src/__locales/pt-br.json @@ -1,4 +1,26 @@ { + "refresh_status": "Atualizar status", + "save_config": "Salvar configura\u00e7\u00e3o", + "enabled_dhcp": "Servidor DHCP ativado", + "disabled_dhcp": "Servidor DHCP desativado", + "dhcp_title": "Servidor DHCP", + "dhcp_description": "Se o seu roteador n\u00e3o fornecer configura\u00e7\u00f5es de DHCP, voc\u00ea poder\u00e1 usar o servidor DHCP integrado do AdGuard.", + "dhcp_enable": "Ativar servidor DHCP", + "dhcp_disable": "Desativar servidor DHCP", + "dhcp_not_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. \u00c9 seguro ativar o servidor DHCP integrado.", + "dhcp_leases": "Concess\u00f5es DHCP", + "dhcp_leases_not_found": "Nenhuma concess\u00e3o DHCP encontrada", + "dhcp_config_saved": "Salvar configura\u00e7\u00f5es do servidor DHCP", + "form_error_required": "Campo obrigat\u00f3rio", + "form_error_ip_format": "formato de endere\u00e7o IPv4 inv\u00e1lido", + "form_error_positive": "Deve ser maior que 0", + "dhcp_form_gateway_input": "IP do gateway", + "dhcp_form_subnet_input": "M\u00e1scara de sub-rede", + "dhcp_form_range_title": "Faixa de endere\u00e7os IP", + "dhcp_form_range_start": "In\u00edcio da faixa", + "dhcp_form_range_end": "Final da faixa", + "dhcp_form_lease_title": "Tempo de concess\u00e3o do DHCP (em segundos)", + "dhcp_form_lease_input": "Dura\u00e7\u00e3o da concess\u00e3o", "back": "Voltar", "dashboard": "Painel", "settings": "Configura\u00e7\u00f5es", @@ -18,7 +40,7 @@ "disabled_protection": "Prote\u00e7\u00e3o desativada", "refresh_statics": "Atualizar estat\u00edsticas", "dns_query": "Consultas de DNS", - "blocked_by": "Bloqueador por Filtros", + "blocked_by": "Bloqueador por filtros", "stats_malware_phishing": "Bloqueado malware\/phishing", "stats_adult": "Bloqueado sites adultos", "stats_query_domain": "Principais dom\u00ednios consultados", @@ -89,6 +111,7 @@ "example_upstream_regular": "DNS regular (atrav\u00e9s do UDP)", "example_upstream_dot": "DNS criptografado atrav\u00e9s do TLS<\/a>", "example_upstream_doh": "DNS criptografado atrav\u00e9s do HTTPS<\/a>", + "example_upstream_sdns": "Voc\u00ea pode usar DNS Stamps<\/a>para oDNSCrypt<\/a>ou usar resolvedoresDNS-sobre-HTTPS<\/a>", "example_upstream_tcp": "DNS regular (atrav\u00e9s do TCP)", "all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados", "updated_upstream_dns_toast": "Atualizado os servidores DNS upstream", @@ -125,5 +148,6 @@ "found_in_known_domain_db": "Encontrado no banco de dados de dom\u00ednios conhecidos.", "category_label": "Categoria", "rule_label": "Regra", - "filter_label": "Filtro" + "filter_label": "Filtro", + "unknown_filter": "Filtro desconhecido {{filterId}}" } \ No newline at end of file diff --git a/client/src/__locales/sv.json b/client/src/__locales/sv.json index fb75e557..fdea033a 100644 --- a/client/src/__locales/sv.json +++ b/client/src/__locales/sv.json @@ -1,4 +1,26 @@ { + "refresh_status": "Uppdatera status", + "save_config": "Spara inst\u00e4llningar", + "enabled_dhcp": "DHCP-server aktiverad", + "disabled_dhcp": "Dhcp-server avaktiverad", + "dhcp_title": "DHCP-server", + "dhcp_description": "Om din router inte har inst\u00e4llningar f\u00f6r DHCP kan du anv\u00e4nda AdGuards inbyggda server.", + "dhcp_enable": "Aktivera DHCP.-server", + "dhcp_disable": "Avaktivera DHCP-server", + "dhcp_not_found": "Ingen aktiv DHCP-server hittades i n\u00e4tverkat.", + "dhcp_leases": "DHCP-lease", + "dhcp_leases_not_found": "Ingen DHCP-lease hittad", + "dhcp_config_saved": "Sparade inst\u00e4llningar f\u00f6r DHCP-servern", + "form_error_required": "Obligatoriskt f\u00e4lt", + "form_error_ip_format": "Ogiltigt IPv4-format", + "form_error_positive": "M\u00e5ste vara st\u00f6rre \u00e4n noll", + "dhcp_form_gateway_input": "Gateway-IP", + "dhcp_form_subnet_input": "Subnetmask", + "dhcp_form_range_title": "IP-adressgr\u00e4nser", + "dhcp_form_range_start": "Startgr\u00e4ns", + "dhcp_form_range_end": "Gr\u00e4nsslut", + "dhcp_form_lease_title": "DHCP-leasetid (i sekunder)", + "dhcp_form_lease_input": "Leasetid", "back": "Tiilbaka", "dashboard": "Kontrollpanel", "settings": "Inst\u00e4llningar", @@ -18,7 +40,7 @@ "disabled_protection": "Kopplade bort skydd", "refresh_statics": "Uppdatera statistik", "dns_query": "DNS-f\u00f6rfr\u00e5gningar", - "blocked_by": "Blockerat av Filter", + "blocked_by": "Blockerat av filter", "stats_malware_phishing": "Blockerad skadekod\/phising", "stats_adult": "Blockerade vuxensajter", "stats_query_domain": "Mest efters\u00f6kta dom\u00e4ner", @@ -89,6 +111,7 @@ "example_upstream_regular": "vanlig DNS (\u00f6ver UDP)", "example_upstream_dot": "krypterat DNS-over-TLS<\/a>", "example_upstream_doh": "krypterat DNS-over-HTTPS<\/a>", + "example_upstream_sdns": "Du kan anv\u00e4nda DNS-stamps<\/a> f\u00f6r DNSCrypt<\/a> eller DNS-\u00f6ver-HTTPS<\/a>\n-resolvers", "example_upstream_tcp": "vanlig DNS (\u00f6ver UDP)", "all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella", "updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar", @@ -125,5 +148,6 @@ "found_in_known_domain_db": "Hittad i dom\u00e4ndatabas.", "category_label": "Kategori", "rule_label": "Regel", - "filter_label": "Filter" + "filter_label": "Filter", + "unknown_filter": "Ok\u00e4nt filter {{filterId}}" } \ No newline at end of file diff --git a/client/src/actions/index.js b/client/src/actions/index.js index a4da8c9a..949e3a5d 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -522,3 +522,130 @@ export const getLanguage = () => async (dispatch) => { dispatch(getLanguageFailure()); } }; + +export const getDhcpStatusRequest = createAction('GET_DHCP_STATUS_REQUEST'); +export const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS'); +export const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE'); + +export const getDhcpStatus = () => async (dispatch) => { + dispatch(getDhcpStatusRequest()); + try { + const status = await apiClient.getDhcpStatus(); + dispatch(getDhcpStatusSuccess(status)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getDhcpStatusFailure()); + } +}; + +export const getDhcpInterfacesRequest = createAction('GET_DHCP_INTERFACES_REQUEST'); +export const getDhcpInterfacesSuccess = createAction('GET_DHCP_INTERFACES_SUCCESS'); +export const getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE'); + +export const getDhcpInterfaces = () => async (dispatch) => { + dispatch(getDhcpInterfacesRequest()); + try { + const interfaces = await apiClient.getDhcpInterfaces(); + dispatch(getDhcpInterfacesSuccess(interfaces)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getDhcpInterfacesFailure()); + } +}; + +export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST'); +export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS'); +export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE'); + +export const findActiveDhcp = name => async (dispatch) => { + dispatch(findActiveDhcpRequest()); + try { + const activeDhcp = await apiClient.findActiveDhcp(name); + dispatch(findActiveDhcpSuccess(activeDhcp)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(findActiveDhcpFailure()); + } +}; + +export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST'); +export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS'); +export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE'); + +// TODO rewrite findActiveDhcp part +export const setDhcpConfig = config => async (dispatch) => { + dispatch(setDhcpConfigRequest()); + try { + if (config.interface_name) { + dispatch(findActiveDhcpRequest()); + try { + const activeDhcp = await apiClient.findActiveDhcp(config.interface_name); + dispatch(findActiveDhcpSuccess(activeDhcp)); + + if (!activeDhcp.found) { + await apiClient.setDhcpConfig(config); + dispatch(addSuccessToast('dhcp_config_saved')); + dispatch(setDhcpConfigSuccess()); + dispatch(getDhcpStatus()); + } else { + dispatch(addErrorToast({ error: 'dhcp_found' })); + } + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(findActiveDhcpFailure()); + } + } else { + await apiClient.setDhcpConfig(config); + dispatch(addSuccessToast('dhcp_config_saved')); + dispatch(setDhcpConfigSuccess()); + dispatch(getDhcpStatus()); + } + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setDhcpConfigFailure()); + } +}; + +export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST'); +export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE'); +export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS'); + +// TODO rewrite findActiveDhcp part +export const toggleDhcp = config => async (dispatch) => { + dispatch(toggleDhcpRequest()); + + if (config.enabled) { + dispatch(addSuccessToast('disabled_dhcp')); + try { + await apiClient.setDhcpConfig({ ...config, enabled: false }); + dispatch(toggleDhcpSuccess()); + dispatch(getDhcpStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleDhcpFailure()); + } + } else { + dispatch(findActiveDhcpRequest()); + try { + const activeDhcp = await apiClient.findActiveDhcp(config.interface_name); + dispatch(findActiveDhcpSuccess(activeDhcp)); + + if (!activeDhcp.found) { + try { + await apiClient.setDhcpConfig({ ...config, enabled: true }); + dispatch(toggleDhcpSuccess()); + dispatch(getDhcpStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleDhcpFailure()); + } + dispatch(addSuccessToast('enabled_dhcp')); + } else { + dispatch(addErrorToast({ error: 'dhcp_found' })); + } + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(findActiveDhcpFailure()); + } + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index f0d90941..4592fae7 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -302,4 +302,38 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // DHCP + DHCP_STATUS = { path: 'dhcp/status', method: 'GET' }; + DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' }; + DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' }; + DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' }; + + getDhcpStatus() { + const { path, method } = this.DHCP_STATUS; + return this.makeRequest(path, method); + } + + getDhcpInterfaces() { + const { path, method } = this.DHCP_INTERFACES; + return this.makeRequest(path, method); + } + + setDhcpConfig(config) { + const { path, method } = this.DHCP_SET_CONFIG; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + + findActiveDhcp(name) { + const { path, method } = this.DHCP_FIND_ACTIVE; + const parameters = { + data: name, + headers: { 'Content-Type': 'text/plain' }, + }; + return this.makeRequest(path, method, parameters); + } } diff --git a/client/src/components/Settings/Dhcp/Form.js b/client/src/components/Settings/Dhcp/Form.js new file mode 100644 index 00000000..9fc497d2 --- /dev/null +++ b/client/src/components/Settings/Dhcp/Form.js @@ -0,0 +1,149 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { withNamespaces, Trans } from 'react-i18next'; +import flow from 'lodash/flow'; + +import { R_IPV4 } from '../../../helpers/constants'; + +const required = (value) => { + if (value || value === 0) { + return false; + } + return form_error_required; +}; + +const ipv4 = (value) => { + if (value && !new RegExp(R_IPV4).test(value)) { + return form_error_ip_format; + } + return false; +}; + +const isPositive = (value) => { + if ((value || value === 0) && (value <= 0)) { + return form_error_positive; + } + return false; +}; + +const toNumber = value => value && parseInt(value, 10); + +const renderField = ({ + input, className, placeholder, type, disabled, meta: { touched, error }, +}) => ( + + + {!disabled && touched && (error && {error})} + +); + +const Form = (props) => { + const { + t, + handleSubmit, + pristine, + submitting, + } = props; + + return ( + + + + + {t('dhcp_form_gateway_input')} + + + + {t('dhcp_form_subnet_input')} + + + + + + + + {t('dhcp_form_range_title')} + + + + + + + + + + + {t('dhcp_form_lease_title')} + + + + + + + {t('save_config')} + + + ); +}; + +Form.propTypes = { + handleSubmit: PropTypes.func, + pristine: PropTypes.bool, + submitting: PropTypes.bool, + interfaces: PropTypes.object, + processing: PropTypes.bool, + initialValues: PropTypes.object, + t: PropTypes.func, +}; + +export default flow([ + withNamespaces(), + reduxForm({ form: 'dhcpForm' }), +])(Form); diff --git a/client/src/components/Settings/Dhcp/Interface.js b/client/src/components/Settings/Dhcp/Interface.js new file mode 100644 index 00000000..fe4206a7 --- /dev/null +++ b/client/src/components/Settings/Dhcp/Interface.js @@ -0,0 +1,113 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Field, reduxForm, formValueSelector } from 'redux-form'; +import { withNamespaces, Trans } from 'react-i18next'; +import flow from 'lodash/flow'; + +const renderInterfaces = (interfaces => ( + Object.keys(interfaces).map((item) => { + const option = interfaces[item]; + const { name } = option; + const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':')); + let interfaceIP = option.ip_addresses[0]; + + if (!onlyIPv6) { + option.ip_addresses.forEach((ip) => { + if (!ip.includes(':')) { + interfaceIP = ip; + } + }); + } + + return ( + + {name} - {interfaceIP} + + ); + }) +)); + +const renderInterfaceValues = (interfaceValues => ( + + + MTU: + {interfaceValues.mtu} + + + dhcp_hardware_address: + {interfaceValues.hardware_address} + + + dhcp_ip_addresses: + { + interfaceValues.ip_addresses + .map(ip => {ip}) + } + + +)); + +let Interface = (props) => { + const { + t, + handleChange, + interfaces, + processing, + interfaceValue, + enabled, + } = props; + + return ( + + {!processing && interfaces && + + + + {t('dhcp_interface_select')} + + {t('dhcp_interface_select')} + {renderInterfaces(interfaces)} + + + + {interfaceValue && + + {renderInterfaceValues(interfaces[interfaceValue])} + + } + + } + + + ); +}; + +Interface.propTypes = { + handleChange: PropTypes.func, + interfaces: PropTypes.object, + processing: PropTypes.bool, + interfaceValue: PropTypes.string, + initialValues: PropTypes.object, + enabled: PropTypes.bool, + t: PropTypes.func, +}; + +const selector = formValueSelector('dhcpInterface'); + +Interface = connect((state) => { + const interfaceValue = selector(state, 'interface_name'); + return { + interfaceValue, + }; +})(Interface); + +export default flow([ + withNamespaces(), + reduxForm({ form: 'dhcpInterface' }), +])(Interface); diff --git a/client/src/components/Settings/Dhcp/Leases.js b/client/src/components/Settings/Dhcp/Leases.js new file mode 100644 index 00000000..562f3243 --- /dev/null +++ b/client/src/components/Settings/Dhcp/Leases.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactTable from 'react-table'; +import { withNamespaces } from 'react-i18next'; + +const columns = [{ + Header: 'MAC', + accessor: 'mac', +}, { + Header: 'IP', + accessor: 'ip', +}, { + Header: 'Hostname', + accessor: 'hostname', +}, { + Header: 'Expires', + accessor: 'expires', +}]; + +const Leases = props => ( + +); + +Leases.propTypes = { + leases: PropTypes.array, + t: PropTypes.func, +}; + +export default withNamespaces()(Leases); diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js new file mode 100644 index 00000000..3beb136f --- /dev/null +++ b/client/src/components/Settings/Dhcp/index.js @@ -0,0 +1,159 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Trans, withNamespaces } from 'react-i18next'; + +import Form from './Form'; +import Leases from './Leases'; +import Interface from './Interface'; +import Card from '../../ui/Card'; + +class Dhcp extends Component { + handleFormSubmit = (values) => { + this.props.setDhcpConfig(values); + }; + + handleFormChange = (value) => { + this.props.setDhcpConfig(value); + } + + handleToggle = (config) => { + this.props.toggleDhcp(config); + this.props.findActiveDhcp(config.interface_name); + } + + getToggleDhcpButton = () => { + const { config, active } = this.props.dhcp; + const activeDhcpFound = active && active.found; + const filledConfig = Object.keys(config).every((key) => { + if (key === 'enabled') { + return true; + } + + return config[key]; + }); + + if (config.enabled) { + return ( + this.props.toggleDhcp(config)} + > + dhcp_disable + + ); + } + + return ( + this.handleToggle(config)} + disabled={!filledConfig || activeDhcpFound} + > + dhcp_enable + + ); + } + + getActiveDhcpMessage = () => { + const { active } = this.props.dhcp; + + if (active) { + if (active.error) { + return ( + + {active.error} + + ); + } + + return ( + + {active.found ? ( + + dhcp_found + + ) : ( + + dhcp_not_found + + )} + + ); + } + + return ''; + } + + render() { + const { t, dhcp } = this.props; + const statusButtonClass = classnames({ + 'btn btn-primary btn-standart': true, + 'btn btn-primary btn-standart btn-loading': dhcp.processingStatus, + }); + + return ( + + + + {!dhcp.processing && + + + + + + {this.getToggleDhcpButton()} + + this.props.findActiveDhcp(dhcp.config.interface_name) + } + disabled={!dhcp.config.interface_name} + > + check_dhcp_servers + + + {this.getActiveDhcpMessage()} + + } + + + {!dhcp.processing && dhcp.config.enabled && + + + + + + + + } + + ); + } +} + +Dhcp.propTypes = { + dhcp: PropTypes.object, + toggleDhcp: PropTypes.func, + getDhcpStatus: PropTypes.func, + setDhcpConfig: PropTypes.func, + findActiveDhcp: PropTypes.func, + handleSubmit: PropTypes.func, + t: PropTypes.func, +}; + +export default withNamespaces()(Dhcp); diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 380a70f6..a4ff2394 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -1,4 +1,5 @@ .form__group { + position: relative; margin-bottom: 15px; } @@ -6,6 +7,10 @@ margin-bottom: 0; } +.form__group--dhcp:last-child { + margin-bottom: 15px; +} + .btn-standart { padding-left: 20px; padding-right: 20px; @@ -18,3 +23,28 @@ .form-control--textarea-large { min-height: 240px; } + +.form__message { + font-size: 11px; +} + +.form__message--error { + color: #cd201f; +} + +.interface__title { + font-size: 13px; + font-weight: 700; +} + +.interface__ip:after { + content: ", "; +} + +.interface__ip:last-child:after { + content: ""; +} + +.dhcp { + min-height: 450px; +} diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 74a02a20..24e56329 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { withNamespaces, Trans } from 'react-i18next'; import Upstream from './Upstream'; +import Dhcp from './Dhcp'; import Checkbox from '../ui/Checkbox'; import Loading from '../ui/Loading'; import PageTitle from '../ui/PageTitle'; @@ -34,6 +35,8 @@ class Settings extends Component { componentDidMount() { this.props.initSettings(this.settings); + this.props.getDhcpStatus(); + this.props.getDhcpInterfaces(); } handleUpstreamChange = (value) => { @@ -92,6 +95,13 @@ class Settings extends Component { handleUpstreamSubmit={this.handleUpstreamSubmit} handleUpstreamTest={this.handleUpstreamTest} /> + diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js index 144e968a..7d46b751 100644 --- a/client/src/containers/Settings.js +++ b/client/src/containers/Settings.js @@ -1,10 +1,22 @@ import { connect } from 'react-redux'; -import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions'; +import { + initSettings, + toggleSetting, + handleUpstreamChange, + setUpstream, + testUpstream, + addErrorToast, + toggleDhcp, + getDhcpStatus, + getDhcpInterfaces, + setDhcpConfig, + findActiveDhcp, +} from '../actions'; import Settings from '../components/Settings'; const mapStateToProps = (state) => { - const { settings, dashboard } = state; - const props = { settings, dashboard }; + const { settings, dashboard, dhcp } = state; + const props = { settings, dashboard, dhcp }; return props; }; @@ -15,6 +27,11 @@ const mapDispatchToProps = { setUpstream, testUpstream, addErrorToast, + toggleDhcp, + getDhcpStatus, + getDhcpInterfaces, + setDhcpConfig, + findActiveDhcp, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 1fe9348a..f0b2aea7 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -1,4 +1,5 @@ export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/; +export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g; export const STATS_NAMES = { avg_processing_time: 'average_processing_time', diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 9a54f249..fc202a0b 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -2,6 +2,7 @@ import { combineReducers } from 'redux'; import { handleActions } from 'redux-actions'; import { loadingBarReducer } from 'react-redux-loading-bar'; import nanoid from 'nanoid'; +import { reducer as formReducer } from 'redux-form'; import versionCompare from '../helpers/versionCompare'; import * as actions from '../actions'; @@ -35,6 +36,7 @@ const settings = handleActions({ processing: true, processingTestUpstream: false, processingSetUpstream: false, + processingDhcpStatus: false, }); const dashboard = handleActions({ @@ -258,11 +260,61 @@ const toasts = handleActions({ }, }, { notices: [] }); +const dhcp = handleActions({ + [actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }), + [actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }), + [actions.getDhcpStatusSuccess]: (state, { payload }) => { + const newState = { + ...state, + ...payload, + processing: false, + }; + return newState; + }, + + [actions.getDhcpInterfacesRequest]: state => ({ ...state, processingInterfaces: true }), + [actions.getDhcpInterfacesFailure]: state => ({ ...state, processingInterfaces: false }), + [actions.getDhcpInterfacesSuccess]: (state, { payload }) => { + const newState = { + ...state, + interfaces: payload, + processingInterfaces: false, + }; + return newState; + }, + + [actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }), + [actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }), + [actions.findActiveDhcpSuccess]: (state, { payload }) => ({ + ...state, + active: payload, + processingStatus: false, + }), + + [actions.toggleDhcpSuccess]: (state) => { + const { config } = state; + const newConfig = { ...config, enabled: !config.enabled }; + const newState = { ...state, config: newConfig }; + return newState; + }, +}, { + processing: true, + processingStatus: false, + processingInterfaces: false, + config: { + enabled: false, + }, + active: null, + leases: [], +}); + export default combineReducers({ settings, dashboard, queryLogs, filtering, toasts, + dhcp, loadingBar: loadingBarReducer, + form: formReducer, }); diff --git a/config.go b/config.go index 1f7464b2..f759ff4a 100644 --- a/config.go +++ b/config.go @@ -2,13 +2,14 @@ package main import ( "io/ioutil" - "log" "os" "path/filepath" "sync" + "github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsforward" + "github.com/hmage/golibs/log" "gopkg.in/yaml.v2" ) @@ -23,14 +24,15 @@ type configuration struct { ourConfigFilename string // Config filename (can be overriden via the command line arguments) ourBinaryDir string // Location of our directory, used to protect against CWD being somewhere else - BindHost string `yaml:"bind_host"` - BindPort int `yaml:"bind_port"` - AuthName string `yaml:"auth_name"` - AuthPass string `yaml:"auth_pass"` - Language string `yaml:"language"` // two-letter ISO 639-1 language code - DNS dnsConfig `yaml:"dns"` - Filters []filter `yaml:"filters"` - UserRules []string `yaml:"user_rules"` + BindHost string `yaml:"bind_host"` + BindPort int `yaml:"bind_port"` + AuthName string `yaml:"auth_name"` + AuthPass string `yaml:"auth_pass"` + Language string `yaml:"language"` // two-letter ISO 639-1 language code + DNS dnsConfig `yaml:"dns"` + Filters []filter `yaml:"filters"` + UserRules []string `yaml:"user_rules"` + DHCP dhcpd.ServerConfig `yaml:"dhcp"` sync.RWMutex `yaml:"-"` diff --git a/control.go b/control.go index 78235a48..e35c2529 100644 --- a/control.go +++ b/control.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net" "net/http" "os" @@ -12,11 +11,10 @@ import ( "strings" "time" - "github.com/AdguardTeam/dnsproxy/upstream" - "github.com/AdguardTeam/AdGuardHome/dnsforward" + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/hmage/golibs/log" "github.com/miekg/dns" - "gopkg.in/asaskevich/govalidator.v4" ) @@ -719,4 +717,8 @@ func registerControlHandlers() { http.HandleFunc("/control/safesearch/enable", optionalAuth(ensurePOST(handleSafeSearchEnable))) http.HandleFunc("/control/safesearch/disable", optionalAuth(ensurePOST(handleSafeSearchDisable))) http.HandleFunc("/control/safesearch/status", optionalAuth(ensureGET(handleSafeSearchStatus))) + http.HandleFunc("/control/dhcp/status", optionalAuth(ensureGET(handleDHCPStatus))) + http.HandleFunc("/control/dhcp/interfaces", optionalAuth(ensureGET(handleDHCPInterfaces))) + http.HandleFunc("/control/dhcp/set_config", optionalAuth(ensurePOST(handleDHCPSetConfig))) + http.HandleFunc("/control/dhcp/find_active_dhcp", optionalAuth(ensurePOST(handleDHCPFindActiveServer))) } diff --git a/dhcp.go b/dhcp.go new file mode 100644 index 00000000..4a54e82f --- /dev/null +++ b/dhcp.go @@ -0,0 +1,169 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "strings" + "time" + + "github.com/AdguardTeam/AdGuardHome/dhcpd" + "github.com/hmage/golibs/log" + "github.com/joomcode/errorx" +) + +var dhcpServer = dhcpd.Server{} + +func handleDHCPStatus(w http.ResponseWriter, r *http.Request) { + rawLeases := dhcpServer.Leases() + leases := []map[string]string{} + for i := range rawLeases { + lease := map[string]string{ + "mac": rawLeases[i].HWAddr.String(), + "ip": rawLeases[i].IP.String(), + "hostname": rawLeases[i].Hostname, + "expires": rawLeases[i].Expiry.Format(time.RFC3339), + } + leases = append(leases, lease) + + } + status := map[string]interface{}{ + "config": config.DHCP, + "leases": leases, + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(status) + if err != nil { + httpError(w, http.StatusInternalServerError, "Unable to marshal DHCP status json: %s", err) + return + } +} + +func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { + newconfig := dhcpd.ServerConfig{} + err := json.NewDecoder(r.Body).Decode(&newconfig) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err) + return + } + + if newconfig.Enabled { + err := dhcpServer.Start(&newconfig) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err) + return + } + } + if !newconfig.Enabled { + dhcpServer.Stop() + } + config.DHCP = newconfig + httpUpdateConfigReloadDNSReturnOK(w, r) +} + +func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { + response := map[string]interface{}{} + + ifaces, err := net.Interfaces() + if err != nil { + httpError(w, http.StatusInternalServerError, "Couldn't get list of interfaces: %s", err) + return + } + + type address struct { + IP string + Netmask string + } + + type responseInterface struct { + Name string `json:"name"` + MTU int `json:"mtu"` + HardwareAddr string `json:"hardware_address"` + Addresses []string `json:"ip_addresses"` + } + + for i := range ifaces { + if ifaces[i].Flags&net.FlagLoopback != 0 { + // it's a loopback, skip it + continue + } + if ifaces[i].Flags&net.FlagBroadcast == 0 { + // this interface doesn't support broadcast, skip it + continue + } + if ifaces[i].Flags&net.FlagPointToPoint != 0 { + // this interface is ppp, don't do dhcp over it + continue + } + iface := responseInterface{ + Name: ifaces[i].Name, + MTU: ifaces[i].MTU, + HardwareAddr: ifaces[i].HardwareAddr.String(), + } + addrs, err := ifaces[i].Addrs() + if err != nil { + httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %v: %s", ifaces[i].Name, err) + return + } + for _, addr := range addrs { + iface.Addresses = append(iface.Addresses, addr.String()) + } + if len(iface.Addresses) == 0 { + // this interface has no addresses, skip it + continue + } + response[ifaces[i].Name] = iface + } + + err = json.NewEncoder(w).Encode(response) + if err != nil { + httpError(w, http.StatusInternalServerError, "Failed to marshal json with available interfaces: %s", err) + return + } +} + +func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + errorText := fmt.Sprintf("failed to read request body: %s", err) + log.Println(errorText) + http.Error(w, errorText, http.StatusBadRequest) + return + } + + interfaceName := strings.TrimSpace(string(body)) + if interfaceName == "" { + errorText := fmt.Sprintf("empty interface name specified") + log.Println(errorText) + http.Error(w, errorText, http.StatusBadRequest) + return + } + found, err := dhcpd.CheckIfOtherDHCPServersPresent(interfaceName) + result := map[string]interface{}{} + if err != nil { + result["error"] = err.Error() + } else { + result["found"] = found + } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(result) + if err != nil { + httpError(w, http.StatusInternalServerError, "Failed to marshal DHCP found json: %s", err) + return + } +} + +func startDHCPServer() error { + if config.DHCP.Enabled == false { + // not enabled, don't do anything + return nil + } + err := dhcpServer.Start(&config.DHCP) + if err != nil { + return errorx.Decorate(err, "Couldn't start DHCP server") + } + return nil +} diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go new file mode 100644 index 00000000..661b2daa --- /dev/null +++ b/dhcpd/check_other_dhcp.go @@ -0,0 +1,144 @@ +package dhcpd + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "math" + "net" + "os" + "time" + + "github.com/hmage/golibs/log" + "github.com/krolaw/dhcp4" +) + +func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return false, wrapErrPrint(err, "Couldn't find interface by name %s", ifaceName) + } + + // get ipv4 address of an interface + ifaceIPNet := getIfaceIPv4(iface) + if ifaceIPNet == nil { + return false, fmt.Errorf("Couldn't find IPv4 address of interface %s %+v", ifaceName, iface) + } + + srcIP := ifaceIPNet.IP + src := net.JoinHostPort(srcIP.String(), "68") + dst := "255.255.255.255:67" + + // form a DHCP request packet, try to emulate existing client as much as possible + xId := make([]byte, 8) + n, err := rand.Read(xId) + if n != 8 && err == nil { + err = fmt.Errorf("Generated less than 8 bytes") + } + if err != nil { + return false, wrapErrPrint(err, "Couldn't generate 8 random bytes") + } + hostname, err := os.Hostname() + if err != nil { + return false, wrapErrPrint(err, "Couldn't get hostname") + } + requestList := []byte{ + byte(dhcp4.OptionSubnetMask), + byte(dhcp4.OptionClasslessRouteFormat), + byte(dhcp4.OptionRouter), + byte(dhcp4.OptionDomainNameServer), + byte(dhcp4.OptionDomainName), + byte(dhcp4.OptionDomainSearch), + 252, // private/proxy autodiscovery + 95, // LDAP + byte(dhcp4.OptionNetBIOSOverTCPIPNameServer), + byte(dhcp4.OptionNetBIOSOverTCPIPNodeType), + } + maxUDPsizeRaw := make([]byte, 2) + binary.BigEndian.PutUint16(maxUDPsizeRaw, 1500) + leaseTimeRaw := make([]byte, 4) + leaseTime := uint32(math.RoundToEven(time.Duration(time.Hour * 24 * 90).Seconds())) + binary.BigEndian.PutUint32(leaseTimeRaw, leaseTime) + options := []dhcp4.Option{ + {dhcp4.OptionParameterRequestList, requestList}, + {dhcp4.OptionMaximumDHCPMessageSize, maxUDPsizeRaw}, + {dhcp4.OptionClientIdentifier, append([]byte{0x01}, iface.HardwareAddr...)}, + {dhcp4.OptionIPAddressLeaseTime, leaseTimeRaw}, + {dhcp4.OptionHostName, []byte(hostname)}, + } + packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xId, false, options) + + // resolve 0.0.0.0:68 + udpAddr, err := net.ResolveUDPAddr("udp4", src) + if err != nil { + return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", src) + } + // spew.Dump(udpAddr, err) + + if !udpAddr.IP.To4().Equal(srcIP) { + return false, wrapErrPrint(err, "Resolved UDP address is not %s", src) + } + + // resolve 255.255.255.255:67 + dstAddr, err := net.ResolveUDPAddr("udp4", dst) + if err != nil { + return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", dst) + } + + // bind to 0.0.0.0:68 + log.Tracef("Listening to udp4 %+v", udpAddr) + c, err := net.ListenPacket("udp4", src) + if c != nil { + defer c.Close() + } + // spew.Dump(c, err) + // spew.Printf("net.ListenUDP returned %v, %v\n", c, err) + if err != nil { + return false, wrapErrPrint(err, "Couldn't listen to %s", src) + } + + // send to 255.255.255.255:67 + n, err = c.WriteTo(packet, dstAddr) + // spew.Dump(n, err) + if err != nil { + return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst) + } + + // wait for answer + log.Tracef("Waiting %v for an answer", defaultDiscoverTime) + // TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts + b := make([]byte, 1500) + c.SetReadDeadline(time.Now().Add(defaultDiscoverTime)) + n, _, err = c.ReadFrom(b) + if isTimeout(err) { + // timed out -- no DHCP servers + return false, nil + } + if err != nil { + return false, wrapErrPrint(err, "Couldn't receive packet") + } + if n > 0 { + b = b[:n] + } + // spew.Dump(n, fromAddr, err, b) + + if n < 240 { + // packet too small for dhcp + return false, wrapErrPrint(err, "got packet that's too small for DHCP") + } + + response := dhcp4.Packet(b[:n]) + if response.HLen() > 16 { + // invalid size + return false, wrapErrPrint(err, "got malformed packet with HLen() > 16") + } + + parsedOptions := response.ParseOptions() + _, ok := parsedOptions[dhcp4.OptionDHCPMessageType] + if !ok { + return false, wrapErrPrint(err, "got malformed packet without DHCP message type") + } + + // that's a DHCP server there + return true, nil +} diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go new file mode 100644 index 00000000..deeaac02 --- /dev/null +++ b/dhcpd/dhcpd.go @@ -0,0 +1,398 @@ +package dhcpd + +import ( + "bytes" + "fmt" + "net" + "sync" + "time" + + "github.com/hmage/golibs/log" + "github.com/krolaw/dhcp4" +) + +const defaultDiscoverTime = time.Second * 3 + +// field ordering is important -- yaml fields will mirror ordering from here +type Lease struct { + HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"` + IP net.IP `json:"ip"` + Hostname string `json:"hostname"` + Expiry time.Time `json:"expires"` +} + +// field ordering is important -- yaml fields will mirror ordering from here +type ServerConfig struct { + Enabled bool `json:"enabled" yaml:"enabled"` + InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on + GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"` + SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"` + RangeStart string `json:"range_start" yaml:"range_start"` + RangeEnd string `json:"range_end" yaml:"range_end"` + LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds +} + +type Server struct { + conn *filterConn // listening UDP socket + + ipnet *net.IPNet // if interface name changes, this needs to be reset + + // leases + leases []*Lease + leaseStart net.IP // parsed from config RangeStart + leaseStop net.IP // parsed from config RangeEnd + leaseTime time.Duration // parsed from config LeaseDuration + leaseOptions dhcp4.Options // parsed from config GatewayIP and SubnetMask + + // IP address pool -- if entry is in the pool, then it's attached to a lease + IPpool map[[4]byte]net.HardwareAddr + + ServerConfig + sync.RWMutex +} + +// Start will listen on port 67 and serve DHCP requests. +// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet). +func (s *Server) Start(config *ServerConfig) error { + if config != nil { + s.ServerConfig = *config + } + + iface, err := net.InterfaceByName(s.InterfaceName) + if err != nil { + s.closeConn() // in case it was already started + return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName) + } + + // get ipv4 address of an interface + s.ipnet = getIfaceIPv4(iface) + if s.ipnet == nil { + s.closeConn() // in case it was already started + return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface) + } + + if s.LeaseDuration == 0 { + s.leaseTime = time.Hour * 2 + s.LeaseDuration = uint(s.leaseTime.Seconds()) + } else { + s.leaseTime = time.Second * time.Duration(s.LeaseDuration) + } + + s.leaseStart, err = parseIPv4(s.RangeStart) + if err != nil { + s.closeConn() // in case it was already started + return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart) + } + + s.leaseStop, err = parseIPv4(s.RangeEnd) + if err != nil { + s.closeConn() // in case it was already started + return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd) + } + + subnet, err := parseIPv4(s.SubnetMask) + if err != nil { + s.closeConn() // in case it was already started + return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask) + } + + // if !bytes.Equal(subnet, s.ipnet.Mask) { + // s.closeConn() // in case it was already started + // return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask) + // } + + router, err := parseIPv4(s.GatewayIP) + if err != nil { + s.closeConn() // in case it was already started + return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP) + } + + s.leaseOptions = dhcp4.Options{ + dhcp4.OptionSubnetMask: subnet, + dhcp4.OptionRouter: router, + dhcp4.OptionDomainNameServer: s.ipnet.IP, + } + + // TODO: don't close if interface and addresses are the same + if s.conn != nil { + s.closeConn() + } + + c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets + if err != nil { + return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") + } + + s.conn = c + + go func() { + // operate on c instead of c.conn because c.conn can change over time + err := dhcp4.Serve(c, s) + if err != nil { + log.Printf("dhcp4.Serve() returned with error: %s", err) + } + c.Close() // in case Serve() exits for other reason than listening socket closure + }() + + return nil +} + +func (s *Server) Stop() error { + if s.conn == nil { + // nothing to do, return silently + return nil + } + err := s.closeConn() + if err != nil { + return wrapErrPrint(err, "Couldn't close UDP listening socket") + } + + return nil +} + +// closeConn will close the connection and set it to zero +func (s *Server) closeConn() error { + if s.conn == nil { + return nil + } + err := s.conn.Close() + s.conn = nil + return err +} + +func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { + // WARNING: do not remove copy() + // the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call + // since we need to retain it we need to make our own copy + hwaddrCOW := p.CHAddr() + hwaddr := make(net.HardwareAddr, len(hwaddrCOW)) + copy(hwaddr, hwaddrCOW) + foundLease := s.locateLease(p) + if foundLease != nil { + // log.Tracef("found lease for %s: %+v", hwaddr, foundLease) + return foundLease, nil + } + // not assigned a lease, create new one, find IP from LRU + log.Tracef("Lease not found for %s: creating new one", hwaddr) + ip, err := s.findFreeIP(p, hwaddr) + if err != nil { + return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) + } + log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String()) + hostname := p.ParseOptions()[dhcp4.OptionHostName] + lease := &Lease{HWAddr: hwaddr, IP: ip, Hostname: string(hostname)} + s.Lock() + s.leases = append(s.leases, lease) + s.Unlock() + return lease, nil +} + +func (s *Server) locateLease(p dhcp4.Packet) *Lease { + hwaddr := p.CHAddr() + for i := range s.leases { + if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) { + // log.Tracef("bytes.Equal(%s, %s) returned true", hwaddr, s.leases[i].hwaddr) + return s.leases[i] + } + } + return nil +} + +func (s *Server) findFreeIP(p dhcp4.Packet, hwaddr net.HardwareAddr) (net.IP, error) { + // if IP pool is nil, lazy initialize it + if s.IPpool == nil { + s.IPpool = make(map[[4]byte]net.HardwareAddr) + } + + // go from start to end, find unreserved IP + var foundIP net.IP + for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ { + newIP := dhcp4.IPAdd(s.leaseStart, i) + foundHWaddr := s.getIPpool(newIP) + log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr) + if foundHWaddr != nil && len(foundHWaddr) != 0 { + // if !bytes.Equal(foundHWaddr, hwaddr) { + // log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr) + // } + log.Tracef("will try again") + continue + } + foundIP = newIP + break + } + + if foundIP == nil { + // TODO: LRU + return nil, fmt.Errorf("Couldn't find free entry in IP pool") + } + + s.reserveIP(foundIP, hwaddr) + + return foundIP, nil +} + +func (s *Server) getIPpool(ip net.IP) net.HardwareAddr { + rawIP := []byte(ip) + IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} + return s.IPpool[IP4] +} + +func (s *Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) { + rawIP := []byte(ip) + IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} + s.IPpool[IP4] = hwaddr +} + +func (s *Server) unreserveIP(ip net.IP) { + rawIP := []byte(ip) + IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} + delete(s.IPpool, IP4) +} + +func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { + log.Tracef("Got %v message", msgType) + log.Tracef("Leases:") + for i, lease := range s.leases { + log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.HWAddr, lease.IP, lease.Expiry) + } + log.Tracef("IP pool:") + for ip, hwaddr := range s.IPpool { + log.Tracef("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr) + } + // spew.Dump(s.leases, s.IPpool) + // log.Printf("Called with msgType = %v, options = %+v", msgType, options) + // spew.Dump(p) + // log.Printf("%14s %v", "p.Broadcast", p.Broadcast()) // false + // log.Printf("%14s %v", "p.CHAddr", p.CHAddr()) // 2c:f0:a2:f2:31:00 + // log.Printf("%14s %v", "p.CIAddr", p.CIAddr()) // 0.0.0.0 + // log.Printf("%14s %v", "p.Cookie", p.Cookie()) // [99 130 83 99] + // log.Printf("%14s %v", "p.File", p.File()) // [] + // log.Printf("%14s %v", "p.Flags", p.Flags()) // [0 0] + // log.Printf("%14s %v", "p.GIAddr", p.GIAddr()) // 0.0.0.0 + // log.Printf("%14s %v", "p.HLen", p.HLen()) // 6 + // log.Printf("%14s %v", "p.HType", p.HType()) // 1 + // log.Printf("%14s %v", "p.Hops", p.Hops()) // 0 + // log.Printf("%14s %v", "p.OpCode", p.OpCode()) // BootRequest + // log.Printf("%14s %v", "p.Options", p.Options()) // [53 1 1 55 10 1 121 3 6 15 119 252 95 44 46 57 2 5 220 61 7 1 44 240 162 242 49 0 51 4 0 118 167 0 12 4 119 104 109 100 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] + // log.Printf("%14s %v", "p.ParseOptions", p.ParseOptions()) // map[OptionParameterRequestList:[1 121 3 6 15 119 252 95 44 46] OptionDHCPMessageType:[1] OptionMaximumDHCPMessageSize:[5 220] OptionClientIdentifier:[1 44 240 162 242 49 0] OptionIPAddressLeaseTime:[0 118 167 0] OptionHostName:[119 104 109 100]] + // log.Printf("%14s %v", "p.SIAddr", p.SIAddr()) // 0.0.0.0 + // log.Printf("%14s %v", "p.SName", p.SName()) // [] + // log.Printf("%14s %v", "p.Secs", p.Secs()) // [0 8] + // log.Printf("%14s %v", "p.XId", p.XId()) // [211 184 20 44] + // log.Printf("%14s %v", "p.YIAddr", p.YIAddr()) // 0.0.0.0 + + switch msgType { + case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? + // find a lease, but don't update lease time + log.Tracef("Got from client: Discover") + lease, err := s.reserveLease(p) + if err != nil { + log.Tracef("Couldn't find free lease: %s", err) + // couldn't find lease, don't respond + return nil + } + reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) + log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions()) + return reply + case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals) + // start/renew a lease -- update lease time + // some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request + log.Tracef("Got from client: Request") + if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) { + log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) + return nil // Message not for this dhcp server + } + + reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) + if reqIP == nil { + reqIP = net.IP(p.CIAddr()) + } + + if reqIP.To4() == nil { + log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP) + return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) + } + + if reqIP.Equal(net.IPv4zero) { + log.Tracef("Replying with NAK: request IP is 0.0.0.0") + return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) + } + + log.Tracef("requested IP is %s", reqIP) + lease, err := s.reserveLease(p) + if err != nil { + log.Tracef("Couldn't find free lease: %s", err) + // couldn't find lease, don't respond + return nil + } + + if lease.IP.Equal(reqIP) { + // IP matches lease IP, nothing else to do + lease.Expiry = time.Now().Add(s.leaseTime) + log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr()) + return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) + } + + // + // requested IP different from lease + // + + log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP) + + hwaddr := s.getIPpool(reqIP) + if hwaddr == nil { + // not in pool, check if it's in DHCP range + if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) { + // okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP + log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr()) + s.unreserveIP(lease.IP) + lease.IP = reqIP + s.reserveIP(reqIP, p.CHAddr()) + lease.Expiry = time.Now().Add(s.leaseTime) + return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) + } + } + + if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) { + log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr) + } + + // requsted IP is not sufficient, reply with NAK + if hwaddr != nil { + log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr()) + return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) + } + + // requested IP is outside of DHCP range + log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr()) + return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) + case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP + log.Tracef("Got from client: Decline") + + case dhcp4.Release: // From Client, I don't need that IP anymore + log.Tracef("Got from client: Release") + + case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it + log.Tracef("Got from client: Inform") + // do nothing + + // from server -- ignore those but enumerate just in case + case dhcp4.Offer: // Broadcast From Server - Here's an IP + log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: Offer") + case dhcp4.ACK: // From Server, Yes you can have that IP + log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK") + case dhcp4.NAK: // From Server, No you cannot have that IP + log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK") + default: + log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType) + return nil + } + return nil +} + +func (s *Server) Leases() []*Lease { + s.RLock() + result := s.leases + s.RUnlock() + return result +} diff --git a/dhcpd/filter_conn.go b/dhcpd/filter_conn.go new file mode 100644 index 00000000..ec34c795 --- /dev/null +++ b/dhcpd/filter_conn.go @@ -0,0 +1,64 @@ +package dhcpd + +import ( + "net" + + "github.com/joomcode/errorx" + "golang.org/x/net/ipv4" +) + +// filterConn listens to 0.0.0.0:67, but accepts packets only from specific interface +// This is neccessary for DHCP daemon to work, since binding to IP address doesn't +// us access to see Discover/Request packets from clients. +// +// TODO: on windows, controlmessage does not work, try to find out another way +// https://github.com/golang/net/blob/master/ipv4/payload.go#L13 +type filterConn struct { + iface net.Interface + conn *ipv4.PacketConn +} + +func newFilterConn(iface net.Interface, address string) (*filterConn, error) { + c, err := net.ListenPacket("udp4", address) + if err != nil { + return nil, errorx.Decorate(err, "Couldn't listen to %s on UDP4", address) + } + + p := ipv4.NewPacketConn(c) + err = p.SetControlMessage(ipv4.FlagInterface, true) + if err != nil { + c.Close() + return nil, errorx.Decorate(err, "Couldn't set control message FlagInterface on connection") + } + + return &filterConn{iface: iface, conn: p}, nil +} + +func (f *filterConn) ReadFrom(b []byte) (int, net.Addr, error) { + for { // read until we find a suitable packet + n, cm, addr, err := f.conn.ReadFrom(b) + if err != nil { + return 0, addr, errorx.Decorate(err, "Error when reading from socket") + } + if cm == nil { + // no controlmessage was passed, so pass the packet to the caller + return n, addr, nil + } + if cm.IfIndex == f.iface.Index { + return n, addr, nil + } + // packet doesn't match criteria, drop it + } + return 0, nil, nil +} + +func (f *filterConn) WriteTo(b []byte, addr net.Addr) (int, error) { + cm := ipv4.ControlMessage{ + IfIndex: f.iface.Index, + } + return f.conn.WriteTo(b, &cm, addr) +} + +func (f *filterConn) Close() error { + return f.conn.Close() +} diff --git a/dhcpd/helpers.go b/dhcpd/helpers.go new file mode 100644 index 00000000..20793f52 --- /dev/null +++ b/dhcpd/helpers.go @@ -0,0 +1,84 @@ +package dhcpd + +import ( + "fmt" + "net" + "strings" + + "github.com/hmage/golibs/log" + "github.com/joomcode/errorx" +) + +func isTimeout(err error) bool { + operr, ok := err.(*net.OpError) + if !ok { + return false + } + return operr.Timeout() +} + +// return first IPv4 address of an interface, if there is any +func getIfaceIPv4(iface *net.Interface) *net.IPNet { + ifaceAddrs, err := iface.Addrs() + if err != nil { + panic(err) + } + + for _, addr := range ifaceAddrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + // not an IPNet, should not happen + log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr) + } + + if ipnet.IP.To4() == nil { + log.Printf("Got IP that is not IPv4: %v", ipnet.IP) + continue + } + + log.Printf("Got IP that is IPv4: %v", ipnet.IP) + return &net.IPNet{ + IP: ipnet.IP.To4(), + Mask: ipnet.Mask, + } + } + return nil +} + +func isConnClosed(err error) bool { + if err == nil { + return false + } + nerr, ok := err.(*net.OpError) + if !ok { + return false + } + + if strings.Contains(nerr.Err.Error(), "use of closed network connection") { + return true + } + + return false +} + +func wrapErrPrint(err error, message string, args ...interface{}) error { + var errx error + if err == nil { + errx = fmt.Errorf(message, args...) + } else { + errx = errorx.Decorate(err, message, args...) + } + log.Println(errx.Error()) + return errx +} + +func parseIPv4(text string) (net.IP, error) { + result := net.ParseIP(text) + if result == nil { + return nil, fmt.Errorf("%s is not an IP address", text) + } + if result.To4() == nil { + return nil, fmt.Errorf("%s is not an IPv4 address", text) + } + return result.To4(), nil +} diff --git a/dhcpd/standalone/main.go b/dhcpd/standalone/main.go new file mode 100644 index 00000000..fff02664 --- /dev/null +++ b/dhcpd/standalone/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/AdguardTeam/AdGuardHome/dhcpd" + "github.com/hmage/golibs/log" + "github.com/krolaw/dhcp4" +) + +func main() { + if len(os.Args) < 2 { + log.Printf("Usage: %s ", os.Args[0]) + os.Exit(64) + } + + ifaceName := os.Args[1] + present, err := dhcpd.CheckIfOtherDHCPServersPresent(ifaceName) + if err != nil { + panic(err) + } + log.Printf("Found DHCP server? %v", present) + if present { + log.Printf("Will not start DHCP server because there's already running one on the network") + os.Exit(1) + } + + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + panic(err) + } + + // get ipv4 address of an interface + ifaceIPNet := getIfaceIPv4(iface) + if ifaceIPNet == nil { + panic(err) + } + + // append 10 to server's IP address as start + start := dhcp4.IPAdd(ifaceIPNet.IP, 10) + // lease range is 100 IP's, but TODO: don't go beyond end of subnet mask + stop := dhcp4.IPAdd(start, 100) + + server := dhcpd.Server{} + config := dhcpd.ServerConfig{ + InterfaceName: ifaceName, + RangeStart: start.String(), + RangeEnd: stop.String(), + SubnetMask: "255.255.255.0", + GatewayIP: "192.168.7.1", + } + log.Printf("Starting DHCP server") + err = server.Start(&config) + if err != nil { + panic(err) + } + + time.Sleep(time.Second) + log.Printf("Stopping DHCP server") + err = server.Stop() + if err != nil { + panic(err) + } + log.Printf("Starting DHCP server") + err = server.Start(&config) + if err != nil { + panic(err) + } + log.Printf("Starting DHCP server while it's already running") + err = server.Start(&config) + if err != nil { + panic(err) + } + log.Printf("Now serving DHCP") + signal_channel := make(chan os.Signal) + signal.Notify(signal_channel, syscall.SIGINT, syscall.SIGTERM) + <-signal_channel + +} + +// return first IPv4 address of an interface, if there is any +func getIfaceIPv4(iface *net.Interface) *net.IPNet { + ifaceAddrs, err := iface.Addrs() + if err != nil { + panic(err) + } + + for _, addr := range ifaceAddrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + // not an IPNet, should not happen + log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr) + } + + if ipnet.IP.To4() == nil { + log.Printf("Got IP that is not IPv4: %v", ipnet.IP) + continue + } + + log.Printf("Got IP that is IPv4: %v", ipnet.IP) + return &net.IPNet{ + IP: ipnet.IP.To4(), + Mask: ipnet.Mask, + } + } + return nil +} diff --git a/dns.go b/dns.go index 7f94bc77..54e56184 100644 --- a/dns.go +++ b/dns.go @@ -2,12 +2,12 @@ package main import ( "fmt" - "log" "net" "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/hmage/golibs/log" "github.com/joomcode/errorx" ) diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go index cd408a4d..51204e1b 100644 --- a/dnsfilter/dnsfilter.go +++ b/dnsfilter/dnsfilter.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io/ioutil" - "log" "net" "net/http" "regexp" @@ -18,6 +17,7 @@ import ( "time" "github.com/bluele/gcache" + "github.com/hmage/golibs/log" "golang.org/x/net/publicsuffix" ) diff --git a/dnsfilter/dnsfilter_test.go b/dnsfilter/dnsfilter_test.go index a93fadfc..02feb0c7 100644 --- a/dnsfilter/dnsfilter_test.go +++ b/dnsfilter/dnsfilter_test.go @@ -17,6 +17,7 @@ import ( "os" "runtime" + "github.com/hmage/golibs/log" "github.com/shirou/gopsutil/process" "go.uber.org/goleak" ) @@ -24,7 +25,7 @@ import ( // first in file because it must be run first func TestLotsOfRulesMemoryUsage(t *testing.T) { start := getRSS() - trace("RSS before loading rules - %d kB\n", start/1024) + log.Tracef("RSS before loading rules - %d kB\n", start/1024) dumpMemProfile(_Func() + "1.pprof") d := NewForTest() @@ -35,7 +36,7 @@ func TestLotsOfRulesMemoryUsage(t *testing.T) { } afterLoad := getRSS() - trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024) + log.Tracef("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024) dumpMemProfile(_Func() + "2.pprof") tests := []struct { @@ -58,7 +59,7 @@ func TestLotsOfRulesMemoryUsage(t *testing.T) { } } afterMatch := getRSS() - trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024) + log.Tracef("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024) dumpMemProfile(_Func() + "3.pprof") } @@ -88,20 +89,20 @@ func dumpMemProfile(name string) { const topHostsFilename = "../tests/top-1m.csv" func fetchTopHostsFromNet() { - trace("Fetching top hosts from network") + log.Tracef("Fetching top hosts from network") resp, err := http.Get("http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip") if err != nil { panic(err) } defer resp.Body.Close() - trace("Reading zipfile body") + log.Tracef("Reading zipfile body") zipfile, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } - trace("Opening zipfile") + log.Tracef("Opening zipfile") r, err := zip.NewReader(bytes.NewReader(zipfile), int64(len(zipfile))) if err != nil { panic(err) @@ -111,19 +112,19 @@ func fetchTopHostsFromNet() { panic(fmt.Errorf("zipfile must have only one entry: %+v", r)) } f := r.File[0] - trace("Unpacking file %s from zipfile", f.Name) + log.Tracef("Unpacking file %s from zipfile", f.Name) rc, err := f.Open() if err != nil { panic(err) } - trace("Reading file %s contents", f.Name) + log.Tracef("Reading file %s contents", f.Name) body, err := ioutil.ReadAll(rc) if err != nil { panic(err) } rc.Close() - trace("Writing file %s contents to disk", f.Name) + log.Tracef("Writing file %s contents to disk", f.Name) err = ioutil.WriteFile(topHostsFilename+".tmp", body, 0644) if err != nil { panic(err) @@ -144,16 +145,16 @@ func getTopHosts() { func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) { start := getRSS() - trace("RSS before loading rules - %d kB\n", start/1024) + log.Tracef("RSS before loading rules - %d kB\n", start/1024) dumpMemProfile(_Func() + "1.pprof") d := NewForTest() defer d.Destroy() mustLoadTestRules(d) - trace("Have %d rules", d.Count()) + log.Tracef("Have %d rules", d.Count()) afterLoad := getRSS() - trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024) + log.Tracef("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024) dumpMemProfile(_Func() + "2.pprof") getTopHosts() @@ -163,7 +164,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) { } defer hostnames.Close() afterHosts := getRSS() - trace("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024) + log.Tracef("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024) dumpMemProfile(_Func() + "2.pprof") { @@ -182,7 +183,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) { } afterMatch := getRSS() - trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024) + log.Tracef("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024) dumpMemProfile(_Func() + "3.pprof") } @@ -236,7 +237,7 @@ func TestSuffixRule(t *testing.T) { t.Errorf("Result suffix does not match for \"%s\": got \"%s\" expected \"%s\"", testcase.rule, suffix, testcase.suffix) continue } - // trace("\"%s\": %v: %s", testcase.rule, isSuffix, suffix) + // log.Tracef("\"%s\": %v: %s", testcase.rule, isSuffix, suffix) } } diff --git a/dnsfilter/helpers.go b/dnsfilter/helpers.go index 8152f402..68d4ba26 100644 --- a/dnsfilter/helpers.go +++ b/dnsfilter/helpers.go @@ -1,10 +1,6 @@ package dnsfilter import ( - "fmt" - "os" - "path" - "runtime" "strings" "sync/atomic" ) @@ -62,17 +58,3 @@ func updateMax(valuePtr *int64, maxPtr *int64) { // swapping failed because value has changed after reading, try again } } - -func trace(format string, args ...interface{}) { - pc := make([]uintptr, 10) // at least 1 entry needed - runtime.Callers(2, pc) - f := runtime.FuncForPC(pc[0]) - var buf strings.Builder - buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name()))) - text := fmt.Sprintf(format, args...) - buf.WriteString(text) - if len(text) == 0 || text[len(text)-1] != '\n' { - buf.WriteRune('\n') - } - fmt.Fprint(os.Stderr, buf.String()) -} diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index 34f64694..a242f460 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -11,9 +11,9 @@ import ( "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/hmage/golibs/log" "github.com/joomcode/errorx" "github.com/miekg/dns" - log "github.com/sirupsen/logrus" ) // DefaultTimeout is the default upstream timeout @@ -283,7 +283,7 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error // Return immediately if there's an error return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host) } else if res.IsFiltered { - log.Debugf("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule) + // log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule) d.Res = s.genDNSFilterMessage(d, &res) } @@ -324,7 +324,7 @@ func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg { resp.SetReply(request) answer, err := dns.NewRR(fmt.Sprintf("%s %d A %s", request.Question[0].Name, s.BlockedResponseTTL, ip.String())) if err != nil { - log.Warnf("Couldn't generate A record for up replacement host '%s': %s", ip.String(), err) + log.Printf("Couldn't generate A record for replacement host '%s': %s", ip.String(), err) return s.genServerFailure(request) } resp.Answer = append(resp.Answer, answer) diff --git a/dnsforward/querylog.go b/dnsforward/querylog.go index 51ca3575..901fa1f3 100644 --- a/dnsforward/querylog.go +++ b/dnsforward/querylog.go @@ -11,8 +11,8 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/hmage/golibs/log" "github.com/miekg/dns" - log "github.com/sirupsen/logrus" ) const ( diff --git a/dnsforward/querylog_file.go b/dnsforward/querylog_file.go index 43a93093..930c4a8d 100644 --- a/dnsforward/querylog_file.go +++ b/dnsforward/querylog_file.go @@ -9,9 +9,8 @@ import ( "sync" "time" - log "github.com/sirupsen/logrus" - "github.com/go-test/deep" + "github.com/hmage/golibs/log" ) var ( @@ -222,7 +221,7 @@ func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, ti } if now.Sub(entry.Time) > timeWindow { - // trace("skipping entry") // debug logging + // log.Tracef("skipping entry") // debug logging continue } diff --git a/dnsforward/querylog_top.go b/dnsforward/querylog_top.go index 8ca5f24d..4c150093 100644 --- a/dnsforward/querylog_top.go +++ b/dnsforward/querylog_top.go @@ -13,9 +13,8 @@ import ( "sync" "time" - log "github.com/sirupsen/logrus" - "github.com/bluele/gcache" + "github.com/hmage/golibs/log" "github.com/miekg/dns" ) diff --git a/dnsforward/stats.go b/dnsforward/stats.go index 2befcad2..9d11504b 100644 --- a/dnsforward/stats.go +++ b/dnsforward/stats.go @@ -7,9 +7,8 @@ import ( "sync" "time" - log "github.com/sirupsen/logrus" - "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/hmage/golibs/log" ) var ( @@ -70,7 +69,7 @@ func purgeStats() { func (p *periodicStats) Inc(name string, when time.Time) { // calculate how many periods ago this happened elapsed := int64(time.Since(when) / p.period) - // trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed) + // log.Tracef("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed) if elapsed >= statsHistoryElements { return // outside of our timeframe } @@ -84,7 +83,7 @@ func (p *periodicStats) Inc(name string, when time.Time) { func (p *periodicStats) Observe(name string, when time.Time, value float64) { // calculate how many periods ago this happened elapsed := int64(time.Since(when) / p.period) - // trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed) + // log.Tracef("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed) if elapsed >= statsHistoryElements { return // outside of our timeframe } @@ -93,7 +92,7 @@ func (p *periodicStats) Observe(name string, when time.Time, value float64) { countname := name + "_count" currentValues := p.Entries[countname] value := currentValues[elapsed] - // trace("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1) + // log.Tracef("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1) value += 1 currentValues[elapsed] = value p.Entries[countname] = currentValues @@ -143,10 +142,12 @@ func statsRotator() { type counter struct { name string // used as key in periodic stats value int64 + + sync.Mutex } func newDNSCounter(name string) *counter { - // trace("called") + // log.Tracef("called") return &counter{ name: name, } @@ -157,7 +158,9 @@ func (c *counter) IncWithTime(when time.Time) { statistics.PerMinute.Inc(c.name, when) statistics.PerHour.Inc(c.name, when) statistics.PerDay.Inc(c.name, when) + c.Lock() c.value++ + c.Unlock() } func (c *counter) Inc() { @@ -168,6 +171,8 @@ type histogram struct { name string // used as key in periodic stats count int64 total float64 + + sync.Mutex } func newDNSHistogram(name string) *histogram { @@ -181,8 +186,10 @@ func (h *histogram) ObserveWithTime(value float64, when time.Time) { statistics.PerMinute.Observe(h.name, when, value) statistics.PerHour.Observe(h.name, when, value) statistics.PerDay.Observe(h.name, when, value) + h.Lock() h.count++ h.total += value + h.Unlock() } func (h *histogram) Observe(value float64) { diff --git a/filter.go b/filter.go index 1150d292..a776f458 100644 --- a/filter.go +++ b/filter.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io/ioutil" - "log" "os" "path/filepath" "reflect" @@ -13,6 +12,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/hmage/golibs/log" ) var ( diff --git a/go.mod b/go.mod index 36ddb6a2..5b9b68f9 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,18 @@ module github.com/AdguardTeam/AdGuardHome require ( - github.com/AdguardTeam/dnsproxy v0.9.3 + github.com/AdguardTeam/dnsproxy v0.9.9 github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect - github.com/ameshkov/dnscrypt v1.0.1 - github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 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/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86 + github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 github.com/joomcode/errorx v0.1.0 + github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 github.com/miekg/dns v1.1.1 - github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pkg/errors v0.8.0 github.com/shirou/gopsutil v2.18.10+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect - github.com/sirupsen/logrus v1.2.0 go.uber.org/goleak v0.10.0 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 golang.org/x/net v0.0.0-20181220203305-927f97764cc3 diff --git a/go.sum b/go.sum index 1a32fbbb..370c62f4 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,15 @@ -github.com/AdguardTeam/dnsproxy v0.9.0 h1:doHDmVE9bV1fhiBV8rX76WWaSAB9w1H3u8WIiez5OFs= -github.com/AdguardTeam/dnsproxy v0.9.0/go.mod h1:CKZVVknYdoHVirXqqbALEkC+DBY65yCQrzSKYS78GoE= -github.com/AdguardTeam/dnsproxy v0.9.1 h1:+F6jqrVOrUjpbzhALjtbwqHfxW4M2YS3mYdhGxLXQ08= -github.com/AdguardTeam/dnsproxy v0.9.1/go.mod h1:CKZVVknYdoHVirXqqbALEkC+DBY65yCQrzSKYS78GoE= -github.com/AdguardTeam/dnsproxy v0.9.2 h1:P3B2IECZejGv8sxjyLXDbCKMgWqUEFb5rq67lxXCKZ0= -github.com/AdguardTeam/dnsproxy v0.9.2/go.mod h1:CKZVVknYdoHVirXqqbALEkC+DBY65yCQrzSKYS78GoE= -github.com/AdguardTeam/dnsproxy v0.9.3 h1:zgLcsMEQ0hPhU0LjFwPMz4qeXDF+Yy1MO9xc9QaGjbk= -github.com/AdguardTeam/dnsproxy v0.9.3/go.mod h1:GsppU3a1x0hIRtIh7Te8CWHKNHtJaoRXQh08DSRqk0A= +github.com/AdguardTeam/dnsproxy v0.9.9 h1:eFBqEZbWi0IEhux7abNP5VaS91caqR7a1W44Tfi99As= +github.com/AdguardTeam/dnsproxy v0.9.9/go.mod h1:IqBhopgNpzB168kMurbjXf86dn50geasBIuGVxY63j0= 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.0 h1:Y7YexPCxtVCTDXlXu9n17+1H5YS25vftx8vV8Dhuu+E= -github.com/ameshkov/dnscrypt v1.0.0/go.mod h1:EC7Z1GguyEEwhuLXrcgkRTE3GdyPDSWq2OXefhydGWo= -github.com/ameshkov/dnscrypt v1.0.1 h1:Aoy/Sqiqk1b/AlBwdLb31QFUi+O02gzB+wDjhdePie0= -github.com/ameshkov/dnscrypt v1.0.1/go.mod h1:fEeZ+/h8DTt4FxEv9sxN61ygy/8m/vFRqRJcNGJR+r0= +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/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= github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 h1:NpQ+gkFOH27AyDypSCJ/LdsIi/b4rdnEb1N5+IpFfYs= @@ -32,9 +26,9 @@ 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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86 h1:Olj4M6T1omUfx7yTTcnhLf4xo6gYMmRHSJIfeA1NZy0= -github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86/go.mod h1:j/ONpSHHmPgDwmFKXg9vhQvIjADe/ft1X4a3TVOmp9g= 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= @@ -43,8 +37,8 @@ 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= github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho= +github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o= github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc= github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o= @@ -59,39 +53,31 @@ github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBh github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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/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-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4= -golang.org/x/net v0.0.0-20181217023233-e147a9138326/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/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20181217223516-dcdaa6325bcb h1:zzdd4xkMwu/GRxhSUJaCPh4/jil9kAbsU7AUmXboO+A= -golang.org/x/sys v0.0.0-20181217223516-dcdaa6325bcb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 h1:IcgEB62HYgAhX0Nd/QrVgZlxlcyxbGQHElLUhW2X4Fo= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/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= 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= diff --git a/helpers.go b/helpers.go index cf3156da..3ba42544 100644 --- a/helpers.go +++ b/helpers.go @@ -3,7 +3,6 @@ package main import ( "bufio" "errors" - "fmt" "io" "io/ioutil" "net/http" @@ -135,17 +134,3 @@ func _Func() string { f := runtime.FuncForPC(pc[0]) return path.Base(f.Name()) } - -func trace(format string, args ...interface{}) { - pc := make([]uintptr, 10) // at least 1 entry needed - runtime.Callers(2, pc) - f := runtime.FuncForPC(pc[0]) - var buf strings.Builder - buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name()))) - text := fmt.Sprintf(format, args...) - buf.WriteString(text) - if len(text) == 0 || text[len(text)-1] != '\n' { - buf.WriteRune('\n') - } - fmt.Fprint(os.Stderr, buf.String()) -} diff --git a/i18n.go b/i18n.go index 4a52813a..5b8ad62f 100644 --- a/i18n.go +++ b/i18n.go @@ -3,9 +3,10 @@ package main import ( "fmt" "io/ioutil" - "log" "net/http" "strings" + + "github.com/hmage/golibs/log" ) // -------------------- diff --git a/openapi.yaml b/openapi.yaml deleted file mode 100644 index 25ba133c..00000000 --- a/openapi.yaml +++ /dev/null @@ -1,622 +0,0 @@ -swagger: '2.0' -info: - title: 'AdGuard Home' - description: 'Control AdGuard Home server with this API' - version: 0.0.0 -basePath: /control -schemes: - - http -produces: - - application/json -tags: - - - name: global - description: 'DNS server controls' - - - name: filtering - description: 'Rule-based filtering' - - - name: safebrowsing - description: 'Malware/hazardous sites' - - - name: parental - description: 'Sites inappropriate for children' - - - name: safesearch - description: 'Enforce family-friendly results in search engines' -paths: - /status: - get: - tags: - - global - operationId: status - summary: 'Get DNS server status' - responses: - 200: - description: OK - examples: - application/json: - dns_address: 127.0.0.1 - dns_port: 53 - protection_enabled: true - querylog_enabled: true - running: true - bootstrap_dns: 8.8.8.8:53 - upstream_dns: - - 1.1.1.1 - - 1.0.0.1 - version: "v0.1" - language: "en" - /enable_protection: - post: - tags: - -global - operationId: enableProtection - summary: "Enable protection (turns on dnsfilter module in coredns)" - responses: - 200: - description: OK - /disable_protection: - post: - tags: - -global - operationId: disableProtection - summary: "Disable protection (turns off filtering, sb, parental, safesearch temporarily by disabling dnsfilter module in coredns)" - responses: - 200: - description: OK - /querylog: - get: - tags: - - global - operationId: queryLog - summary: 'Get DNS server query log' - parameters: - - in: query - name: download - type: boolean - description: 'If any value is set, make the browser download the query instead of displaying it by setting Content-Disposition header' - responses: - 200: - description: OK - examples: - application/json: - - answer: - - ttl: 55 - type: A - value: 217.69.139.201 - - ttl: 55 - type: A - value: 94.100.180.200 - - ttl: 55 - type: A - value: 94.100.180.201 - - ttl: 55 - type: A - value: 217.69.139.200 - elapsedMs: '65.469556' - question: - class: IN - host: mail.ru - type: A - reason: DNSFILTER_NOTFILTERED_NOTFOUND - status: NOERROR - time: '2018-07-16T22:24:02+03:00' - - elapsedMs: '0.15716999999999998' - question: - class: IN - host: doubleclick.net - type: A - reason: DNSFILTER_FILTERED_BLACKLIST - rule: "||doubleclick.net^" - status: NXDOMAIN - time: '2018-07-16T22:24:02+03:00' - - answer: - - ttl: 299 - type: A - value: 176.103.133.78 - elapsedMs: '132.110929' - question: - class: IN - host: wmconvirus.narod.ru - type: A - reason: DNSFILTER_FILTERED_SAFEBROWSING - rule: adguard-malware-shavar - filterId: 1 - status: NOERROR - time: '2018-07-16T22:24:02+03:00' - /querylog_enable: - post: - tags: - - global - operationId: querylogEnable - summary: 'Enable querylog' - responses: - 200: - description: OK - /querylog_disable: - post: - tags: - - global - operationId: querylogDisable - summary: 'Disable filtering' - responses: - 200: - description: OK - /set_upstream_dns: - post: - tags: - - global - operationId: setUpstreamDNS - summary: 'Set upstream DNS for coredns, empty value will reset it to default values' - consumes: - - text/plain - parameters: - - in: body - name: upstream - description: 'Upstream servers, separated by newline or space, port is optional after colon' - schema: - type: string - example: | - 1.1.1.1 - 1.0.0.1 - 8.8.8.8 8.8.4.4 - 192.168.1.104:53535 - responses: - 200: - description: OK - /test_upstream_dns: - post: - tags: - - global - operationId: testUpstreamDNS - summary: 'Test upstream DNS' - consumes: - - text/plain - parameters: - - in: body - name: upstream - description: 'Upstream servers, separated by newline or space, port is optional after colon' - schema: - type: string - example: | - 1.1.1.1 - 1.0.0.1 - 8.8.8.8 8.8.4.4 - 192.168.1.104:53535 - responses: - 200: - description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.' - examples: - application/json: - 1.1.1.1: OK - 1.0.0.1: OK - 8.8.8.8: OK - 8.8.4.4: OK - "192.168.1.104:53535": "Couldn't communicate with DNS server" - /i18n/change_language: - post: - tags: - - i18n - operationId: changeLanguage - summary: "Change current language. Argument must be an ISO 639-1 two-letter code" - consumes: - - text/plain - parameters: - - in: body - name: language - description: "New language. It must be known to the server and must be an ISO 639-1 two-letter code" - schema: - type: string - example: en - /i18n/current_language: - get: - tags: - - i18n - operationId: currentLanguage - summary: "Get currently set language. Result is ISO 639-1 two-letter code. Empty result means default language." - responses: - 200: - description: OK - examples: - text/plain: - en - /stats_top: - get: - tags: - - global - operationId: statusTop - summary: 'Get DNS server top client, domain and blocked statistics' - responses: - 200: - description: OK - examples: - application/json: - top_queried_domains: - example.org: 12312 - example.com: 12312 - example.net: 12312 - example.ru: 12312 - top_clients: - 127.0.0.1: 12312 - 192.168.0.1: 13211 - 192.168.0.2: 13211 - 192.168.0.3: 13211 - 192.168.0.4: 13211 - 192.168.0.5: 13211 - 192.168.0.6: 13211 - top_blocked_domains: - example.org: 12312 - example.com: 12312 - example.net: 12312 - example.ru: 12312 - /stats: - get: - tags: - - global - operationId: stats - summary: 'Get DNS server statistics' - responses: - 200: - description: 'Gives statistics since start of the server' - examples: - application/json: - dns_queries: 1201 - blocked_filtering: 123 - replaced_safebrowsing: 5 - replaced_parental: 18 - replaced_safesearch: 94 - avg_processing_time: 123 - /stats_history: - get: - tags: - - global - operationId: stats_history - summary: 'Get historical DNS server statistics' - parameters: - - - name: start_time - in: query - type: string - description: 'Start time in ISO8601 (example: `2018-05-04T17:55:33+00:00`)' - required: true - - - name: end_time - in: query - type: string - description: 'End time in ISO8601 (example: `2018-05-04T17:55:33+00:00`)' - required: true - - - name: time_unit - in: query - type: string - description: 'Time unit (`minutes` or `hours`)' - required: true - enum: - - minutes - - hours - responses: - 501: - description: 'Requested time window is outside of supported range. It will be supported later, but not now.' - 200: - description: 'Gives statistics since start of the server. Example below is for 5 minutes. Values are from oldest to newest.' - examples: - application/json: - dns_queries: - - 1201 - - 1201 - - 1201 - - 1201 - - 1201 - blocked_filtering: - - 123 - - 123 - - 123 - - 123 - - 123 - replaced_safebrowsing: - - 5 - - 5 - - 5 - - 5 - - 5 - replaced_parental: - - 18 - - 18 - - 18 - - 18 - - 18 - replaced_safesearch: - - 94 - - 94 - - 94 - - 94 - - 94 - avg_processing_time: - - 123 - - 123 - - 123 - - 123 - - 123 - /stats_reset: - post: - tags: - -global - operationId: statsReset - summary: "Reset all statistics to zeroes" - responses: - 200: - description: OK - /filtering/enable: - post: - tags: - - filtering - operationId: filteringEnable - summary: 'Enable filtering' - responses: - 200: - description: OK - /filtering/disable: - post: - tags: - - filtering - operationId: filteringDisable - summary: 'Disable filtering' - responses: - 200: - description: OK - /filtering/add_url: - put: - tags: - - filtering - operationId: filteringAddURL - summary: 'Add filter URL' - consumes: - - text/plain - parameters: - - in: body - name: url - description: 'URL containing filtering rules' - required: true - schema: - type: string - example: 'url=https://filters.adtidy.org/windows/filters/15.txt' - responses: - 200: - description: OK - /filtering/remove_url: - delete: - tags: - - filtering - operationId: filteringRemoveURL - summary: 'Remove filter URL' - consumes: - - text/plain - parameters: - - in: body - name: url - description: 'Previously added URL containing filtering rules' - required: true - schema: - type: string - example: 'url=https://filters.adtidy.org/windows/filters/15.txt' - responses: - 200: - description: OK - /filtering/enable_url: - post: - tags: - - filtering - operationId: filteringEnableURL - summary: 'Enable filter URL' - consumes: - - text/plain - parameters: - - in: body - name: url - description: 'Previously added URL containing filtering rules' - required: true - schema: - type: string - example: 'url=https://filters.adtidy.org/windows/filters/15.txt' - responses: - 200: - description: OK - /filtering/disable_url: - post: - tags: - - filtering - operationId: filteringDisableURL - summary: 'Disable filter URL' - consumes: - - text/plain - parameters: - - in: body - name: url - description: 'Previously added URL containing filtering rules' - required: true - schema: - type: string - example: 'url=https://filters.adtidy.org/windows/filters/15.txt' - responses: - 200: - description: OK - /filtering/refresh: - post: - tags: - - filtering - operationId: filteringRefresh - summary: | - Reload filtering rules from URLs - - This might be needed if new URL was just added and you dont want to wait for automatic refresh to kick in. - - This API request is ratelimited, so you can call it freely as often as you like, it wont create unneccessary burden on servers that host the URL. - - This should work as intended, a `force` parameter is offered as last-resort attempt to make filter lists fresh. - - If you ever find yourself using `force` to make something work that otherwise wont, this is a bug and report it accordingly. - - parameters: - - - name: force - in: query - type: boolean - description: 'If any value is set, ignore cache and force re-download of all filters' - responses: - 200: - description: OK with how many filters were actually updated - /filtering/status: - get: - tags: - - filtering - operationId: filteringStatus - summary: 'Get status of rules-based filter' - responses: - 200: - description: OK - examples: - application/json: - enabled: false - - filters: - enabled: true - id: 1 - lastUpdated: "2018-10-30T12:18:57.223101822+03:00" - name: "AdGuard Simplified Domain Names filter" - rulesCount: 24896 - url: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt" - rules: - - '@@||yandex.ru^|' - /filtering/set_rules: - put: - tags: - - filtering - operationId: filteringSetRules - summary: 'Set user-defined filter rules' - consumes: - - text/plain - parameters: - - in: body - name: rules - description: 'All filtering rules, one line per rule' - schema: - type: string - example: '@@||yandex.ru^|' - responses: - 200: - description: OK - /safebrowsing/enable: - post: - tags: - - safebrowsing - operationId: safebrowsingEnable - summary: 'Enable safebrowsing' - responses: - 200: - description: OK - /safebrowsing/disable: - post: - tags: - - safebrowsing - operationId: safebrowsingDisable - summary: 'Disable safebrowsing' - responses: - 200: - description: OK - /safebrowsing/status: - get: - tags: - - safebrowsing - operationId: safebrowsingStatus - summary: 'Get safebrowsing status' - responses: - 200: - description: OK - examples: - application/json: - enabled: false - /parental/enable: - post: - tags: - - parental - operationId: parentalEnable - summary: 'Enable parental filtering' - consumes: - - text/plain - parameters: - - in: body - name: sensitivity - description: | - Age sensitivity for parental filtering, - EARLY_CHILDHOOD is 3 - YOUNG is 10 - TEEN is 13 - MATURE is 17 - - required: true - schema: - type: string - enum: - - EARLY_CHILDHOOD - - YOUNG - - TEEN - - MATURE - example: 'sensitivity=TEEN' - responses: - 200: - description: OK - /parental/disable: - post: - tags: - - parental - operationId: parentalDisable - summary: 'Disable parental filtering' - responses: - 200: - description: OK - /parental/status: - get: - tags: - - parental - operationId: parentalStatus - summary: 'Get parental filtering status' - responses: - 200: - description: OK - examples: - application/json: - enabled: true - sensitivity: 13 - /safesearch/enable: - post: - tags: - - safesearch - operationId: safesearchEnable - summary: 'Enable safesearch' - responses: - 200: - description: OK - /safesearch/disable: - post: - tags: - - safesearch - operationId: safesearchDisable - summary: 'Disable safesearch' - responses: - 200: - description: OK - /safesearch/status: - get: - tags: - - safesearch - operationId: safesearchStatus - summary: 'Get safesearch status' - responses: - 200: - description: OK - examples: - application/json: - enabled: false -definitions: - rule: - type: string diff --git a/openapi/.gitignore b/openapi/.gitignore new file mode 100644 index 00000000..91dfed8d --- /dev/null +++ b/openapi/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +node_modules \ No newline at end of file diff --git a/openapi/README.md b/openapi/README.md new file mode 100644 index 00000000..cd689408 --- /dev/null +++ b/openapi/README.md @@ -0,0 +1,13 @@ +## AdGuard Home OpenAPI + +We are using [OpenAPI specification](https://swagger.io/docs/specification/about/) to generate AdGuard Home API specification. + +### How to edit the API spec + +The easiest way would be to use [Swagger Editor](http://editor.swagger.io/) and just copy/paste the YAML file there. + +### How to read the API doc + +1. `yarn install` +2. `yarn start` +3. Open `http://localhost:4000/` diff --git a/openapi/index.html b/openapi/index.html new file mode 100644 index 00000000..e02e40e5 --- /dev/null +++ b/openapi/index.html @@ -0,0 +1,60 @@ + + + + + + AdGuard Home API + + + + + + + + + + + + + + \ No newline at end of file diff --git a/openapi/index.js b/openapi/index.js new file mode 100644 index 00000000..a63d206c --- /dev/null +++ b/openapi/index.js @@ -0,0 +1,8 @@ +const express = require('express') + +const app = express() + +app.use(express.static(__dirname)) + +console.log('Open http://localhost:4000/ to examine the API spec') +app.listen(4000) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml new file mode 100644 index 00000000..97b94c99 --- /dev/null +++ b/openapi/openapi.yaml @@ -0,0 +1,1066 @@ +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.91.0 +schemes: + - http +basePath: /control +produces: + - application/json +tags: + - + name: global + description: 'AdGuard Home server general settings and controls' + - + name: log + description: 'AdGuard Home query log' + - + name: stats + description: 'AdGuard Home statistics' + - + name: i18n + description: 'Application localization' + - + name: filtering + description: 'Rule-based filtering' + - + name: safebrowsing + description: 'Blocking malware/phishing sites' + - + name: parental + description: 'Blocking adult and explicit materials' + - + name: safesearch + description: 'Enforce family-friendly results in search engines' + - + name: dhcp + description: 'Built-in DHCP server controls' +paths: + + # API TO-DO LIST + # TODO: Use JSON where it is possible + # TODO: Use lower_case for all objects' properties + # TODO: Move to definitions what's missing from there + + # -------------------------------------------------- + # General settings and controls + # -------------------------------------------------- + + /status: + get: + tags: + - global + operationId: status + summary: 'Get DNS server current status and general settings' + produces: + - application/json + responses: + 200: + description: OK + schema: + $ref: "#/definitions/ServerStatus" + + /enable_protection: + post: + tags: + - global + operationId: enableProtection + summary: "Enable protection (turns on dnsfilter module in coredns)" + responses: + 200: + description: OK + + /disable_protection: + post: + tags: + - global + operationId: disableProtection + summary: "Disable protection (turns off filtering, sb, parental, safesearch temporarily by disabling dnsfilter module in coredns)" + responses: + 200: + description: OK + + /set_upstream_dns: + post: + tags: + - global + operationId: setUpstreamDNS + summary: 'Set upstream DNS for coredns, empty value will reset it to default values' + consumes: + - text/plain + parameters: + - in: body + name: upstream + description: 'Upstream servers, separated by newline or space, port is optional after colon' + schema: + # TODO: use JSON + type: string + example: | + 1.1.1.1 + 1.0.0.1 + 8.8.8.8 8.8.4.4 + 192.168.1.104:53535 + responses: + 200: + description: OK + + /test_upstream_dns: + post: + tags: + - global + operationId: testUpstreamDNS + summary: 'Test upstream DNS' + consumes: + - text/plain + parameters: + - in: body + name: upstream + description: 'Upstream servers, separated by newline or space, port is optional after colon' + schema: + # TODO: use JSON + type: string + example: | + 1.1.1.1 + 1.0.0.1 + 8.8.8.8 8.8.4.4 + 192.168.1.104:53535 + responses: + 200: + description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.' + examples: + application/json: + 1.1.1.1: OK + 1.0.0.1: OK + 8.8.8.8: OK + 8.8.4.4: OK + "192.168.1.104:53535": "Couldn't communicate with DNS server" + + /version.json: + get: + tags: + - global + operationId: getVersionJson + summary: 'Gets information about the latest available version of AdGuard' + produces: + - 'application/json' + responses: + 200: + description: 'Version info' + schema: + $ref: "#/definitions/VersionInfo" + 500: + description: 'Cannot write answer' + 502: + description: 'Cannot retrieve the version.json file contents' + + # -------------------------------------------------- + # Query log methods + # -------------------------------------------------- + + /querylog: + get: + tags: + - log + operationId: queryLog + summary: 'Get DNS server query log' + parameters: + - in: query + name: download + type: boolean + description: 'If any value is set, make the browser download the query instead of displaying it by setting Content-Disposition header' + responses: + 200: + description: OK + schema: + $ref: '#/definitions/QueryLog' + /querylog_enable: + post: + tags: + - log + operationId: querylogEnable + summary: 'Enable querylog' + responses: + 200: + description: OK + /querylog_disable: + post: + tags: + - log + operationId: querylogDisable + summary: 'Disable filtering' + responses: + 200: + description: OK + + # -------------------------------------------------- + # General statistics methods + # -------------------------------------------------- + + /stats_top: + get: + tags: + - stats + operationId: statusTop + summary: 'Get DNS server top client, domain and blocked statistics' + responses: + 200: + description: OK + schema: + $ref: "#/definitions/StatsTop" + + /stats: + get: + tags: + - stats + operationId: stats + summary: 'Get DNS server statistics' + responses: + 200: + description: 'Returns general statistics for the last 24 hours' + schema: + $ref: "#/definitions/Stats" + + /stats_history: + get: + tags: + - stats + operationId: stats_history + summary: 'Get historical DNS server statistics for the last 24 hours' + parameters: + - + name: start_time + in: query + type: string + description: 'Start time in ISO8601 (example: `2018-05-04T17:55:33+00:00`)' + required: true + - + name: end_time + in: query + type: string + description: 'End time in ISO8601 (example: `2018-05-04T17:55:33+00:00`)' + required: true + - + name: time_unit + in: query + type: string + description: 'Time unit (`minutes` or `hours`)' + required: true + enum: + - minutes + - hours + responses: + 501: + description: 'Requested time window is outside of supported range. It will be supported later, but not now.' + 200: + description: 'Returns historical stats for the specified time interval.' + schema: + $ref: '#/definitions/StatsHistory' + + /stats_reset: + post: + tags: + - stats + operationId: statsReset + summary: "Reset all statistics to zeroes" + responses: + 200: + description: OK + + # -------------------------------------------------- + # DHCP server methods + # -------------------------------------------------- + + /dhcp/status: + get: + tags: + - dhcp + operationId: dhcpStatus + summary: "Gets the current DHCP settings and status" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/DhcpStatus" + + /dhcp/set_config: + post: + tags: + - dhcp + operationId: dhcpSetConfig + summary: "Updates the current DHCP server configuration" + consumes: + - application/json + parameters: + - in: "body" + name: "body" + description: "DHCP configuration JSON" + required: true + schema: + $ref: "#/definitions/DhcpConfig" + responses: + 200: + description: OK + + /dhcp/find_active_dhcp: + post: + tags: + - dhcp + operationId: checkActiveDhcp + summary: "Searches for an active DHCP server on the network" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/DhcpSearchResult" + + # -------------------------------------------------- + # Filtering status methods + # -------------------------------------------------- + + /filtering/status: + get: + tags: + - filtering + operationId: filteringStatus + summary: 'Get status of rules-based filter' + responses: + 200: + description: OK + schema: + $ref: "#/definitions/FilteringStatus" + + /filtering/enable: + post: + tags: + - filtering + operationId: filteringEnable + summary: 'Enable filtering' + responses: + 200: + description: OK + + /filtering/disable: + post: + tags: + - filtering + operationId: filteringDisable + summary: 'Disable filtering' + responses: + 200: + description: OK + + /filtering/add_url: + put: + tags: + - filtering + operationId: filteringAddURL + summary: 'Add filter URL' + consumes: + - text/plain + parameters: + - in: body + name: url + description: 'URL containing filtering rules' + required: true + schema: + type: string + example: 'url=https://filters.adtidy.org/windows/filters/15.txt' + responses: + 200: + description: OK + + /filtering/remove_url: + delete: + tags: + - filtering + operationId: filteringRemoveURL + summary: 'Remove filter URL' + consumes: + - text/plain + parameters: + - in: body + name: url + description: 'Previously added URL containing filtering rules' + required: true + schema: + type: string + example: 'url=https://filters.adtidy.org/windows/filters/15.txt' + responses: + 200: + description: OK + + /filtering/enable_url: + post: + tags: + - filtering + operationId: filteringEnableURL + summary: 'Enable filter URL' + consumes: + - text/plain + parameters: + - in: body + name: url + description: 'Previously added URL containing filtering rules' + required: true + schema: + type: string + example: 'url=https://filters.adtidy.org/windows/filters/15.txt' + responses: + 200: + description: OK + + /filtering/disable_url: + post: + tags: + - filtering + operationId: filteringDisableURL + summary: 'Disable filter URL' + consumes: + - text/plain + parameters: + - in: body + name: url + description: 'Previously added URL containing filtering rules' + required: true + schema: + type: string + example: 'url=https://filters.adtidy.org/windows/filters/15.txt' + responses: + 200: + description: OK + + /filtering/refresh: + post: + tags: + - filtering + operationId: filteringRefresh + summary: | + Reload filtering rules from URLs + + This might be needed if new URL was just added and you dont want to wait for automatic refresh to kick in. + + This API request is ratelimited, so you can call it freely as often as you like, it wont create unneccessary burden on servers that host the URL. + + This should work as intended, a `force` parameter is offered as last-resort attempt to make filter lists fresh. + + If you ever find yourself using `force` to make something work that otherwise wont, this is a bug and report it accordingly. + + parameters: + - + name: force + in: query + type: boolean + description: 'If any value is set, ignore cache and force re-download of all filters' + responses: + 200: + description: OK with how many filters were actually updated + + /filtering/set_rules: + put: + tags: + - filtering + operationId: filteringSetRules + summary: 'Set user-defined filter rules' + consumes: + - text/plain + parameters: + - in: body + name: rules + description: 'All filtering rules, one line per rule' + schema: + # TODO: move to definitions + type: string + example: '@@||yandex.ru^|' + responses: + 200: + description: OK + + # -------------------------------------------------- + # Safebrowsing methods + # -------------------------------------------------- + + /safebrowsing/enable: + post: + tags: + - safebrowsing + operationId: safebrowsingEnable + summary: 'Enable safebrowsing' + responses: + 200: + description: OK + + /safebrowsing/disable: + post: + tags: + - safebrowsing + operationId: safebrowsingDisable + summary: 'Disable safebrowsing' + responses: + 200: + description: OK + + /safebrowsing/status: + get: + tags: + - safebrowsing + operationId: safebrowsingStatus + summary: 'Get safebrowsing status' + responses: + 200: + description: OK + examples: + application/json: + enabled: false + + # -------------------------------------------------- + # Parental control methods + # -------------------------------------------------- + + /parental/enable: + post: + tags: + - parental + operationId: parentalEnable + summary: 'Enable parental filtering' + consumes: + - text/plain + parameters: + - in: body + name: sensitivity + description: | + Age sensitivity for parental filtering, + EARLY_CHILDHOOD is 3 + YOUNG is 10 + TEEN is 13 + MATURE is 17 + + required: true + schema: + type: string + enum: + - EARLY_CHILDHOOD + - YOUNG + - TEEN + - MATURE + example: 'sensitivity=TEEN' + responses: + 200: + description: OK + + /parental/disable: + post: + tags: + - parental + operationId: parentalDisable + summary: 'Disable parental filtering' + responses: + 200: + description: OK + + /parental/status: + get: + tags: + - parental + operationId: parentalStatus + summary: 'Get parental filtering status' + responses: + 200: + description: OK + examples: + application/json: + enabled: true + sensitivity: 13 + + # -------------------------------------------------- + # Safe search methods + # -------------------------------------------------- + + /safesearch/enable: + post: + tags: + - safesearch + operationId: safesearchEnable + summary: 'Enable safesearch' + responses: + 200: + description: OK + + /safesearch/disable: + post: + tags: + - safesearch + operationId: safesearchDisable + summary: 'Disable safesearch' + responses: + 200: + description: OK + + /safesearch/status: + get: + tags: + - safesearch + operationId: safesearchStatus + summary: 'Get safesearch status' + responses: + 200: + description: OK + examples: + application/json: + enabled: false + + # -------------------------------------------------- + # I18N methods + # -------------------------------------------------- + + /i18n/change_language: + post: + tags: + - i18n + operationId: changeLanguage + summary: "Change current language. Argument must be an ISO 639-1 two-letter code" + consumes: + - text/plain + parameters: + - in: body + name: language + description: "New language. It must be known to the server and must be an ISO 639-1 two-letter code" + schema: + # TODO: use JSON? + type: string + example: en + responses: + 200: + description: OK + + /i18n/current_language: + get: + tags: + - i18n + operationId: currentLanguage + summary: "Get currently set language. Result is ISO 639-1 two-letter code. Empty result means default language." + responses: + 200: + description: OK + examples: + text/plain: + en + +definitions: + ServerStatus: + type: "object" + description: "AdGuard Home server status and configuration" + required: + - "dns_address" + - "dns_port" + - "protection_enabled" + - "querylog_enabled" + - "running" + - "bootstrap_dns" + - "upstream_dns" + - "version" + - "language" + properties: + dns_address: + type: "string" + example: "127.0.0.1" + dns_port: + type: "integer" + format: "int32" + example: 53 + minimum: 1 + maximum: 65535 + protection_enabled: + type: "boolean" + querylog_enabled: + type: "boolean" + running: + type: "boolean" + bootstrap_dns: + type: "string" + example: "8.8.8.8:53" + upstream_dns: + type: "array" + items: + type: "string" + example: + - "tls://1.1.1.1" + - "tls://1.0.0.1" + version: + type: "string" + example: "0.1" + language: + type: "string" + example: "en" + Filter: + type: "object" + description: "Filter subscription info" + required: + - "enabled" + - "id" + - "lastUpdated" + - "name" + - "rulesCount" + - "url" + properties: + enabled: + type: "boolean" + id: + type: "integer" + example: 1234 + lastUpdated: + type: "string" + format: "date-time" + example: "2018-10-30T12:18:57.223101822+03:00" + name: + type: "string" + example: "AdGuard Simplified Domain Names filter" + rulesCount: + type: "integer" + example: 5912 + url: + type: "string" + example: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt" + FilteringStatus: + type: "object" + description: "Filtering settings" + required: + - "enabled" + - "filters" + - "user_rules" + properties: + enabled: + type: "boolean" + filters: + type: "array" + items: + $ref: "#/definitions/Filter" + user_rules: + type: "array" + items: + type: "string" + example: + - '||example.org^' + - '||example.com^' + VersionInfo: + type: "object" + description: "Information about the latest available version of AdGuard Home" + required: + - "version" + - "announcement" + - "announcement_url" + - "download_darwin_amd64" + - "download_linux_amd64" + - "download_linux_386" + - "download_linux_arm" + - "selfupdate_min_version" + properties: + version: + type: "string" + example: "v0.9" + announcement: + type: "string" + example: "AdGuard Home v0.9 is now available!" + announcement_url: + type: "string" + example: "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9" + download_darwin_amd64: + type: "string" + example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_MacOS.zip" + download_linux_amd64: + type: "string" + example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_amd64.tar.gz" + download_linux_386: + type: "string" + example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_386.tar.gz" + download_linux_arm: + type: "string" + example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_arm.tar.gz" + selfupdate_min_version: + type: "string" + example: "v0.0" + Stats: + type: "object" + description: "General server stats for the last 24 hours" + required: + - "dns_queries" + - "blocked_filtering" + - "replaced_safebrowsing" + - "replaced_parental" + - "replaced_safesearch" + - "avg_processing_time" + properties: + dns_queries: + type: "integer" + description: "Total number of DNS queries" + example: 123 + blocked_filtering: + type: "integer" + description: "Number of requests blocked by filtering rules" + example: 50 + replaced_safebrowsing: + type: "integer" + description: "Number of requests blocked by the safebrowsing module" + example: 5 + replaced_parental: + type: "integer" + description: "Number of blocked adult websites" + example: 15 + avg_processing_time: + type: "number" + format: "float" + description: "Average time in milliseconds on processing a DNS" + example: 0.34 + StatsTop: + type: "object" + description: "Server stats top charts" + required: + - "top_queried_domains" + - "top_clients" + - "top_blocked_domains" + properties: + top_queried_domains: + type: "array" + items: + type: "object" + example: + example.org: 12312 + example.com: 321 + example.net: 5555 + top_clients: + type: "array" + items: + type: "object" + example: + 127.0.0.1: 12312 + 192.168.0.1: 13211 + 192.168.0.3: 13211 + top_blocked_domains: + type: "array" + items: + type: "object" + example: + example.org: 12312 + example.com: 321 + example.net: 5555 + StatsHistory: + type: "object" + description: "Historical stats of the DNS server. Example below is for 5 minutes. Values are from oldest to newest." + required: + - "dns_queries" + - "blocked_filtering" + - "replaced_safebrowsing" + - "replaced_parental" + - "replaced_safesearch" + - "avg_processing_time" + properties: + dns_queries: + type: "array" + items: + type: "integer" + example: + - 1201 + - 1501 + - 1251 + - 1231 + - 120 + blocked_filtering: + type: "array" + items: + type: "integer" + example: + - 421 + - 124 + - 5 + - 12 + - 43 + replaced_safebrowsing: + type: "array" + items: + type: "integer" + example: + - 1 + - 0 + - 5 + - 0 + - 0 + replaced_parental: + type: "array" + items: + type: "integer" + example: + - 120 + - 10 + - 5 + - 12 + - 1 + replaced_safesearch: + type: "array" + items: + type: "integer" + example: + - 1 + - 0 + - 0 + - 0 + - 5 + avg_processing_time: + type: "array" + items: + type: "number" + format: "float" + example: + - 1.25 + - 5.12 + - 4.12 + - 123.12 + - 0.12 + DhcpConfig: + type: "object" + description: "Built-in DHCP server configuration" + required: + - "enabled" + - "gateway_ip" + - "subnet_mask" + - "range_start" + - "range_end" + - "lease_duration" + properties: + enabled: + type: "boolean" + gateway_ip: + type: "string" + example: "192.168.1.1" + subnet_mask: + type: "string" + example: "255.255.255.0" + range_start: + type: "string" + example: "192.168.1.2" + range_end: + type: "string" + example: "192.168.10.50" + lease_duration: + type: "string" + example: "12h" + DhcpLease: + type: "object" + description: "DHCP lease information" + required: + - "mac" + - "ip" + - "hostname" + - "expires" + properties: + mac: + type: "string" + example: "001109b3b3b8" + ip: + type: "string" + example: "192.168.1.22" + hostname: + type: "string" + example: "dell" + expires: + type: "string" + format: "date-time" + example: "2017-07-21T17:32:28Z" + DhcpStatus: + type: "object" + description: "Built-in DHCP server configuration and status" + required: + - "config" + - "leases" + properties: + config: + $ref: "#/definitions/DhcpConfig" + leases: + type: "array" + items: + $ref: "#/definitions/DhcpLease" + DhcpSearchResult: + type: "object" + description: "Information about a DHCP server discovered in the current network" + required: + - "found" + properties: + found: + type: "boolean" + gateway_ip: + type: "string" + example: "192.168.1.1" + DnsAnswer: + type: "object" + description: "DNS answer section" + properties: + ttl: + type: "integer" + example: 55 + type: + type: "string" + example: "A" + value: + type: "string" + example: "217.69.139.201" + DnsQuestion: + type: "object" + description: "DNS question section" + properties: + class: + type: "string" + example: "IN" + host: + type: "string" + example: "example.org" + type: + type: "string" + example: "A" + QueryLogItem: + type: "object" + description: "Query log item" + properties: + answer: + type: "array" + items: + $ref: "#/definitions/DnsAnswer" + client: + type: "string" + example: "192.168.0.1" + elapsedMs: + type: "string" + example: "54.023928" + question: + $ref: "#/definitions/DnsQuestion" + filterId: + type: "integer" + example: 123123 + description: "In case if there's a rule applied to this DNS request, this is ID of the filter that rule belongs to." + rule: + type: "string" + example: "||example.org^" + description: "Filtering rule applied to the request (if any)" + reason: + type: "string" + description: "DNS filter status" + enum: + - "NotFilteredNotFound" + - "NotFilteredWhiteList" + - "NotFilteredError" + - "FilteredBlackList" + - "FilteredSafeBrowsing" + - "FilteredParental" + - "FilteredInvalid" + - "FilteredSafeSearch" + status: + type: "string" + description: "DNS response status" + example: "NOERROR" + time: + type: "string" + description: "DNS request processing start time" + example: "2018-11-26T00:02:41+03:00" + QueryLog: + type: "array" + description: "Query log" + items: + $ref: "#/definitions/QueryLogItem" \ No newline at end of file diff --git a/openapi/package.json b/openapi/package.json new file mode 100644 index 00000000..9b3d78ed --- /dev/null +++ b/openapi/package.json @@ -0,0 +1,12 @@ +{ + "name": "adguard-home-api", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "express": "^4.16.4", + "swagger-ui-dist": "^3.20.1" + } +} diff --git a/openapi/yarn.lock b/openapi/yarn.lock new file mode 100644 index 00000000..2eeacbe0 --- /dev/null +++ b/openapi/yarn.lock @@ -0,0 +1,349 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.16.4: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ipaddr.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== + +mime-types@~2.1.18: + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== + dependencies: + mime-db "~1.37.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +proxy-addr@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" + integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.8.0" + +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + +swagger-ui-dist@^3.20.1: + version "3.20.1" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.20.1.tgz#2e871faf29984bc5f253b8641ba38b244001cfa4" + integrity sha512-iqFNNmJWH24leUj/ohS5iZTHLZSPZse8c9F+WSCMi6ZJcRBgYKcT413c8BR5BEdKvU1kkIwvYy7C8DOjTRq9hQ== + +type-is@~1.6.16: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= diff --git a/upgrade.go b/upgrade.go index 21d7686d..452df4d0 100644 --- a/upgrade.go +++ b/upgrade.go @@ -3,10 +3,10 @@ package main import ( "fmt" "io/ioutil" - "log" "os" "path/filepath" + "github.com/hmage/golibs/log" "gopkg.in/yaml.v2" )