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 <e.burkov@adguard.com>
Date:   Thu Jan 21 19:36:20 2021 +0300

    all: imp docs

commit e8ac1815c492b0a9434596e35a48755cac2b9f3b
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Jan 20 12:38:48 2021 +0300

    all: imp JSON encoding, decoding
This commit is contained in:
Eugene Burkov 2021-01-21 19:55:41 +03:00
parent 7fab31beae
commit d9482b7588
10 changed files with 226 additions and 189 deletions

View File

@ -44,41 +44,44 @@ func addDNSAddress(dnsAddresses *[]string, addr net.IP) {
*dnsAddresses = append(*dnsAddresses, hostport) *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) { 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 { if Context.dnsServer != nil {
Context.dnsServer.WriteDiskConfig(&c) c = &dnsforward.FilteringConfig{}
Context.dnsServer.WriteDiskConfig(c)
resp.IsProtectionEnabled = c.ProtectionEnabled
} }
data := map[string]interface{}{ // IsDHCPAvailable field is now false by default for Windows.
"dns_addresses": getDNSAddresses(), if runtime.GOOS != "windows" {
"http_port": config.BindPort, resp.IsDHCPAvailable = Context.dhcpServer != nil
"dns_port": config.DNS.Port,
"running": isRunning(),
"version": version.Version(),
"language": config.Language,
"protection_enabled": c.ProtectionEnabled,
} }
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") w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal) err := json.NewEncoder(w).Encode(resp)
if err != nil { if err != nil {
httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err) httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err)
return return

View File

@ -21,23 +21,16 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
type firstRunData struct { // getAddrsResponse is the response for /install/get_addresses endpoint.
WebPort int `json:"web_port"` type getAddrsResponse struct {
DNSPort int `json:"dns_port"` WebPort int `json:"web_port"`
Interfaces map[string]interface{} `json:"interfaces"` DNSPort int `json:"dns_port"`
Interfaces map[string]*util.NetInterface `json:"interfaces"`
} }
type netInterfaceJSON struct { // handleInstallGetAddresses is the handler for /install/get_addresses endpoint.
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
func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := firstRunData{} data := getAddrsResponse{}
data.WebPort = 80 data.WebPort = 80
data.DNSPort = 53 data.DNSPort = 53
@ -47,16 +40,9 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
return return
} }
data.Interfaces = make(map[string]interface{}) data.Interfaces = make(map[string]*util.NetInterface)
for _, iface := range ifaces { for _, iface := range ifaces {
ifaceJSON := netInterfaceJSON{ data.Interfaces[iface.Name] = iface
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr,
Addresses: iface.Addresses,
Flags: iface.Flags,
}
data.Interfaces[iface.Name] = ifaceJSON
} }
w.Header().Set("Content-Type", "application/json") 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) 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. // request body. It uses array of structs instead of map.
// //
// TODO(e.burkov): This should removed with the API v1 when the appropriate // TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default firstRunData. // functionality will appear in default firstRunData.
type firstRunDataBeta struct { type getAddrsResponseBeta struct {
WebPort int `json:"web_port"` WebPort int `json:"web_port"`
DNSPort int `json:"dns_port"` DNSPort int `json:"dns_port"`
Interfaces []netInterfaceJSON `json:"interfaces"` Interfaces []*util.NetInterface `json:"interfaces"`
} }
// handleInstallConfigureBeta is a substitution of /install/get_addresses // 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 // TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default handleInstallGetAddresses. // functionality will appear in default handleInstallGetAddresses.
func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Request) { func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Request) {
data := firstRunDataBeta{} data := getAddrsResponseBeta{}
data.WebPort = 80 data.WebPort = 80
data.DNSPort = 53 data.DNSPort = 53
@ -547,17 +533,7 @@ func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Req
return return
} }
data.Interfaces = make([]netInterfaceJSON, 0, len(ifaces)) data.Interfaces = 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)
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(data) err = json.NewEncoder(w).Encode(data)

View File

