Pull request 2713: AGDNS-4240-install-lang

Squashed commit of the following:

commit 1baf3ec81abb8f90cc2e9789945d1c10fcc23225
Merge: f8eaf24cb 4d0ae0d87
Author: Ainar Garipov <a.garipov@adguard.com>
Date:   Wed Jul 15 13:18:02 2026 +0300

    Merge branch 'master' into AGDNS-4240-install-lang

commit f8eaf24cbe91bdf825b58be6b0b565310767c001
Author: Ainar Garipov <a.garipov@adguard.com>
Date:   Wed Jul 15 13:17:16 2026 +0300

    openapi: imp language codes

commit f811b3f5c2
Author: Ainar Garipov <a.garipov@adguard.com>
Date:   Tue Jul 14 18:56:28 2026 +0300

    home: use validation more

commit 7e8fd77aa8
Author: Ainar Garipov <a.garipov@adguard.com>
Date:   Tue Jul 14 18:49:38 2026 +0300

    home: fix copying install properties

commit 2178b01e9f
Author: Ainar Garipov <a.garipov@adguard.com>
Date:   Tue Jul 14 18:43:07 2026 +0300

    home: rm unnecessary validation

commit 407ef86aa9
Author: Ainar Garipov <a.garipov@adguard.com>
Date:   Tue Jul 14 17:53:23 2026 +0300

    home: add language to install api
This commit is contained in:
Ainar Garipov 2026-07-15 10:34:17 +00:00
parent 4d0ae0d87b
commit 5b864cefc2
6 changed files with 122 additions and 30 deletions

View file

@ -20,13 +20,15 @@ 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.
### Fixed
- Blocked requests without an EDNS(0) OPT record ([#8183]).
[#8183]: https://github.com/AdguardTeam/AdGuardHome/issues/8183
[#8183]: https://github.com/AdguardTeam/AdGuardHome/issues/8183
<!--
NOTE: Add new changes ABOVE THIS COMMENT.

View file

@ -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")
}

View file

@ -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
}

View file

@ -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
}

View file

@ -2,9 +2,13 @@
<!-- TODO(a.garipov): Reformat in accordance with the KeepAChangelog spec. -->
## v0.107.78: API changes
## v0.107.79: API changes
### New `interval` values in `GET /control/filtering/status` and `POST /control/filtering/config` APIs
### Setting the UI language through the install API
- 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).

View file

@ -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':
'type': 'string'
'description': 'Basic auth username'
'example': 'admin'
'language':
'$ref': '#/components/schemas/Lang'
'password':
'type': 'string'
'description': 'Basic auth password'
'example': 'password'
'type': 'string'
'username':
'description': 'Basic auth username'
'example': 'admin'
'type': 'string'
'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'