1package httputil 2 3import ( 4 "encoding/json" 5 "errors" 6 "html/template" 7 "net/http" 8 9 "github.com/pomerium/pomerium/internal/frontend" 10 "github.com/pomerium/pomerium/internal/log" 11 "github.com/pomerium/pomerium/internal/urlutil" 12 "github.com/pomerium/pomerium/internal/version" 13) 14 15// ErrRedirectOnly is the error used when a user's session failed to be refreshed. 16// The user should be redirected to the authorization service, and the original 17// request should not be permitted to hit the upstream service. 18var ErrRedirectOnly = errors.New("httputil: redirecting to authenticate service") 19 20var errorTemplate = template.Must(frontend.NewTemplates()) 21var fullVersion = version.FullVersion() 22 23// HTTPError contains an HTTP status code and wrapped error. 24type HTTPError struct { 25 // HTTP status codes as registered with IANA. 26 Status int 27 // Err is the wrapped error 28 Err error 29} 30 31// NewError returns an error that contains a HTTP status and error. 32func NewError(status int, err error) error { 33 return &HTTPError{Status: status, Err: err} 34} 35 36// Error implements the `error` interface. 37func (e *HTTPError) Error() string { 38 return http.StatusText(e.Status) + ": " + e.Err.Error() 39} 40 41// Unwrap implements the `error` Unwrap interface. 42func (e *HTTPError) Unwrap() error { return e.Err } 43 44// Debugable reports whether this error represents a user debuggable error. 45func (e *HTTPError) Debugable() bool { 46 return e.Status == http.StatusUnauthorized || e.Status == http.StatusForbidden 47} 48 49// RetryURL returns the requests intended destination, if any. 50func (e *HTTPError) RetryURL(r *http.Request) string { 51 return r.FormValue(urlutil.QueryRedirectURI) 52} 53 54type errResponse struct { 55 Status int 56 Error string 57 58 StatusText string `json:"-"` 59 RequestID string `json:",omitempty"` 60 CanDebug bool `json:"-"` 61 RetryURL string `json:"-"` 62 Version string `json:"-"` 63} 64 65// ErrorResponse replies to the request with the specified error message and HTTP code. 66// It does not otherwise end the request; the caller should ensure no further 67// writes are done to w. 68func (e *HTTPError) ErrorResponse(w http.ResponseWriter, r *http.Request) { 69 log.FromRequest(r).Info().Err(e).Msg("httputil: ErrorResponse") 70 if errors.Is(e, ErrRedirectOnly) { 71 return 72 } 73 // indicate to clients that the error originates from Pomerium, not the app 74 w.Header().Set(HeaderPomeriumResponse, "true") 75 w.WriteHeader(e.Status) 76 77 var requestID string 78 if id, ok := log.IDFromRequest(r); ok { 79 requestID = id 80 } 81 response := errResponse{ 82 Status: e.Status, 83 StatusText: http.StatusText(e.Status), 84 Error: e.Error(), 85 RequestID: requestID, 86 CanDebug: e.Debugable(), 87 RetryURL: e.RetryURL(r), 88 Version: fullVersion, 89 } 90 91 if r.Header.Get("Accept") == "application/json" { 92 w.Header().Set("Content-Type", "application/json") 93 err := json.NewEncoder(w).Encode(response) 94 if err != nil { 95 http.Error(w, err.Error(), http.StatusInternalServerError) 96 } 97 } else { 98 w.Header().Set("Content-Type", "text/html; charset=UTF-8") 99 errorTemplate.ExecuteTemplate(w, "error.html", response) 100 } 101} 102