github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/errors.go (about)

     1  package common
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"net/http"
     7  	"runtime/debug"
     8  	"strings"
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	p "github.com/Azareal/Gosora/common/phrases"
    13  )
    14  
    15  type ErrorItem struct {
    16  	error
    17  	Stack []byte
    18  }
    19  
    20  // ! The errorBuffer uses o(n) memory, we should probably do something about that
    21  // TODO: Use the errorBuffer variable to construct the system log in the Control Panel. Should we log errors caused by users too? Or just collect statistics on those or do nothing? Intercept recover()? Could we intercept the logger instead here? We might get too much information, if we intercept the logger, maybe make it part of the Debug page?
    22  // ? - Should we pass Header / HeaderLite rather than forcing the errors to pull the global Header instance?
    23  var errorBufferMutex sync.RWMutex
    24  //var errorBuffer []ErrorItem
    25  var ErrorCountSinceStartup int64
    26  
    27  //var notfoundCountPerSecond int
    28  //var nopermsCountPerSecond int
    29  
    30  // A blank list to fill out that parameter in Page for routes which don't use it
    31  var tList []interface{}
    32  
    33  // WIP, a new system to propagate errors up from routes
    34  type RouteError interface {
    35  	Type() string
    36  	Error() string
    37  	Cause() string
    38  	JSON() bool
    39  	Handled() bool
    40  
    41  	Wrap(string)
    42  }
    43  
    44  type RouteErrorImpl struct {
    45  	userText string
    46  	sysText  string
    47  	system   bool
    48  	json     bool
    49  	handled  bool
    50  }
    51  
    52  func (err *RouteErrorImpl) Type() string {
    53  	// System errors may contain sensitive information we don't want the user to see
    54  	if err.system {
    55  		return "system"
    56  	}
    57  	return "user"
    58  }
    59  
    60  func (err *RouteErrorImpl) Error() string {
    61  	return err.userText
    62  }
    63  
    64  func (err *RouteErrorImpl) Cause() string {
    65  	if err.sysText == "" {
    66  		return err.Error()
    67  	}
    68  	return err.sysText
    69  }
    70  
    71  // Respond with JSON?
    72  func (err *RouteErrorImpl) JSON() bool {
    73  	return err.json
    74  }
    75  
    76  // Has this error been dealt with elsewhere?
    77  func (err *RouteErrorImpl) Handled() bool {
    78  	return err.handled
    79  }
    80  
    81  // Move the current error into the system error slot and add a new one to the user error slot to show the user
    82  func (err *RouteErrorImpl) Wrap(userErr string) {
    83  	err.sysText = err.userText
    84  	err.userText = userErr
    85  }
    86  
    87  func HandledRouteError() RouteError {
    88  	return &RouteErrorImpl{"", "", false, false, true}
    89  }
    90  
    91  func Error(errmsg string) RouteError {
    92  	return &RouteErrorImpl{errmsg, "", false, false, false}
    93  }
    94  
    95  func FromError(err error) RouteError {
    96  	return &RouteErrorImpl{err.Error(), "", false, false, false}
    97  }
    98  
    99  func ErrorJSQ(errmsg string, js bool) RouteError {
   100  	return &RouteErrorImpl{errmsg, "", false, js, false}
   101  }
   102  
   103  func SysError(errmsg string) RouteError {
   104  	return &RouteErrorImpl{errmsg, errmsg, true, false, false}
   105  }
   106  
   107  // LogError logs internal handler errors which can't be handled with InternalError() as a wrapper for log.Fatal(), we might do more with it in the future.
   108  // TODO: Clean-up extra as a way of passing additional context
   109  func LogError(err error, extra ...string) {
   110  	LogWarning(err, extra...)
   111  	ErrLogger.Fatal("")
   112  }
   113  
   114  func LogWarning(err error, extra ...string) {
   115  	var esb strings.Builder
   116  	for _, extraBit := range extra {
   117  		esb.WriteString(extraBit)
   118  		esb.WriteRune(10)
   119  	}
   120  	if err == nil {
   121  		esb.WriteString("nil error found")
   122  	} else {
   123  		esb.WriteString(err.Error())
   124  	}
   125  	esb.WriteRune(10)
   126  	errmsg := esb.String()
   127  
   128  	errorBufferMutex.Lock()
   129  	defer errorBufferMutex.Unlock()
   130  	stack := debug.Stack() // debug.Stack() can't be executed concurrently, so we'll guard this with a mutex too
   131  	Err(errmsg, string(stack))
   132  	//errorBuffer = append(errorBuffer, ErrorItem{err, stack})
   133  	atomic.AddInt64(&ErrorCountSinceStartup,1)
   134  }
   135  
   136  func errorHeader(w http.ResponseWriter, u *User, title string) *Header {
   137  	h := DefaultHeader(w, u)
   138  	h.Title = title
   139  	h.Zone = "error"
   140  	return h
   141  }
   142  
   143  // TODO: Dump the request?
   144  // InternalError is the main function for handling internal errors, while simultaneously printing out a page for the end-user to let them know that *something* has gone wrong
   145  // ? - Add a user parameter?
   146  // ! Do not call CustomError here or we might get an error loop
   147  func InternalError(err error, w http.ResponseWriter, r *http.Request) RouteError {
   148  	pi := ErrorPage{errorHeader(w, &GuestUser, p.GetErrorPhrase("internal_error_title")), p.GetErrorPhrase("internal_error_body")}
   149  	handleErrorTemplate(w, r, pi, 500)
   150  	LogError(err)
   151  	return HandledRouteError()
   152  }
   153  
   154  // InternalErrorJSQ is the JSON "maybe" version of InternalError which can handle both JSON and normal requests
   155  // ? - Add a user parameter?
   156  func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, js bool) RouteError {
   157  	if !js {
   158  		return InternalError(err, w, r)
   159  	}
   160  	return InternalErrorJS(err, w, r)
   161  }
   162  
   163  // InternalErrorJS is the JSON version of InternalError on routes we know will only be requested via JSON. E.g. An API.
   164  // ? - Add a user parameter?
   165  func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteError {
   166  	w.WriteHeader(500)
   167  	writeJsonError(p.GetErrorPhrase("internal_error_body"), w)
   168  	LogError(err)
   169  	return HandledRouteError()
   170  }
   171  
   172  // When the task system detects if the database is down, some database errors might slip by this
   173  func DatabaseError(w http.ResponseWriter, r *http.Request) RouteError {
   174  	pi := ErrorPage{errorHeader(w, &GuestUser, p.GetErrorPhrase("internal_error_title")), p.GetErrorPhrase("internal_error_body")}
   175  	handleErrorTemplate(w, r, pi, 500)
   176  	return HandledRouteError()
   177  }
   178  
   179  func InternalErrorXML(err error, w http.ResponseWriter, r *http.Request) RouteError {
   180  	w.Header().Set("Content-Type", "application/xml")
   181  	w.WriteHeader(500)
   182  	w.Write([]byte(`<?xml version="1.0"encoding="UTF-8"?>
   183  <error>` + p.GetErrorPhrase("internal_error_body") + `</error>`))
   184  	LogError(err)
   185  	return HandledRouteError()
   186  }
   187  
   188  // TODO: Stop killing the instance upon hitting an error with InternalError* and deprecate this
   189  func SilentInternalErrorXML(err error, w http.ResponseWriter, r *http.Request) RouteError {
   190  	w.Header().Set("Content-Type", "application/xml")
   191  	w.WriteHeader(500)
   192  	w.Write([]byte(`<?xml version="1.0"encoding="UTF-8"?>
   193  <error>` + p.GetErrorPhrase("internal_error_body") + `</error>`))
   194  	log.Print("InternalError: ", err)
   195  	return HandledRouteError()
   196  }
   197  
   198  // ! Do not call CustomError here otherwise we might get an error loop
   199  func PreError(errmsg string, w http.ResponseWriter, r *http.Request) RouteError {
   200  	pi := ErrorPage{errorHeader(w, &GuestUser, p.GetErrorPhrase("error_title")), errmsg}
   201  	handleErrorTemplate(w, r, pi, 500)
   202  	return HandledRouteError()
   203  }
   204  
   205  func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError {
   206  	w.WriteHeader(500)
   207  	writeJsonError(errmsg, w)
   208  	return HandledRouteError()
   209  }
   210  
   211  func PreErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, js bool) RouteError {
   212  	if !js {
   213  		return PreError(errmsg, w, r)
   214  	}
   215  	return PreErrorJS(errmsg, w, r)
   216  }
   217  
   218  // LocalError is an error shown to the end-user when something goes wrong and it's not the software's fault
   219  // TODO: Pass header in for this and similar errors instead of having to pass in both user and w? Would also allow for more stateful things, although this could be a problem
   220  /*func LocalError(errmsg string, w http.ResponseWriter, r *http.Request, user *User) RouteError {
   221  	w.WriteHeader(500)
   222  	pi := ErrorPage{errorHeader(w, user, p.GetErrorPhrase("local_error_title")), errmsg}
   223  	handleErrorTemplate(w, r, pi)
   224  	return HandledRouteError()
   225  }*/
   226  
   227  func LocalError(errmsg string, w http.ResponseWriter, r *http.Request, u *User) RouteError {
   228  	return SimpleError(errmsg, w, r, errorHeader(w, u, ""))
   229  }
   230  
   231  func LocalErrorf(errmsg string, w http.ResponseWriter, r *http.Request, u *User, params ...interface{}) RouteError {
   232  	return LocalError(fmt.Sprintf(errmsg, params), w, r, u)
   233  }
   234  
   235  func SimpleError(errmsg string, w http.ResponseWriter, r *http.Request, h *Header) RouteError {
   236  	if h == nil {
   237  		h = errorHeader(w, &GuestUser, p.GetErrorPhrase("local_error_title"))
   238  	} else {
   239  		h.Title = p.GetErrorPhrase("local_error_title")
   240  	}
   241  	pi := ErrorPage{h, errmsg}
   242  	handleErrorTemplate(w, r, pi, 500)
   243  	return HandledRouteError()
   244  }
   245  
   246  func LocalErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, u *User, js bool) RouteError {
   247  	if !js {
   248  		return SimpleError(errmsg, w, r, errorHeader(w, u, ""))
   249  	}
   250  	return LocalErrorJS(errmsg, w, r)
   251  }
   252  
   253  func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError {
   254  	w.WriteHeader(500)
   255  	writeJsonError(errmsg, w)
   256  	return HandledRouteError()
   257  }
   258  
   259  // TODO: We might want to centralise the error logic in the future and just return what the error handler needs to construct the response rather than handling it here
   260  // NoPermissions is an error shown to the end-user when they try to access an area which they aren't authorised to access
   261  func NoPermissions(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   262  	pi := ErrorPage{errorHeader(w, u, p.GetErrorPhrase("no_permissions_title")), p.GetErrorPhrase("no_permissions_body")}
   263  	handleErrorTemplate(w, r, pi, 403)
   264  	return HandledRouteError()
   265  }
   266  
   267  func NoPermissionsJSQ(w http.ResponseWriter, r *http.Request, u *User, js bool) RouteError {
   268  	if !js {
   269  		return NoPermissions(w, r, u)
   270  	}
   271  	return NoPermissionsJS(w, r, u)
   272  }
   273  
   274  func NoPermissionsJS(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   275  	w.WriteHeader(403)
   276  	writeJsonError(p.GetErrorPhrase("no_permissions_body"), w)
   277  	return HandledRouteError()
   278  }
   279  
   280  // ? - Is this actually used? Should it be used? A ban in Gosora should be more of a permission revocation to stop them posting rather than something which spits up an error page, right?
   281  func Banned(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   282  	pi := ErrorPage{errorHeader(w, u, p.GetErrorPhrase("banned_title")), p.GetErrorPhrase("banned_body")}
   283  	handleErrorTemplate(w, r, pi, 403)
   284  	return HandledRouteError()
   285  }
   286  
   287  // nolint
   288  // BannedJSQ is the version of the banned error page which handles both JavaScript requests and normal page loads
   289  func BannedJSQ(w http.ResponseWriter, r *http.Request, user *User, js bool) RouteError {
   290  	if !js {
   291  		return Banned(w, r, user)
   292  	}
   293  	return BannedJS(w, r, user)
   294  }
   295  
   296  func BannedJS(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   297  	w.WriteHeader(403)
   298  	writeJsonError(p.GetErrorPhrase("banned_body"), w)
   299  	return HandledRouteError()
   300  }
   301  
   302  // nolint
   303  func LoginRequiredJSQ(w http.ResponseWriter, r *http.Request, u *User, js bool) RouteError {
   304  	if !js {
   305  		return LoginRequired(w, r, u)
   306  	}
   307  	return LoginRequiredJS(w, r, u)
   308  }
   309  
   310  // ? - Where is this used? Should we use it more?
   311  // LoginRequired is an error shown to the end-user when they try to access an area which requires them to login
   312  func LoginRequired(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   313  	return CustomError(p.GetErrorPhrase("login_required_body"), 401, p.GetErrorPhrase("no_permissions_title"), w, r, nil, u)
   314  }
   315  
   316  // nolint
   317  func LoginRequiredJS(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   318  	w.WriteHeader(401)
   319  	writeJsonError(p.GetErrorPhrase("login_required_body"), w)
   320  	return HandledRouteError()
   321  }
   322  
   323  // SecurityError is used whenever a session mismatch is found
   324  // ? - Should we add JS and JSQ versions of this?
   325  func SecurityError(w http.ResponseWriter, r *http.Request, u *User) RouteError {
   326  	pi := ErrorPage{errorHeader(w, u, p.GetErrorPhrase("security_error_title")), p.GetErrorPhrase("security_error_body")}
   327  	w.Header().Set("Content-Type", "text/html;charset=utf-8")
   328  	w.WriteHeader(403)
   329  	e := RenderTemplateAlias("error", "security_error", w, r, pi.Header, pi)
   330  	if e != nil {
   331  		LogError(e)
   332  	}
   333  	return HandledRouteError()
   334  }
   335  
   336  var microNotFoundBytes = []byte("file not found")
   337  func MicroNotFound(w http.ResponseWriter, r *http.Request) RouteError {
   338  	w.Header().Set("Content-Type", "text/html;charset=utf-8")
   339  	w.WriteHeader(404)
   340  	_, _ = w.Write(microNotFoundBytes)
   341  	return HandledRouteError()
   342  }
   343  
   344  // NotFound is used when the requested page doesn't exist
   345  // ? - Add a JSQ version of this?
   346  // ? - Add a user parameter?
   347  func NotFound(w http.ResponseWriter, r *http.Request, h *Header) RouteError {
   348  	return CustomError(p.GetErrorPhrase("not_found_body"), 404, p.GetErrorPhrase("not_found_title"), w, r, h, &GuestUser)
   349  }
   350  
   351  // ? - Add a user parameter?
   352  func NotFoundJS(w http.ResponseWriter, r *http.Request) RouteError {
   353  	w.WriteHeader(404)
   354  	writeJsonError(p.GetErrorPhrase("not_found_body"), w)
   355  	return HandledRouteError()
   356  }
   357  
   358  func NotFoundJSQ(w http.ResponseWriter, r *http.Request, h *Header, js bool) RouteError {
   359  	if js {
   360  		return NotFoundJS(w, r)
   361  	}
   362  	if h == nil {
   363  		h = DefaultHeader(w, &GuestUser)
   364  	}
   365  	return NotFound(w, r, h)
   366  }
   367  
   368  // CustomError lets us make custom error types which aren't covered by the generic functions above
   369  func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, h *Header, u *User) (rerr RouteError) {
   370  	if h == nil {
   371  		h, rerr = UserCheck(w, r, u)
   372  		if rerr != nil {
   373  			h = errorHeader(w, u, errtitle)
   374  		}
   375  	}
   376  	h.Title = errtitle
   377  	h.Zone = "error"
   378  	pi := ErrorPage{h, errmsg}
   379  	handleErrorTemplate(w, r, pi, errcode)
   380  	return HandledRouteError()
   381  }
   382  
   383  // CustomErrorJSQ is a version of CustomError which lets us handle both JSON and regular pages depending on how it's being accessed
   384  func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, h *Header, u *User, js bool) RouteError {
   385  	if !js {
   386  		return CustomError(errmsg, errcode, errtitle, w, r, h, u)
   387  	}
   388  	return CustomErrorJS(errmsg, errcode, w, r, u)
   389  }
   390  
   391  // CustomErrorJS is the pure JSON version of CustomError
   392  func CustomErrorJS(errmsg string, errcode int, w http.ResponseWriter, r *http.Request, u *User) RouteError {
   393  	w.WriteHeader(errcode)
   394  	writeJsonError(errmsg, w)
   395  	return HandledRouteError()
   396  }
   397  
   398  // TODO: Should we optimise this by caching these json strings?
   399  func writeJsonError(errmsg string, w http.ResponseWriter) {
   400  	_, _ = w.Write([]byte(`{"errmsg":"` + strings.Replace(errmsg, "\"", "", -1) + `"}`))
   401  }
   402  
   403  func handleErrorTemplate(w http.ResponseWriter, r *http.Request, pi ErrorPage, errcode int) {
   404  	w.Header().Set("Content-Type", "text/html;charset=utf-8")
   405  	w.WriteHeader(errcode)
   406  	err := RenderTemplateAlias("error", "error", w, r, pi.Header, pi)
   407  	if err != nil {
   408  		LogError(err)
   409  	}
   410  }
   411  
   412  // Alias of routes.renderTemplate
   413  var RenderTemplateAlias func(tmplName, hookName string, w http.ResponseWriter, r *http.Request, h *Header, pi interface{}) error