package web import ( "context" "encoding/json" "fmt" "net/http" "os" "path" "path/filepath" "strings" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" ) const ( charsetUTF8 = "charset=UTF-8" ) // MIME types const ( MIMEApplicationJSON = "application/json" MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8 MIMETextHTML = "text/html" MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + charsetUTF8 MIMETextPlain = "text/plain" MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + charsetUTF8 MIMEOctetStream = "application/octet-stream" ) // RespondJsonError sends an error formatted as JSON response back to the client. func RespondJsonError(ctx context.Context, w http.ResponseWriter, er error) error { // Set the status code for the request logger middleware. // If the context is missing this value, request the service // to be shutdown gracefully. v, err := webcontext.ContextValues(ctx) if err != nil { return err } webErr, ok := er.(*weberror.Error) if !ok { // If the error was of the type *Error, the handler has // a specific status code and error to return. webErr = weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error) } v.StatusCode = webErr.Status return RespondJson(ctx, w, webErr.Response(ctx, false), webErr.Status) } // RespondJson converts a Go value to JSON and sends it to the client. // If code is StatusNoContent, v is expected to be nil. func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, statusCode int) error { // Set the status code for the request logger middleware. // If the context is missing this value, request the service // to be shutdown gracefully. v, err := webcontext.ContextValues(ctx) if err != nil { return err } v.StatusCode = statusCode // If there is nothing to marshal then set status code and return. if statusCode == http.StatusNoContent { w.WriteHeader(statusCode) return nil } // Check to see if the json has already been encoded. jsonData, ok := data.([]byte) if !ok { // Convert the response value to JSON. var err error jsonData, err = json.Marshal(data) if err != nil { return err } } // Set the content type and headers once we know marshaling has succeeded. w.Header().Set("Content-Type", MIMEApplicationJSONCharsetUTF8) // Write the status code to the response. w.WriteHeader(statusCode) // Send the result back to the client. if _, err := w.Write(jsonData); err != nil { return err } return nil } // RespondError sends an error back to the client as plain text with // the status code 500 Internal Service Error func RespondError(ctx context.Context, w http.ResponseWriter, er error) error { return RespondErrorStatus(ctx, w, er, http.StatusInternalServerError) } // RespondErrorStatus sends an error back to the client as plain text with // the specified HTTP status code. func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, statusCode int) error { // Set the status code for the request logger middleware. // If the context is missing this value, request the service // to be shutdown gracefully. v, err := webcontext.ContextValues(ctx) if err != nil { return err } // If the error was of the type *Error, the handler has // a specific status code and error to return. webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error) v.StatusCode = webErr.Status respErr := webErr.Response(ctx, false).String() switch webcontext.ContextEnv(ctx) { case webcontext.Env_Dev, webcontext.Env_Stage: respErr = respErr + fmt.Sprintf("\n%s\n%+v", webErr.Error(), webErr.Cause) } return RespondText(ctx, w, respErr, statusCode) } // RespondText sends text back to the client as plain text with the specified HTTP status code. func RespondText(ctx context.Context, w http.ResponseWriter, text string, statusCode int) error { return Respond(ctx, w, []byte(text), statusCode, MIMETextPlainCharsetUTF8) } // Respond writes the data to the client with the specified HTTP status code and // content type. func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode int, contentType string) error { // Set the status code for the request logger middleware. // If the context is missing this value, request the service // to be shutdown gracefully. v, err := webcontext.ContextValues(ctx) if err != nil { return err } v.StatusCode = statusCode // If there is nothing to marshal then set status code and return. if statusCode == http.StatusNoContent { w.WriteHeader(statusCode) return nil } // Set the content type and headers once we know marshaling has succeeded. w.Header().Set("Content-Type", contentType) // Write the status code to the response. w.WriteHeader(statusCode) // Send the result back to the client. if _, err := w.Write(data); err != nil { return err } return nil } // RenderError sends an error back to the client as html with // the specified HTTP status code. func RenderError(ctx context.Context, w http.ResponseWriter, r *http.Request, er error, renderer Renderer, templateLayoutName, templateContentName, contentType string) error { // Set the status code for the request logger middleware. // If the context is missing this value, request the service // to be shutdown gracefully. v, err := webcontext.ContextValues(ctx) if err != nil { return err } webErr, ok := er.(*weberror.Error) if !ok { if v.StatusCode == 0 { v.StatusCode = http.StatusInternalServerError } // If the error was of the type *Error, the handler has // a specific status code and error to return. webErr = weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error) } v.StatusCode = webErr.Status if RequestIsImage(r) { return nil } resp := webErr.Response(ctx, true) data := map[string]interface{}{ "StatusCode": resp.StatusCode, "Error": resp.Error, "Details": resp.Details, "Fields": resp.Fields, } return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.Status, data) } // Static registers a new route with path prefix to serve static files from the // provided root directory. All errors will result in 404 File Not Found. func Static(rootDir, prefix string) Handler { h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { err := StaticHandler(ctx, w, r, params, rootDir, prefix) if err != nil { return RespondErrorStatus(ctx, w, err, http.StatusNotFound) } return nil } return h } // StaticHandler sends a static file wo the client. The error is returned directly // from this function allowing it to be wrapped by a Handler. The handler then was the // the ability to format/display the error before responding to the client. func StaticHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string, rootDir, prefix string) error { // Parse the URL from the http request. urlPath := path.Clean("/" + r.URL.Path) // "/"+ for security urlPath = strings.TrimLeft(urlPath, "/") // Remove the static directory name from the url rootDirName := filepath.Base(rootDir) if strings.HasPrefix(urlPath, rootDirName) { urlPath = strings.Replace(urlPath, rootDirName, "", 1) } // Also remove the URL prefix used to serve the static file since // this does not need to match any existing directory structure. if prefix != "" { urlPath = strings.TrimLeft(urlPath, prefix) } // Resolve the root directory to an absolute path sd, err := filepath.Abs(rootDir) if err != nil { return err } // Append the requested file to the root directory filePath := filepath.Join(sd, urlPath) // Make sure the file exists before attempting to serve it so // have the opportunity to handle the when a file does not exist. if _, err := os.Stat(filePath); err != nil { return err } // Serve the file from the local file system. http.ServeFile(w, r, filePath) return nil } // Redirect ensures the session is flushed to the browser before the redirect is issued. func Redirect(ctx context.Context, w http.ResponseWriter, r *http.Request, url string, code int) error { if sess := webcontext.ContextSession(ctx); sess != nil { if err := sess.Save(r, w); err != nil { return err } } http.Redirect(w, r, url, code) return nil }