mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
control plane: add request id to all error pages (#2149)
* controlplane: add request id to all error pages - use a single http error handler for both envoy and go control plane - add http lib style status text for our custom statuses. Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
91c7dc742f
commit
9215833a0b
7 changed files with 126 additions and 136 deletions
|
@ -1,12 +1,13 @@
|
||||||
package authorize
|
package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
|
|
||||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
||||||
|
@ -18,6 +19,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
"github.com/pomerium/pomerium/internal/telemetry/requestid"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,79 +43,51 @@ func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v3.C
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) deniedResponse(
|
func (a *Authorize) deniedResponse(
|
||||||
|
ctx context.Context,
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
code int32, reason string, headers map[string]string,
|
code int32, reason string, headers map[string]string,
|
||||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
returnHTMLError := true
|
|
||||||
inHeaders := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
|
||||||
if inHeaders != nil {
|
|
||||||
returnHTMLError = strings.Contains(inHeaders["accept"], "text/html")
|
|
||||||
}
|
|
||||||
|
|
||||||
if returnHTMLError {
|
|
||||||
return a.htmlDeniedResponse(in, code, reason, headers)
|
|
||||||
}
|
|
||||||
return a.plainTextDeniedResponse(code, reason, headers), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Authorize) htmlDeniedResponse(
|
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
|
||||||
code int32, reason string, headers map[string]string,
|
|
||||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
|
||||||
opts := a.currentOptions.Load()
|
|
||||||
authenticateURL, err := opts.GetAuthenticateURL()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
debugEndpoint := authenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/"})
|
|
||||||
|
|
||||||
// create go-style http request
|
|
||||||
r := getHTTPRequestFromCheckRequest(in)
|
|
||||||
redirectURL := urlutil.GetAbsoluteURL(r).String()
|
|
||||||
if ref := r.Header.Get(httputil.HeaderReferrer); ref != "" {
|
|
||||||
redirectURL = ref
|
|
||||||
}
|
|
||||||
|
|
||||||
debugEndpoint = debugEndpoint.ResolveReference(&url.URL{
|
|
||||||
RawQuery: url.Values{
|
|
||||||
urlutil.QueryRedirectURI: {redirectURL},
|
|
||||||
}.Encode(),
|
|
||||||
})
|
|
||||||
|
|
||||||
debugEndpoint = urlutil.NewSignedURL(a.state.Load().sharedKey, debugEndpoint).Sign()
|
|
||||||
|
|
||||||
var details string
|
var details string
|
||||||
switch code {
|
switch code {
|
||||||
case httputil.StatusInvalidClientCertificate:
|
case httputil.StatusInvalidClientCertificate:
|
||||||
details = "a valid client certificate is required to access this page"
|
details = httputil.StatusText(httputil.StatusInvalidClientCertificate)
|
||||||
case http.StatusForbidden:
|
case http.StatusForbidden:
|
||||||
details = "access to this page is forbidden"
|
details = http.StatusText(http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
details = reason
|
details = reason
|
||||||
}
|
}
|
||||||
|
|
||||||
if reason == "" {
|
// create a http response writer recorder
|
||||||
reason = http.StatusText(int(code))
|
w := httptest.NewRecorder()
|
||||||
}
|
r := getHTTPRequestFromCheckRequest(in)
|
||||||
|
|
||||||
var buf bytes.Buffer
|
// build the user info / debug endpoint
|
||||||
err = a.templates.ExecuteTemplate(&buf, "error.html", map[string]interface{}{
|
debugEndpoint, _ := a.userInfoEndpointURL(in) // if there's an error, we just wont display it
|
||||||
"Status": code,
|
|
||||||
"StatusText": reason,
|
// run the request through our go error handler
|
||||||
"CanDebug": code/100 == 4,
|
httpErr := httputil.HTTPError{
|
||||||
"DebugURL": debugEndpoint,
|
Status: int(code),
|
||||||
"Error": details,
|
Err: errors.New(details),
|
||||||
})
|
DebugURL: debugEndpoint,
|
||||||
|
RequestID: requestid.FromContext(ctx),
|
||||||
|
}
|
||||||
|
httpErr.ErrorResponse(w, r)
|
||||||
|
|
||||||
|
// transpose the go http response writer into a envoy response
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
buf.WriteString(reason)
|
log.Error(ctx).Err(err).Msg("error executing error template")
|
||||||
log.Error(context.TODO()).Err(err).Msg("error executing error template")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// convert go headers to envoy headers
|
||||||
|
respHeader := toEnvoyHeaders(resp.Header)
|
||||||
|
|
||||||
envoyHeaders := []*envoy_config_core_v3.HeaderValueOption{
|
// add any additional headers
|
||||||
mkHeader("Content-Type", "text/html", false),
|
|
||||||
}
|
|
||||||
for k, v := range headers {
|
for k, v := range headers {
|
||||||
envoyHeaders = append(envoyHeaders, mkHeader(k, v, false))
|
respHeader = append(respHeader, mkHeader(k, v, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &envoy_service_auth_v3.CheckResponse{
|
return &envoy_service_auth_v3.CheckResponse{
|
||||||
|
@ -123,36 +97,14 @@ func (a *Authorize) htmlDeniedResponse(
|
||||||
Status: &envoy_type_v3.HttpStatus{
|
Status: &envoy_type_v3.HttpStatus{
|
||||||
Code: envoy_type_v3.StatusCode(code),
|
Code: envoy_type_v3.StatusCode(code),
|
||||||
},
|
},
|
||||||
Headers: envoyHeaders,
|
Headers: respHeader,
|
||||||
Body: buf.String(),
|
Body: string(respBody),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) plainTextDeniedResponse(code int32, reason string, headers map[string]string) *envoy_service_auth_v3.CheckResponse {
|
func (a *Authorize) redirectResponse(ctx context.Context, in *envoy_service_auth_v3.CheckRequest) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
envoyHeaders := []*envoy_config_core_v3.HeaderValueOption{
|
|
||||||
mkHeader("Content-Type", "text/plain", false),
|
|
||||||
}
|
|
||||||
for k, v := range headers {
|
|
||||||
envoyHeaders = append(envoyHeaders, mkHeader(k, v, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &envoy_service_auth_v3.CheckResponse{
|
|
||||||
Status: &status.Status{Code: int32(codes.PermissionDenied), Message: "Access Denied"},
|
|
||||||
HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{
|
|
||||||
DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{
|
|
||||||
Status: &envoy_type_v3.HttpStatus{
|
|
||||||
Code: envoy_type_v3.StatusCode(code),
|
|
||||||
},
|
|
||||||
Headers: envoyHeaders,
|
|
||||||
Body: reason,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Authorize) redirectResponse(in *envoy_service_auth_v3.CheckRequest) (*envoy_service_auth_v3.CheckResponse, error) {
|
|
||||||
opts := a.currentOptions.Load()
|
opts := a.currentOptions.Load()
|
||||||
state := a.state.Load()
|
state := a.state.Load()
|
||||||
authenticateURL, err := opts.GetAuthenticateURL()
|
authenticateURL, err := opts.GetAuthenticateURL()
|
||||||
|
@ -173,7 +125,7 @@ func (a *Authorize) redirectResponse(in *envoy_service_auth_v3.CheckRequest) (*e
|
||||||
signinURL.RawQuery = q.Encode()
|
signinURL.RawQuery = q.Encode()
|
||||||
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String()
|
||||||
|
|
||||||
return a.deniedResponse(in, http.StatusFound, "Login", map[string]string{
|
return a.deniedResponse(ctx, in, http.StatusFound, "Login", map[string]string{
|
||||||
"Location": redirectTo,
|
"Location": redirectTo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -189,3 +141,42 @@ func mkHeader(k, v string, shouldAppend bool) *envoy_config_core_v3.HeaderValueO
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toEnvoyHeaders(headers http.Header) []*envoy_config_core_v3.HeaderValueOption {
|
||||||
|
var ks []string
|
||||||
|
for k := range headers {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
|
||||||
|
envoyHeaders := make([]*envoy_config_core_v3.HeaderValueOption, 0, len(headers))
|
||||||
|
for _, k := range ks {
|
||||||
|
envoyHeaders = append(envoyHeaders, mkHeader(k, headers.Get(k), false))
|
||||||
|
}
|
||||||
|
return envoyHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
// userInfoEndpointURL returns the user info endpoint url which can be used to debug the user's
|
||||||
|
// session that lives on the authenticate service.
|
||||||
|
func (a *Authorize) userInfoEndpointURL(in *envoy_service_auth_v3.CheckRequest) (*url.URL, error) {
|
||||||
|
opts := a.currentOptions.Load()
|
||||||
|
authenticateURL, err := opts.GetAuthenticateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
debugEndpoint := authenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/"})
|
||||||
|
|
||||||
|
r := getHTTPRequestFromCheckRequest(in)
|
||||||
|
redirectURL := urlutil.GetAbsoluteURL(r).String()
|
||||||
|
if ref := r.Header.Get(httputil.HeaderReferrer); ref != "" {
|
||||||
|
redirectURL = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
debugEndpoint = debugEndpoint.ResolveReference(&url.URL{
|
||||||
|
RawQuery: url.Values{
|
||||||
|
urlutil.QueryRedirectURI: {redirectURL},
|
||||||
|
}.Encode(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return urlutil.NewSignedURL(a.state.Load().sharedKey, debugEndpoint).Sign(), nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package authorize
|
package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -151,36 +152,8 @@ func TestAuthorize_deniedResponse(t *testing.T) {
|
||||||
Code: envoy_type_v3.StatusCode(codes.InvalidArgument),
|
Code: envoy_type_v3.StatusCode(codes.InvalidArgument),
|
||||||
},
|
},
|
||||||
Headers: []*envoy_config_core_v3.HeaderValueOption{
|
Headers: []*envoy_config_core_v3.HeaderValueOption{
|
||||||
mkHeader("Content-Type", "text/html", false),
|
mkHeader("Content-Type", "text/html; charset=UTF-8", false),
|
||||||
},
|
mkHeader("X-Pomerium-Intercepted-Response", "true", false),
|
||||||
Body: "Access Denied",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"plain text denied",
|
|
||||||
&envoy_service_auth_v3.CheckRequest{
|
|
||||||
Attributes: &envoy_service_auth_v3.AttributeContext{
|
|
||||||
Request: &envoy_service_auth_v3.AttributeContext_Request{
|
|
||||||
Http: &envoy_service_auth_v3.AttributeContext_HttpRequest{
|
|
||||||
Headers: map[string]string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
http.StatusBadRequest,
|
|
||||||
"Access Denied",
|
|
||||||
map[string]string{},
|
|
||||||
&envoy_service_auth_v3.CheckResponse{
|
|
||||||
Status: &status.Status{Code: int32(codes.PermissionDenied), Message: "Access Denied"},
|
|
||||||
HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{
|
|
||||||
DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{
|
|
||||||
Status: &envoy_type_v3.HttpStatus{
|
|
||||||
Code: envoy_type_v3.StatusCode(codes.InvalidArgument),
|
|
||||||
},
|
|
||||||
Headers: []*envoy_config_core_v3.HeaderValueOption{
|
|
||||||
mkHeader("Content-Type", "text/plain", false),
|
|
||||||
},
|
},
|
||||||
Body: "Access Denied",
|
Body: "Access Denied",
|
||||||
},
|
},
|
||||||
|
@ -192,7 +165,7 @@ func TestAuthorize_deniedResponse(t *testing.T) {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
got, err := a.deniedResponse(tc.in, tc.code, tc.reason, tc.headers)
|
got, err := a.deniedResponse(context.TODO(), tc.in, tc.code, tc.reason, tc.headers)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tc.want.Status.Code, got.Status.Code)
|
assert.Equal(t, tc.want.Status.Code, got.Status.Code)
|
||||||
assert.Equal(t, tc.want.Status.Message, got.Status.Message)
|
assert.Equal(t, tc.want.Status.Message, got.Status.Message)
|
||||||
|
|
|
@ -70,11 +70,11 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe
|
||||||
return a.okResponse(reply), nil
|
return a.okResponse(reply), nil
|
||||||
case reply.Status == http.StatusUnauthorized:
|
case reply.Status == http.StatusUnauthorized:
|
||||||
if isForwardAuth && hreq.URL.Path == "/verify" {
|
if isForwardAuth && hreq.URL.Path == "/verify" {
|
||||||
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil)
|
return a.deniedResponse(ctx, in, http.StatusUnauthorized, "Unauthenticated", nil)
|
||||||
}
|
}
|
||||||
return a.redirectResponse(in)
|
return a.redirectResponse(ctx, in)
|
||||||
}
|
}
|
||||||
return a.deniedResponse(in, int32(reply.Status), reply.Message, nil)
|
return a.deniedResponse(ctx, in, int32(reply.Status), reply.Message, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getForwardAuthURL(r *http.Request) *url.URL {
|
func getForwardAuthURL(r *http.Request) *url.URL {
|
||||||
|
|
|
@ -24,8 +24,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="category-link">
|
<div class="category-link">
|
||||||
{{if and .CanDebug .DebugURL }}
|
{{if .CanDebug}}
|
||||||
If you should have access, contact your administrator with your <a href="{{.DebugURL}}">session details</a>.
|
If you should have access, contact your administrator with your
|
||||||
|
{{if .RequestID }}
|
||||||
|
request id {{.RequestID}}
|
||||||
|
{{end}}
|
||||||
|
{{if and .RequestID .DebugURL }}
|
||||||
|
and
|
||||||
|
{{end}}
|
||||||
|
{{if .DebugURL }}
|
||||||
|
<a href="{{.DebugURL}}">session details</a>.
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,9 +3,9 @@ package httputil
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/frontend"
|
"github.com/pomerium/pomerium/internal/frontend"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/requestid"
|
"github.com/pomerium/pomerium/internal/telemetry/requestid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,8 +15,12 @@ var errorTemplate = template.Must(frontend.NewTemplates())
|
||||||
type HTTPError struct {
|
type HTTPError struct {
|
||||||
// HTTP status codes as registered with IANA.
|
// HTTP status codes as registered with IANA.
|
||||||
Status int
|
Status int
|
||||||
// Err is the wrapped error
|
// Err is the wrapped error.
|
||||||
Err error
|
Err error
|
||||||
|
// DebugURL is the URL to the debug endpoint.
|
||||||
|
DebugURL *url.URL
|
||||||
|
// The request ID.
|
||||||
|
RequestID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewError returns an error that contains a HTTP status and error.
|
// NewError returns an error that contains a HTTP status and error.
|
||||||
|
@ -36,32 +40,35 @@ func (e *HTTPError) Unwrap() error { return e.Err }
|
||||||
// It does not otherwise end the request; the caller should ensure no further
|
// It does not otherwise end the request; the caller should ensure no further
|
||||||
// writes are done to w.
|
// writes are done to w.
|
||||||
func (e *HTTPError) ErrorResponse(w http.ResponseWriter, r *http.Request) {
|
func (e *HTTPError) ErrorResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
// indicate to clients that the error originates from Pomerium, not the app
|
reqID := e.RequestID
|
||||||
w.Header().Set(HeaderPomeriumResponse, "true")
|
if e.RequestID == "" {
|
||||||
w.WriteHeader(e.Status)
|
// if empty, try to grab from the request id from the request context
|
||||||
|
reqID = requestid.FromContext(r.Context())
|
||||||
log.FromRequest(r).Info().Err(e).Msg("httputil: ErrorResponse")
|
}
|
||||||
requestID := requestid.FromContext(r.Context())
|
|
||||||
|
|
||||||
response := struct {
|
response := struct {
|
||||||
Status int
|
Status int
|
||||||
Error string
|
Error string
|
||||||
StatusText string `json:"-"`
|
StatusText string `json:"-"`
|
||||||
RequestID string `json:",omitempty"`
|
RequestID string `json:",omitempty"`
|
||||||
CanDebug bool `json:"-"`
|
CanDebug bool `json:"-"`
|
||||||
Version string `json:"-"`
|
Version string `json:"-"`
|
||||||
|
DebugURL *url.URL `json:",omitempty"`
|
||||||
}{
|
}{
|
||||||
Status: e.Status,
|
Status: e.Status,
|
||||||
StatusText: http.StatusText(e.Status),
|
StatusText: http.StatusText(e.Status),
|
||||||
Error: e.Error(),
|
Error: e.Error(),
|
||||||
RequestID: requestID,
|
RequestID: reqID,
|
||||||
CanDebug: e.Status/100 == 4,
|
CanDebug: e.Status/100 == 4 && (e.DebugURL != nil || reqID != ""),
|
||||||
|
DebugURL: e.DebugURL,
|
||||||
}
|
}
|
||||||
|
// indicate to clients that the error originates from Pomerium, not the app
|
||||||
|
w.Header().Set(HeaderPomeriumResponse, "true")
|
||||||
|
|
||||||
if r.Header.Get("Accept") == "application/json" {
|
if r.Header.Get("Accept") == "application/json" {
|
||||||
RenderJSON(w, e.Status, response)
|
RenderJSON(w, e.Status, response)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||||
|
w.WriteHeader(e.Status)
|
||||||
errorTemplate.ExecuteTemplate(w, "error.html", response)
|
errorTemplate.ExecuteTemplate(w, "error.html", response)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := f(w, r); err != nil {
|
if err := f(w, r); err != nil {
|
||||||
var e *HTTPError
|
var e *HTTPError
|
||||||
if !errors.As(err, &e) {
|
if !errors.As(err, &e) {
|
||||||
e = &HTTPError{http.StatusInternalServerError, err}
|
e = &HTTPError{Status: http.StatusInternalServerError, Err: err}
|
||||||
}
|
}
|
||||||
e.ErrorResponse(w, r)
|
e.ErrorResponse(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,13 @@ package httputil
|
||||||
// client's certificate is invalid. This is the same status code used
|
// client's certificate is invalid. This is the same status code used
|
||||||
// by nginx for this purpose.
|
// by nginx for this purpose.
|
||||||
const StatusInvalidClientCertificate = 495
|
const StatusInvalidClientCertificate = 495
|
||||||
|
|
||||||
|
var statusText = map[int]string{
|
||||||
|
StatusInvalidClientCertificate: "a valid client certificate is required to access this page",
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusText returns a text for the HTTP status code. It returns the empty
|
||||||
|
// string if the code is unknown.
|
||||||
|
func StatusText(code int) string {
|
||||||
|
return statusText[code]
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue