From d9482b75884e7c969419bfdb522103c69028adbc Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Thu, 21 Jan 2021 19:55:41 +0300 Subject: [PATCH] Pull request: 2509 type-safety vol.2 Merge in DNS/adguard-home from 2509-type-safety-vol2 to master Updates #2509. Squashed commit of the following: commit c944e4e0a9949fc894c90b4bc1f739148a67fd9d Author: Eugene Burkov Date: Thu Jan 21 19:36:20 2021 +0300 all: imp docs commit e8ac1815c492b0a9434596e35a48755cac2b9f3b Author: Eugene Burkov Date: Wed Jan 20 12:38:48 2021 +0300 all: imp JSON encoding, decoding --- internal/home/control.go | 59 +++++++++++++++++--------------- internal/home/controlinstall.go | 56 +++++++++--------------------- internal/home/controlupdate.go | 58 ++++++++++++++++--------------- internal/stats/http.go | 37 ++++++++++++++------ internal/stats/stats_test.go | 35 ++++++++++--------- internal/stats/unit.go | 45 ++++++++++-------------- internal/updater/check.go | 44 +++++++++++++++--------- internal/updater/updater_test.go | 12 +++++-- internal/util/network.go | 54 ++++++++++++++++++----------- openapi/openapi.yaml | 15 +++++++- 10 files changed, 226 insertions(+), 189 deletions(-) diff --git a/internal/home/control.go b/internal/home/control.go index f309663f..49d6b31c 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -44,41 +44,44 @@ func addDNSAddress(dnsAddresses *[]string, addr net.IP) { *dnsAddresses = append(*dnsAddresses, hostport) } +// statusResponse is a response for /control/status endpoint. +type statusResponse struct { + DNSAddrs []string `json:"dns_addresses"` + DNSPort int `json:"dns_port"` + HTTPPort int `json:"http_port"` + IsProtectionEnabled bool `json:"protection_enabled"` + // TODO(e.burkov): Inspect if front-end doesn't requires this field as + // openapi.yaml declares. + IsDHCPAvailable bool `json:"dhcp_available"` + IsRunning bool `json:"running"` + Version string `json:"version"` + Language string `json:"language"` +} + func handleStatus(w http.ResponseWriter, _ *http.Request) { - c := dnsforward.FilteringConfig{} + resp := statusResponse{ + DNSAddrs: getDNSAddresses(), + DNSPort: config.DNS.Port, + HTTPPort: config.BindPort, + IsRunning: isRunning(), + Version: version.Version(), + Language: config.Language, + } + + var c *dnsforward.FilteringConfig if Context.dnsServer != nil { - Context.dnsServer.WriteDiskConfig(&c) + c = &dnsforward.FilteringConfig{} + Context.dnsServer.WriteDiskConfig(c) + resp.IsProtectionEnabled = c.ProtectionEnabled } - data := map[string]interface{}{ - "dns_addresses": getDNSAddresses(), - "http_port": config.BindPort, - "dns_port": config.DNS.Port, - "running": isRunning(), - "version": version.Version(), - "language": config.Language, - - "protection_enabled": c.ProtectionEnabled, + // IsDHCPAvailable field is now false by default for Windows. + if runtime.GOOS != "windows" { + resp.IsDHCPAvailable = Context.dhcpServer != nil } - if runtime.GOOS == "windows" { - // Set the DHCP to false explicitly, because Context.dhcpServer - // is probably not nil, despite the fact that there is no - // support for DHCP on Windows in AdGuardHome. - // - // See also the TODO in dhcpd.Create. - data["dhcp_available"] = false - } else { - data["dhcp_available"] = (Context.dhcpServer != nil) - } - - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(resp) if err != nil { httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err) return diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index ae93184c..da223ebd 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -21,23 +21,16 @@ import ( "github.com/AdguardTeam/golibs/log" ) -type firstRunData struct { - WebPort int `json:"web_port"` - DNSPort int `json:"dns_port"` - Interfaces map[string]interface{} `json:"interfaces"` +// getAddrsResponse is the response for /install/get_addresses endpoint. +type getAddrsResponse struct { + WebPort int `json:"web_port"` + DNSPort int `json:"dns_port"` + Interfaces map[string]*util.NetInterface `json:"interfaces"` } -type netInterfaceJSON struct { - Name string `json:"name"` - MTU int `json:"mtu"` - HardwareAddr string `json:"hardware_address"` - Addresses []net.IP `json:"ip_addresses"` - Flags string `json:"flags"` -} - -// Get initial installation settings +// handleInstallGetAddresses is the handler for /install/get_addresses endpoint. func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { - data := firstRunData{} + data := getAddrsResponse{} data.WebPort = 80 data.DNSPort = 53 @@ -47,16 +40,9 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request return } - data.Interfaces = make(map[string]interface{}) + data.Interfaces = make(map[string]*util.NetInterface) for _, iface := range ifaces { - ifaceJSON := netInterfaceJSON{ - Name: iface.Name, - MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr, - Addresses: iface.Addresses, - Flags: iface.Flags, - } - data.Interfaces[iface.Name] = ifaceJSON + data.Interfaces[iface.Name] = iface } w.Header().Set("Content-Type", "application/json") @@ -520,15 +506,15 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques web.handleInstallConfigure(w, r) } -// firstRunDataBeta is a struct representing new client's getting addresses +// getAddrsResponseBeta is a struct representing new client's getting addresses // request body. It uses array of structs instead of map. // // TODO(e.burkov): This should removed with the API v1 when the appropriate // functionality will appear in default firstRunData. -type firstRunDataBeta struct { - WebPort int `json:"web_port"` - DNSPort int `json:"dns_port"` - Interfaces []netInterfaceJSON `json:"interfaces"` +type getAddrsResponseBeta struct { + WebPort int `json:"web_port"` + DNSPort int `json:"dns_port"` + Interfaces []*util.NetInterface `json:"interfaces"` } // handleInstallConfigureBeta is a substitution of /install/get_addresses @@ -537,7 +523,7 @@ type firstRunDataBeta struct { // TODO(e.burkov): This should removed with the API v1 when the appropriate // functionality will appear in default handleInstallGetAddresses. func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Request) { - data := firstRunDataBeta{} + data := getAddrsResponseBeta{} data.WebPort = 80 data.DNSPort = 53 @@ -547,17 +533,7 @@ func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Req return } - data.Interfaces = make([]netInterfaceJSON, 0, len(ifaces)) - for _, iface := range ifaces { - ifaceJSON := netInterfaceJSON{ - Name: iface.Name, - MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr, - Addresses: iface.Addresses, - Flags: iface.Flags, - } - data.Interfaces = append(data.Interfaces, ifaceJSON) - } + data.Interfaces = ifaces w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(data) diff --git a/internal/home/controlupdate.go b/internal/home/controlupdate.go index a502b902..327f2f90 100644 --- a/internal/home/controlupdate.go +++ b/internal/home/controlupdate.go @@ -16,10 +16,6 @@ import ( "github.com/AdguardTeam/golibs/log" ) -type getVersionJSONRequest struct { - RecheckNow bool `json:"recheck_now"` -} - // temporaryError is the interface for temporary errors from the Go standard // library. type temporaryError interface { @@ -29,31 +25,34 @@ type temporaryError interface { // Get the latest available version from the Internet func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { + resp := &versionResponse{} if Context.disableUpdate { - resp := make(map[string]interface{}) - resp["disabled"] = true - d, _ := json.Marshal(resp) - _, _ = w.Write(d) + // w.Header().Set("Content-Type", "application/json") + resp.Disabled = true + _ = json.NewEncoder(w).Encode(resp) + // TODO(e.burkov): Add error handling and deal with headers. return } - req := getVersionJSONRequest{} + req := &struct { + Recheck bool `json:"recheck_now"` + }{} + var err error if r.ContentLength != 0 { - err = json.NewDecoder(r.Body).Decode(&req) + err = json.NewDecoder(r.Body).Decode(req) if err != nil { httpError(w, http.StatusBadRequest, "JSON parse: %s", err) return } } - var info updater.VersionInfo for i := 0; i != 3; i++ { func() { Context.controlLock.Lock() defer Context.controlLock.Unlock() - info, err = Context.updater.VersionInfo(req.RecheckNow) + resp.VersionInfo, err = Context.updater.VersionInfo(req.Recheck) }() if err != nil { @@ -76,13 +75,16 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { } if err != nil { vcu := Context.updater.VersionCheckURL() + // TODO(a.garipov): Figure out the purpose of %T verb. httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", vcu, err, err) return } + resp.confirmAutoUpdate() + w.Header().Set("Content-Type", "application/json") - _, err = w.Write(getVersionResp(info)) + err = json.NewEncoder(w).Encode(resp) if err != nil { httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err) } @@ -109,21 +111,24 @@ func handleUpdate(w http.ResponseWriter, _ *http.Request) { go finishUpdate() } -// Convert version.json data to our JSON response -func getVersionResp(info updater.VersionInfo) []byte { - ret := make(map[string]interface{}) - ret["can_autoupdate"] = false - ret["new_version"] = info.NewVersion - ret["announcement"] = info.Announcement - ret["announcement_url"] = info.AnnouncementURL +// versionResponse is the response for /control/version.json endpoint. +type versionResponse struct { + Disabled bool `json:"disabled"` + updater.VersionInfo +} - if info.CanAutoUpdate { +// confirmAutoUpdate checks the real possibility of auto update. +func (vr *versionResponse) confirmAutoUpdate() { + if vr.CanAutoUpdate != nil && *vr.CanAutoUpdate { canUpdate := true - tlsConf := tlsConfigSettings{} - Context.tls.WriteDiskConfig(&tlsConf) + var tlsConf *tlsConfigSettings + if runtime.GOOS != "windows" { + tlsConf = &tlsConfigSettings{} + Context.tls.WriteDiskConfig(tlsConf) + } - if runtime.GOOS != "windows" && + if tlsConf != nil && ((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 || tlsConf.PortDNSOverTLS < 1024 || tlsConf.PortDNSOverQUIC < 1024)) || @@ -131,11 +136,8 @@ func getVersionResp(info updater.VersionInfo) []byte { config.DNS.Port < 1024) { canUpdate, _ = sysutil.CanBindPrivilegedPorts() } - ret["can_autoupdate"] = canUpdate + vr.CanAutoUpdate = &canUpdate } - - d, _ := json.Marshal(ret) - return d } // Complete an update procedure diff --git a/internal/stats/http.go b/internal/stats/http.go index 794bedf1..4d7c0b15 100644 --- a/internal/stats/http.go +++ b/internal/stats/http.go @@ -19,26 +19,43 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -// Return data +// statsResponse is a response for getting statistics. +type statsResponse struct { + TimeUnits string `json:"time_units"` + + NumDNSQueries uint64 `json:"num_dns_queries"` + NumBlockedFiltering uint64 `json:"num_blocked_filtering"` + NumReplacedSafebrowsing uint64 `json:"num_replaced_safebrowsing"` + NumReplacedSafesearch uint64 `json:"num_replaced_safesearch"` + NumReplacedParental uint64 `json:"num_replaced_parental"` + + AvgProcessingTime float64 `json:"avg_processing_time"` + + TopQueried []map[string]uint64 `json:"top_queried_domains"` + TopClients []map[string]uint64 `json:"top_clients"` + TopBlocked []map[string]uint64 `json:"top_blocked_domains"` + + DNSQueries []uint64 `json:"dns_queries"` + + BlockedFiltering []uint64 `json:"blocked_filtering"` + ReplacedSafebrowsing []uint64 `json:"replaced_safebrowsing"` + ReplacedParental []uint64 `json:"replaced_parental"` +} + +// handleStats is a handler for getting statistics. func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) { start := time.Now() - d := s.getData() + response, ok := s.getData() log.Debug("Stats: prepared data in %v", time.Since(start)) - if d == nil { + if !ok { httpError(r, w, http.StatusInternalServerError, "Couldn't get statistics data") - return - } - data, err := json.Marshal(d) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "json encode: %s", err) return } w.Header().Set("Content-Type", "application/json") - - _, err = w.Write(data) + err := json.NewEncoder(w).Encode(response) if err != nil { httpError(r, w, http.StatusInternalServerError, "json encode: %s", err) diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go index 5fd9ff55..b4be4db0 100644 --- a/internal/stats/stats_test.go +++ b/internal/stats/stats_test.go @@ -50,34 +50,36 @@ func TestStats(t *testing.T) { e.Time = 123456 s.Update(e) - d := s.getData() + d, ok := s.getData() + assert.True(t, ok) + a := []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} - assert.True(t, UIntArrayEquals(d["dns_queries"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.DNSQueries, a)) a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} - assert.True(t, UIntArrayEquals(d["blocked_filtering"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.BlockedFiltering, a)) a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - assert.True(t, UIntArrayEquals(d["replaced_safebrowsing"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.ReplacedSafebrowsing, a)) a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - assert.True(t, UIntArrayEquals(d["replaced_parental"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.ReplacedParental, a)) - m := d["top_queried_domains"].([]map[string]uint64) + m := d.TopQueried assert.EqualValues(t, 1, m[0]["domain"]) - m = d["top_blocked_domains"].([]map[string]uint64) + m = d.TopBlocked assert.EqualValues(t, 1, m[0]["domain"]) - m = d["top_clients"].([]map[string]uint64) + m = d.TopClients assert.EqualValues(t, 2, m[0]["127.0.0.1"]) - assert.EqualValues(t, 2, d["num_dns_queries"].(uint64)) - assert.EqualValues(t, 1, d["num_blocked_filtering"].(uint64)) - assert.EqualValues(t, 0, d["num_replaced_safebrowsing"].(uint64)) - assert.EqualValues(t, 0, d["num_replaced_safesearch"].(uint64)) - assert.EqualValues(t, 0, d["num_replaced_parental"].(uint64)) - assert.EqualValues(t, 0.123456, d["avg_processing_time"].(float64)) + assert.EqualValues(t, 2, d.NumDNSQueries) + assert.EqualValues(t, 1, d.NumBlockedFiltering) + assert.EqualValues(t, 0, d.NumReplacedSafebrowsing) + assert.EqualValues(t, 0, d.NumReplacedSafesearch) + assert.EqualValues(t, 0, d.NumReplacedParental) + assert.EqualValues(t, 0.123456, d.AvgProcessingTime) topClients := s.GetTopClientsIP(2) assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0])) @@ -120,8 +122,9 @@ func TestLargeNumbers(t *testing.T) { } } - d := s.getData() - assert.EqualValues(t, int(hour)*n, d["num_dns_queries"]) + d, ok := s.getData() + assert.True(t, ok) + assert.EqualValues(t, int(hour)*n, d.NumDNSQueries) s.Close() os.Remove(conf.Filename) diff --git a/internal/stats/unit.go b/internal/stats/unit.go index 35deb327..962fe85b 100644 --- a/internal/stats/unit.go +++ b/internal/stats/unit.go @@ -545,10 +545,9 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) { * parental-blocked These values are just the sum of data for all units. */ -func (s *statsCtx) getData() map[string]interface{} { +func (s *statsCtx) getData() (statsResponse, bool) { limit := s.conf.limit - d := map[string]interface{}{} timeUnit := Hours if limit/24 > 7 { timeUnit = Days @@ -556,7 +555,7 @@ func (s *statsCtx) getData() map[string]interface{} { units, firstID := s.loadUnits(limit) if units == nil { - return nil + return statsResponse{}, false } // per time unit counters: @@ -604,18 +603,14 @@ func (s *statsCtx) getData() map[string]interface{} { log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit) } - statsData := map[string]interface{}{ - "dns_queries": dnsQueries, - "blocked_filtering": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), - "replaced_safebrowsing": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), - "replaced_parental": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }), - "top_queried_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), - "top_blocked_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), - "top_clients": topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), - } - - for dataKey, dataValue := range statsData { - d[dataKey] = dataValue + data := statsResponse{ + DNSQueries: dnsQueries, + BlockedFiltering: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), + ReplacedSafebrowsing: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), + ReplacedParental: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }), + TopQueried: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), + TopBlocked: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), + TopClients: topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), } // total counters: @@ -635,24 +630,22 @@ func (s *statsCtx) getData() map[string]interface{} { sum.NResult[RParental] += u.NResult[RParental] } - d["num_dns_queries"] = sum.NTotal - d["num_blocked_filtering"] = sum.NResult[RFiltered] - d["num_replaced_safebrowsing"] = sum.NResult[RSafeBrowsing] - d["num_replaced_safesearch"] = sum.NResult[RSafeSearch] - d["num_replaced_parental"] = sum.NResult[RParental] + data.NumDNSQueries = sum.NTotal + data.NumBlockedFiltering = sum.NResult[RFiltered] + data.NumReplacedSafebrowsing = sum.NResult[RSafeBrowsing] + data.NumReplacedSafesearch = sum.NResult[RSafeSearch] + data.NumReplacedParental = sum.NResult[RParental] - avgTime := float64(0) if timeN != 0 { - avgTime = float64(sum.TimeAvg/uint32(timeN)) / 1000000 + data.AvgProcessingTime = float64(sum.TimeAvg/uint32(timeN)) / 1000000 } - d["avg_processing_time"] = avgTime - d["time_units"] = "hours" + data.TimeUnits = "hours" if timeUnit == Days { - d["time_units"] = "days" + data.TimeUnits = "days" } - return d + return data, true } func (s *statsCtx) GetTopClientsIP(maxCount uint) []net.IP { diff --git a/internal/updater/check.go b/internal/updater/check.go index 6418ffb6..a23cfc05 100644 --- a/internal/updater/check.go +++ b/internal/updater/check.go @@ -15,11 +15,11 @@ const versionCheckPeriod = 8 * time.Hour // VersionInfo contains information about a new version. type VersionInfo struct { - NewVersion string - Announcement string - AnnouncementURL string - SelfUpdateMinVersion string - CanAutoUpdate bool + NewVersion string `json:"new_version,omitempty"` + Announcement string `json:"announcement,omitempty"` + AnnouncementURL string `json:"announcement_url,omitempty"` + SelfUpdateMinVersion string `json:"-"` + CanAutoUpdate *bool `json:"can_autoupdate,omitempty"` } // MaxResponseSize is responses on server's requests maximum length in bytes. @@ -64,27 +64,37 @@ func (u *Updater) VersionInfo(forceRecheck bool) (VersionInfo, error) { } func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { - info := VersionInfo{} - versionJSON := make(map[string]interface{}) + var canAutoUpdate bool + info := VersionInfo{ + CanAutoUpdate: &canAutoUpdate, + } + versionJSON := map[string]string{ + "version": "", + "announcement": "", + "announcement_url": "", + "selfupdate_min_version": "", + } err := json.Unmarshal(data, &versionJSON) if err != nil { return info, fmt.Errorf("version.json: %w", err) } - var ok1, ok2, ok3, ok4 bool - info.NewVersion, ok1 = versionJSON["version"].(string) - info.Announcement, ok2 = versionJSON["announcement"].(string) - info.AnnouncementURL, ok3 = versionJSON["announcement_url"].(string) - info.SelfUpdateMinVersion, ok4 = versionJSON["selfupdate_min_version"].(string) - if !ok1 || !ok2 || !ok3 || !ok4 { - return info, fmt.Errorf("version.json: invalid data") + for _, v := range versionJSON { + if v == "" { + return info, fmt.Errorf("version.json: invalid data") + } } + info.NewVersion = versionJSON["version"] + info.Announcement = versionJSON["announcement"] + info.AnnouncementURL = versionJSON["announcement_url"] + info.SelfUpdateMinVersion = versionJSON["selfupdate_min_version"] + packageURL, ok := u.downloadURL(versionJSON) if ok && info.NewVersion != u.version && strings.TrimPrefix(u.version, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") { - info.CanAutoUpdate = true + canAutoUpdate = true } u.newVersion = info.NewVersion @@ -94,7 +104,7 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { } // downloadURL returns the download URL for current build. -func (u *Updater) downloadURL(json map[string]interface{}) (string, bool) { +func (u *Updater) downloadURL(json map[string]string) (string, bool) { var key string if u.goarch == "arm" && u.goarm != "" { @@ -113,5 +123,5 @@ func (u *Updater) downloadURL(json map[string]interface{}) (string, bool) { return "", false } - return val.(string), true + return val, true } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index abadc17b..adb12c2d 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -90,7 +90,9 @@ func TestUpdateGetVersion(t *testing.T) { assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) - assert.True(t, info.CanAutoUpdate) + if assert.NotNil(t, info.CanAutoUpdate) { + assert.True(t, *info.CanAutoUpdate) + } // check cached _, err = u.VersionInfo(false) @@ -275,7 +277,9 @@ func TestUpdater_VersionInto_ARM(t *testing.T) { assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) - assert.True(t, info.CanAutoUpdate) + if assert.NotNil(t, info.CanAutoUpdate) { + assert.True(t, *info.CanAutoUpdate) + } } func TestUpdater_VersionInto_MIPS(t *testing.T) { @@ -312,5 +316,7 @@ func TestUpdater_VersionInto_MIPS(t *testing.T) { assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) - assert.True(t, info.CanAutoUpdate) + if assert.NotNil(t, info.CanAutoUpdate) { + assert.True(t, *info.CanAutoUpdate) + } } diff --git a/internal/util/network.go b/internal/util/network.go index f83bbd63..52507617 100644 --- a/internal/util/network.go +++ b/internal/util/network.go @@ -1,6 +1,7 @@ package util import ( + "encoding/json" "errors" "fmt" "net" @@ -13,14 +14,30 @@ import ( "github.com/AdguardTeam/golibs/log" ) -// NetInterface represents a list of network interfaces +// NetInterface represents an entry of network interfaces map. type NetInterface struct { - Name string // Network interface name - MTU int // MTU - HardwareAddr string // Hardware address - Addresses []net.IP // Array with the network interface addresses - Subnets []*net.IPNet // Array with CIDR addresses of this network interface - Flags string // Network interface flags (up, broadcast, etc) + MTU int `json:"mtu"` + Name string `json:"name"` + HardwareAddr net.HardwareAddr `json:"hardware_address"` + Flags net.Flags `json:"flags"` + // Array with the network interface addresses. + Addresses []net.IP `json:"ip_addresses,omitempty"` + // Array with IP networks for this network interface. + Subnets []*net.IPNet `json:"-"` +} + +// MarshalJSON implements the json.Marshaler interface for *NetInterface. +func (iface *NetInterface) MarshalJSON() ([]byte, error) { + type netInterface NetInterface + return json.Marshal(&struct { + HardwareAddr string `json:"hardware_address"` + Flags string `json:"flags"` + *netInterface + }{ + HardwareAddr: iface.HardwareAddr.String(), + Flags: iface.Flags.String(), + netInterface: (*netInterface)(iface), + }) } // GetValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP @@ -40,7 +57,7 @@ func GetValidNetInterfaces() ([]net.Interface, error) { // GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and WEB only // we do not return link-local addresses here -func GetValidNetInterfacesForWeb() ([]NetInterface, error) { +func GetValidNetInterfacesForWeb() ([]*NetInterface, error) { ifaces, err := GetValidNetInterfaces() if err != nil { return nil, fmt.Errorf("couldn't get interfaces: %w", err) @@ -49,7 +66,7 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) { return nil, errors.New("couldn't find any legible interface") } - var netInterfaces []NetInterface + var netInterfaces []*NetInterface for _, iface := range ifaces { addrs, err := iface.Addrs() @@ -57,24 +74,21 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) { return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err) } - netIface := NetInterface{ - Name: iface.Name, + netIface := &NetInterface{ MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr.String(), + Name: iface.Name, + HardwareAddr: iface.HardwareAddr, + Flags: iface.Flags, } - if iface.Flags != 0 { - netIface.Flags = iface.Flags.String() - } - - // Collect network interface addresses + // Collect network interface addresses. for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) if !ok { - // not an IPNet, should not happen + // Should be net.IPNet, this is weird. return nil, fmt.Errorf("got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) } - // ignore link-local + // Ignore link-local. if ipNet.IP.IsLinkLocalUnicast() { continue } @@ -82,7 +96,7 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) { netIface.Subnets = append(netIface.Subnets, ipNet) } - // Discard interfaces with no addresses + // Discard interfaces with no addresses. if len(netIface.Addresses) != 0 { netInterfaces = append(netInterfaces, netIface) } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index e41eac24..490fd65d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1452,7 +1452,13 @@ 'type': 'object' 'description': > Information about the latest available version of AdGuard Home. + 'required': + - 'disabled' 'properties': + 'disabled': + 'type': 'boolean' + 'description': > + If true then other fields doesn't appear. 'new_version': 'type': 'string' 'example': 'v0.9' @@ -1471,7 +1477,10 @@ 'properties': 'time_units': 'type': 'string' - 'description': 'Time units (hours | days)' + 'enum': + - 'hours' + - 'days' + 'description': 'Time units' 'example': 'hours' 'num_dns_queries': 'type': 'integer' @@ -1988,6 +1997,10 @@ 'properties': 'flags': 'type': 'string' + 'description': > + Flags could be any combination of the following values, divided by + the "|" character: "up", "broadcast", "loopback", "pointtopoint" and + "multicast". 'example': 'up|broadcast|multicast' 'hardware_address': 'type': 'string'