diff --git a/config/envoyconfig/http_connection_manager.go b/config/envoyconfig/http_connection_manager.go index 1b2531c92..2fcce745d 100644 --- a/config/envoyconfig/http_connection_manager.go +++ b/config/envoyconfig/http_connection_manager.go @@ -1,12 +1,16 @@ package envoyconfig import ( + "fmt" + envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/httputil" + "github.com/pomerium/pomerium/ui" ) func (b *Builder) buildVirtualHost( @@ -33,7 +37,7 @@ func (b *Builder) buildVirtualHost( // coming directly from envoy func (b *Builder) buildLocalReplyConfig( options *config.Options, -) *envoy_http_connection_manager.LocalReplyConfig { +) (*envoy_http_connection_manager.LocalReplyConfig, error) { // add global headers for HSTS headers (#2110) var headers []*envoy_config_core_v3.HeaderValueOption // if we're the proxy or authenticate service, add our global headers @@ -41,6 +45,18 @@ func (b *Builder) buildLocalReplyConfig( headers = toEnvoyHeaders(options.GetSetResponseHeaders()) } + data := map[string]any{ + "status": "%RESPONSE_CODE%", + "statusText": "%RESPONSE_CODE_DETAILS%", + "requestId": "%STREAM_ID%", + } + httputil.AddBrandingOptionsToMap(data, options.BrandingOptions) + + bs, err := ui.RenderPage("Error", "Error", data) + if err != nil { + return nil, fmt.Errorf("error rendering error page for local reply: %w", err) + } + return &envoy_http_connection_manager.LocalReplyConfig{ Mappers: []*envoy_http_connection_manager.ResponseMapper{{ Filter: &envoy_config_accesslog_v3.AccessLogFilter{ @@ -48,7 +64,17 @@ func (b *Builder) buildLocalReplyConfig( ResponseFlagFilter: &envoy_config_accesslog_v3.ResponseFlagFilter{}, }, }, + BodyFormatOverride: &envoy_config_core_v3.SubstitutionFormatString{ + ContentType: "text/html; charset=UTF-8", + Format: &envoy_config_core_v3.SubstitutionFormatString_TextFormatSource{ + TextFormatSource: &envoy_config_core_v3.DataSource{ + Specifier: &envoy_config_core_v3.DataSource_InlineBytes{ + InlineBytes: bs, + }, + }, + }, + }, HeadersToAdd: headers, }}, - } + }, nil } diff --git a/config/envoyconfig/listeners.go b/config/envoyconfig/listeners.go index 185a1cfdc..b48a6236a 100644 --- a/config/envoyconfig/listeners.go +++ b/config/envoyconfig/listeners.go @@ -284,6 +284,11 @@ func (b *Builder) buildMainHTTPConnectionManagerFilter( return nil, err } + localReply, err := b.buildLocalReplyConfig(cfg.Options) + if err != nil { + return nil, err + } + mgr := &envoy_http_connection_manager.HttpConnectionManager{ AlwaysSetRequestIdInResponse: true, CodecType: cfg.Options.GetCodecType().ToEnvoy(), @@ -304,7 +309,7 @@ func (b *Builder) buildMainHTTPConnectionManagerFilter( UseRemoteAddress: &wrapperspb.BoolValue{Value: true}, SkipXffAppend: cfg.Options.SkipXffAppend, XffNumTrustedHops: cfg.Options.XffNumTrustedHops, - LocalReplyConfig: b.buildLocalReplyConfig(cfg.Options), + LocalReplyConfig: localReply, NormalizePath: wrapperspb.Bool(true), } diff --git a/config/envoyconfig/testdata/main_http_connection_manager_filter.json b/config/envoyconfig/testdata/main_http_connection_manager_filter.json index 82a78649e..58f5fe961 100644 --- a/config/envoyconfig/testdata/main_http_connection_manager_filter.json +++ b/config/envoyconfig/testdata/main_http_connection_manager_filter.json @@ -52,9 +52,7 @@ }, "timeout": "10s" }, - "metadataContextNamespaces": [ - "com.pomerium.client-certificate-info" - ], + "metadataContextNamespaces": ["com.pomerium.client-certificate-info"], "statusOnError": { "code": "InternalServerError" }, @@ -108,6 +106,12 @@ "localReplyConfig": { "mappers": [ { + "bodyFormatOverride": { + "contentType": "text/html; charset=UTF-8", + "textFormatSource": { + "inlineBytes": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KICA8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04IiAvPgogICAgPGxpbmsgaWQ9ImZhdmljb24iIHJlbD0ic2hvcnRjdXQgaWNvbiIgaHJlZj0iLy5wb21lcml1bS9mYXZpY29uLmljbz92PTIiIC8+CiAgICA8bGluawogICAgICBjbGFzcz0icG9tZXJpdW1fZmF2aWNvbiIKICAgICAgcmVsPSJhcHBsZS10b3VjaC1pY29uIgogICAgICBzaXplcz0iMTgweDE4MCIKICAgICAgaHJlZj0iLy5wb21lcml1bS9hcHBsZS10b3VjaC1pY29uLnBuZyIKICAgIC8+CiAgICA8bGluawogICAgICBjbGFzcz0icG9tZXJpdW1fZmF2aWNvbiIKICAgICAgcmVsPSJpY29uIgogICAgICBzaXplcz0iMzJ4MzIiCiAgICAgIGhyZWY9Ii8ucG9tZXJpdW0vZmF2aWNvbi0zMngzMi5wbmciCiAgICAvPgogICAgPGxpbmsKICAgICAgY2xhc3M9InBvbWVyaXVtX2Zhdmljb24iCiAgICAgIHJlbD0iaWNvbiIKICAgICAgc2l6ZXM9IjE2eDE2IgogICAgICBocmVmPSIvLnBvbWVyaXVtL2Zhdmljb24tMTZ4MTYucG5nIgogICAgLz4KICAgIDxtZXRhCiAgICAgIG5hbWU9InZpZXdwb3J0IgogICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSwgc2hyaW5rLXRvLWZpdD1ubyIKICAgIC8+CiAgICA8dGl0bGU+RXJyb3I8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSIvLnBvbWVyaXVtL2luZGV4LmNzcyIgLz4KICA8L2hlYWQ+CiAgPGJvZHk+CiAgICA8bm9zY3JpcHQ+WW91IG5lZWQgdG8gZW5hYmxlIEphdmFTY3JpcHQgdG8gcnVuIHRoaXMgYXBwLjwvbm9zY3JpcHQ+CiAgICA8ZGl2IGlkPSJyb290Ij48L2Rpdj4KICAgIDxzY3JpcHQ+CiAgICAgIHdpbmRvdy5QT01FUklVTV9EQVRBID0geyJwYWdlIjoiRXJyb3IiLCJyZXF1ZXN0SWQiOiIlU1RSRUFNX0lEJSIsInN0YXR1cyI6IiVSRVNQT05TRV9DT0RFJSIsInN0YXR1c1RleHQiOiIlUkVTUE9OU0VfQ09ERV9ERVRBSUxTJSJ9OwogICAgPC9zY3JpcHQ+CiAgICA8c2NyaXB0IHNyYz0iLy5wb21lcml1bS9pbmRleC5qcyI+PC9zY3JpcHQ+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==" + } + }, "filter": { "responseFlagFilter": {} }, diff --git a/ui/embed.go b/ui/embed.go index e765f41f2..4162c0d21 100644 --- a/ui/embed.go +++ b/ui/embed.go @@ -27,18 +27,27 @@ func ServeFile(w http.ResponseWriter, r *http.Request, filePath string) error { return nil } +// RenderPage rends the index.html page. +func RenderPage(page, title string, data map[string]any) ([]byte, error) { + if data == nil { + data = make(map[string]any) + } + data["page"] = page + + return renderIndex(map[string]any{ + "Title": title, + "Data": data, + }) +} + // ServePage serves the index.html page. -func ServePage(w http.ResponseWriter, r *http.Request, page, title string, data map[string]interface{}) error { +func ServePage(w http.ResponseWriter, r *http.Request, page, title string, data map[string]any) error { if data == nil { data = make(map[string]any) } data["csrfToken"] = csrf.Token(r) - data["page"] = page - bs, err := renderIndex(map[string]any{ - "Title": title, - "Data": data, - }) + bs, err := RenderPage(page, title, data) if err != nil { return err }