146 lines
5.1 KiB
Go
146 lines
5.1 KiB
Go
package tools
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"runtime/debug"
|
|
"strings"
|
|
)
|
|
|
|
type APIError struct {
|
|
Status int `json:"-"`
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
var (
|
|
ERROR_GENERIC_SERVER = APIError{Status: 500, Code: 10000, Message: "Server Error"}
|
|
ERROR_GENERIC_NOT_FOUND = APIError{Status: 404, Code: 10001, Message: "Endpoint Not Found"}
|
|
ERROR_GENERIC_RATELIMIT = APIError{Status: 429, Code: 10002, Message: "Too Many Requests"}
|
|
ERROR_GENERIC_UNAUTHORIZED = APIError{Status: 401, Code: 10003, Message: "Unauthorized"}
|
|
ERROR_GENERIC_FORBIDDEN = APIError{Status: 403, Code: 10004, Message: "Forbidden"}
|
|
ERROR_GENERIC_METHOD_NOT_ALLOWED = APIError{Status: 405, Code: 10005, Message: "Method Not Allowed"}
|
|
ERROR_SERVER_RESOURCES_EXHAUSTED = APIError{Status: 507, Code: 11006, Message: "Resources Exhausted"}
|
|
ERROR_BODY_EMPTY = APIError{Status: 411, Code: 12000, Message: "Request Body is Empty"}
|
|
ERROR_BODY_TOO_LARGE = APIError{Status: 413, Code: 12001, Message: "Request Body is Too Large"}
|
|
ERROR_BODY_INVALID_CONTENT_TYPE = APIError{Status: 400, Code: 12002, Message: "Invalid 'Content-Type' Header"}
|
|
ERROR_BODY_INVALID_CHALLENGE = APIError{Status: 400, Code: 12003, Message: "Invalid 'X-Challenge-*' Header"}
|
|
ERROR_BODY_INVALID_FIELD = APIError{Status: 400, Code: 12004, Message: "Invalid Body Field"}
|
|
ERROR_BODY_INVALID_DATA = APIError{Status: 422, Code: 12005, Message: "Invalid Body"}
|
|
ERROR_UNKNOWN_ENDPOINT = APIError{Status: 404, Code: 13000, Message: "Unknown Endpoint"}
|
|
ERROR_UNKNOWN_FUNCTION = APIError{Status: 404, Code: 13001, Message: "Unknown Function"}
|
|
ERROR_UNKNOWN_ANIMATION = APIError{Status: 404, Code: 13002, Message: "Unknown Animation"}
|
|
ERROR_UNKNOWN_TASK = APIError{Status: 404, Code: 13003, Message: "Unknown Task"}
|
|
ERROR_UNKNOWN_CHALLENGE = APIError{Status: 404, Code: 13004, Message: "Unknown Challenge"}
|
|
ERROR_CHALLENGE_INVALID = APIError{Status: 400, Code: 14000, Message: "Invalid Challenge Result"}
|
|
ERROR_CHALLENGE_TOO_EASY = APIError{Status: 400, Code: 14001, Message: "Challenge Difficulty Too Low"}
|
|
ERROR_CHALLENGE_EXPIRED = APIError{Status: 400, Code: 14002, Message: "Challenge Expired"}
|
|
ERROR_MEDIA_INVALID = APIError{Status: 400, Code: 15000, Message: "Media Invalid"}
|
|
ERROR_MEDIA_INAPPROPRIATE = APIError{Status: 400, Code: 15001, Message: "Media Inappropriate"}
|
|
)
|
|
|
|
// Reject request due to a Client Mistake
|
|
func SendClientError(w http.ResponseWriter, r *http.Request, err APIError) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(err.Status)
|
|
fmt.Fprintf(w, `{"code":%d,"message":%q}`, err.Code, err.Message)
|
|
}
|
|
|
|
// Reject request due to a Server Error
|
|
// Additionally collects debug information and logs it to the console
|
|
func SendServerError(w http.ResponseWriter, r *http.Request, err error) {
|
|
|
|
debugStack := strings.Split(string(debug.Stack()), "\n")
|
|
for i, item := range debugStack {
|
|
debugStack[i] = strings.ReplaceAll(item, "\t", " ")
|
|
}
|
|
if len(debugStack) > 5 {
|
|
debugStack = debugStack[5:] // skip header
|
|
}
|
|
|
|
reqHeader := make(map[string]string, len(r.Header))
|
|
for key, header := range r.Header {
|
|
reqHeader[key] = strings.Join(header, ", ")
|
|
}
|
|
|
|
LoggerHTTP.Data(ERROR, err.Error(), map[string]any{
|
|
"request": map[string]any{
|
|
"method": r.Method,
|
|
"url": r.URL.String(),
|
|
"headers": reqHeader,
|
|
},
|
|
"error": map[string]any{
|
|
"raw": err,
|
|
"message": err.Error(),
|
|
"stack": debugStack,
|
|
},
|
|
})
|
|
|
|
if w != nil {
|
|
SendClientError(w, r, ERROR_GENERIC_SERVER)
|
|
}
|
|
}
|
|
|
|
// Respond to the request with a JSON object
|
|
func SendJSON(w http.ResponseWriter, r *http.Request, statusCode int, responseObject any) (int, error) {
|
|
|
|
// Check Compression
|
|
var g io.Writer
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
z := gzip.NewWriter(w)
|
|
defer z.Close()
|
|
g = z
|
|
} else {
|
|
g = w
|
|
}
|
|
|
|
// Stream Object
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
if b, ok := responseObject.([]byte); ok {
|
|
return g.Write(b)
|
|
} else {
|
|
j := json.NewEncoder(g)
|
|
return 0, j.Encode(responseObject)
|
|
}
|
|
}
|
|
|
|
// Encode Object as JSON and gzipped version
|
|
func PrepareStaticJSON(responseObject any) ([]byte, []byte, error) {
|
|
|
|
// Encode Object
|
|
buf, err := json.Marshal(responseObject)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Compress Object
|
|
cmp := bytes.Buffer{}
|
|
zip := gzip.NewWriter(&cmp)
|
|
|
|
if _, err := zip.Write(buf); err != nil {
|
|
zip.Close()
|
|
return nil, nil, err
|
|
}
|
|
if err := zip.Close(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return buf, cmp.Bytes(), nil
|
|
}
|
|
|
|
// Respond to the request with a Static JSON Object
|
|
func SendStaticJSON(w http.ResponseWriter, r *http.Request, statusCode int, content []byte, gzipped []byte) (int, error) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if gzipped != nil && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
return w.Write(gzipped)
|
|
}
|
|
return w.Write(content)
|
|
}
|