/
fiberstanza.go
157 lines (139 loc) · 4.7 KB
/
fiberstanza.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
package fiberstanza
import (
"context"
"fmt"
"io"
"net/http"
"github.com/StanzaSystems/sdk-go/keys"
"github.com/StanzaSystems/sdk-go/logging"
"github.com/StanzaSystems/sdk-go/stanza"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp/fasthttpadaptor"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)
// Client defines options for a new Stanza Client
type Client struct {
// Required
APIKey string // customer generated API key
// Optional
Name string // defines applications name
Release string // defines applications version
Environment string // defines applications environment
StanzaHub string // host:port (ipv4, ipv6, or resolvable hostname)
Guard []string // prefetch config for these guards
}
// Optional arguments
type Opt struct {
Headers http.Header
Feature string
PriorityBoost int32
DefaultWeight float32
Tags map[string]string
}
// New creates a new fiberstanza middleware fiber.Handler
func New(guardName string, opts ...Opt) fiber.Handler {
h, err := stanza.HttpServer(guardName, withOpts(opts...))
if err != nil {
logging.Error(fmt.Errorf("failed to create HTTP inbound handler: %v", err))
return func(c *fiber.Ctx) error {
// with no InboundHandler there is nothing we can do but fail open
logging.Error(fmt.Errorf("no HTTP inbound handler, failing open"))
if h != nil {
h.FailOpen(c.UserContext())
}
return c.Next()
}
}
return func(c *fiber.Ctx) error {
if h == nil {
// with no InboundHandler there is nothing we can do but fail open
logging.Error(fmt.Errorf("no HTTP inbound handler, failing open"))
return c.Next()
}
var req http.Request
if err := fasthttpadaptor.ConvertRequest(c.Context(), &req, true); err != nil {
// if we can't convert from fasthttp to http.Request, log the error and fail open
logging.Error(fmt.Errorf("failed to convert request from fasthttp: %v", err))
if h != nil {
h.FailOpen(c.UserContext())
}
return c.Next()
}
ctx, span, tokens := h.Start(&req)
defer span.End()
guard := h.Guard(ctx, span, tokens)
c.SetUserContext(guard.Context())
// Stanza Blocked
if guard.Blocked() {
span.SetAttributes(semconv.HTTPStatusCode(http.StatusTooManyRequests))
span.SetStatus(codes.Error, guard.BlockMessage())
c.SendString(guard.BlockMessage())
return c.SendStatus(http.StatusTooManyRequests)
}
// Stanza Allowed
err := c.Next() // intercept c.Next() for guard.End() status
span.SetAttributes(semconv.HTTPStatusCode(c.Response().StatusCode()))
span.SetStatus(h.HTTPServerStatus(c.Response().StatusCode()))
if err != nil {
span.RecordError(err)
// invokes the registered HTTP error handler
// to get the correct response status code
_ = c.App().Config().ErrorHandler(c, err)
guard.End(guard.Failure)
} else {
guard.End(guard.Success)
}
return nil
}
}
// Init is a fiberstanza helper function (passthrough to stanza.Init)
func Init(ctx context.Context, client Client) (func(), error) {
exit, err := stanza.Init(ctx, stanza.ClientOptions(client))
if err != nil {
return nil, err
}
return exit, nil
}
// HttpGet is a fiberstanza helper function (passthrough to stanza.HttpGet)
func HttpGet(c *fiber.Ctx, guardName string, url string, opts ...Opt) (*http.Response, error) {
var req http.Request
fasthttpadaptor.ConvertRequest(c.Context(), &req, true)
ctx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header))
return stanza.HttpGet(withHeaders(ctx, opts...), guardName, url, withOpts(opts...))
}
// HttpPost is a fiberstanza helper function (passthrough to stanza.HttpPost)
func HttpPost(c *fiber.Ctx, guardName string, url string, body io.Reader, opts ...Opt) (*http.Response, error) {
var req http.Request
fasthttpadaptor.ConvertRequest(c.Context(), &req, true)
ctx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header))
return stanza.HttpPost(withHeaders(ctx, opts...), guardName, url, body, withOpts(opts...))
}
func withHeaders(ctx context.Context, opts ...Opt) context.Context {
if len(opts) == 1 {
if opts[0].Headers != nil {
ctx = context.WithValue(ctx, keys.OutboundHeadersKey, opts[0].Headers)
}
}
return ctx
}
func withOpts(opts ...Opt) stanza.GuardOpt {
guardOpt := stanza.GuardOpt{}
if len(opts) == 1 {
if opts[0].Feature != "" {
guardOpt.Feature = &opts[0].Feature
}
if opts[0].PriorityBoost != 0 {
guardOpt.PriorityBoost = &opts[0].PriorityBoost
}
if opts[0].DefaultWeight != 0 {
guardOpt.DefaultWeight = &opts[0].DefaultWeight
}
if len(opts[0].Tags) > 0 {
guardOpt.Tags = &opts[0].Tags
}
}
return guardOpt
}