http-util.go (7482B)
1 // This file is part of taler-cashless2ecash. 2 // Copyright (C) 2024 Joel Häberli 3 // 4 // taler-cashless2ecash is free software: you can redistribute it and/or modify it 5 // under the terms of the GNU Affero General Public License as published 6 // by the Free Software Foundation, either version 3 of the License, 7 // or (at your option) any later version. 8 // 9 // taler-cashless2ecash is distributed in the hope that it will be useful, but 10 // WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 // Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 // 17 // SPDX-License-Identifier: AGPL3.0-or-later 18 19 package internal_utils 20 21 import ( 22 "bytes" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "strings" 28 ) 29 30 const HTTP_GET = "GET" 31 const HTTP_POST = "POST" 32 33 const HTTP_OK = 200 34 const HTTP_NO_CONTENT = 204 35 const HTTP_BAD_REQUEST = 400 36 const HTTP_UNAUTHORIZED = 401 37 const HTTP_NOT_FOUND = 404 38 const HTTP_METHOD_NOT_ALLOWED = 405 39 const HTTP_CONFLICT = 409 40 const HTTP_INTERNAL_SERVER_ERROR = 500 41 const HTTP_NOT_IMPLEMENTED = 501 42 43 const CONTENT_TYPE_HEADER = "Content-Type" 44 45 // Function reads and validates a param of a request in the 46 // correct format according to the transform function supplied. 47 // When the transform fails, it returns false as second return 48 // value. This indicates the caller, that the request shall not 49 // be further processed and the handle must be returned by the 50 // caller. Since the parameter is optional, it can be null, even 51 // if the boolean return value is set to true. 52 func AcceptOptionalParamOrWriteResponse[T any]( 53 name string, 54 transform func(s string) (T, error), 55 req *http.Request, 56 res http.ResponseWriter, 57 ) (*T, bool) { 58 59 ptr, err := OptionalQueryParamOrError(name, transform, req) 60 if err != nil { 61 SetLastResponseCodeForLogger(HTTP_BAD_REQUEST) 62 res.WriteHeader(HTTP_BAD_REQUEST) 63 return nil, false 64 } 65 66 if ptr == nil { 67 LogInfo("http", "optional parameter "+name+" was not set") 68 return nil, true 69 } 70 71 obj := *ptr 72 assertedObj, ok := any(obj).(T) 73 if !ok { 74 // this should generally not happen (due to the implementation) 75 SetLastResponseCodeForLogger(HTTP_INTERNAL_SERVER_ERROR) 76 res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) 77 return nil, false 78 } 79 return &assertedObj, true 80 } 81 82 // The function parses a parameter of the query 83 // of the request. If the parameter is not present 84 // (empty string) it will not create an error and 85 // just return nil. 86 func OptionalQueryParamOrError[T any]( 87 name string, 88 transform func(s string) (T, error), 89 req *http.Request, 90 ) (*T, error) { 91 92 paramStr := req.URL.Query().Get(name) 93 if paramStr != "" { 94 95 if t, err := transform(paramStr); err != nil { 96 return nil, err 97 } else { 98 return &t, nil 99 } 100 } 101 return nil, nil 102 } 103 104 // Reads a generic argument struct from the requests 105 // body. It takes the codec as argument which is used to 106 // decode the struct from the request. If an error occurs 107 // nil and the error are returned. 108 func ReadStructFromBody[T any](req *http.Request, codec Codec[T]) (*T, error) { 109 110 bodyBytes, err := ReadBody(req) 111 if err != nil { 112 return nil, err 113 } 114 115 return codec.Decode(bytes.NewReader(bodyBytes)) 116 } 117 118 // Reads the body of a request into a byte array. 119 // If the body is empty, an empty array is returned. 120 // If an error occurs while reading the body, nil and 121 // the respective error is returned. 122 func ReadBody(req *http.Request) ([]byte, error) { 123 124 if req.ContentLength < 0 { 125 return nil, errors.New("malformed body") 126 } 127 128 body, err := io.ReadAll(req.Body) 129 if err != nil { 130 LogError("http-util", err) 131 return nil, err 132 } 133 LogInfo("http-util", "read body from request. body="+string(body)) 134 return body, nil 135 } 136 137 // Executes a GET request at the given url. 138 // Use FormatUrl for to build the url. 139 // Headers can be defined using the headers map. 140 func HttpGet[T any]( 141 url string, 142 headers map[string]string, 143 codec Codec[T], 144 ) (*T, int, error) { 145 146 req, err := http.NewRequest(HTTP_GET, url, bytes.NewBufferString("")) 147 if err != nil { 148 return nil, -1, err 149 } 150 151 for k, v := range headers { 152 req.Header.Add(k, v) 153 } 154 req.Header.Add("Accept", codec.HttpApplicationContentHeader()) 155 156 res, err := http.DefaultClient.Do(req) 157 if err != nil { 158 return nil, -1, err 159 } 160 161 if codec == nil { 162 return nil, res.StatusCode, err 163 } else { 164 b, err := io.ReadAll(res.Body) 165 if err != nil { 166 LogError("http-util", err) 167 if res.StatusCode > 299 { 168 return nil, res.StatusCode, nil 169 } 170 return nil, -1, err 171 } 172 if res.StatusCode > 299 { 173 LogInfo("http-util", fmt.Sprintf("response: %s", string(b))) 174 return nil, res.StatusCode, nil 175 } 176 resBody, err := codec.Decode(bytes.NewReader(b)) 177 return resBody, res.StatusCode, err 178 } 179 } 180 181 // execute a POST request and parse response or retrieve error 182 // path- and query-parameters can be set to add query and path parameters 183 func HttpPost[T any, R any]( 184 url string, 185 headers map[string]string, 186 body *T, 187 reqCodec Codec[T], 188 resCodec Codec[R], 189 ) (*R, int, error) { 190 191 bodyEncoded, err := reqCodec.EncodeToBytes(body) 192 if err != nil { 193 return nil, -1, err 194 } 195 LogInfo("http-util", string(bodyEncoded)) 196 197 req, err := http.NewRequest(HTTP_POST, url, bytes.NewBuffer(bodyEncoded)) 198 if err != nil { 199 return nil, -1, err 200 } 201 202 for k, v := range headers { 203 req.Header.Add(k, v) 204 } 205 if resCodec != nil { 206 req.Header.Add("Accept", resCodec.HttpApplicationContentHeader()) 207 } 208 req.Header.Add("Content-Type", reqCodec.HttpApplicationContentHeader()) 209 210 res, err := http.DefaultClient.Do(req) 211 if err != nil { 212 return nil, -1, err 213 } 214 215 if resCodec == nil { 216 return nil, res.StatusCode, err 217 } else { 218 b, err := io.ReadAll(res.Body) 219 if err != nil { 220 LogError("http-util", err) 221 if res.StatusCode > 299 { 222 return nil, res.StatusCode, nil 223 } 224 return nil, -1, err 225 } 226 if res.StatusCode > 299 { 227 LogInfo("http-util", fmt.Sprintf("response: %s", string(b))) 228 return nil, res.StatusCode, nil 229 } 230 resBody, err := resCodec.Decode(bytes.NewReader(b)) 231 return resBody, res.StatusCode, err 232 } 233 } 234 235 // builds request URL containing the path and query 236 // parameters of the respective parameter map. 237 func FormatUrl( 238 req string, 239 pathParams map[string]string, 240 queryParams map[string]string, 241 ) string { 242 243 return setUrlQuery(setUrlPath(req, pathParams), queryParams) 244 } 245 246 // Sets the parameters which are part of the url. 247 // The function expects each parameter in the path to be prefixed 248 // using a ':'. The function handles url as follows: 249 // 250 // /some/:param/tobereplaced -> ':param' will be replaced with value. 251 // 252 // For replacements, the pathParams map must be supplied. The map contains 253 // the name of the parameter with the value mapped to it. 254 // The names MUST not contain the prefix ':'! 255 func setUrlPath( 256 req string, 257 pathParams map[string]string, 258 ) string { 259 260 if pathParams == nil || len(pathParams) < 1 { 261 return req 262 } 263 264 var url = req 265 for k, v := range pathParams { 266 267 if !strings.HasPrefix(k, "/") { 268 // prevent scheme postfix replacements 269 url = strings.Replace(url, ":"+k, v, 1) 270 } 271 } 272 return url 273 } 274 275 func setUrlQuery( 276 req string, 277 queryParams map[string]string, 278 ) string { 279 280 if queryParams == nil || len(queryParams) < 1 { 281 return req 282 } 283 284 var url = req + "?" 285 for k, v := range queryParams { 286 287 url = strings.Join([]string{url, k, "=", v, "&"}, "") 288 } 289 290 url, _ = strings.CutSuffix(url, "&") 291 return url 292 }