summaryrefslogtreecommitdiff
path: root/c2ec/http-util.go
blob: f75305027c8bb56b66c5ce5d87276c8faf9555ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package main

import (
	"bytes"
	"errors"
	"io"
	"net/http"
	"strings"
)

const HTTP_GET = "GET"
const HTTP_POST = "POST"

const HTTP_OK = 200
const HTTP_NO_CONTENT = 204
const HTTP_BAD_REQUEST = 400
const HTTP_UNAUTHORIZED = 401
const HTTP_NOT_FOUND = 404
const HTTP_METHOD_NOT_ALLOWED = 405
const HTTP_CONFLICT = 409
const HTTP_INTERNAL_SERVER_ERROR = 500

const TALER_URI_PROBLEM_PREFIX = "taler://problem"

type RFC9457Problem struct {
	TypeUri  string `json:"type"`
	Title    string `json:"title"`
	Detail   string `json:"detail"`
	Instance string `json:"instance"`
}

// Writes a problem as specified by RFC 9457 to
// the response. The problem is always serialized
// as JSON.
func WriteProblem(res http.ResponseWriter, status int, problem *RFC9457Problem) error {

	c := NewJsonCodec[RFC9457Problem]()
	problm, err := c.EncodeToBytes(problem)
	if err != nil {
		return err
	}

	res.WriteHeader(status)
	res.Write(problm)
	return nil
}

// Function reads and validates a param of a request in the
// correct format according to the transform function supplied.
// When the transform fails, it returns false as second return
// value. This indicates the caller, that the request shall not
// be further processed and the handle must be returned by the
// caller. Since the parameter is optional, it can be null, even
// if the boolean return value is set to true.
func AcceptOptionalParamOrWriteResponse[T any](
	name string,
	transform func(s string) (T, error),
	req *http.Request,
	res http.ResponseWriter,
) (*T, bool) {

	ptr, err := OptionalQueryParamOrError(name, transform, req)
	if err != nil {
		err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{
			TypeUri:  TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_REQUEST_QUERY_PARAMETER",
			Title:    "invalid request query parameter",
			Detail:   "the withdrawal status request parameter '" + name + "' is malformed (error: " + err.Error() + ")",
			Instance: req.RequestURI,
		})
		if err != nil {
			res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
		}
		return nil, false
	}

	if ptr == nil {
		LogInfo("http", "optional parameter "+name+" was not set")
		return nil, false
	}

	obj := *ptr
	assertedObj, ok := any(obj).(T)
	if !ok {
		// this should generally not happen (due to the implementation)
		err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{
			TypeUri:  TALER_URI_PROBLEM_PREFIX + "/C2EC_FATAL_ERROR",
			Title:    "Fatal Error",
			Detail:   "Something strange happened. Probably not your fault.",
			Instance: req.RequestURI,
		})
		if err != nil {
			res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR)
		}
		return nil, false
	}
	return &assertedObj, true
}

// The function parses a parameter of the query
// of the request. If the parameter is not present
// (empty string) it will not create an error and
// just return nil.
func OptionalQueryParamOrError[T any](
	name string,
	transform func(s string) (T, error),
	req *http.Request,
) (*T, error) {

	paramStr := req.URL.Query().Get(name)
	if paramStr != "" {

		if t, err := transform(paramStr); err != nil {
			return nil, err
		} else {
			return &t, nil
		}
	}
	return nil, nil
}

// Reads a generic argument struct from the requests
// body. It takes the codec as argument which is used to
// decode the struct from the request. If an error occurs
// nil and the error are returned.
func ReadStructFromBody[T any](req *http.Request, codec Codec[T]) (*T, error) {

	bodyBytes, err := ReadBody(req)
	if err != nil {
		return nil, err
	}

	return codec.Decode(bytes.NewReader(bodyBytes))
}

// Reads the body of a request into a byte array.
// If the body is empty, an empty array is returned.
// If an error occurs while reading the body, nil and
// the respective error is returned.
func ReadBody(req *http.Request) ([]byte, error) {

	if req.ContentLength < 0 {
		return nil, errors.New("malformed body")
	}

	body, err := io.ReadAll(req.Body)
	if err != nil {
		LogError("http-util", err)
		return nil, err
	}
	LogInfo("http-util", "read body from request. body="+string(body))
	return body, nil
}

// Executes a GET request at the given url.
// Use FormatUrl for to build the url.
// Headers can be defined using the headers map.
func HttpGet[T any](
	url string,
	headers map[string]string,
	codec Codec[T],
) (*T, int, error) {

	req, err := http.NewRequest(HTTP_GET, url, bytes.NewBufferString(""))
	if err != nil {
		return nil, -1, err
	}

	for k, v := range headers {
		req.Header.Add(k, v)
	}
	req.Header.Add("Accept", codec.HttpApplicationContentHeader())

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, -1, err
	}

	if codec == nil {
		return nil, res.StatusCode, err
	} else {
		resBody, err := codec.Decode(res.Body)
		return resBody, res.StatusCode, err
	}
}

// execute a POST request and parse response or retrieve error
// path- and query-parameters can be set to add query and path parameters
func HttpPost[T any, R any](
	url string,
	headers map[string]string,
	body *T,
	reqCodec Codec[T],
	resCodec Codec[R],
) (*R, int, error) {

	bodyEncoded, err := reqCodec.EncodeToBytes(body)
	if err != nil {
		return nil, -1, err
	}

	req, err := http.NewRequest(HTTP_POST, url, bytes.NewBuffer(bodyEncoded))
	if err != nil {
		return nil, -1, err
	}

	for k, v := range headers {
		req.Header.Add(k, v)
	}
	req.Header.Add("Accept", resCodec.HttpApplicationContentHeader())

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, -1, err
	}

	if resCodec == nil {
		return nil, res.StatusCode, err
	} else {
		resBody, err := resCodec.Decode(res.Body)
		return resBody, res.StatusCode, err
	}
}

// builds request URL containing the path and query
// parameters of the respective parameter map.
func FormatUrl(
	req string,
	pathParams map[string]string,
	queryParams map[string]string,
) string {

	return setUrlQuery(setUrlPath(req, pathParams), queryParams)
}

// Sets the parameters which are part of the url.
// The function expects each parameter in the path to be prefixed
// using a ':'. The function handles url as follows:
//
//	/some/:param/tobereplaced -> ':param' will be replaced with value.
//
// For replacements, the pathParams map must be supplied. The map contains
// the name of the parameter with the value mapped to it.
// The names MUST not contain the prefix ':'!
func setUrlPath(
	req string,
	pathParams map[string]string,
) string {

	if pathParams == nil || len(pathParams) < 1 {
		return req
	}

	var url = req
	for k, v := range pathParams {

		if !strings.HasPrefix(k, "/") {
			// prevent scheme postfix replacements
			url = strings.Replace(url, ":"+k, v, 1)
		}
	}
	return url
}

func setUrlQuery(
	req string,
	queryParams map[string]string,
) string {

	if queryParams == nil || len(queryParams) < 1 {
		return req
	}

	var url = req + "?"
	for k, v := range queryParams {

		url = strings.Join([]string{url, k, "=", v, "&"}, "")
	}

	url, _ = strings.CutSuffix(url, "&")
	return url
}