http.go 8.6 KB

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