http-util.go (7184B)
1 // COPIED FROM C2EC 2 package main 3 4 import ( 5 "bytes" 6 "fmt" 7 "net/http" 8 "strings" 9 ) 10 11 const HTTP_GET = "GET" 12 const HTTP_POST = "POST" 13 14 const HTTP_OK = 200 15 const HTTP_NO_CONTENT = 204 16 const HTTP_BAD_REQUEST = 400 17 const HTTP_UNAUTHORIZED = 401 18 const HTTP_NOT_FOUND = 404 19 const HTTP_METHOD_NOT_ALLOWED = 405 20 const HTTP_CONFLICT = 409 21 const HTTP_INTERNAL_SERVER_ERROR = 500 22 23 const TALER_URI_PROBLEM_PREFIX = "taler://problem" 24 25 type RFC9457Problem struct { 26 TypeUri string `json:"type"` 27 Title string `json:"title"` 28 Detail string `json:"detail"` 29 Instance string `json:"instance"` 30 } 31 32 // Writes a problem as specified by RFC 9457 to 33 // the response. The problem is always serialized 34 // as JSON. 35 func WriteProblem(res http.ResponseWriter, status int, problem *RFC9457Problem) error { 36 37 c := NewJsonCodec[RFC9457Problem]() 38 problm, err := c.EncodeToBytes(problem) 39 if err != nil { 40 return err 41 } 42 43 res.WriteHeader(status) 44 res.Write(problm) 45 return nil 46 } 47 48 // Function reads and validates a param of a request in the 49 // correct format according to the transform function supplied. 50 // When the transform fails, it returns false as second return 51 // value. This indicates the caller, that the request shall not 52 // be further processed and the handle must be returned by the 53 // caller. Since the parameter is optional, it can be null, even 54 // if the boolean return value is set to true. 55 func AcceptOptionalParamOrWriteResponse[T any]( 56 name string, 57 transform func(s string) (T, error), 58 req *http.Request, 59 res http.ResponseWriter, 60 ) (*T, bool) { 61 62 ptr, err := OptionalQueryParamOrError(name, transform, req) 63 if err != nil { 64 err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ 65 TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_REQUEST_QUERY_PARAMETER", 66 Title: "invalid request query parameter", 67 Detail: "the withdrawal status request parameter '" + name + "' is malformed (error: " + err.Error() + ")", 68 Instance: req.RequestURI, 69 }) 70 if err != nil { 71 res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) 72 } 73 return nil, false 74 } 75 76 if ptr == nil { 77 err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{ 78 TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_REQUEST_QUERY_PARAMETER", 79 Title: "invalid request query parameter", 80 Detail: "the withdrawal status request parameter '" + name + "' resulted in a nil pointer)", 81 Instance: req.RequestURI, 82 }) 83 if err != nil { 84 res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) 85 } 86 return nil, false 87 } 88 89 obj := *ptr 90 assertedObj, ok := any(obj).(T) 91 if !ok { 92 // this should generally not happen (due to the implementation) 93 err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{ 94 TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_FATAL_ERROR", 95 Title: "Fatal Error", 96 Detail: "Something strange happened. Probably not your fault.", 97 Instance: req.RequestURI, 98 }) 99 if err != nil { 100 res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) 101 } 102 return nil, false 103 } 104 return &assertedObj, true 105 } 106 107 // The function parses a parameter of the query 108 // of the request. If the parameter is not present 109 // (empty string) it will not create an error and 110 // just return nil. 111 func OptionalQueryParamOrError[T any]( 112 name string, 113 transform func(s string) (T, error), 114 req *http.Request, 115 ) (*T, error) { 116 117 paramStr := req.URL.Query().Get(name) 118 if paramStr != "" { 119 120 if t, err := transform(paramStr); err != nil { 121 return nil, err 122 } else { 123 return &t, nil 124 } 125 } 126 return nil, nil 127 } 128 129 // Reads a generic argument struct from the requests 130 // body. It takes the codec as argument which is used to 131 // decode the struct from the request. If an error occurs 132 // nil and the error are returned. 133 func ReadStructFromBody[T any](req *http.Request, codec Codec[T]) (*T, error) { 134 135 bodyBytes, err := ReadBody(req) 136 if err != nil { 137 return nil, err 138 } 139 140 return codec.Decode(bytes.NewReader(bodyBytes)) 141 } 142 143 // Reads the body of a request into a byte array. 144 // If the body is empty, an empty array is returned. 145 // If an error occurs while reading the body, nil and 146 // the respective error is returned. 147 func ReadBody(req *http.Request) ([]byte, error) { 148 149 if req.ContentLength < 0 { 150 return make([]byte, 0), nil 151 } 152 153 body := make([]byte, req.ContentLength) 154 _, err := req.Body.Read(body) 155 if err != nil { 156 return nil, err 157 } 158 return body, nil 159 } 160 161 // Executes a GET request at the given url. 162 // Use FormatUrl for to build the url. 163 // Headers can be defined using the headers map. 164 func HttpGet[T any]( 165 url string, 166 headers map[string]string, 167 codec Codec[T], 168 ) (*T, int, error) { 169 170 req, err := http.NewRequest(HTTP_GET, url, bytes.NewBufferString("")) 171 if err != nil { 172 return nil, -1, err 173 } 174 175 for k, v := range headers { 176 req.Header.Add(k, v) 177 } 178 req.Header.Add("Accept", codec.HttpApplicationContentHeader()) 179 180 fmt.Printf("HTTP : requesting GET %s\n", url) 181 res, err := http.DefaultClient.Do(req) 182 if err != nil { 183 return nil, -1, err 184 } 185 186 if res.StatusCode > 299 { 187 return nil, res.StatusCode, nil 188 } 189 190 if codec == nil { 191 return nil, res.StatusCode, err 192 } else { 193 resBody, err := codec.Decode(res.Body) 194 return resBody, res.StatusCode, err 195 } 196 } 197 198 func HttpPost[T any, R any]( 199 url string, 200 headers map[string]string, 201 body *T, 202 reqCodec Codec[T], 203 resCodec Codec[R], 204 ) (*R, int, error) { 205 206 bodyEncoded, err := reqCodec.EncodeToBytes(body) 207 if err != nil { 208 return nil, -1, err 209 } 210 211 req, err := http.NewRequest(HTTP_POST, url, bytes.NewBuffer(bodyEncoded)) 212 if err != nil { 213 return nil, -1, err 214 } 215 216 for k, v := range headers { 217 req.Header.Add(k, v) 218 } 219 req.Header.Add("Accept", reqCodec.HttpApplicationContentHeader()) 220 221 res, err := http.DefaultClient.Do(req) 222 if err != nil { 223 return nil, -1, err 224 } 225 226 if resCodec == nil { 227 return nil, res.StatusCode, err 228 } else { 229 resBody, err := resCodec.Decode(res.Body) 230 return resBody, res.StatusCode, err 231 } 232 } 233 234 // builds request URL containing the path and query 235 // parameters of the respective parameter map. 236 func FormatUrl( 237 req string, 238 pathParams map[string]string, 239 queryParams map[string]string, 240 ) string { 241 242 return setUrlQuery(setUrlPath(req, pathParams), queryParams) 243 } 244 245 // Sets the parameters which are part of the url. 246 // The function expects each parameter in the path to be prefixed 247 // using a ':'. The function handles url as follows: 248 // 249 // /some/:param/tobereplaced -> ':param' will be replaced with value. 250 // 251 // For replacements, the pathParams map must be supplied. The map contains 252 // the name of the parameter with the value mapped to it. 253 // The names MUST not contain the prefix ':'! 254 func setUrlPath( 255 req string, 256 pathParams map[string]string, 257 ) string { 258 259 if pathParams == nil || len(pathParams) < 1 { 260 return req 261 } 262 263 var url = req 264 for k, v := range pathParams { 265 266 if !strings.HasPrefix(k, "/") { 267 // prevent scheme postfix replacements 268 url = strings.Replace(url, ":"+k, v, 1) 269 } 270 } 271 return url 272 } 273 274 func setUrlQuery( 275 req string, 276 queryParams map[string]string, 277 ) string { 278 279 if queryParams == nil || len(queryParams) < 1 { 280 return req 281 } 282 283 var url = req + "?" 284 for k, v := range queryParams { 285 286 url = strings.Join([]string{url, k, "=", v, "&"}, "") 287 } 288 289 url, _ = strings.CutSuffix(url, "&") 290 return url 291 }