http.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package protocol
  2. import (
  3. "net/http"
  4. "github.com/opencost/opencost/core/pkg/log"
  5. "github.com/opencost/opencost/core/pkg/util/json"
  6. "google.golang.org/protobuf/encoding/protojson"
  7. "google.golang.org/protobuf/proto"
  8. )
  9. // HTTPProtocol is a struct used as a selector for request/response protocol utility methods
  10. type HTTPProtocol struct{}
  11. // HTTPError represents an http error response
  12. type HTTPError struct {
  13. StatusCode int
  14. Body string
  15. }
  16. // Error returns the error string
  17. func (he HTTPError) Error() string {
  18. return string(he.Body)
  19. }
  20. // BadRequest creates a BadRequest HTTPError
  21. func (hp HTTPProtocol) BadRequest(message string) HTTPError {
  22. return HTTPError{
  23. StatusCode: http.StatusBadRequest,
  24. Body: message,
  25. }
  26. }
  27. // UnprocessableEntity creates an UnprocessableEntity HTTPError
  28. func (hp HTTPProtocol) UnprocessableEntity(message string) HTTPError {
  29. if message == "" {
  30. message = "Unprocessable Entity"
  31. }
  32. return HTTPError{
  33. StatusCode: http.StatusUnprocessableEntity,
  34. Body: message,
  35. }
  36. }
  37. // InternalServerError creates an InternalServerError HTTPError
  38. func (hp HTTPProtocol) InternalServerError(message string) HTTPError {
  39. if message == "" {
  40. message = "Internal Server Error"
  41. }
  42. return HTTPError{
  43. StatusCode: http.StatusInternalServerError,
  44. Body: message,
  45. }
  46. }
  47. func (hp HTTPProtocol) NotImplemented(message string) HTTPError {
  48. if message == "" {
  49. message = "Not Implemented"
  50. }
  51. return HTTPError{
  52. StatusCode: http.StatusNotImplemented,
  53. Body: message,
  54. }
  55. }
  56. func (hp HTTPProtocol) Forbidden(message string) HTTPError {
  57. if message == "" {
  58. message = "Forbidden"
  59. }
  60. return HTTPError{
  61. StatusCode: http.StatusForbidden,
  62. Body: message,
  63. }
  64. }
  65. // NotFound creates a NotFound HTTPError
  66. func (hp HTTPProtocol) NotFound() HTTPError {
  67. return HTTPError{
  68. StatusCode: http.StatusNotFound,
  69. Body: "Not Found",
  70. }
  71. }
  72. // HTTPResponse represents a data envelope for our HTTP messaging
  73. type HTTPResponse struct {
  74. Code int `json:"code"`
  75. Data interface{} `json:"data"`
  76. Message string `json:"message,omitempty"`
  77. Warning string `json:"warning,omitempty"`
  78. }
  79. // ToResponse accepts a data payload and/or error to encode into a new HTTPResponse instance. Responses
  80. // which should not contain an error should pass nil for err.
  81. func (hp HTTPProtocol) ToResponse(data interface{}, err error) *HTTPResponse {
  82. if err != nil {
  83. return &HTTPResponse{
  84. Code: http.StatusInternalServerError,
  85. Data: data,
  86. Message: err.Error(),
  87. }
  88. }
  89. return &HTTPResponse{
  90. Code: http.StatusOK,
  91. Data: data,
  92. }
  93. }
  94. func (hp HTTPProtocol) WriteRawOK(w http.ResponseWriter) {
  95. w.Header().Set("Content-Type", "application/json")
  96. w.Header().Set("Content-Length", "0")
  97. w.WriteHeader(http.StatusOK)
  98. }
  99. func (hp HTTPProtocol) WriteRawNoContent(w http.ResponseWriter) {
  100. w.Header().Set("Content-Type", "application/json")
  101. w.WriteHeader(http.StatusNoContent)
  102. }
  103. // WriteJSONData uses json content-type and json encoder with no data envelope allowing to remove
  104. // xss CWE as well as backwards compatibility to exisitng FE expectations
  105. func (hp HTTPProtocol) WriteJSONData(w http.ResponseWriter, data interface{}) {
  106. w.Header().Set("Content-Type", "application/json")
  107. status := http.StatusOK
  108. w.WriteHeader(status)
  109. if err := json.NewEncoder(w).Encode(data); err != nil {
  110. log.Error("Failed to encode JSON response: " + err.Error())
  111. }
  112. }
  113. // WriteRawError uses json content-type and outputs raw error message for backwards compatibility to existing
  114. // frontend expectations.
  115. func (hp HTTPProtocol) WriteRawError(w http.ResponseWriter, httpStatusCode int, err string) {
  116. // I know this isn't json, but its what we've done and don't want to break frontned while we fix CWE
  117. w.Header().Set("Content-Type", "application/json")
  118. http.Error(w, err, httpStatusCode)
  119. }
  120. // WriteEncodedError writes an error response in the format of HTTPResponse
  121. func (hp HTTPProtocol) WriteEncodedError(w http.ResponseWriter, httpStatusCode int, errorResponse interface{}) {
  122. w.Header().Set("Content-Type", "application/json")
  123. w.WriteHeader(httpStatusCode)
  124. if err := json.NewEncoder(w).Encode(errorResponse); err != nil {
  125. log.Error("Failed to encode error response: " + err.Error())
  126. }
  127. }
  128. // WriteData wraps the data payload in an HTTPResponse and writes the resulting response using the
  129. // http.ResponseWriter
  130. func (hp HTTPProtocol) WriteData(w http.ResponseWriter, data interface{}) {
  131. w.Header().Set("Content-Type", "application/json")
  132. status := http.StatusOK
  133. w.WriteHeader(status)
  134. if err := json.NewEncoder(w).Encode(data); err != nil {
  135. log.Error("Failed to encode response: " + err.Error())
  136. }
  137. }
  138. // WriteDataWithWarning writes the data payload similiar to WriteData except it provides an additional warning message.
  139. func (hp HTTPProtocol) WriteDataWithWarning(w http.ResponseWriter, data interface{}, warning string) {
  140. w.Header().Set("Content-Type", "application/json")
  141. status := http.StatusOK
  142. resp := &HTTPResponse{
  143. Code: status,
  144. Data: data,
  145. Warning: warning,
  146. }
  147. w.WriteHeader(status)
  148. if err := json.NewEncoder(w).Encode(resp); err != nil {
  149. log.Error("Failed to encode response with warning: " + err.Error())
  150. }
  151. }
  152. // WriteDataWithMessage writes the data payload similiar to WriteData except it provides an additional string message.
  153. func (hp HTTPProtocol) WriteDataWithMessage(w http.ResponseWriter, data interface{}, message string) {
  154. w.Header().Set("Content-Type", "application/json")
  155. status := http.StatusOK
  156. resp := &HTTPResponse{
  157. Code: status,
  158. Data: data,
  159. Message: message,
  160. }
  161. w.WriteHeader(status)
  162. if err := json.NewEncoder(w).Encode(resp); err != nil {
  163. log.Error("Failed to encode response with message: " + err.Error())
  164. }
  165. }
  166. // WriteProtoWithMessage uses the protojson package to convert proto3 response to json response and
  167. // return it to the requester. Proto3 drops messages with default values but overriding the param
  168. // EmitUnpopulated to true it returns default values in the Json response payload. If error is
  169. // encountered it sent InternalServerError and the error why the json conversion failed.
  170. func (hp HTTPProtocol) WriteProtoWithMessage(w http.ResponseWriter, data proto.Message) {
  171. w.Header().Set("Content-Type", "application/json")
  172. m := protojson.MarshalOptions{
  173. EmitUnpopulated: true,
  174. }
  175. status := http.StatusOK
  176. w.WriteHeader(status)
  177. b, err := m.Marshal(data)
  178. if err != nil {
  179. hp.WriteError(w, hp.InternalServerError(err.Error()))
  180. log.Error("Failed to marshal proto to json: " + err.Error())
  181. return
  182. }
  183. w.Write(b)
  184. }
  185. // WriteDataWithMessageAndWarning writes the data payload similiar to WriteData except it provides a warning and additional message string.
  186. func (hp HTTPProtocol) WriteDataWithMessageAndWarning(w http.ResponseWriter, data interface{}, message string, warning string) {
  187. w.Header().Set("Content-Type", "application/json")
  188. status := http.StatusOK
  189. resp := &HTTPResponse{
  190. Code: status,
  191. Data: data,
  192. Message: message,
  193. Warning: warning,
  194. }
  195. w.WriteHeader(status)
  196. if err := json.NewEncoder(w).Encode(resp); err != nil {
  197. log.Error("Failed to encode response with message and warning: " + err.Error())
  198. }
  199. }
  200. // WriteError wraps the HTTPError in a HTTPResponse and writes it via http.ResponseWriter
  201. func (hp HTTPProtocol) WriteError(w http.ResponseWriter, err HTTPError) {
  202. w.Header().Set("Content-Type", "application/json")
  203. status := err.StatusCode
  204. if status == 0 {
  205. status = http.StatusInternalServerError
  206. }
  207. w.WriteHeader(status)
  208. resp, _ := json.Marshal(&HTTPResponse{
  209. Code: status,
  210. Message: err.Body,
  211. })
  212. if err := json.NewEncoder(w).Encode(resp); err != nil {
  213. log.Error("Failed to encode error response: " + err.Error())
  214. }
  215. }
  216. // WriteResponse writes the provided HTTPResponse instance via http.ResponseWriter
  217. func (hp HTTPProtocol) WriteResponse(w http.ResponseWriter, r *HTTPResponse) {
  218. w.Header().Set("Content-Type", "application/json")
  219. status := r.Code
  220. w.WriteHeader(status)
  221. if err := json.NewEncoder(w).Encode(r); err != nil {
  222. log.Error("Failed to encode response: " + err.Error())
  223. }
  224. }