372 lines
11 KiB
Go
372 lines
11 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1" //nolint:gosec
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/valyala/fasthttp"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
|
"github.com/authelia/authelia/v4/internal/random"
|
|
"github.com/authelia/authelia/v4/internal/session"
|
|
"github.com/authelia/authelia/v4/internal/templates"
|
|
)
|
|
|
|
// 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(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler {
|
|
isDevEnvironment := os.Getenv(environment) == dev
|
|
ext := path.Ext(t.Name())
|
|
|
|
return func(ctx *middlewares.AutheliaCtx) {
|
|
var err error
|
|
|
|
logoOverride := strFalse
|
|
|
|
if opts.AssetPath != "" {
|
|
if _, err = os.Stat(filepath.Join(opts.AssetPath, fileLogo)); err == nil {
|
|
logoOverride = strTrue
|
|
}
|
|
}
|
|
|
|
switch ext {
|
|
case extHTML:
|
|
ctx.SetContentTypeTextHTML()
|
|
case extJSON:
|
|
ctx.SetContentTypeApplicationJSON()
|
|
default:
|
|
ctx.SetContentTypeTextPlain()
|
|
}
|
|
|
|
nonce := ctx.Providers.Random.StringCustom(32, random.CharSetAlphaNumeric)
|
|
|
|
switch {
|
|
case ctx.Configuration.Server.Headers.CSPTemplate != "":
|
|
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, placeholderCSPNonce, nonce))
|
|
case isDevEnvironment:
|
|
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDevelopment, nonce))
|
|
default:
|
|
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce))
|
|
}
|
|
|
|
var (
|
|
rememberMe string
|
|
provider *session.Session
|
|
)
|
|
|
|
if provider, err = ctx.GetSessionProvider(); err == nil {
|
|
rememberMe = strconv.FormatBool(!provider.Config.DisableRememberMe)
|
|
}
|
|
|
|
data := &bytes.Buffer{}
|
|
|
|
if err = t.Execute(data, opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride, rememberMe)); err != nil {
|
|
ctx.RequestCtx.Error("an error occurred", fasthttp.StatusServiceUnavailable)
|
|
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
|
|
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsHead():
|
|
ctx.Response.ResetBody()
|
|
ctx.Response.SkipBody = true
|
|
ctx.Response.Header.Set(fasthttp.HeaderContentLength, strconv.Itoa(data.Len()))
|
|
default:
|
|
if _, err = data.WriteTo(ctx.Response.BodyWriter()); err != nil {
|
|
ctx.RequestCtx.Error("an error occurred", fasthttp.StatusServiceUnavailable)
|
|
ctx.Logger.WithError(err).Errorf("Error occcurred writing body")
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ServeTemplatedOpenAPI serves templated OpenAPI related files.
|
|
func ServeTemplatedOpenAPI(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler {
|
|
ext := path.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 = ctx.Providers.Random.StringCustom(32, random.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
|
|
|
|
data := &bytes.Buffer{}
|
|
|
|
if err = t.Execute(data, opts.OpenAPIData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce)); err != nil {
|
|
ctx.RequestCtx.Error("an error occurred", fasthttp.StatusServiceUnavailable)
|
|
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
|
|
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsHead():
|
|
ctx.Response.ResetBody()
|
|
ctx.Response.SkipBody = true
|
|
ctx.Response.Header.Set(fasthttp.HeaderContentLength, strconv.Itoa(data.Len()))
|
|
default:
|
|
if _, err = data.WriteTo(ctx.Response.BodyWriter()); err != nil {
|
|
ctx.RequestCtx.Error("an error occurred", fasthttp.StatusServiceUnavailable)
|
|
ctx.Logger.WithError(err).Errorf("Error occcurred writing body")
|
|
|
|
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)
|
|
|
|
if ctx.Response.SkipBody || ctx.Response.StatusCode() != fasthttp.StatusOK {
|
|
// Skip generating the ETag as the response body should be empty.
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
_, err = os.Stat("/app/healthcheck.sh")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
_, err = os.Stat("/app/.healthcheck.env")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
file, err := os.OpenFile("/app/.healthcheck.env", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
_ = file.Close()
|
|
}()
|
|
|
|
if host == "0.0.0.0" {
|
|
host = localhost
|
|
} else if strings.Contains(host, ":") {
|
|
host = "[" + host + "]"
|
|
}
|
|
|
|
_, err = file.WriteString(fmt.Sprintf(healthCheckEnv, scheme, host, port, path))
|
|
|
|
return err
|
|
}
|
|
|
|
// NewTemplatedFileOptions returns a new *TemplatedFileOptions.
|
|
func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileOptions) {
|
|
opts = &TemplatedFileOptions{
|
|
AssetPath: config.Server.AssetPath,
|
|
DuoSelfEnrollment: strFalse,
|
|
RememberMe: strconv.FormatBool(!config.Session.DisableRememberMe),
|
|
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),
|
|
EndpointsAuthz: config.Server.Endpoints.Authz,
|
|
}
|
|
|
|
if config.PrivacyPolicy.Enabled {
|
|
opts.PrivacyPolicyURL = config.PrivacyPolicy.PolicyURL.String()
|
|
opts.PrivacyPolicyAccept = strconv.FormatBool(config.PrivacyPolicy.RequireUserAcceptance)
|
|
}
|
|
|
|
if !config.DuoAPI.Disable {
|
|
opts.DuoSelfEnrollment = strconv.FormatBool(config.DuoAPI.EnableSelfEnrollment)
|
|
}
|
|
|
|
return opts
|
|
}
|
|
|
|
// TemplatedFileOptions is a struct which is used for many templated files.
|
|
type TemplatedFileOptions struct {
|
|
AssetPath string
|
|
DuoSelfEnrollment string
|
|
RememberMe string
|
|
ResetPassword string
|
|
ResetPasswordCustomURL string
|
|
PrivacyPolicyURL string
|
|
PrivacyPolicyAccept string
|
|
Session string
|
|
Theme string
|
|
|
|
EndpointsPasswordReset bool
|
|
EndpointsWebauthn bool
|
|
EndpointsTOTP bool
|
|
EndpointsDuo bool
|
|
EndpointsOpenIDConnect bool
|
|
|
|
EndpointsAuthz map[string]schema.ServerAuthzEndpoint
|
|
}
|
|
|
|
// CommonData returns a TemplatedFileCommonData with the dynamic options.
|
|
func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData {
|
|
if rememberMe != "" {
|
|
return options.commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe)
|
|
}
|
|
|
|
return TemplatedFileCommonData{
|
|
Base: base,
|
|
BaseURL: baseURL,
|
|
CSPNonce: nonce,
|
|
LogoOverride: logoOverride,
|
|
DuoSelfEnrollment: options.DuoSelfEnrollment,
|
|
RememberMe: options.RememberMe,
|
|
ResetPassword: options.ResetPassword,
|
|
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
|
PrivacyPolicyURL: options.PrivacyPolicyURL,
|
|
PrivacyPolicyAccept: options.PrivacyPolicyAccept,
|
|
Session: options.Session,
|
|
Theme: options.Theme,
|
|
}
|
|
}
|
|
|
|
// CommonDataWithRememberMe returns a TemplatedFileCommonData with the dynamic options.
|
|
func (options *TemplatedFileOptions) commonDataWithRememberMe(base, baseURL, nonce, logoOverride, rememberMe string) TemplatedFileCommonData {
|
|
return TemplatedFileCommonData{
|
|
Base: base,
|
|
BaseURL: baseURL,
|
|
CSPNonce: nonce,
|
|
LogoOverride: logoOverride,
|
|
DuoSelfEnrollment: options.DuoSelfEnrollment,
|
|
RememberMe: rememberMe,
|
|
ResetPassword: options.ResetPassword,
|
|
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
|
Session: options.Session,
|
|
Theme: options.Theme,
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
EndpointsAuthz: options.EndpointsAuthz,
|
|
}
|
|
}
|
|
|
|
// TemplatedFileCommonData is a struct which is used for many templated files.
|
|
type TemplatedFileCommonData struct {
|
|
Base string
|
|
BaseURL string
|
|
CSPNonce string
|
|
LogoOverride string
|
|
DuoSelfEnrollment string
|
|
RememberMe string
|
|
ResetPassword string
|
|
ResetPasswordCustomURL string
|
|
PrivacyPolicyURL string
|
|
PrivacyPolicyAccept string
|
|
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
|
|
|
|
EndpointsAuthz map[string]schema.ServerAuthzEndpoint
|
|
}
|