-
Notifications
You must be signed in to change notification settings - Fork 16
/
apirest.go
293 lines (267 loc) · 9.01 KB
/
apirest.go
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
282
283
284
285
286
287
288
289
290
291
292
293
package apirest
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path"
"strings"
"sync"
"time"
"go.vocdoni.io/dvote/httprouter"
"go.vocdoni.io/dvote/log"
)
const (
// MethodAccessTypePrivate for private requests
MethodAccessTypePrivate = "private"
// MethodAccessTypeQuota for rate-limited quota requests
MethodAccessTypeQuota = "quota"
// MethodAccessTypePublic for public requests
MethodAccessTypePublic = "public"
// MethodAccessTypeAdmin for admin requests
MethodAccessTypeAdmin = "admin"
namespace = "bearerStd"
bearerPrefix = "Bearer "
maxRequestBodyLog = 1024 // maximum request body size to log
)
// HTTPstatus* equal http.Status*, simple sugar to avoid importing http everywhere
const (
HTTPstatusOK = http.StatusOK
HTTPstatusNoContent = http.StatusNoContent
HTTPstatusBadRequest = http.StatusBadRequest
HTTPstatusInternalErr = http.StatusInternalServerError
HTTPstatusNotFound = http.StatusNotFound
HTTPstatusServiceUnavailable = http.StatusServiceUnavailable
)
// API is a namespace handler for the httpRouter with Bearer authorization
type API struct {
router *httprouter.HTTProuter
basePath string
authTokens sync.Map
adminToken string
adminTokenLock sync.RWMutex
verboseAuthLog bool
}
// APIdata is the data type used by the API.
// On handler functions Message.Data can be cast safely to this type.
type APIdata struct {
Data []byte
AuthToken string
}
// APIhandler is the handler function used by the bearer std API httprouter implementation
type APIhandler = func(*APIdata, *httprouter.HTTPContext) error
// APIerror is used by handler functions to wrap errors, assigning a unique error code
// and also specifying which HTTP Status should be used.
type APIerror struct {
Err error
Code int
HTTPstatus int
}
// MarshalJSON returns a JSON containing Err.Error() and Code. Field HTTPstatus is ignored.
//
// Example output: {"error":"account not found","code":4003}
func (e APIerror) MarshalJSON() ([]byte, error) {
// This anon struct is needed to actually include the error string,
// since it wouldn't be marshaled otherwise. (json.Marshal doesn't call Err.Error())
return json.Marshal(
struct {
Err string `json:"error"`
Code int `json:"code"`
}{
Err: e.Err.Error(),
Code: e.Code,
})
}
// Error returns the Message contained inside the APIerror
func (e APIerror) Error() string {
return e.Err.Error()
}
// Send serializes a JSON msg using APIerror.Message and APIerror.Code
// and passes that to ctx.Send()
func (e APIerror) Send(ctx *httprouter.HTTPContext) error {
msg, err := json.Marshal(e)
if err != nil {
log.Warn(err)
return ctx.Send([]byte("marshal failed"), HTTPstatusInternalErr)
}
return ctx.Send(msg, e.HTTPstatus)
}
// Withf returns a copy of APIerror with the Sprintf formatted string appended at the end of e.Err
func (e APIerror) Withf(format string, args ...any) APIerror {
return APIerror{
Err: fmt.Errorf("%w: %v", e.Err, fmt.Sprintf(format, args...)),
Code: e.Code,
HTTPstatus: e.HTTPstatus,
}
}
// With returns a copy of APIerror with the string appended at the end of e.Err
func (e APIerror) With(s string) APIerror {
return APIerror{
Err: fmt.Errorf("%w: %v", e.Err, s),
Code: e.Code,
HTTPstatus: e.HTTPstatus,
}
}
// WithErr returns a copy of APIerror with err.Error() appended at the end of e.Err
func (e APIerror) WithErr(err error) APIerror {
return APIerror{
Err: fmt.Errorf("%w: %v", e.Err, err.Error()),
Code: e.Code,
HTTPstatus: e.HTTPstatus,
}
}
// NewAPI returns an API initialized type
func NewAPI(router *httprouter.HTTProuter, baseRoute string) (*API, error) {
if router == nil {
panic("httprouter is nil")
}
if len(baseRoute) == 0 || baseRoute[0] != '/' {
return nil, fmt.Errorf("invalid base route (%s), it must start with /", baseRoute)
}
// Remove trailing slash
if len(baseRoute) > 1 {
baseRoute = strings.TrimSuffix(baseRoute, "/")
}
bsa := API{router: router, basePath: baseRoute}
router.AddNamespace(namespace, &bsa)
return &bsa, nil
}
// AuthorizeRequest is a function for the RouterNamespace interface.
// On private handlers checks if the supplied bearer token have still request credits
func (a *API) AuthorizeRequest(data any, accessType httprouter.AuthAccessType) (bool, error) {
msg, ok := data.(*APIdata)
if !ok {
panic("type is not bearerStandardApi")
}
switch accessType {
case httprouter.AccessTypeAdmin:
a.adminTokenLock.RLock()
defer a.adminTokenLock.RUnlock()
if msg.AuthToken != a.adminToken {
return false, fmt.Errorf("admin token not valid")
}
return true, nil
case httprouter.AccessTypePrivate:
_, ok = a.authTokens.Load(msg.AuthToken)
if !ok {
return false, fmt.Errorf("auth token not valid")
}
return true, nil
case httprouter.AccessTypeQuota:
remainingReqs, ok := a.authTokens.Load(msg.AuthToken)
if !ok || remainingReqs.(int64) < 1 {
return false, fmt.Errorf("no more requests available")
}
a.authTokens.Store(msg.AuthToken, remainingReqs.(int64)-1)
return true, nil
default:
return true, nil
}
}
// ProcessData processes the HTTP request and returns structured data.
// The body of the http requests and the bearer auth token are readed.
func (a *API) ProcessData(req *http.Request) (any, error) {
// Read and handle the request body
reqBody, err := io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("error reading request body: %v", err)
}
// Log the request body if it exists
if len(reqBody) > 0 {
displayReq := string(reqBody)
if len(displayReq) > maxRequestBodyLog {
displayReq = displayReq[:maxRequestBodyLog] + "..." // truncate for display
}
// This assumes you have a configured logging method. Replace with your logger instance.
log.Debugf("request: %s", displayReq)
}
// Get and validate the Authorization header
token := ""
authHeader := req.Header.Get("Authorization")
if authHeader != "" {
if !strings.HasPrefix(authHeader, bearerPrefix) {
return nil, errors.New("authorization header is not a Bearer token")
}
// Extract the token
token = strings.TrimPrefix(authHeader, bearerPrefix)
}
// Prepare the structured data
msg := &APIdata{
Data: reqBody,
AuthToken: token,
}
// If verbose logging is enabled, log the verbose authentication information
if a.verboseAuthLog && msg.AuthToken != "" {
fmt.Printf("[BearerAPI/%d/%s] %s {%s}\n", time.Now().Unix(), msg.AuthToken, req.URL.RequestURI(), reqBody)
}
return msg, nil
}
// RegisterMethod adds a new method under the URL pattern.
// The pattern URL can contain variable names by using braces, such as /send/{name}/hello
// The pattern can also contain wildcard at the end of the path, such as /send/{name}/hello/*
// The accessType can be of type private, public or admin.
func (a *API) RegisterMethod(pattern, HTTPmethod string, accessType string, handler APIhandler) error {
if pattern[0] != '/' {
panic("pattern must start with /")
}
routerHandler := func(msg httprouter.Message) {
bsaMsg := msg.Data.(*APIdata)
if err := handler(bsaMsg, msg.Context); err != nil {
// err should be an APIError, use those properties
if apierror, ok := err.(APIerror); ok {
err := apierror.Send(msg.Context)
if err != nil {
log.Warnf("couldn't send apierror: %v", err)
}
return
}
// else, it's a plain error (this shouldn't happen)
// send it in plaintext with HTTP Status 500 Internal Server Error
if err := msg.Context.Send([]byte(err.Error()), HTTPstatusInternalErr); err != nil {
log.Warn(err)
}
}
}
path := path.Join(a.basePath, pattern)
switch accessType {
case "public":
a.router.AddPublicHandler(namespace, path, HTTPmethod, routerHandler)
case "quota":
a.router.AddQuotaHandler(namespace, path, HTTPmethod, routerHandler)
case "private":
a.router.AddPrivateHandler(namespace, path, HTTPmethod, routerHandler)
case "admin":
a.router.AddAdminHandler(namespace, path, HTTPmethod, routerHandler)
default:
return fmt.Errorf("method access type not implemented: %s", accessType)
}
return nil
}
// SetAdminToken sets the bearer admin token capable to execute admin handlers
func (a *API) SetAdminToken(bearerToken string) {
a.adminTokenLock.Lock()
defer a.adminTokenLock.Unlock()
a.adminToken = bearerToken
}
// AddAuthToken adds a new bearer token capable to perform up to n requests
func (a *API) AddAuthToken(bearerToken string, requests int64) {
a.authTokens.Store(bearerToken, requests)
}
// DelAuthToken removes a bearer token (will be not longer valid)
func (a *API) DelAuthToken(bearerToken string) {
a.authTokens.Delete(bearerToken)
}
// GetAuthTokens returns the number of pending requests credits for a bearer token
func (a *API) GetAuthTokens(bearerToken string) int64 {
ts, ok := a.authTokens.Load(bearerToken)
if !ok {
return 0
}
return ts.(int64)
}
// EnableVerboseAuthLog prints on stdout the details of every request performed with auth token.
// It can be used for keeping track of private/admin actions on a service.
func (a *API) EnableVerboseAuthLog() {
a.verboseAuthLog = true
}