@ -16,10 +16,6 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
type getVersionJSONRequest struct {
RecheckNow bool `json:"recheck_now"`
}
// temporaryError is the interface for temporary errors from the Go standard // temporaryError is the interface for temporary errors from the Go standard
// library. // library.
type temporaryError interface { type temporaryError interface {
@ -29,31 +25,34 @@ type temporaryError interface {
// Get the latest available version from the Internet // Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
resp := &versionResponse{}
if Context.disableUpdate { if Context.disableUpdate {
resp := make(map[string]interface{}) // w.Header().Set("Content-Type", "application/json")
resp["disabled"] = true resp.Disabled = true
d, _ := json.Marshal(resp) _ = json.NewEncoder(w).Encode(resp)
_, _ = w.Write(d) // TODO(e.burkov): Add error handling and deal with headers.
return return
} }
req := getVersionJSONRequest{} req := &struct {
Recheck bool `json:"recheck_now"`
}{}
var err error var err error
if r.ContentLength != 0 { if r.ContentLength != 0 {
err = json.NewDecoder(r.Body).Decode(&req) err = json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "JSON parse: %s", err) httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
return return
} }
} }
var info updater.VersionInfo
for i := 0; i != 3; i++ { for i := 0; i != 3; i++ {
func() { func() {
Context.controlLock.Lock() Context.controlLock.Lock()
defer Context.controlLock.Unlock() defer Context.controlLock.Unlock()
info, err = Context.updater.VersionInfo(req.RecheckNow) resp.VersionInfo, err = Context.updater.VersionInfo(req.Recheck)
}() }()
if err != nil { if err != nil {
@ -76,13 +75,16 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
} }
if err != nil { if err != nil {
vcu := Context.updater.VersionCheckURL() 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) httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", vcu, err, err)
return return
} }
resp.confirmAutoUpdate()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_, err = w.Write(getVersionResp(info)) err = json.NewEncoder(w).Encode(resp)
if err != nil { if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err) httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
} }
@ -109,21 +111,24 @@ func handleUpdate(w http.ResponseWriter, _ *http.Request) {
go finishUpdate() go finishUpdate()
} }
// Convert version.json data to our JSON response // versionResponse is the response for /control/version.json endpoint.
func getVersionResp(info updater.VersionInfo) []byte { type versionResponse struct {
ret := make(map[string]interface{}) Disabled bool `json:"disabled"`
ret["can_autoupdate"] = false updater.VersionInfo
ret["new_version"] = info.NewVersion }
ret["announcement"] = info.Announcement
ret["announcement_url"] = info.AnnouncementURL
if info.CanAutoUpdate { // confirmAutoUpdate checks the real possibility of auto update.
func (vr *versionResponse) confirmAutoUpdate() {
if vr.CanAutoUpdate != nil && *vr.CanAutoUpdate {
canUpdate := true canUpdate := true
tlsConf := tlsConfigSettings{} var tlsConf *tlsConfigSettings
Context.tls.WriteDiskConfig(&tlsConf) if runtime.GOOS != "windows" {
tlsConf = &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
}
if runtime.GOOS != "windows" && if tlsConf != nil &&
((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 || ((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 ||
tlsConf.PortDNSOverTLS < 1024 || tlsConf.PortDNSOverTLS < 1024 ||
tlsConf.PortDNSOverQUIC < 1024)) || tlsConf.PortDNSOverQUIC < 1024)) ||
@ -131,11 +136,8 @@ func getVersionResp(info updater.VersionInfo) []byte {
config.DNS.Port < 1024) { config.DNS.Port < 1024) {
canUpdate, _ = sysutil.CanBindPrivilegedPorts() canUpdate, _ = sysutil.CanBindPrivilegedPorts()
} }
ret["can_autoupdate"] = canUpdate vr.CanAutoUpdate = &canUpdate
} }
d, _ := json.Marshal(ret)
return d
} }
// Complete an update procedure // Complete an update procedure

View File

@ -19,26 +19,43 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string,
http.Error(w, text, code) 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) { func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
d := s.getData() response, ok := s.getData()
log.Debug("Stats: prepared data in %v", time.Since(start)) 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") 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 return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
_, err = w.Write(data)
if err != nil { if err != nil {
httpError(r, w, http.StatusInternalServerError, "json encode: %s", err) httpError(r, w, http.StatusInternalServerError, "json encode: %s", err)

View File

@ -50,34 +50,36 @@ func TestStats(t *testing.T) {
e.Time = 123456 e.Time = 123456
s.Update(e) 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} 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} 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} 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} 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"]) 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"]) 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, m[0]["127.0.0.1"])
assert.EqualValues(t, 2, d["num_dns_queries"].(uint64)) assert.EqualValues(t, 2, d.NumDNSQueries)
assert.EqualValues(t, 1, d["num_blocked_filtering"].(uint64)) assert.EqualValues(t, 1, d.NumBlockedFiltering)
assert.EqualValues(t, 0, d["num_replaced_safebrowsing"].(uint64)) assert.EqualValues(t, 0, d.NumReplacedSafebrowsing)
assert.EqualValues(t, 0, d["num_replaced_safesearch"].(uint64)) assert.EqualValues(t, 0, d.NumReplacedSafesearch)
assert.EqualValues(t, 0, d["num_replaced_parental"].(uint64)) assert.EqualValues(t, 0, d.NumReplacedParental)
assert.EqualValues(t, 0.123456, d["avg_processing_time"].(float64)) assert.EqualValues(t, 0.123456, d.AvgProcessingTime)
topClients := s.GetTopClientsIP(2) topClients := s.GetTopClientsIP(2)
assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0])) assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0]))
@ -120,8 +122,9 @@ func TestLargeNumbers(t *testing.T) {
} }
} }
d := s.getData() d, ok := s.getData()
assert.EqualValues(t, int(hour)*n, d["num_dns_queries"]) assert.True(t, ok)
assert.EqualValues(t, int(hour)*n, d.NumDNSQueries)
s.Close() s.Close()
os.Remove(conf.Filename) os.Remove(conf.Filename)

