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