This repository has been archived by the owner on Apr 15, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
conf.go
347 lines (286 loc) · 7.86 KB
/
conf.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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main
import (
"bytes"
"encoding/json"
"fmt"
"github.com/bingoohuang/golog/pkg/rotate"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/antchfx/xmlquery"
"github.com/bingoohuang/gg/pkg/fn"
"github.com/bingoohuang/gg/pkg/rest"
"github.com/bingoohuang/jj"
"github.com/goccy/go-yaml"
_ "embed"
)
// Conf defines the structure to unmrshal the configuration yaml file.
type Conf struct {
Ifaces []string `yaml:"ifaces"`
Bpfs []string `yaml:"bpfs"`
MetricsKeys []string `yaml:"metricsKeys"`
Relays []Replay `yaml:"relays"`
}
// ReplayCondition is the condition which should be specified for replay requests.
type ReplayCondition struct {
MethodPatterns []string `yaml:"methodPatterns"`
URLPatterns []string `yaml:"urlPatterns"`
}
// MatchMethod tests the HTTP method matches the specified pattern.
func (c *ReplayCondition) MatchMethod(method string) bool {
if len(c.MethodPatterns) == 0 {
return true
}
for _, m := range c.MethodPatterns {
yes := !strings.HasPrefix(m, "!")
if !yes {
m = m[1:]
}
if yes == fn.Match(m, method, fn.WithCaseSensitive(true)) {
return true
}
}
return false
}
// MatchURI tests the HTTP RequestURI matches the specified pattern.
func (c *ReplayCondition) MatchURI(uri string) bool {
if len(c.URLPatterns) == 0 {
return true
}
for _, m := range c.URLPatterns {
yes := !strings.HasPrefix(m, "!")
if !yes {
m = m[1:]
}
if yes == fn.Match(m, uri, fn.WithCaseSensitive(true)) {
return true
}
}
return false
}
// Matches test the http request matches the replay condition or not.
func (c *ReplayCondition) Matches(method string, uri string, _ http.Header) bool {
return c.MatchMethod(method) && c.MatchURI(uri)
}
// Replay defines the replay action in configuration.
type Replay struct {
Addrs []string `yaml:"addrs"`
Conditions []ReplayCondition `yaml:"conditions"`
RecordFails []RecordFail `yaml:"recordFails"`
FailLogFile string `yaml:"failLogFile"`
failLog *rotate.Rotate
}
func (r *Replay) setup() {
if r.FailLogFile == "" {
return
}
var err error
r.failLog, err = rotate.New(r.FailLogFile)
if err != nil {
log.Fatalf("failed to create rotate log file %s, error: %v", r.FailLogFile, err)
}
}
// RecordFail defines the structure of RecordFail.
type RecordFail struct {
Key string `yaml:"key"`
Path string `yaml:"path"`
}
// Relay relays the http requests.
func (r *Replay) Relay(method string, uri string, header http.Header, body []byte) (matches bool) {
if !r.Matches(method, uri, header) {
return false
}
pairs := map[string]string{XHttpCapRelay: "true"}
for k, v := range header {
pairs[k] = v[0]
}
errMsgs := make([]string, 0, len(r.Addrs))
for _, addr := range r.Addrs {
u := fmt.Sprintf("http://%s%s", addr, uri)
r, err := rest.Rest{Method: method, Addr: u, Headers: pairs, Body: body}.Do()
if err != nil {
log.Printf("E! Replay %s %s error:%v", method, u, err)
errMsgs = append(errMsgs, fmt.Sprintf("write %s fail:%v", u, err))
} else if r != nil {
log.Printf("Replay %s %s status: %d, message: %s", method, u, r.Status, r.Body)
if r.Status < 200 || r.Status >= 300 {
errMsgs = append(errMsgs, fmt.Sprintf("write %s status:%v", u, r.Status))
}
}
}
if len(errMsgs) == 0 {
return true
}
vm := r.recordReqValues(header, body)
vmJSON, _ := json.Marshal(struct {
Time string
Keys map[string]string
Errors []string
}{
Time: time.Now().Format(`2006-01-02 15:04:05.000`),
Keys: vm,
Errors: errMsgs,
})
log.Printf("Records failed request: %s", vmJSON)
if r.failLog != nil {
_, _ = r.failLog.Write(vmJSON)
_, _ = r.failLog.Write([]byte("\n"))
}
return true
}
func (r *Replay) recordReqValues(headers http.Header, body []byte) map[string]string {
switch contentType := headers.Get("Content-Type"); {
case strings.Contains(contentType, "application/xml"):
return r.recordXMLValues(body)
case strings.Contains(contentType, "application/json"):
return r.recordJSONValues(body)
}
return nil
}
func (r *Replay) recordJSONValues(b []byte) map[string]string {
vm := make(map[string]string, len(r.RecordFails))
for _, xp := range r.RecordFails {
value := jj.GetBytes(b, xp.Path)
vm[xp.Key] = value.String()
}
return vm
}
func (r *Replay) recordXMLValues(b []byte) map[string]string {
doc, err := xmlquery.Parse(bytes.NewReader(b))
if err != nil {
log.Printf("E! failed to parse xml %s, errors: %v", b, err)
return nil
}
vm := make(map[string]string, len(r.RecordFails))
for _, xp := range r.RecordFails {
l := xmlquery.Find(doc, xp.Path)
values := make([]string, len(l))
for i, n := range l {
values[i] = n.Data
}
vm[xp.Key] = strings.Join(values, ",")
}
return vm
}
// Matches tests the request matches the replay's specified conditions or not.
// If matches not condition, return false directly.
// If no yes conditions defined, returns true.
// Or if any yes conditions matches, return true.
// Else return false.
func (r *Replay) Matches(method string, uri string, headers http.Header) bool {
if len(r.Conditions) == 0 {
return true
}
for _, cond := range r.Conditions {
if cond.Matches(method, uri, headers) {
return true
}
}
return false
}
// requestReplayer defines the func prototype to replay a request.
// return -1: no relays defined, or number of replays applied.
type requestReplayer func(method, requestURI string, headers http.Header, body []byte) int
const XHttpCapRelay = "X-Httpcap-Replay"
func (c *Conf) createRequestReplayer() requestReplayer {
if len(c.Relays) == 0 {
return func(_, _ string, _ http.Header, _ []byte) int { return -1 }
}
return func(method, requestURI string, header http.Header, body []byte) int {
if header.Get(XHttpCapRelay) == "true" {
log.Printf("%s = true, ignored", XHttpCapRelay)
return 1
}
found := 0
for _, relay := range c.Relays {
if relay.Relay(method, requestURI, header, body) {
found++
}
}
return found
}
}
// UnmarshalConfFile parses the conf yaml file.
func UnmarshalConfFile(confFile string) (*Conf, error) {
confBytes, err := os.ReadFile(confFile)
if err != nil {
return nil, fmt.Errorf("read conf file %s error: %q", confFile, err)
}
ci := &Conf{}
if err := yaml.Unmarshal(confBytes, ci); err != nil {
return nil, fmt.Errorf("decode conf file %s error:%q", confFile, err)
}
return ci, nil
}
// ParseConfFile parses conf yaml file and flags to *Conf.
func ParseConfFile(confFile, bpf, ifaces string) *Conf {
conf := loadConfFile(confFile)
if conf == nil {
conf = &Conf{}
}
if ifaces != "" {
conf.Ifaces = Split(ifaces)
}
if bpf != "" {
conf.Bpfs = []string{bpf}
}
if len(conf.Bpfs) == 0 {
log.Fatal("At least one TCP port should be specified")
}
conf.fixIfaces()
confJSON, _ := json.Marshal(conf)
log.Printf("Configuration: %s", confJSON)
conf.setup()
return conf
}
func loadConfFile(confFile string) *Conf {
if confFile == "" {
if s, err := os.Stat(defaultConfFile); err != nil || s.IsDir() {
return nil // not exists
}
confFile = defaultConfFile
}
conf, err := UnmarshalConfFile(confFile)
if err != nil {
log.Fatal(err)
}
return conf
}
func (c *Conf) fixIfaces() {
hasAny := len(c.Ifaces) == 0
if hasAny {
c.Ifaces = []string{"any"}
return
}
var availIfaces map[string]Iface
usedIfaces := make([]string, 0, len(c.Ifaces))
for _, ifa := range c.Ifaces {
if ifa == "any" {
c.Ifaces = []string{"any"}
return
}
if _, err := os.Stat(ifa); err == nil {
usedIfaces = append(usedIfaces, ifa)
continue
}
if availIfaces == nil {
availIfaces = ListIfaces()
}
if _, ok := availIfaces[ifa]; ok {
usedIfaces = append(usedIfaces, ifa)
} else {
log.Printf("W! iface name %s is unknown, it will be ignored", ifa)
}
}
if len(usedIfaces) == 0 {
log.Fatalf("E! at least one valid iface name should be specified")
}
c.Ifaces = usedIfaces
}
func (c *Conf) setup() {
for i := range c.Relays {
c.Relays[i].setup()
}
}