diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e64c854620f..e4399ad40d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,5 @@ +# TODO(a.garipov): Use client_v2 for the frontend tests once Windows build is +# fixed. 'name': 'build' # Permissions are disabled for all the jobs by default, and then overridden for # specific jobs if needed. This is a recommended practice for security reasons, @@ -64,7 +66,7 @@ 'restore-keys': '${{ runner.os }}-node-' - 'name': 'Run tests' 'shell': 'bash' - 'run': 'make VERBOSE=1 deps test go-bench go-fuzz' + 'run': 'make CLIENT_DIR=client VERBOSE=1 deps test go-bench go-fuzz' - 'name': 'Upload coverage' 'uses': 'codecov/codecov-action@v1' 'if': "success() && matrix.os == 'ubuntu-latest'" diff --git a/CHANGELOG.md b/CHANGELOG.md index 531b9c674f1..0ce0805ee21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,22 @@ See also the [v0.107.79 GitHub milestone][ms-v0.107.79]. NOTE: Add new changes BELOW THIS COMMENT. --> +### Added + +- New property `"language"` in `POST /control/install/check_config` and `POST /control/install/configure` HTTP APIs. + +- The user is able to remove the static lease's hostname via the HTTP API. + +### Changed + +- The `edge` channel has been switched to the new UI and versioning scheme. + +### Fixed + +- Blocked requests without an EDNS(0) OPT record ([#8183]). + +[#8183]: https://github.com/AdguardTeam/AdGuardHome/issues/8183 + @@ -62,8 +78,6 @@ See also the [v0.107.78 GitHub milestone][ms-v0.107.78]. - The `filtering` object of the YAML configuration now includes a new property, `max_http_size`, which defines the maximum size of the HTTP request for rulelists. To disable the limitation, set a large size, such as `1 TB`. -### Fixed - - Invalid AA flag in DNS responses ([#7955]). - The parsing of the `ech` parameter in DNS rewrite rules for the HTTPS record type ([#8276]). diff --git a/Makefile b/Makefile index 935932ecf14..e947fcefa0a 100644 --- a/Makefile +++ b/Makefile @@ -19,8 +19,7 @@ GO.MACRO = $${GO:-go} VERBOSE.MACRO = $${VERBOSE:-0} CHANNEL = development -# TODO(ik): Update CLIENT_DIR to client_v2 for new frontend migration -CLIENT_DIR = client +CLIENT_DIR = client_v2 DEPLOY_SCRIPT_PATH = not/a/real/path DIST_DIR = dist GOAMD64 = v1 diff --git a/bamboo-specs/release.yaml b/bamboo-specs/release.yaml index 82b2dcd6934..cbb3fc9f5fc 100644 --- a/bamboo-specs/release.yaml +++ b/bamboo-specs/release.yaml @@ -10,6 +10,7 @@ # flaky test suite. 'cacheBuster': '0' 'channel': 'edge' + 'clientDir': 'client_v2' 'dockerFrontend': 'adguard/home-js-builder:4.0' 'dockerGo': 'adguard/go-builder:1.26.5--1' @@ -67,8 +68,9 @@ docker info docker build \ - --build-arg "BASE_IMAGE=${bamboo.dockerFrontend}" \ + --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ + --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ --output '.' \ --progress 'plain' \ --target 'builder-exporter' \ @@ -104,7 +106,7 @@ set -e -f -u -x # Explicitly checkout the revision that we need. - git checkout "${bamboo.repository.revision.number}" + git checkout "${bamboo_repository_revision_number}" version="$(env CHANNEL=${bamboo_channel} sh ./scripts/make/version.sh)" readonly version @@ -160,8 +162,8 @@ # Login to DockerHub. docker login \ - -u="${bamboo.dockerHubUsername}" \ - -p="${bamboo.dockerHubPassword}" + -u="${bamboo_dockerHubUsername}" \ + -p="${bamboo_dockerHubPassword}" # Boot the builder. docker buildx inspect --bootstrap @@ -172,8 +174,8 @@ # Prepare and push the build. env \ - CHANNEL="${bamboo.channel}" \ - REVISION="${bamboo.repository.revision.number}" \ + CHANNEL="${bamboo_channel}" \ + REVISION="${bamboo_repository_revision_number}" \ DIST_DIR='dist' \ DOCKER_IMAGE_NAME='adguard/adguardhome' \ DOCKER_PUSH='1' \ @@ -202,7 +204,7 @@ cd ./dist/ - CHANNEL="${bamboo.channel}" + CHANNEL="${bamboo_channel}" export CHANNEL ../bamboo-deploy-publisher/deploy.sh adguard-home-"$CHANNEL" @@ -227,7 +229,7 @@ set -e -f -u -x - channel="${bamboo.channel}" + channel="${bamboo_channel}" readonly channel if [ "$channel" != 'release' ] && [ "${channel}" != 'beta' ] @@ -240,7 +242,7 @@ cd ./dist/ env\ - GITHUB_TOKEN="${bamboo.githubPublicRepoPassword}"\ + GITHUB_TOKEN="${bamboo_githubPublicRepoPassword}"\ ../bamboo-deploy-publisher/deploy.sh adguard-home-github 'triggers': @@ -279,6 +281,7 @@ # need to build a few of these. 'variables': 'channel': 'beta' + 'clientDir': 'client' 'dockerFrontend': 'adguard/home-js-builder:4.0' 'dockerGo': 'adguard/go-builder:1.26.5--1' # release-vX.Y.Z branches are the branches from which the actual final @@ -295,5 +298,6 @@ # are the ones that actually get released. 'variables': 'channel': 'release' + 'clientDir': 'client' 'dockerFrontend': 'adguard/home-js-builder:4.0' 'dockerGo': 'adguard/go-builder:1.26.5--1' diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml index faedf10e39c..f78992e0a15 100644 --- a/bamboo-specs/test.yaml +++ b/bamboo-specs/test.yaml @@ -9,6 +9,7 @@ # flaky test suite. 'cacheBuster': '0' 'channel': 'development' + 'clientDir': 'client_v2' 'dockerFrontend': 'adguard/home-js-builder:4.0' 'dockerGo': 'adguard/go-builder:1.26.5--1' @@ -58,8 +59,9 @@ docker info docker build \ - --build-arg "BASE_IMAGE=${bamboo.dockerFrontend}" \ + --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ + --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ --output '.' \ --progress 'plain' \ --target 'tester' \ @@ -127,8 +129,9 @@ docker info docker build \ - --build-arg "BASE_IMAGE=${bamboo.dockerFrontend}" \ + --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ + --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ --output '.' \ --progress 'plain' \ --target 'builder-exporter' \ @@ -216,8 +219,9 @@ docker info docker build \ - --build-arg "BASE_IMAGE=${bamboo.dockerFrontend}" \ + --build-arg "BASE_IMAGE=${bamboo_dockerFrontend}" \ --build-arg "CACHE_BUSTER=${bamboo_cacheBuster}" \ + --build-arg "CLIENT_DIR=${bamboo_clientDir}" \ --output '.' \ --progress 'plain' \ --target 'e2etester' \ @@ -254,6 +258,7 @@ # Set the default release channel on the release branch to beta, as we # may need to build a few of these. 'variables': + 'channel': 'candidate' + 'clientDir': 'client' 'dockerFrontend': 'adguard/home-js-builder:4.0' 'dockerGo': 'adguard/go-builder:1.26.5--1' - 'channel': 'candidate' diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile index 0f4d3a9f38e..d37bdab7def 100644 --- a/docker/frontend.Dockerfile +++ b/docker/frontend.Dockerfile @@ -29,6 +29,7 @@ # NOTE: Keep in sync with bamboo-specs/bamboo.yaml. ARG BASE_IMAGE=adguard/home-js-builder:4.0 +ARG CLIENT_DIR=client # The dependencies stage is needed to install packages and tool dependencies. # This is also where binaries like osslsigncode, which may be required for tests @@ -38,13 +39,15 @@ ARG BASE_IMAGE=adguard/home-js-builder:4.0 FROM "$BASE_IMAGE" AS dependencies ADD Makefile /app/ ADD scripts /app/scripts -ADD client /app/client +ARG CLIENT_DIR +ADD "${CLIENT_DIR}" "/app/${CLIENT_DIR}" WORKDIR /app RUN \ --mount=type=cache,id=npm-root-cache,target=/root/.npm \ <<-'EOF' set -e -f -u -x make \ + CLIENT_DIR="${CLIENT_DIR}" \ VERBOSE=1 \ js-deps \ ; @@ -53,14 +56,16 @@ EOF # The linter stage is separated from the tester stage to make catching test # failures easier. FROM dependencies AS linter -ARG CACHE_BUSTER=0 ADD . /app +ARG CACHE_BUSTER=0 +ARG CLIENT_DIR WORKDIR /app RUN \ --mount=type=cache,id=npm-root-cache,target=/root/.npm \ <<-'EOF' set -e -f -u -x make \ + CLIENT_DIR="${CLIENT_DIR}" \ VERBOSE=1 \ js-typecheck \ js-lint \ @@ -70,11 +75,13 @@ EOF # The test stage. FROM linter AS tester ARG CACHE_BUSTER=0 +ARG CLIENT_DIR RUN \ --mount=type=cache,id=npm-root-cache,target=/root/.npm \ <<-'EOF' set -e -f -u -x make \ + CLIENT_DIR="${CLIENT_DIR}" \ VERBOSE=1 \ js-test \ ; @@ -83,6 +90,7 @@ EOF # The e2e test stage. FROM dependencies AS e2etester ARG CACHE_BUSTER=0 +ARG CLIENT_DIR ADD . /app WORKDIR /app RUN \ @@ -90,6 +98,7 @@ RUN \ <<-'EOF' set -e -f -u -x make \ + CLIENT_DIR="${CLIENT_DIR}" \ CI='true' \ VERBOSE=1 \ js-test-e2e \ @@ -99,6 +108,7 @@ EOF # The builder stage. FROM dependencies AS builder ARG CACHE_BUSTER=0 +ARG CLIENT_DIR ADD . /app WORKDIR /app RUN \ @@ -106,6 +116,7 @@ RUN \ <<-'EOF' set -e -f -u -x make \ + CLIENT_DIR="${CLIENT_DIR}" \ VERBOSE=1 \ js-build \ ; @@ -115,4 +126,5 @@ EOF # could be published. This stage should only be used in a CI. FROM scratch AS builder-exporter ARG CACHE_BUSTER=0 +ARG CLIENT_DIR COPY --from=builder /app/build /build diff --git a/internal/dhcpd/http_unix_internal_test.go b/internal/dhcpd/http_unix_internal_test.go index 059e40ac994..ed5fe2401da 100644 --- a/internal/dhcpd/http_unix_internal_test.go +++ b/internal/dhcpd/http_unix_internal_test.go @@ -209,6 +209,14 @@ func TestServer_HandleUpdateStaticLease(t *testing.T) { IP: leaseV4IP, Hostname: "updated-client-v4", }, + }, { + name: "update_v4_empty_hostname", + pos: leaseV4Pos, + lease: &leaseStatic{ + HWAddr: leaseV4MAC, + IP: leaseV4IP, + Hostname: "", + }, }, { name: "update_v4_ip", pos: leaseV4Pos, @@ -306,7 +314,7 @@ func TestServer_HandleUpdateStaticLease_validation(t *testing.T) { IP: anotherV4IP, Hostname: leaseV4Name, }, - want: "dhcpv4: updating static lease: ip address is not unique\n", + want: "dhcpv4: updating static lease: ip address: duplicated value\n", }, { name: "update_v4_same_name", lease: &leaseStatic{ @@ -314,7 +322,7 @@ func TestServer_HandleUpdateStaticLease_validation(t *testing.T) { IP: leaseV4IP, Hostname: anotherV4Name, }, - want: "dhcpv4: updating static lease: hostname is not unique\n", + want: "dhcpv4: updating static lease: hostname: duplicated value\n", }} for _, tc := range testCases { diff --git a/internal/dhcpd/v4_unix.go b/internal/dhcpd/v4_unix.go index 0350d7f3b51..e5814012e1a 100644 --- a/internal/dhcpd/v4_unix.go +++ b/internal/dhcpd/v4_unix.go @@ -313,16 +313,6 @@ func (s *v4Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) { return nil } -const ( - // ErrDupHostname is returned by addLease, validateStaticLease when the - // modified lease has a not empty non-unique hostname. - ErrDupHostname = errors.Error("hostname is not unique") - - // ErrDupIP is returned by addLease, validateStaticLease when the modified - // lease has a non-unique IP address. - ErrDupIP = errors.Error("ip address is not unique") -) - // addLease adds a dynamic or static lease. func (s *v4Server) addLease(l *dhcpsvc.Lease) (err error) { r := s.conf.ipRange @@ -342,7 +332,7 @@ func (s *v4Server) addLease(l *dhcpsvc.Lease) (err error) { // TODO(e.burkov): l must have a valid hostname here, investigate. if l.Hostname != "" { if _, ok := s.hostsIndex[l.Hostname]; ok { - return ErrDupHostname + return fmt.Errorf("hostname: %w", errors.ErrDuplicated) } s.hostsIndex[l.Hostname] = l @@ -485,23 +475,25 @@ func (s *v4Server) validateStaticLease(l *dhcpsvc.Lease) (err error) { return err } - err = netutil.ValidateHostname(hostname) - if err != nil { - return fmt.Errorf("validating hostname: %w", err) - } + if hostname != "" { + err = netutil.ValidateHostname(hostname) + if err != nil { + return fmt.Errorf("hostname: %w", err) + } - dup, ok := s.hostsIndex[hostname] - if ok && !bytes.Equal(dup.HWAddr, l.HWAddr) { - return ErrDupHostname + dup, ok := s.hostsIndex[hostname] + if ok && !bytes.Equal(dup.HWAddr, l.HWAddr) { + return fmt.Errorf("hostname: %w", errors.ErrDuplicated) + } } - dup, ok = s.ipIndex[l.IP] + l.Hostname = hostname + + dup, ok := s.ipIndex[l.IP] if ok && !bytes.Equal(dup.HWAddr, l.HWAddr) { - return ErrDupIP + return fmt.Errorf("ip address: %w", errors.ErrDuplicated) } - l.Hostname = hostname - if gwIP := s.conf.GatewayIP; gwIP == l.IP { return fmt.Errorf("can't assign the gateway IP %q to the lease", gwIP) } diff --git a/internal/dhcpd/v4_unix_internal_test.go b/internal/dhcpd/v4_unix_internal_test.go index ae80b5bdcb4..9d09687f2ba 100644 --- a/internal/dhcpd/v4_unix_internal_test.go +++ b/internal/dhcpd/v4_unix_internal_test.go @@ -12,6 +12,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/testutil" @@ -83,7 +84,7 @@ func TestV4Server_leasing(t *testing.T) { IP: anotherIP, IsStatic: true, }) - assert.ErrorIs(t, err, ErrDupHostname) + assert.ErrorIs(t, err, errors.ErrDuplicated) }) t.Run("same_mac", func(t *testing.T) { @@ -914,3 +915,71 @@ func TestV4Server_handleRelease(t *testing.T) { require.Equal(t, wantResp, resp) } + +func TestV4Server_validateStaticLease_emptyHostname(t *testing.T) { + t.Parallel() + + const ( + existingHostname = "existing-client" + ) + + existingIP := netip.MustParseAddr("192.168.10.150") + nonExistingIP := existingIP.Next() + existingMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} + otherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB} + + s := defaultSrv(t) + + s4, ok := s.(*v4Server) + require.True(t, ok) + + existingLease := &dhcpsvc.Lease{ + Hostname: existingHostname, + HWAddr: existingMAC, + IP: existingIP, + IsStatic: true, + } + err := s4.addLease(existingLease) + require.NoError(t, err) + + testCases := []struct { + name string + wantErr string + lease *dhcpsvc.Lease + }{{ + name: "empty_hostname_allowed", + lease: &dhcpsvc.Lease{ + Hostname: "", + HWAddr: otherMAC, + IP: nonExistingIP, + IsStatic: true, + }, + }, { + name: "non_empty_hostname_duplicate_hostname", + wantErr: "hostname: duplicated value", + lease: &dhcpsvc.Lease{ + Hostname: existingHostname, + HWAddr: otherMAC, + IP: nonExistingIP, + IsStatic: true, + }, + }, { + name: "non_empty_hostname_duplicate_ip", + wantErr: "ip address: duplicated value", + lease: &dhcpsvc.Lease{ + Hostname: "new-client", + HWAddr: otherMAC, + IP: existingIP, + IsStatic: true, + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Don't run subtests in parallel because they modify the server's + // leases. + err = s4.validateStaticLease(tc.lease) + testutil.AssertErrorMsg(t, tc.wantErr, err) + }) + } +} diff --git a/internal/dnsforward/msg.go b/internal/dnsforward/msg.go index 5dba5927aef..b361f22d7ee 100644 --- a/internal/dnsforward/msg.go +++ b/internal/dnsforward/msg.go @@ -17,10 +17,17 @@ import ( // template. Also extract all the methods to a separate entity. // reply creates a DNS response for req. +// +// NOTE: If req uses EDNS(0), the response copies its UDP size and DO flag. func (*Server) reply(req *dns.Msg, code int) (resp *dns.Msg) { resp = (&dns.Msg{}).SetRcode(req, code) resp.RecursionAvailable = true + opt := req.IsEdns0() + if opt != nil { + resp.SetEdns0(opt.UDPSize(), opt.Do()) + } + return resp } @@ -404,7 +411,11 @@ func (s *Server) NewMsgSERVFAIL(req *dns.Msg) (resp *dns.Msg) { // NewMsgNOTIMPLEMENTED implements the [proxy.MessageConstructor] interface for // *Server. func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) { - resp = s.reply(req, dns.RcodeNotImplemented) + // NOTE: [Server.reply] must not be used there, because it unconditionally + // copies UDP size and DO bit from the request, when in this case we want to + // use constant values. + resp = (&dns.Msg{}).SetRcode(req, dns.RcodeNotImplemented) + resp.RecursionAvailable = true // Most of the Internet and especially the inner core has an MTU of at least // 1500 octets. Maximum DNS/UDP payload size for IPv6 on MTU 1500 ethernet @@ -415,7 +426,10 @@ func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) { // NOTIMPLEMENTED without EDNS is treated as 'we don't support EDNS', so // explicitly set it. - resp.SetEdns0(maxUDPPayload, false) + opt := req.IsEdns0() + if opt != nil { + resp.SetEdns0(maxUDPPayload, false) + } return resp } diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index 4e26c4bcf29..c217ea3f9ae 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -84,9 +84,12 @@ type checkConfReqEnt struct { } type checkConfReq struct { - Web checkConfReqEnt `json:"web"` - DNS checkConfReqEnt `json:"dns"` - SetStaticIP bool `json:"set_static_ip"` + Web checkConfReqEnt `json:"web"` + DNS checkConfReqEnt `json:"dns"` + + Language string `json:"language"` + + SetStaticIP bool `json:"set_static_ip"` } type checkConfRespEnt struct { @@ -102,8 +105,9 @@ type staticIPJSON struct { type checkConfResp struct { StaticIP staticIPJSON `json:"static_ip"` - Web checkConfRespEnt `json:"web"` DNS checkConfRespEnt `json:"dns"` + Language checkConfRespEnt `json:"language"` + Web checkConfRespEnt `json:"web"` } // validateWeb returns error is the web part if the initial configuration can't @@ -200,6 +204,12 @@ func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Reque } resp := &checkConfResp{} + + err = validateLang(req.Language, true) + if err != nil { + resp.Language.Status = err.Error() + } + tcpPorts := aghalg.UniqChecker[tcpPort]{} if err = req.validateWeb(tcpPorts); err != nil { resp.Web.Status = err.Error() @@ -362,19 +372,21 @@ type applyConfigReqEnt struct { } type applyConfigReq struct { - Username string `json:"username"` + Language string `json:"language"` Password string `json:"password"` + Username string `json:"username"` Web applyConfigReqEnt `json:"web"` DNS applyConfigReqEnt `json:"dns"` } // copyInstallSettings copies the installation parameters between two -// configuration structures. +// configuration structures. All arguments must not be nil. func copyInstallSettings(dst, src *configuration) { - dst.HTTPConfig = src.HTTPConfig dst.DNS.BindHosts = src.DNS.BindHosts dst.DNS.Port = src.DNS.Port + dst.HTTPConfig = src.HTTPConfig + dst.Language = src.Language } // shutdownTimeout is the timeout for shutting HTTP server down operation. @@ -491,6 +503,10 @@ func (web *webAPI) finalizeInstall( } }() + if req.Language != "" { + config.Language = req.Language + } + config.DNS.BindHosts = []netip.Addr{req.DNS.IP} config.DNS.Port = req.DNS.Port config.Filtering.Logger = web.baseLogger.With(slogutil.KeyPrefix, "filtering") @@ -589,6 +605,12 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e return nil, false, fmt.Errorf("parsing request: %w", err) } + err = validateLang(req.Language, true) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, false, err + } + if req.Web.Port == 0 || req.DNS.Port == 0 { return nil, false, errors.Error("ports cannot be 0") } diff --git a/internal/home/i18n.go b/internal/home/i18n.go index b784f006575..4d733cead4c 100644 --- a/internal/home/i18n.go +++ b/internal/home/i18n.go @@ -2,6 +2,7 @@ package home import ( "encoding/json" + "fmt" "net/http" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" @@ -48,6 +49,20 @@ var allowedLanguages = container.NewMapSet( "zh-tw", ) +// validateLang returns a standard error about if lang is an unknown language. +// If allowEmpty is true, the language can also be empty. +func validateLang(lang string, allowEmpty bool) (err error) { + if allowEmpty && lang == "" { + return nil + } + + if !allowedLanguages.Has(lang) { + return fmt.Errorf("unknown language: %q", lang) + } + + return nil +} + // languageJSON is the JSON structure for language requests and responses. type languageJSON struct { Language string `json:"language"` @@ -89,8 +104,9 @@ func (web *webAPI) handleI18nChangeLanguage(w http.ResponseWriter, r *http.Reque } lang := langReq.Language - if !allowedLanguages.Has(lang) { - aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "unknown language: %q", lang) + err = validateLang(lang, false) + if err != nil { + aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "%s", err) return } diff --git a/internal/home/profilehttp.go b/internal/home/profilehttp.go index 73c24e223ac..1b27f5ce834 100644 --- a/internal/home/profilehttp.go +++ b/internal/home/profilehttp.go @@ -94,8 +94,9 @@ func (web *webAPI) handlePutProfile(w http.ResponseWriter, r *http.Request) { } lang := profileReq.Language - if !allowedLanguages.Has(lang) { - aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "unknown language: %q", lang) + err = validateLang(lang, false) + if err != nil { + aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "%s", err) return } diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index 1009cba05d7..e33f21ccc61 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -2,9 +2,13 @@ -## v0.107.78: API changes +## v0.107.79: API changes + +### Setting the UI language through the install API -### New `interval` values in `GET /control/filtering/status` and `POST /control/filtering/config` APIs +- New property `"language"` in `POST /control/install/check_config` and `POST /control/install/configure` HTTP APIs defines the language to use once AdGuard Home is installed. + +## v0.107.78: API changes The property `interval` of the objects returned from and accepted by the aforementioned APIs can now be any integer between 0 and 8760 (365 days). diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index c214be1cdd9..c190efbd099 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,4 +1,4 @@ -'openapi': '3.0.3' +'openapi': '3.2.0' 'info': 'title': 'AdGuard Home' 'description': > @@ -1543,8 +1543,7 @@ 'type': 'string' 'example': 'v0.123.4' 'language': - 'type': 'string' - 'example': 'en' + '$ref': '#/components/schemas/Lang' 'start_time': 'type': 'number' 'format': 'double' @@ -2685,7 +2684,7 @@ 'name': 'type': 'string' 'language': - 'type': 'string' + '$ref': '#/components/schemas/Lang' 'theme': 'type': 'string' 'description': 'Interface theme' @@ -3159,11 +3158,13 @@ 'properties': 'dns': '$ref': '#/components/schemas/CheckConfigRequestInfo' - 'web': - '$ref': '#/components/schemas/CheckConfigRequestInfo' + 'language': + '$ref': '#/components/schemas/Lang' 'set_static_ip': 'type': 'boolean' 'example': false + 'web': + '$ref': '#/components/schemas/CheckConfigRequestInfo' 'CheckConfigRequestInfo': 'type': 'object' 'properties': @@ -3181,15 +3182,18 @@ 'type': 'object' 'required': - 'dns' - - 'web' + - 'language' - 'static_ip' + - 'web' 'properties': 'dns': '$ref': '#/components/schemas/CheckConfigResponseInfo' - 'web': + 'language': '$ref': '#/components/schemas/CheckConfigResponseInfo' 'static_ip': '$ref': '#/components/schemas/CheckConfigStaticIpInfo' + 'web': + '$ref': '#/components/schemas/CheckConfigResponseInfo' 'CheckConfigResponseInfo': 'type': 'object' 'required': @@ -3231,21 +3235,23 @@ 'required': - 'dns' - 'web' - - 'username' - 'password' + - 'username' 'properties': 'dns': '$ref': '#/components/schemas/AddressInfo' 'web': '$ref': '#/components/schemas/AddressInfo' - 'username': + 'language': + '$ref': '#/components/schemas/Lang' + 'password': + 'description': 'Basic auth password' + 'example': 'password' 'type': 'string' + 'username': 'description': 'Basic auth username' 'example': 'admin' - 'password': 'type': 'string' - 'description': 'Basic auth password' - 'example': 'password' 'Login': 'type': 'object' 'description': 'Login request data' @@ -3267,11 +3273,52 @@ 'description': 'Language settings object.' 'properties': 'language': - 'description': 'The current language or the language to set.' - 'type': 'string' + '$ref': '#/components/schemas/Lang' 'required': - 'language' 'type': 'object' + 'Lang': + 'description': > + Language code. + # Hold the enum in sync with .twosky.json. + 'enum': + - 'ar' + - 'be' + - 'bg' + - 'cs' + - 'da' + - 'de' + - 'en' + - 'es' + - 'fa' + - 'fi' + - 'fr' + - 'hr' + - 'hu' + - 'id' + - 'it' + - 'ja' + - 'ko' + - 'nl' + - 'no' + - 'pl' + - 'pt-br' + - 'pt-pt' + - 'ro' + - 'ru' + - 'si-lk' + - 'sk' + - 'sl' + - 'sr-cs' + - 'sv' + - 'th' + - 'tr' + - 'uk' + - 'vi' + - 'zh-cn' + - 'zh-hk' + - 'zh-tw' + 'type': 'string' 'securitySchemes': 'basicAuth': 'type': 'http' diff --git a/scripts/make/version.sh b/scripts/make/version.sh index 46382740fd6..32e38170d17 100644 --- a/scripts/make/version.sh +++ b/scripts/make/version.sh @@ -85,6 +85,19 @@ case "$channel" in version="v0.0.0-dev.${commit_number}+$(git rev-parse --short HEAD)" ;; 'edge') + # TODO(a.garipov): Remove this and use edge_todo below instead once v1.0.0 + # is out. + + # Temporarily, use the last v0.108.0 beta as the starting point for the + # v1.0.0 alpha (edge, nightly) releases. + num_commits="$(git rev-list --count "v0.108.0-b.89..HEAD")" + readonly num_commits + + version="v1.0.0-a.${num_commits}+$(git rev-parse --short HEAD)" + ;; +'edge_todo') + # TODO(a.garipov): Use this again once v1.0.0 is out. + # last_minor_zero is the last new minor release. last_minor_zero="$(get_last_minor_zero)" readonly last_minor_zero