From 15110b732a4a24c9bd611477627bdd06291e10b2 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Fri, 16 Sep 2022 11:19:16 +1000 Subject: [PATCH] fix(server): i18n etags missing (#3973) This fixes missing etags from locales assets. --- crowdin.yml | 10 -- internal/middlewares/asset_override.go | 20 ++-- internal/server/asset.go | 97 ++++++++++++++----- internal/server/const.go | 17 ++-- internal/server/handlers.go | 18 ++-- .../server/locales/{de => de-DE}/portal.json | 0 .../server/locales/{es => es-ES}/portal.json | 0 .../server/locales/{fr => fr-FR}/portal.json | 0 .../server/locales/{nl => nl-NL}/portal.json | 0 .../server/locales/{pt => pt-PT}/portal.json | 0 .../server/locales/{ru => ru-RU}/portal.json | 0 internal/server/locales/sv/portal.json | 69 ------------- .../server/locales/{zh => zh-CN}/portal.json | 0 internal/server/template.go | 17 ++-- web/src/i18n/index.ts | 4 +- 15 files changed, 116 insertions(+), 136 deletions(-) rename internal/server/locales/{de => de-DE}/portal.json (100%) rename internal/server/locales/{es => es-ES}/portal.json (100%) rename internal/server/locales/{fr => fr-FR}/portal.json (100%) rename internal/server/locales/{nl => nl-NL}/portal.json (100%) rename internal/server/locales/{pt => pt-PT}/portal.json (100%) rename internal/server/locales/{ru => ru-RU}/portal.json (100%) delete mode 100644 internal/server/locales/sv/portal.json rename internal/server/locales/{zh => zh-CN}/portal.json (100%) diff --git a/crowdin.yml b/crowdin.yml index 51597bf01..88613ec99 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -6,14 +6,4 @@ files: - source: /internal/server/locales/en/* translation: /internal/server/locales/%locale%/%original_file_name% skip_untranslated_files: true - languages_mapping: - locale: - "de-DE": de - "en-EN": en - "es-ES": es - "fr-FR": fr - "nl-NL": nl - "pt-PT": pt - "ru-RU": ru - "zh-CH": zh ... diff --git a/internal/middlewares/asset_override.go b/internal/middlewares/asset_override.go index e25444aba..c8ed111c4 100644 --- a/internal/middlewares/asset_override.go +++ b/internal/middlewares/asset_override.go @@ -9,20 +9,22 @@ import ( // AssetOverride allows overriding and serving of specific embedded assets from disk. func AssetOverride(root string, strip int, next fasthttp.RequestHandler) fasthttp.RequestHandler { + if root == "" { + return next + } + + handler := fasthttp.FSHandler(root, strip) + stripper := fasthttp.NewPathSlashesStripper(strip) + return func(ctx *fasthttp.RequestCtx) { - if root == "" { + asset := filepath.Join(root, string(stripper(ctx))) + + if _, err := os.Stat(asset); err != nil { next(ctx) return } - _, err := os.Stat(filepath.Join(root, string(fasthttp.NewPathSlashesStripper(strip)(ctx)))) - if err != nil { - next(ctx) - - return - } - - fasthttp.FSHandler(root, strip)(ctx) + handler(ctx) } } diff --git a/internal/server/asset.go b/internal/server/asset.go index 5c4559a9b..57905a283 100644 --- a/internal/server/asset.go +++ b/internal/server/asset.go @@ -20,19 +20,21 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) -//go:embed locales -var locales embed.FS +var ( + //go:embed public_html + assets embed.FS -//go:embed public_html -var assets embed.FS + //go:embed locales + locales embed.FS +) func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler { etags := map[string][]byte{} - getEmbedETags(assets, "public_html", etags) + getEmbedETags(assets, assetsRoot, etags) return func(ctx *fasthttp.RequestCtx) { - p := path.Join("public_html", string(ctx.Path())) + p := path.Join(assetsRoot, string(ctx.Path())) if etag, ok := etags[p]; ok { ctx.Response.Header.SetBytesKV(headerETag, etag) @@ -66,8 +68,10 @@ func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler { } } -func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) { - var languages []string +func newLocalesPathResolver() func(ctx *fasthttp.RequestCtx) (supported bool, asset string) { + var ( + languages, dirs []string + ) entries, err := locales.ReadDir("locales") if err == nil { @@ -84,6 +88,10 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) { lng = strings.SplitN(entry.Name(), "-", 2)[0] } + if !utils.IsStringInSlice(entry.Name(), dirs) { + dirs = append(dirs, entry.Name()) + } + if utils.IsStringInSlice(lng, languages) { continue } @@ -93,34 +101,79 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) { } } - return func(ctx *fasthttp.RequestCtx) { - var ( - language, variant, locale, namespace string - ) + aliases := map[string]string{ + "sv": "sv-SE", + "zh": "zh-CN", + } - language = ctx.UserValue("language").(string) - namespace = ctx.UserValue("namespace").(string) - locale = language + return func(ctx *fasthttp.RequestCtx) (supported bool, asset string) { + var language, namespace, variant, locale string + + language, namespace = ctx.UserValue("language").(string), ctx.UserValue("namespace").(string) + + if !utils.IsStringInSlice(language, languages) { + return false, "" + } if v := ctx.UserValue("variant"); v != nil { variant = v.(string) locale = fmt.Sprintf("%s-%s", language, variant) + } else { + locale = language } - var data []byte + ll := language + "-" + strings.ToUpper(language) + alias, ok := aliases[locale] - if data, err = locales.ReadFile(fmt.Sprintf("locales/%s/%s.json", locale, namespace)); err != nil { - if utils.IsStringInSliceFold(language, languages) { - data = []byte("{}") - } + switch { + case ok: + return true, fmt.Sprintf("locales/%s/%s.json", alias, namespace) + case utils.IsStringInSlice(locale, dirs): + return true, fmt.Sprintf("locales/%s/%s.json", locale, namespace) + case utils.IsStringInSlice(ll, dirs): + return true, fmt.Sprintf("locales/%s-%s/%s.json", language, strings.ToUpper(language), namespace) + default: + return true, fmt.Sprintf("locales/%s/%s.json", locale, namespace) + } + } +} - if len(data) == 0 { - hfsHandleErr(ctx, err) +func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) { + etags := map[string][]byte{} + + getEmbedETags(locales, "locales", etags) + + getAssetName := newLocalesPathResolver() + + return func(ctx *fasthttp.RequestCtx) { + supported, asset := getAssetName(ctx) + + if !supported { + handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound) + + return + } + + if etag, ok := etags[asset]; ok { + ctx.Response.Header.SetBytesKV(headerETag, etag) + ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets) + + if bytes.Equal(etag, ctx.Request.Header.PeekBytes(headerIfNoneMatch)) { + ctx.SetStatusCode(fasthttp.StatusNotModified) return } } + var ( + data []byte + err error + ) + + if data, err = locales.ReadFile(asset); err != nil { + data = []byte("{}") + } + middlewares.SetContentTypeApplicationJSON(ctx) ctx.SetBody(data) diff --git a/internal/server/const.go b/internal/server/const.go index 9c9a34098..4d7a636b2 100644 --- a/internal/server/const.go +++ b/internal/server/const.go @@ -5,16 +5,17 @@ import ( ) const ( - embeddedAssets = "public_html/" - swaggerAssets = embeddedAssets + "api/" - apiFile = "openapi.yml" - indexFile = "index.html" - logoFile = "logo.png" + assetsRoot = "public_html" + assetsSwagger = assetsRoot + "/api" + + fileOpenAPI = "openapi.yml" + fileIndexHTML = "index.html" + fileLogo = "logo.png" ) var ( - rootFiles = []string{"manifest.json", "robots.txt"} - swaggerFiles = []string{ + filesRoot = []string{"manifest.json", "robots.txt"} + filesSwagger = []string{ "favicon-16x16.png", "favicon-32x32.png", "index.css", @@ -35,7 +36,7 @@ var ( } // Directories excluded from the not found handler proceeding to the next() handler. - httpServerDirs = []struct { + dirsHTTPServer = []struct { name, prefix string }{ {name: "/api", prefix: "/api/"}, diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 389743d39..9f77e0bbb 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -79,8 +79,8 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { path := strings.ToLower(string(ctx.Path())) - for i := 0; i < len(httpServerDirs); i++ { - if path == httpServerDirs[i].name || strings.HasPrefix(path, httpServerDirs[i].prefix) { + for i := 0; i < len(dirsHTTPServer); i++ { + if path == dirsHTTPServer[i].name || strings.HasPrefix(path, dirsHTTPServer[i].prefix) { handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound) return @@ -104,9 +104,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers) https := config.Server.TLS.Key != "" && config.Server.TLS.Certificate != "" - serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https) - serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https) - serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https) + serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https) + serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https) + serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https) handlerPublicHTML := newPublicHTMLEmbeddedHandler() handlerLocales := newLocalesEmbeddedHandler() @@ -124,7 +124,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers) // Static Assets. r.GET("/", middleware(serveIndexHandler)) - for _, f := range rootFiles { + for _, f := range filesRoot { r.GET("/"+f, handlerPublicHTML) } @@ -139,10 +139,10 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers) // Swagger. r.GET("/api/", middleware(serveSwaggerHandler)) r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS) - r.GET("/api/"+apiFile, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler))) - r.OPTIONS("/api/"+apiFile, policyCORSPublicGET.HandleOPTIONS) + r.GET("/api/"+fileOpenAPI, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler))) + r.OPTIONS("/api/"+fileOpenAPI, policyCORSPublicGET.HandleOPTIONS) - for _, file := range swaggerFiles { + for _, file := range filesSwagger { r.GET("/api/"+file, handlerPublicHTML) } diff --git a/internal/server/locales/de/portal.json b/internal/server/locales/de-DE/portal.json similarity index 100% rename from internal/server/locales/de/portal.json rename to internal/server/locales/de-DE/portal.json diff --git a/internal/server/locales/es/portal.json b/internal/server/locales/es-ES/portal.json similarity index 100% rename from internal/server/locales/es/portal.json rename to internal/server/locales/es-ES/portal.json diff --git a/internal/server/locales/fr/portal.json b/internal/server/locales/fr-FR/portal.json similarity index 100% rename from internal/server/locales/fr/portal.json rename to internal/server/locales/fr-FR/portal.json diff --git a/internal/server/locales/nl/portal.json b/internal/server/locales/nl-NL/portal.json similarity index 100% rename from internal/server/locales/nl/portal.json rename to internal/server/locales/nl-NL/portal.json diff --git a/internal/server/locales/pt/portal.json b/internal/server/locales/pt-PT/portal.json similarity index 100% rename from internal/server/locales/pt/portal.json rename to internal/server/locales/pt-PT/portal.json diff --git a/internal/server/locales/ru/portal.json b/internal/server/locales/ru-RU/portal.json similarity index 100% rename from internal/server/locales/ru/portal.json rename to internal/server/locales/ru-RU/portal.json diff --git a/internal/server/locales/sv/portal.json b/internal/server/locales/sv/portal.json deleted file mode 100644 index 20f8e1948..000000000 --- a/internal/server/locales/sv/portal.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "Accept": "Acceptera", - "Access your email addresses": "Hantera din e-postadress", - "Access your group membership": "Hantera dina gruppmedlemskap", - "Access your profile information": "Hantera din profilinformation", - "An email has been sent to your address to complete the process": "Ett mejl har skickats till din e-postadress för att slutföra processen.", - "Authenticated": "Autentiserad", - "Cancel": "Avbryt", - "Client ID": "Klient-ID: {{client_id}}", - "Consent Request": "Begäran om medgivande", - "Contact your administrator to register a device": "Kontakta din administratör för att registrera en enhet.", - "Could not obtain user settings": "Misslyckades med att hämta användarinställningarna.", - "Deny": "Avböj", - "Done": "Klar", - "Enter new password": "Skriv ditt nya lösenord", - "Enter one-time password": "Skriv ditt engångslösenord", - "Failed to register device, the provided link is expired or has already been used": "Enhetsregistreringen misslyckades, den angivna länken har utgått eller redan blivit använd.", - "Hi": "Hej", - "Incorrect username or password": "Fel användarnamn eller lösenord", - "Loading": "Läser in", - "Logout": "Logga ut", - "Lost your device?": "Har du tappat bort din enhet?", - "Methods": "Metoder", - "Must be at least {{len}} characters in length": "Måste vara minst {{len}} tecken långt", - "Must have at least one UPPERCASE letter": "Måste innehålla minst en stor bokstav", - "Must have at least one lowercase letter": "Måste innehålla minst en liten bokstav", - "Must have at least one number": "Måste innehålla minst ett nummer", - "Must have at least one special character": "Måste innehålla minst ett specialtecken", - "Must not be more than {{len}} characters in length": "Får inte vara längre än {{len}} tecken", - "Need Google Authenticator?": "Behöver du Google Authenticator?", - "New password": "Nytt lösenord", - "No verification token provided": "Ingen verifieringskod tillhandahålls", - "OTP Secret copied to clipboard": "OTP koden har kopierats till urklipp", - "OTP URL copied to clipboard": "OTP länken har kopierats till urklipp", - "One-Time Password": "Engångslösenord", - "Password has been reset": "Lösenordet har blivit återställt", - "Password": "Lösenord", - "Passwords do not match": "Lösenorden matchar inte", - "Push Notification": "Push-avisering", - "Register device": "Registrera enhet", - "Register your first device by clicking on the link below": "Registrera din första enhet genom att klicka på länken nedan", - "Remember Consent": "Kom ihåg samtycke", - "Remember me": "Kom ihåg mig", - "Repeat new password": "Upprepa nya lösenordet", - "Reset password": "Återställ lösenord", - "Reset password?": "Återställ lösenord?", - "Reset": "Återställ", - "Scan QR Code": "Skanna QR koden", - "Secret": "Kod", - "Security Key - WebAuthN": "Säkerhetsnyckel - WebAuthN", - "Select a Device": "Välj en enhet", - "Sign in": "Logga in", - "Sign out": "Logga ut", - "The above application is requesting the following permissions": "Ovanstående program begär följande behörigheter", - "The password does not meet the password policy": "Lösenordet uppfyller inte lösenordspolicyn", - "The resource you're attempting to access requires two-factor authentication": "Resursen du vill komma åt kräver tvåstegsverifiering", - "There was a problem initiating the registration process": "Ett problem uppstod när registreringsprocessen skulle starta", - "There was an issue completing the process. The verification token might have expired": "Det uppstod ett problem med att slutföra processen. Verifieringskoden kan ha gått ut", - "There was an issue initiating the password reset process": "Ett problem uppstod när processen för lösenordsåterställning startade", - "There was an issue resetting the password": "Ett problem uppstod med att återställa lösenordet", - "There was an issue signing out": "Ett problem uppstod med att logga ut", - "This saves this consent as a pre-configured consent for future use": "Spara detta samtycke som ett förkonfigurerat samtycke för framtida användning", - "Time-based One-Time Password": "Tidsbaserat engångslösenord", - "Use OpenID to verify your identity": "Använd OpenID till att verifiera din identitet", - "Username": "Användarnamn", - "You must open the link from the same device and browser that initiated the registration process": "Du måste öppna länken från samma enhet och webbläsare som startade registreringsprocessen", - "You're being signed out and redirected": "Du blir utloggad och omdirigerad", - "Your supplied password does not meet the password policy requirements": "Det angivna lösenordet möter inte lösenordskraven" -} diff --git a/internal/server/locales/zh/portal.json b/internal/server/locales/zh-CN/portal.json similarity index 100% rename from internal/server/locales/zh/portal.json rename to internal/server/locales/zh-CN/portal.json diff --git a/internal/server/template.go b/internal/server/template.go index 9c5bb523d..fedf2df57 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -4,10 +4,13 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "strings" "text/template" + "github.com/valyala/fasthttp" + "github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/utils" @@ -19,7 +22,7 @@ import ( func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, session, theme string, https bool) middlewares.RequestHandler { logger := logging.Logger() - a, err := assets.Open(publicDir + file) + a, err := assets.Open(path.Join(publicDir, file)) if err != nil { logger.Fatalf("Unable to open %s: %s", file, err) } @@ -43,7 +46,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM logoOverride := f if assetPath != "" { - if _, err := os.Stat(filepath.Join(assetPath, logoFile)); err == nil { + if _, err := os.Stat(filepath.Join(assetPath, fileLogo)); err == nil { logoOverride = t } } @@ -71,14 +74,14 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM } switch { - case publicDir == swaggerAssets: - ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("base-uri 'self'; default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce)) + case publicDir == assetsSwagger: + ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf("base-uri 'self'; default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce)) case ctx.Configuration.Server.Headers.CSPTemplate != "": - ctx.Response.Header.Add("Content-Security-Policy", strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, cspNoncePlaceholder, nonce)) + ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, cspNoncePlaceholder, nonce)) case os.Getenv("ENVIRONMENT") == dev: - ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, " 'unsafe-eval'", nonce)) + ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(cspDefaultTemplate, " 'unsafe-eval'", nonce)) default: - ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, "", nonce)) + ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(cspDefaultTemplate, "", nonce)) } err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, DuoSelfEnrollment, LogoOverride, RememberMe, ResetPassword, ResetPasswordCustomURL, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, ResetPasswordCustomURL: resetPasswordCustomURL, Session: session, Theme: theme}) diff --git a/web/src/i18n/index.ts b/web/src/i18n/index.ts index 53231ccd1..6cfa4419d 100644 --- a/web/src/i18n/index.ts +++ b/web/src/i18n/index.ts @@ -18,9 +18,9 @@ i18n.use(Backend) backend: { loadPath: basePath + "/locales/{{lng}}/{{ns}}.json", }, + load: "all", ns: ["portal"], defaultNS: "portal", - load: "all", fallbackLng: { default: ["en"], de: ["en"], @@ -33,7 +33,7 @@ i18n.use(Backend) "sv-SE": ["sv", "en"], zh: ["en"], "zh-CN": ["zh", "en"], - "zh-TW": ["zh", "en"], + "zh-TW": ["en"], }, supportedLngs: ["en", "de", "es", "fr", "nl", "pt", "ru", "sv", "sv-SE", "zh", "zh-CN", "zh-TW"], lowerCaseLng: false,