cashless2ecash

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

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 }