github.com/safing/portbase@v0.19.5/formats/dsd/http.go (about) 1 package dsd 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 ) 11 12 // HTTP Related Errors. 13 var ( 14 ErrMissingBody = errors.New("dsd: missing http body") 15 ErrMissingContentType = errors.New("dsd: missing http content type") 16 ) 17 18 const ( 19 httpHeaderContentType = "Content-Type" 20 ) 21 22 // LoadFromHTTPRequest loads the data from the body into the given interface. 23 func LoadFromHTTPRequest(r *http.Request, t interface{}) (format uint8, err error) { 24 return loadFromHTTP(r.Body, r.Header.Get(httpHeaderContentType), t) 25 } 26 27 // LoadFromHTTPResponse loads the data from the body into the given interface. 28 // Closing the body is left to the caller. 29 func LoadFromHTTPResponse(resp *http.Response, t interface{}) (format uint8, err error) { 30 return loadFromHTTP(resp.Body, resp.Header.Get(httpHeaderContentType), t) 31 } 32 33 func loadFromHTTP(body io.Reader, mimeType string, t interface{}) (format uint8, err error) { 34 // Read full body. 35 data, err := io.ReadAll(body) 36 if err != nil { 37 return 0, fmt.Errorf("dsd: failed to read http body: %w", err) 38 } 39 40 // Load depending on mime type. 41 return MimeLoad(data, mimeType, t) 42 } 43 44 // RequestHTTPResponseFormat sets the Accept header to the given format. 45 func RequestHTTPResponseFormat(r *http.Request, format uint8) (mimeType string, err error) { 46 // Get mime type. 47 mimeType, ok := FormatToMimeType[format] 48 if !ok { 49 return "", ErrIncompatibleFormat 50 } 51 52 // Request response format. 53 r.Header.Set("Accept", mimeType) 54 55 return mimeType, nil 56 } 57 58 // DumpToHTTPRequest dumps the given data to the HTTP request using the given 59 // format. It also sets the Accept header to the same format. 60 func DumpToHTTPRequest(r *http.Request, t interface{}, format uint8) error { 61 // Get mime type and set request format. 62 mimeType, err := RequestHTTPResponseFormat(r, format) 63 if err != nil { 64 return err 65 } 66 67 // Serialize data. 68 data, err := dumpWithoutIdentifier(t, format, "") 69 if err != nil { 70 return fmt.Errorf("dsd: failed to serialize: %w", err) 71 } 72 73 // Add data to request. 74 r.Header.Set("Content-Type", mimeType) 75 r.Body = io.NopCloser(bytes.NewReader(data)) 76 77 return nil 78 } 79 80 // DumpToHTTPResponse dumpts the given data to the HTTP response, using the 81 // format defined in the request's Accept header. 82 func DumpToHTTPResponse(w http.ResponseWriter, r *http.Request, t interface{}) error { 83 // Serialize data based on accept header. 84 data, mimeType, _, err := MimeDump(t, r.Header.Get("Accept")) 85 if err != nil { 86 return fmt.Errorf("dsd: failed to serialize: %w", err) 87 } 88 89 // Write data to response 90 w.Header().Set("Content-Type", mimeType) 91 _, err = w.Write(data) 92 if err != nil { 93 return fmt.Errorf("dsd: failed to write response: %w", err) 94 } 95 return nil 96 } 97 98 // MimeLoad loads the given data into the interface based on the given mime type accept header. 99 func MimeLoad(data []byte, accept string, t interface{}) (format uint8, err error) { 100 // Find format. 101 format = FormatFromAccept(accept) 102 if format == 0 { 103 return 0, ErrIncompatibleFormat 104 } 105 106 // Load data. 107 err = LoadAsFormat(data, format, t) 108 return format, err 109 } 110 111 // MimeDump dumps the given interface based on the given mime type accept header. 112 func MimeDump(t any, accept string) (data []byte, mimeType string, format uint8, err error) { 113 // Find format. 114 format = FormatFromAccept(accept) 115 if format == AUTO { 116 return nil, "", 0, ErrIncompatibleFormat 117 } 118 119 // Serialize and return. 120 data, err = dumpWithoutIdentifier(t, format, "") 121 return data, mimeType, format, err 122 } 123 124 // FormatFromAccept returns the format for the given accept definition. 125 // The accept parameter matches the format of the HTTP Accept header. 126 // Special cases, in this order: 127 // - If accept is an empty string: returns default serialization format. 128 // - If accept contains no supported format, but a wildcard: returns default serialization format. 129 // - If accept contains no supported format, and no wildcard: returns AUTO format. 130 func FormatFromAccept(accept string) (format uint8) { 131 if accept == "" { 132 return DefaultSerializationFormat 133 } 134 135 var foundWildcard bool 136 for _, mimeType := range strings.Split(accept, ",") { 137 // Clean mime type. 138 mimeType = strings.TrimSpace(mimeType) 139 mimeType, _, _ = strings.Cut(mimeType, ";") 140 if strings.Contains(mimeType, "/") { 141 _, mimeType, _ = strings.Cut(mimeType, "/") 142 } 143 mimeType = strings.ToLower(mimeType) 144 145 // Check if mime type is supported. 146 format, ok := MimeTypeToFormat[mimeType] 147 if ok { 148 return format 149 } 150 151 // Return default mime type as fallback if any mimetype is okay. 152 if mimeType == "*" { 153 foundWildcard = true 154 } 155 } 156 157 if foundWildcard { 158 return DefaultSerializationFormat 159 } 160 return AUTO 161 } 162 163 // Format and MimeType mappings. 164 var ( 165 FormatToMimeType = map[uint8]string{ 166 CBOR: "application/cbor", 167 JSON: "application/json", 168 MsgPack: "application/msgpack", 169 YAML: "application/yaml", 170 } 171 MimeTypeToFormat = map[string]uint8{ 172 "cbor": CBOR, 173 "json": JSON, 174 "msgpack": MsgPack, 175 "yaml": YAML, 176 "yml": YAML, 177 } 178 )