fix(server): i18n etags missing (#3973)

This fixes missing etags from locales assets.
pull/3972/head
James Elliott 2022-09-16 11:19:16 +10:00 committed by GitHub
parent ec88b67cf1
commit 15110b732a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 116 additions and 136 deletions

View File

@ -6,14 +6,4 @@ files:
- source: /internal/server/locales/en/* - source: /internal/server/locales/en/*
translation: /internal/server/locales/%locale%/%original_file_name% translation: /internal/server/locales/%locale%/%original_file_name%
skip_untranslated_files: true 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
... ...

View File

@ -9,20 +9,22 @@ import (
// AssetOverride allows overriding and serving of specific embedded assets from disk. // AssetOverride allows overriding and serving of specific embedded assets from disk.
func AssetOverride(root string, strip int, next fasthttp.RequestHandler) fasthttp.RequestHandler { 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) { return func(ctx *fasthttp.RequestCtx) {
if root == "" { asset := filepath.Join(root, string(stripper(ctx)))
if _, err := os.Stat(asset); err != nil {
next(ctx) next(ctx)
return return
} }
_, err := os.Stat(filepath.Join(root, string(fasthttp.NewPathSlashesStripper(strip)(ctx)))) handler(ctx)
if err != nil {
next(ctx)
return
}
fasthttp.FSHandler(root, strip)(ctx)
} }
} }

View File

@ -20,19 +20,21 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
//go:embed locales var (
var locales embed.FS //go:embed public_html
assets embed.FS
//go:embed public_html //go:embed locales
var assets embed.FS locales embed.FS
)
func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler { func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
etags := map[string][]byte{} etags := map[string][]byte{}
getEmbedETags(assets, "public_html", etags) getEmbedETags(assets, assetsRoot, etags)
return func(ctx *fasthttp.RequestCtx) { 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 { if etag, ok := etags[p]; ok {
ctx.Response.Header.SetBytesKV(headerETag, etag) ctx.Response.Header.SetBytesKV(headerETag, etag)
@ -66,8 +68,10 @@ func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
} }
} }
func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) { func newLocalesPathResolver() func(ctx *fasthttp.RequestCtx) (supported bool, asset string) {
var languages []string var (
languages, dirs []string
)
entries, err := locales.ReadDir("locales") entries, err := locales.ReadDir("locales")
if err == nil { if err == nil {
@ -84,6 +88,10 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
lng = strings.SplitN(entry.Name(), "-", 2)[0] lng = strings.SplitN(entry.Name(), "-", 2)[0]
} }
if !utils.IsStringInSlice(entry.Name(), dirs) {
dirs = append(dirs, entry.Name())
}
if utils.IsStringInSlice(lng, languages) { if utils.IsStringInSlice(lng, languages) {
continue continue
} }
@ -93,34 +101,79 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
} }
} }
return func(ctx *fasthttp.RequestCtx) { aliases := map[string]string{
var ( "sv": "sv-SE",
language, variant, locale, namespace string "zh": "zh-CN",
) }
language = ctx.UserValue("language").(string) return func(ctx *fasthttp.RequestCtx) (supported bool, asset string) {
namespace = ctx.UserValue("namespace").(string) var language, namespace, variant, locale string
locale = language
language, namespace = ctx.UserValue("language").(string), ctx.UserValue("namespace").(string)
if !utils.IsStringInSlice(language, languages) {
return false, ""
}
if v := ctx.UserValue("variant"); v != nil { if v := ctx.UserValue("variant"); v != nil {
variant = v.(string) variant = v.(string)
locale = fmt.Sprintf("%s-%s", language, variant) 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 { switch {
if utils.IsStringInSliceFold(language, languages) { case ok:
data = []byte("{}") 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 { func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
hfsHandleErr(ctx, err) 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 return
} }
} }
var (
data []byte
err error
)
if data, err = locales.ReadFile(asset); err != nil {
data = []byte("{}")
}
middlewares.SetContentTypeApplicationJSON(ctx) middlewares.SetContentTypeApplicationJSON(ctx)
ctx.SetBody(data) ctx.SetBody(data)

View File

@ -5,16 +5,17 @@ import (
) )
const ( const (
embeddedAssets = "public_html/" assetsRoot = "public_html"
swaggerAssets = embeddedAssets + "api/" assetsSwagger = assetsRoot + "/api"
apiFile = "openapi.yml"
indexFile = "index.html" fileOpenAPI = "openapi.yml"
logoFile = "logo.png" fileIndexHTML = "index.html"
fileLogo = "logo.png"
) )
var ( var (
rootFiles = []string{"manifest.json", "robots.txt"} filesRoot = []string{"manifest.json", "robots.txt"}
swaggerFiles = []string{ filesSwagger = []string{
"favicon-16x16.png", "favicon-16x16.png",
"favicon-32x32.png", "favicon-32x32.png",
"index.css", "index.css",
@ -35,7 +36,7 @@ var (
} }
// Directories excluded from the not found handler proceeding to the next() handler. // Directories excluded from the not found handler proceeding to the next() handler.
httpServerDirs = []struct { dirsHTTPServer = []struct {
name, prefix string name, prefix string
}{ }{
{name: "/api", prefix: "/api/"}, {name: "/api", prefix: "/api/"},

View File

@ -79,8 +79,8 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) {
path := strings.ToLower(string(ctx.Path())) path := strings.ToLower(string(ctx.Path()))
for i := 0; i < len(httpServerDirs); i++ { for i := 0; i < len(dirsHTTPServer); i++ {
if path == httpServerDirs[i].name || strings.HasPrefix(path, httpServerDirs[i].prefix) { if path == dirsHTTPServer[i].name || strings.HasPrefix(path, dirsHTTPServer[i].prefix) {
handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound) handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound)
return return
@ -104,9 +104,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
https := config.Server.TLS.Key != "" && config.Server.TLS.Certificate != "" 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) serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, 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) serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, 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) serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
handlerPublicHTML := newPublicHTMLEmbeddedHandler() handlerPublicHTML := newPublicHTMLEmbeddedHandler()
handlerLocales := newLocalesEmbeddedHandler() handlerLocales := newLocalesEmbeddedHandler()
@ -124,7 +124,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
// Static Assets. // Static Assets.
r.GET("/", middleware(serveIndexHandler)) r.GET("/", middleware(serveIndexHandler))
for _, f := range rootFiles { for _, f := range filesRoot {
r.GET("/"+f, handlerPublicHTML) r.GET("/"+f, handlerPublicHTML)
} }
@ -139,10 +139,10 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
// Swagger. // Swagger.
r.GET("/api/", middleware(serveSwaggerHandler)) r.GET("/api/", middleware(serveSwaggerHandler))
r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS) r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS)
r.GET("/api/"+apiFile, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler))) r.GET("/api/"+fileOpenAPI, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler)))
r.OPTIONS("/api/"+apiFile, policyCORSPublicGET.HandleOPTIONS) r.OPTIONS("/api/"+fileOpenAPI, policyCORSPublicGET.HandleOPTIONS)
for _, file := range swaggerFiles { for _, file := range filesSwagger {
r.GET("/api/"+file, handlerPublicHTML) r.GET("/api/"+file, handlerPublicHTML)
} }

View File

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

View File

@ -4,10 +4,13 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"text/template" "text/template"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils" "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 { func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, session, theme string, https bool) middlewares.RequestHandler {
logger := logging.Logger() logger := logging.Logger()
a, err := assets.Open(publicDir + file) a, err := assets.Open(path.Join(publicDir, file))
if err != nil { if err != nil {
logger.Fatalf("Unable to open %s: %s", file, err) logger.Fatalf("Unable to open %s: %s", file, err)
} }
@ -43,7 +46,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
logoOverride := f logoOverride := f
if assetPath != "" { if assetPath != "" {
if _, err := os.Stat(filepath.Join(assetPath, logoFile)); err == nil { if _, err := os.Stat(filepath.Join(assetPath, fileLogo)); err == nil {
logoOverride = t logoOverride = t
} }
} }
@ -71,14 +74,14 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
} }
switch { switch {
case publicDir == swaggerAssets: case publicDir == assetsSwagger:
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)) 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 != "": 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: 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: 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}) 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})

View File

@ -18,9 +18,9 @@ i18n.use(Backend)
backend: { backend: {
loadPath: basePath + "/locales/{{lng}}/{{ns}}.json", loadPath: basePath + "/locales/{{lng}}/{{ns}}.json",
}, },
load: "all",
ns: ["portal"], ns: ["portal"],
defaultNS: "portal", defaultNS: "portal",
load: "all",
fallbackLng: { fallbackLng: {
default: ["en"], default: ["en"],
de: ["en"], de: ["en"],
@ -33,7 +33,7 @@ i18n.use(Backend)
"sv-SE": ["sv", "en"], "sv-SE": ["sv", "en"],
zh: ["en"], zh: ["en"],
"zh-CN": ["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"], supportedLngs: ["en", "de", "es", "fr", "nl", "pt", "ru", "sv", "sv-SE", "zh", "zh-CN", "zh-TW"],
lowerCaseLng: false, lowerCaseLng: false,