diff --git a/CHANGELOG.md b/CHANGELOG.md index 20088058..4bf94940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,8 +37,27 @@ NOTE: Add new changes BELOW THIS COMMENT. #### Configuration Changes -In this release, the schema version has changed from 20 to 22. +In this release, the schema version has changed from 20 to 23. +- Properties `bind_host`, `bind_port`, and `web_session_ttl` which used to setup + web UI binding configuration, are now moved to a new object `http` containing + new properties `address` and `session_ttl`: + + ```yaml + # BEFORE: + 'bind_host': '1.2.3.4' + 'bind_port': 8080 + 'web_session_ttl': 720 + + # AFTER: + 'http': + 'address': '1.2.3.4:8080' + 'session_ttl': '720h' + ``` + + Note that the new `http.session_ttl` property is now a duration string. To + rollback this change, remove the new object `http`, set back `bind_host`, + `bind_port`, `web_session_ttl`, and change the `schema_version` back to `22`. - Property `clients.persistent.blocked_services`, which in schema versions 21 and earlier used to be a list containing ids of blocked services, is now an object containing ids and schedule for blocked services: diff --git a/docker/web-bind.awk b/docker/web-bind.awk index d3c94abf..2ae64a4c 100644 --- a/docker/web-bind.awk +++ b/docker/web-bind.awk @@ -1,13 +1,5 @@ # Don't consider the HTTPS hostname since the enforced HTTPS redirection should # work if the SSL check skipped. See file docker/healthcheck.sh. -/^bind_host:/ { host = $2 } +/^[^[:space:]]/ { is_http = /^http:/ } -/^bind_port:/ { port = $2 } - -END { - if (match(host, ":")) { - print "http://[" host "]:" port - } else { - print "http://" host ":" port - } -} +/^[[:space:]]+address:/ { if (is_http) print "http://" $2 } diff --git a/internal/home/config.go b/internal/home/config.go index 2eb0aff5..b7ac5bcc 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -91,18 +91,17 @@ type clientSourcesConfig struct { HostsFile bool `yaml:"hosts"` } -// configuration is loaded from YAML -// field ordering is important -- yaml fields will mirror ordering from here +// configuration is loaded from YAML. +// +// Field ordering is important, YAML fields better not to be reordered, if it's +// not absolutely necessary. type configuration struct { // Raw file data to avoid re-reading of configuration file // It's reset after config is parsed fileData []byte - // BindHost is the address for the web interface server to listen on. - BindHost netip.Addr `yaml:"bind_host"` - // BindPort is the port for the web interface server to listen on. - BindPort int `yaml:"bind_port"` - + // HTTPConfig is the block with http conf. + HTTPConfig httpConfig `yaml:"http"` // Users are the clients capable for accessing the web interface. Users []webUser `yaml:"users"` // AuthAttempts is the maximum number of failed login attempts a user @@ -120,10 +119,6 @@ type configuration struct { // DebugPProf defines if the profiling HTTP handler will listen on :6060. DebugPProf bool `yaml:"debug_pprof"` - // TTL for a web session (in hours) - // An active session is automatically refreshed once a day. - WebSessionTTLHours uint32 `yaml:"web_session_ttl"` - DNS dnsConfig `yaml:"dns"` TLS tlsConfigSettings `yaml:"tls"` QueryLog queryLogConfig `yaml:"querylog"` @@ -156,7 +151,23 @@ type configuration struct { SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions } -// field ordering is important -- yaml fields will mirror ordering from here +// httpConfig is a block with HTTP configuration params. +// +// Field ordering is important, YAML fields better not to be reordered, if it's +// not absolutely necessary. +type httpConfig struct { + // Address is the address to serve the web UI on. + Address netip.AddrPort + + // SessionTTL for a web session. + // An active session is automatically refreshed once a day. + SessionTTL timeutil.Duration `yaml:"session_ttl"` +} + +// dnsConfig is a block with DNS configuration params. +// +// Field ordering is important, YAML fields better not to be reordered, if it's +// not absolutely necessary. type dnsConfig struct { BindHosts []netip.Addr `yaml:"bind_hosts"` Port int `yaml:"port"` @@ -261,11 +272,12 @@ type statsConfig struct { // // TODO(a.garipov, e.burkov): This global is awful and must be removed. var config = &configuration{ - BindPort: 3000, - BindHost: netip.IPv4Unspecified(), - AuthAttempts: 5, - AuthBlockMin: 15, - WebSessionTTLHours: 30 * 24, + AuthAttempts: 5, + AuthBlockMin: 15, + HTTPConfig: httpConfig{ + Address: netip.AddrPortFrom(netip.IPv4Unspecified(), 3000), + SessionTTL: timeutil.Duration{Duration: 30 * timeutil.Day}, + }, DNS: dnsConfig{ BindHosts: []netip.Addr{netip.IPv4Unspecified()}, Port: defaultPortDNS, @@ -427,8 +439,8 @@ func readLogSettings() (ls *logSettings) { // validateBindHosts returns error if any of binding hosts from configuration is // not a valid IP address. func validateBindHosts(conf *configuration) (err error) { - if !conf.BindHost.IsValid() { - return errors.Error("bind_host is not a valid ip address") + if !conf.HTTPConfig.Address.IsValid() { + return errors.Error("http.address is not a valid ip address") } for i, addr := range conf.DNS.BindHosts { @@ -462,7 +474,7 @@ func parseConfig() (err error) { } tcpPorts := aghalg.UniqChecker[tcpPort]{} - addPorts(tcpPorts, tcpPort(config.BindPort)) + addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port())) udpPorts := aghalg.UniqChecker[udpPort]{} addPorts(udpPorts, udpPort(config.DNS.Port)) diff --git a/internal/home/control.go b/internal/home/control.go index ae83507c..48afcf71 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -103,7 +103,7 @@ type statusResponse struct { Language string `json:"language"` DNSAddrs []string `json:"dns_addresses"` DNSPort int `json:"dns_port"` - HTTPPort int `json:"http_port"` + HTTPPort uint16 `json:"http_port"` // ProtectionDisabledDuration is the duration of the protection pause in // milliseconds. @@ -158,7 +158,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { Language: config.Language, DNSAddrs: dnsAddrs, DNSPort: config.DNS.Port, - HTTPPort: config.BindPort, + HTTPPort: config.HTTPConfig.Address.Port(), ProtectionDisabledDuration: protectionDisabledDuration, ProtectionEnabled: protectionEnabled, IsRunning: isRunning(), diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index faf62237..c9503fa0 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -96,8 +96,9 @@ type checkConfResp struct { func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err error) { defer func() { err = errors.Annotate(err, "validating ports: %w") }() - portInt := req.Web.Port - port := tcpPort(portInt) + // TODO(a.garipov): Declare all port variables anywhere as uint16. + reqPort := uint16(req.Web.Port) + port := tcpPort(reqPort) addPorts(tcpPorts, port) if err = tcpPorts.Validate(); err != nil { // Reset the value for the port to 1 to make sure that validateDNS @@ -108,15 +109,15 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err return err } - switch portInt { - case 0, config.BindPort: + switch reqPort { + case 0, config.HTTPConfig.Address.Port(): return nil default: // Go on and check the port binding only if it's not zero or won't be // unbound after install. } - return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt))) + return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, reqPort)) } // validateDNS returns error if the DNS part of the initial configuration can't @@ -127,11 +128,11 @@ func (req *checkConfReq) validateDNS( ) (canAutofix bool, err error) { defer func() { err = errors.Annotate(err, "validating ports: %w") }() - port := req.DNS.Port + port := uint16(req.DNS.Port) switch port { case 0: return false, nil - case config.BindPort: + case config.HTTPConfig.Address.Port(): // Go on and only check the UDP port since the TCP one is already bound // by AdGuard Home for web interface. default: @@ -318,8 +319,7 @@ type applyConfigReq struct { // copyInstallSettings copies the installation parameters between two // configuration structures. func copyInstallSettings(dst, src *configuration) { - dst.BindHost = src.BindHost - dst.BindPort = src.BindPort + dst.HTTPConfig = src.HTTPConfig dst.DNS.BindHosts = src.DNS.BindHosts dst.DNS.Port = src.DNS.Port } @@ -413,8 +413,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request copyInstallSettings(curConfig, config) Context.firstRun = false - config.BindHost = req.Web.IP - config.BindPort = req.Web.Port + config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)) config.DNS.BindHosts = []netip.Addr{req.DNS.IP} config.DNS.Port = req.DNS.Port @@ -487,7 +486,8 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e return nil, false, errors.Error("ports cannot be 0") } - restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port + addrPort := config.HTTPConfig.Address + restartHTTP = addrPort.Addr() != req.Web.IP || int(addrPort.Port()) != req.Web.Port if restartHTTP { err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port))) if err != nil { diff --git a/internal/home/controlupdate.go b/internal/home/controlupdate.go index f7e56208..434286ba 100644 --- a/internal/home/controlupdate.go +++ b/internal/home/controlupdate.go @@ -157,7 +157,9 @@ func (vr *versionResponse) setAllowedToAutoUpdate() (err error) { Context.tls.WriteDiskConfig(tlsConf) canUpdate := true - if tlsConfUsesPrivilegedPorts(tlsConf) || config.BindPort < 1024 || config.DNS.Port < 1024 { + if tlsConfUsesPrivilegedPorts(tlsConf) || + config.HTTPConfig.Address.Port() < 1024 || + config.DNS.Port < 1024 { canUpdate, err = aghnet.CanBindPrivilegedPorts() if err != nil { return fmt.Errorf("checking ability to bind privileged ports: %w", err) diff --git a/internal/home/home.go b/internal/home/home.go index 87dba706..572168fd 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -372,8 +372,26 @@ func initContextClients() (err error) { // setupBindOpts overrides bind host/port from the opts. func setupBindOpts(opts options) (err error) { + bindAddr := opts.bindAddr + if bindAddr != (netip.AddrPort{}) { + config.HTTPConfig.Address = bindAddr + + if config.HTTPConfig.Address.Port() != 0 { + err = checkPorts() + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return err + } + } + + return nil + } + if opts.bindPort != 0 { - config.BindPort = opts.bindPort + config.HTTPConfig.Address = netip.AddrPortFrom( + config.HTTPConfig.Address.Addr(), + uint16(opts.bindPort), + ) err = checkPorts() if err != nil { @@ -383,20 +401,10 @@ func setupBindOpts(opts options) (err error) { } if opts.bindHost.IsValid() { - config.BindHost = opts.bindHost - } - - // Rewrite deprecated options. - bindAddr := opts.bindAddr - if bindAddr.IsValid() { - config.BindHost = bindAddr.Addr() - config.BindPort = int(bindAddr.Port()) - - err = checkPorts() - if err != nil { - // Don't wrap the error, because it's informative enough as is. - return err - } + config.HTTPConfig.Address = netip.AddrPortFrom( + opts.bindHost, + config.HTTPConfig.Address.Port(), + ) } return nil @@ -480,7 +488,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) { // checkPorts is a helper for ports validation in config. func checkPorts() (err error) { tcpPorts := aghalg.UniqChecker[tcpPort]{} - addPorts(tcpPorts, tcpPort(config.BindPort)) + addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port())) udpPorts := aghalg.UniqChecker[udpPort]{} addPorts(udpPorts, udpPort(config.DNS.Port)) @@ -520,8 +528,8 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *webAPI, err error) { webConf := webConfig{ firstRun: Context.firstRun, - BindHost: config.BindHost, - BindPort: config.BindPort, + BindHost: config.HTTPConfig.Address.Addr(), + BindPort: int(config.HTTPConfig.Address.Port()), ReadTimeout: readTimeout, ReadHeaderTimeout: readHdrTimeout, @@ -657,8 +665,8 @@ func initUsers() (auth *Auth, err error) { log.Info("authratelimiter is disabled") } - sessionTTL := config.WebSessionTTLHours * 60 * 60 - auth = InitAuth(sessFilename, config.Users, sessionTTL, rateLimiter) + sessionTTL := config.HTTPConfig.SessionTTL.Seconds() + auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter) if auth == nil { return nil, errors.Error("initializing auth module failed") } @@ -936,7 +944,7 @@ func printHTTPAddresses(proto string) { Context.tls.WriteDiskConfig(&tlsConf) } - port := config.BindPort + port := int(config.HTTPConfig.Address.Port()) if proto == aghhttp.SchemeHTTPS { port = tlsConf.PortHTTPS } @@ -948,9 +956,9 @@ func printHTTPAddresses(proto string) { return } - bindhost := config.BindHost - if !bindhost.IsUnspecified() { - printWebAddrs(proto, bindhost.String(), port) + bindHost := config.HTTPConfig.Address.Addr() + if !bindHost.IsUnspecified() { + printWebAddrs(proto, bindHost.String(), port) return } @@ -961,14 +969,14 @@ func printHTTPAddresses(proto string) { // That's weird, but we'll ignore it. // // TODO(e.burkov): Find out when it happens. - printWebAddrs(proto, bindhost.String(), port) + printWebAddrs(proto, bindHost.String(), port) return } for _, iface := range ifaces { for _, addr := range iface.Addresses { - printWebAddrs(proto, addr.String(), config.BindPort) + printWebAddrs(proto, addr.String(), port) } } } diff --git a/internal/home/tls.go b/internal/home/tls.go index 84af6eae..c42d5175 100644 --- a/internal/home/tls.go +++ b/internal/home/tls.go @@ -320,7 +320,7 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) { if setts.Enabled { err = validatePorts( - tcpPort(config.BindPort), + tcpPort(config.HTTPConfig.Address.Port()), tcpPort(setts.PortHTTPS), tcpPort(setts.PortDNSOverTLS), tcpPort(setts.PortDNSCrypt), @@ -407,7 +407,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) if req.Enabled { err = validatePorts( - tcpPort(config.BindPort), + tcpPort(config.HTTPConfig.Address.Port()), tcpPort(req.PortHTTPS), tcpPort(req.PortDNSOverTLS), tcpPort(req.PortDNSCrypt), diff --git a/internal/home/upgrade.go b/internal/home/upgrade.go index d099378b..b6df4cad 100644 --- a/internal/home/upgrade.go +++ b/internal/home/upgrade.go @@ -3,6 +3,7 @@ package home import ( "bytes" "fmt" + "net/netip" "net/url" "os" "path" @@ -22,7 +23,7 @@ import ( ) // currentSchemaVersion is the current schema version. -const currentSchemaVersion = 22 +const currentSchemaVersion = 23 // These aliases are provided for convenience. type ( @@ -96,6 +97,7 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) { upgradeSchema19to20, upgradeSchema20to21, upgradeSchema21to22, + upgradeSchema22to23, } n := 0 @@ -1256,6 +1258,73 @@ func upgradeSchema21to22(diskConf yobj) (err error) { return nil } +// upgradeSchema22to23 performs the following changes: +// +// # BEFORE: +// 'bind_host': '1.2.3.4' +// 'bind_port': 8080 +// 'web_session_ttl': 720 +// +// # AFTER: +// 'http': +// 'address': '1.2.3.4:8080' +// 'session_ttl': '720h' +func upgradeSchema22to23(diskConf yobj) (err error) { + log.Printf("Upgrade yaml: 22 to 23") + diskConf["schema_version"] = 23 + + bindHostVal, ok := diskConf["bind_host"] + if !ok { + return nil + } + + bindHost, ok := bindHostVal.(string) + if !ok { + return fmt.Errorf("unexpected type of bind_host: %T", bindHostVal) + } + + bindHostAddr, err := netip.ParseAddr(bindHost) + if err != nil { + return fmt.Errorf("invalid bind_host value: %s", bindHost) + } + + bindPortVal, ok := diskConf["bind_port"] + if !ok { + return nil + } + + bindPort, ok := bindPortVal.(int) + if !ok { + return fmt.Errorf("unexpected type of bind_port: %T", bindPortVal) + } + + sessionTTLVal, ok := diskConf["web_session_ttl"] + if !ok { + return nil + } + + sessionTTL, ok := sessionTTLVal.(int) + if !ok { + return fmt.Errorf("unexpected type of web_session_ttl: %T", sessionTTLVal) + } + + addr := netip.AddrPortFrom(bindHostAddr, uint16(bindPort)) + if !addr.IsValid() { + return fmt.Errorf("invalid address: %s", addr) + } + + diskConf["http"] = yobj{ + "address": addr.String(), + "session_ttl": timeutil.Duration{Duration: time.Duration(sessionTTL) * time.Hour}.String(), + } + + delete(diskConf, "bind_host") + delete(diskConf, "bind_port") + delete(diskConf, "web_session_ttl") + + return nil +} + // TODO(a.garipov): Replace with log.Output when we port it to our logging // package. func funcName() string { diff --git a/internal/home/upgrade_test.go b/internal/home/upgrade_test.go index 1839cc28..9f3f54dd 100644 --- a/internal/home/upgrade_test.go +++ b/internal/home/upgrade_test.go @@ -1253,3 +1253,56 @@ func TestUpgradeSchema21to22(t *testing.T) { }) } } + +func TestUpgradeSchema22to23(t *testing.T) { + const newSchemaVer = 23 + + testCases := []struct { + in yobj + want yobj + name string + }{{ + name: "empty", + in: yobj{}, + want: yobj{ + "schema_version": newSchemaVer, + }, + }, { + name: "ok", + in: yobj{ + "bind_host": "1.2.3.4", + "bind_port": 8081, + "web_session_ttl": 720, + }, + want: yobj{ + "http": yobj{ + "address": "1.2.3.4:8081", + "session_ttl": "720h", + }, + "schema_version": newSchemaVer, + }, + }, { + name: "v6_address", + in: yobj{ + "bind_host": "2001:db8::1", + "bind_port": 8081, + "web_session_ttl": 720, + }, + want: yobj{ + "http": yobj{ + "address": "[2001:db8::1]:8081", + "session_ttl": "720h", + }, + "schema_version": newSchemaVer, + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := upgradeSchema22to23(tc.in) + require.NoError(t, err) + + assert.Equal(t, tc.want, tc.in) + }) + } +} diff --git a/internal/home/web.go b/internal/home/web.go index e0b45fe5..d53c946b 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -119,7 +119,9 @@ func webCheckPortAvailable(port int) (ok bool) { return true } - return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil + addrPort := netip.AddrPortFrom(config.HTTPConfig.Address.Addr(), uint16(port)) + + return aghnet.CheckPort("tcp", addrPort) == nil } // tlsConfigChanged updates the TLS configuration and restarts the HTTPS server diff --git a/snap/local/adguard-home-web.sh b/snap/local/adguard-home-web.sh index 697f0f92..3396dd17 100755 --- a/snap/local/adguard-home-web.sh +++ b/snap/local/adguard-home-web.sh @@ -1,7 +1,20 @@ #!/bin/sh +conf_file="${SNAP_DATA}/AdGuardHome.yaml" +readonly conf_file + +if ! [ -f "$conf_file" ] +then + xdg-open 'http://localhost:3000' + + exit +fi + # Get the admin interface port from the configuration. -bind_port="$( grep -e 'bind_port' "${SNAP_DATA}/AdGuardHome.yaml" | awk -F ' ' '{print $2}' )" +awk_prog='/^[^[:space:]]/ { is_http = /^http:/ };/^[[:space:]]+address:/ { if (is_http) print $2 }' +readonly awk_prog + +bind_port="$( awk "$awk_prog" "$conf_file" | awk -F ':' '{print $NF}' )" readonly bind_port if [ "$bind_port" = '' ]