cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

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 }