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