View File

@ -545,10 +545,9 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) {
* parental-blocked * parental-blocked
These values are just the sum of data for all units. 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 limit := s.conf.limit
d := map[string]interface{}{}
timeUnit := Hours timeUnit := Hours
if limit/24 > 7 { if limit/24 > 7 {
timeUnit = Days timeUnit = Days
@ -556,7 +555,7 @@ func (s *statsCtx) getData() map[string]interface{} {
units, firstID := s.loadUnits(limit) units, firstID := s.loadUnits(limit)
if units == nil { if units == nil {
return nil return statsResponse{}, false
} }
// per time unit counters: // 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) log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit)
} }
statsData := map[string]interface{}{ data := statsResponse{
"dns_queries": dnsQueries, DNSQueries: dnsQueries,
"blocked_filtering": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), BlockedFiltering: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }),
"replaced_safebrowsing": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), ReplacedSafebrowsing: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }),
"replaced_parental": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }), ReplacedParental: statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }),
"top_queried_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), TopQueried: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }),
"top_blocked_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), TopBlocked: topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }),
"top_clients": topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), TopClients: topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }),
}
for dataKey, dataValue := range statsData {
d[dataKey] = dataValue
} }
// total counters: // total counters:
@ -635,24 +630,22 @@ func (s *statsCtx) getData() map[string]interface{} {
sum.NResult[RParental] += u.NResult[RParental] sum.NResult[RParental] += u.NResult[RParental]
} }
d["num_dns_queries"] = sum.NTotal data.NumDNSQueries = sum.NTotal
d["num_blocked_filtering"] = sum.NResult[RFiltered] data.NumBlockedFiltering = sum.NResult[RFiltered]
d["num_replaced_safebrowsing"] = sum.NResult[RSafeBrowsing] data.NumReplacedSafebrowsing = sum.NResult[RSafeBrowsing]
d["num_replaced_safesearch"] = sum.NResult[RSafeSearch] data.NumReplacedSafesearch = sum.NResult[RSafeSearch]
d["num_replaced_parental"] = sum.NResult[RParental] data.NumReplacedParental = sum.NResult[RParental]
avgTime := float64(0)
if timeN != 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 { if timeUnit == Days {
d["time_units"] = "days" data.TimeUnits = "days"
} }
return d return data, true
} }
func (s *statsCtx) GetTopClientsIP(maxCount uint) []net.IP { func (s *statsCtx) GetTopClientsIP(maxCount uint) []net.IP {

View File

@ -15,11 +15,11 @@ const versionCheckPeriod = 8 * time.Hour
// VersionInfo contains information about a new version. // VersionInfo contains information about a new version.
type VersionInfo struct { type VersionInfo struct {
NewVersion string NewVersion string `json:"new_version,omitempty"`
Announcement string Announcement string `json:"announcement,omitempty"`
AnnouncementURL string AnnouncementURL string `json:"announcement_url,omitempty"`
SelfUpdateMinVersion string SelfUpdateMinVersion string `json:"-"`
CanAutoUpdate bool CanAutoUpdate *bool `json:"can_autoupdate,omitempty"`
} }
// MaxResponseSize is responses on server's requests maximum length in bytes. // 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) { func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
info := VersionInfo{} var canAutoUpdate bool
versionJSON := make(map[string]interface{}) info := VersionInfo{
CanAutoUpdate: &canAutoUpdate,
}
versionJSON := map[string]string{
"version": "",
"announcement": "",
"announcement_url": "",
"selfupdate_min_version": "",
}
err := json.Unmarshal(data, &versionJSON) err := json.Unmarshal(data, &versionJSON)
if err != nil { if err != nil {
return info, fmt.Errorf("version.json: %w", err) return info, fmt.Errorf("version.json: %w", err)
} }
var ok1, ok2, ok3, ok4 bool for _, v := range versionJSON {
info.NewVersion, ok1 = versionJSON["version"].(string) if v == "" {
info.Announcement, ok2 = versionJSON["announcement"].(string) return info, fmt.Errorf("version.json: invalid data")
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")
} }
info.NewVersion = versionJSON["version"]
info.Announcement = versionJSON["announcement"]
info.AnnouncementURL = versionJSON["announcement_url"]
info.SelfUpdateMinVersion = versionJSON["selfupdate_min_version"]
packageURL, ok := u.downloadURL(versionJSON) packageURL, ok := u.downloadURL(versionJSON)
if ok && if ok &&
info.NewVersion != u.version && info.NewVersion != u.version &&
strings.TrimPrefix(u.version, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") { strings.TrimPrefix(u.version, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") {
info.CanAutoUpdate = true canAutoUpdate = true
} }
u.newVersion = info.NewVersion u.newVersion = info.NewVersion
@ -94,7 +104,7 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
} }
// downloadURL returns the download URL for current build. // 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 var key string
if u.goarch == "arm" && u.goarm != "" { if u.goarch == "arm" && u.goarm != "" {
@ -113,5 +123,5 @@ func (u *Updater) downloadURL(json map[string]interface{}) (string, bool) {
return "", false return "", false
} }
return val.(string), true return val, true
} }

