diff options
Diffstat (limited to 'internal/server/template.go')
| -rw-r--r-- | internal/server/template.go | 170 |
1 files changed, 140 insertions, 30 deletions
diff --git a/internal/server/template.go b/internal/server/template.go index d86841005..cecd9d2f1 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -1,56 +1,43 @@ package server import ( + "bytes" + "crypto/sha1" //nolint:gosec + "encoding/hex" "fmt" - "io" "os" - "path" "path/filepath" "strconv" "strings" - "text/template" + "sync" "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/configuration/schema" - "github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/middlewares" + "github.com/authelia/authelia/v4/internal/templates" "github.com/authelia/authelia/v4/internal/utils" ) // ServeTemplatedFile serves a templated version of a specified file, // this is utilised to pass information between the backend and frontend // and generate a nonce to support a restrictive CSP while using material-ui. -func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) middlewares.RequestHandler { - logger := logging.Logger() - - a, err := assets.Open(path.Join(publicDir, file)) - if err != nil { - logger.Fatalf("Unable to open %s: %s", file, err) - } - - b, err := io.ReadAll(a) - if err != nil { - logger.Fatalf("Unable to read %s: %s", file, err) - } - - tmpl, err := template.New("file").Parse(string(b)) - if err != nil { - logger.Fatalf("Unable to parse %s template: %s", file, err) - } - +func ServeTemplatedFile(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler { isDevEnvironment := os.Getenv(environment) == dev + ext := filepath.Ext(t.Name()) return func(ctx *middlewares.AutheliaCtx) { - logoOverride := f + var err error + + logoOverride := strFalse if opts.AssetPath != "" { if _, err = os.Stat(filepath.Join(opts.AssetPath, fileLogo)); err == nil { - logoOverride = t + logoOverride = strTrue } } - switch extension := filepath.Ext(file); extension { + switch ext { case extHTML: ctx.SetContentTypeTextHTML() case extJSON: @@ -62,8 +49,6 @@ func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) midd nonce := utils.RandomString(32, utils.CharSetAlphaNumeric) switch { - case publicDir == assetsSwagger: - ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwagger, nonce, nonce)) case ctx.Configuration.Server.Headers.CSPTemplate != "": ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, placeholderCSPNonce, nonce)) case isDevEnvironment: @@ -72,15 +57,99 @@ func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) midd ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce)) } - if err = tmpl.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil { + if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil { + ctx.RequestCtx.Error("an error occurred", 503) + ctx.Logger.WithError(err).Errorf("Error occcurred rendering template") + + return + } + } +} + +// ServeTemplatedOpenAPI serves templated OpenAPI related files. +func ServeTemplatedOpenAPI(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler { + ext := filepath.Ext(t.Name()) + + spec := ext == extYML + + return func(ctx *middlewares.AutheliaCtx) { + var nonce string + + if spec { + ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, tmplCSPSwagger) + } else { + nonce = utils.RandomString(32, utils.CharSetAlphaNumeric) + ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwaggerNonce, nonce, nonce)) + } + + switch ext { + case extHTML: + ctx.SetContentTypeTextHTML() + case extYML: + ctx.SetContentTypeApplicationYAML() + default: + ctx.SetContentTypeTextPlain() + } + + var err error + + if err = t.Execute(ctx.Response.BodyWriter(), opts.OpenAPIData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce)); err != nil { ctx.RequestCtx.Error("an error occurred", 503) - logger.Errorf("Unable to execute template: %v", err) + ctx.Logger.WithError(err).Errorf("Error occcurred rendering template") return } } } +// ETagRootURL dynamically matches the If-None-Match header and adds the ETag header. +func ETagRootURL(next middlewares.RequestHandler) middlewares.RequestHandler { + etags := map[string][]byte{} + + h := sha1.New() //nolint:gosec // Usage is for collision avoidance not security. + mu := &sync.Mutex{} + + return func(ctx *middlewares.AutheliaCtx) { + k := ctx.RootURLSlash().String() + + mu.Lock() + + etag, ok := etags[k] + + mu.Unlock() + + if ok && bytes.Equal(etag, ctx.Request.Header.PeekBytes(headerIfNoneMatch)) { + ctx.Response.Header.SetBytesKV(headerETag, etag) + ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets) + + ctx.SetStatusCode(fasthttp.StatusNotModified) + + return + } + + next(ctx) + + mu.Lock() + + h.Write(ctx.Response.Body()) + sum := h.Sum(nil) + h.Reset() + + etagNew := make([]byte, hex.EncodedLen(len(sum))) + + hex.Encode(etagNew, sum) + + if !ok || !bytes.Equal(etag, etagNew) { + etags[k] = etagNew + } + + mu.Unlock() + + ctx.Response.Header.SetBytesKV(headerETag, etagNew) + ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets) + } +} + func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (err error) { if disabled { return nil @@ -120,11 +189,17 @@ func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (er func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileOptions) { opts = &TemplatedFileOptions{ AssetPath: config.Server.AssetPath, - DuoSelfEnrollment: f, + DuoSelfEnrollment: strFalse, RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled), ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable), ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(), Theme: config.Theme, + + EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""), + EndpointsWebauthn: !config.Webauthn.Disable, + EndpointsTOTP: !config.TOTP.Disable, + EndpointsDuo: !config.DuoAPI.Disable, + EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil), } if !config.DuoAPI.Disable { @@ -143,6 +218,12 @@ type TemplatedFileOptions struct { ResetPasswordCustomURL string Session string Theme string + + EndpointsPasswordReset bool + EndpointsWebauthn bool + EndpointsTOTP bool + EndpointsDuo bool + EndpointsOpenIDConnect bool } // CommonData returns a TemplatedFileCommonData with the dynamic options. @@ -161,6 +242,22 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri } } +// OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options. +func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData { + return TemplatedFileOpenAPIData{ + Base: base, + BaseURL: baseURL, + CSPNonce: nonce, + + Session: options.Session, + PasswordReset: options.EndpointsPasswordReset, + Webauthn: options.EndpointsWebauthn, + TOTP: options.EndpointsTOTP, + Duo: options.EndpointsDuo, + OpenIDConnect: options.EndpointsOpenIDConnect, + } +} + // TemplatedFileCommonData is a struct which is used for many templated files. type TemplatedFileCommonData struct { Base string @@ -174,3 +271,16 @@ type TemplatedFileCommonData struct { Session string Theme string } + +// TemplatedFileOpenAPIData is a struct which is used for the OpenAPI spec file. +type TemplatedFileOpenAPIData struct { + Base string + BaseURL string + CSPNonce string + Session string + PasswordReset bool + Webauthn bool + TOTP bool + Duo bool + OpenIDConnect bool +} |