View File

@ -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, "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, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL)
assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) 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 // check cached
_, err = u.VersionInfo(false) _, 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, "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, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL)
assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) 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) { 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, "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, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL)
assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) assert.Equal(t, "v0.0", info.SelfUpdateMinVersion)
assert.True(t, info.CanAutoUpdate) if assert.NotNil(t, info.CanAutoUpdate) {
assert.True(t, *info.CanAutoUpdate)
}
} }

View File

@ -1,6 +1,7 @@
package util package util
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -13,14 +14,30 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// NetInterface represents a list of network interfaces // NetInterface represents an entry of network interfaces map.
type NetInterface struct { type NetInterface struct {
Name string // Network interface name MTU int `json:"mtu"`
MTU int // MTU Name string `json:"name"`
HardwareAddr string // Hardware address HardwareAddr net.HardwareAddr `json:"hardware_address"`
Addresses []net.IP // Array with the network interface addresses Flags net.Flags `json:"flags"`
Subnets []*net.IPNet // Array with CIDR addresses of this network interface // Array with the network interface addresses.
Flags string // Network interface flags (up, broadcast, etc) 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 // 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 // GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and WEB only
// we do not return link-local addresses here // we do not return link-local addresses here
func GetValidNetInterfacesForWeb() ([]NetInterface, error) { func GetValidNetInterfacesForWeb() ([]*NetInterface, error) {
ifaces, err := GetValidNetInterfaces() ifaces, err := GetValidNetInterfaces()
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't get interfaces: %w", err) 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") return nil, errors.New("couldn't find any legible interface")
} }
var netInterfaces []NetInterface var netInterfaces []*NetInterface
for _, iface := range ifaces { for _, iface := range ifaces {
addrs, err := iface.Addrs() 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) return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
} }
netIface := NetInterface{ netIface := &NetInterface{
Name: iface.Name,
MTU: iface.MTU, MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(), Name: iface.Name,
HardwareAddr: iface.HardwareAddr,
Flags: iface.Flags,
} }
if iface.Flags != 0 { // Collect network interface addresses.
netIface.Flags = iface.Flags.String()
}
// Collect network interface addresses
for _, addr := range addrs { for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet) ipNet, ok := addr.(*net.IPNet)
if !ok { 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) 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() { if ipNet.IP.IsLinkLocalUnicast() {
continue continue
} }
@ -82,7 +96,7 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) {
netIface.Subnets = append(netIface.Subnets, ipNet) netIface.Subnets = append(netIface.Subnets, ipNet)
} }
// Discard interfaces with no addresses // Discard interfaces with no addresses.
if len(netIface.Addresses) != 0 { if len(netIface.Addresses) != 0 {
netInterfaces = append(netInterfaces, netIface) netInterfaces = append(netInterfaces, netIface)
} }

View File

@ -1452,7 +1452,13 @@
'type': 'object' 'type': 'object'
'description': > 'description': >
Information about the latest available version of AdGuard Home. Information about the latest available version of AdGuard Home.
'required':
- 'disabled'
'properties': 'properties':
'disabled':
'type': 'boolean'
'description': >
If true then other fields doesn't appear.
'new_version': 'new_version':
'type': 'string' 'type': 'string'
'example': 'v0.9' 'example': 'v0.9'
@ -1471,7 +1477,10 @@
'properties': 'properties':
'time_units': 'time_units':
'type': 'string' 'type': 'string'
'description': 'Time units (hours | days)' 'enum':
- 'hours'
- 'days'
'description': 'Time units'
'example': 'hours' 'example': 'hours'
'num_dns_queries': 'num_dns_queries':
'type': 'integer' 'type': 'integer'
@ -1988,6 +1997,10 @@
'properties': 'properties':
'flags': 'flags':
'type': 'string' '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' 'example': 'up|broadcast|multicast'
'hardware_address': 'hardware_address':
'type': 'string' 'type': 'string'