diff --git a/Makefile b/Makefile index efa8ecba..6bdf6368 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ test-race: $(needs-dev-container) $(needs-vendors) $(needs-protobufs) #----------------------------------------------------------------------------- $(needs-vendors): go.mod $(global-deps) $(needs-dev-container) - $(call dockerize, go mod vendor) + $(call dockerize, go mod vendor -v) mkdir -p $(@D) && touch $@ .PHONY: vendor diff --git a/agent/agent.go b/agent/agent.go index 05a97eee..064ae7c2 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -24,10 +24,11 @@ func start() { } var ( - logger = plog.NewLogger("sqreen/agent") - eventMng *eventManager - cancel context.CancelFunc - isDone chan struct{} + logger = plog.NewLogger("sqreen/agent") + eventMng *eventManager + metricsMng *metricsManager + cancel context.CancelFunc + isDone chan struct{} ) func agent() { @@ -35,9 +36,11 @@ func agent() { err := recover() if err != nil { logger.Error("agent stopped: ", err) - return + } else { + logger.Info("agent successfully stopped") } - logger.Info("agent successfully stopped") + // Signal we are done + close(isDone) }() plog.SetLevelFromString(config.LogLevel()) @@ -53,7 +56,9 @@ func agent() { return } - appLoginRes, err := appLogin(ctx, client) + token := config.BackendHTTPAPIToken() + appName := config.AppName() + appLoginRes, err := appLogin(ctx, client, token, appName) if checkErr(err) { return } @@ -80,12 +85,19 @@ func agent() { // Start the event manager's loop go eventMng.Loop(ctx, client, sessionID) + metricsMng = newMetricsManager(ctx) + // Start the heartbeat's loop for { select { case <-ticker: logger.Debug("heartbeat") - var appBeatReq api.AppBeatRequest + + metrics := metricsMng.getObservations() + appBeatReq := api.AppBeatRequest{ + Metrics: metrics, + } + _, err := client.AppBeat(&appBeatReq, sessionID) if err != nil { logger.Error("heartbeat failed: ", err) @@ -101,8 +113,6 @@ func agent() { return } logger.Debug("successfully logged out") - // Signal we are done - close(isDone) return } } @@ -134,7 +144,7 @@ func newEventManager(rulespackID string, count int, maxStaleness time.Duration) } } -func (m *eventManager) addEvent(r *httpRequestRecord) { +func (m *eventManager) add(r *httpRequestRecord) { select { case m.eventsChan <- r: return @@ -203,12 +213,12 @@ func (m *eventManager) send(client *backend.Client, sessionID string) { m.req.Batch = m.req.Batch[0:0] } -func addEvent(r *httpRequestRecord) { +func addTrackEvent(r *httpRequestRecord) { if config.Disable() || eventMng == nil { // Disabled or not yet initialized agent return } - eventMng.addEvent(r) + eventMng.add(r) } // Helper function returning true when having to exit the agent and panic-ing diff --git a/agent/backend/api/api.pb.go b/agent/backend/api/api.pb.go index d9ea6881..0baa9f72 100644 --- a/agent/backend/api/api.pb.go +++ b/agent/backend/api/api.pb.go @@ -241,9 +241,13 @@ func (m *CommandResponse) XXX_DiscardUnknown() { var xxx_messageInfo_CommandResponse proto.InternalMessageInfo type MetricResponse struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Start time.Time `protobuf:"bytes,2,opt,name=start,proto3,stdtime" json:"start"` + Finish time.Time `protobuf:"bytes,3,opt,name=finish,proto3,stdtime" json:"finish"` + Observation Struct `protobuf:"bytes,4,opt,name=observation,proto3,customtype=Struct" json:"observation"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *MetricResponse) Reset() { *m = MetricResponse{} } @@ -773,6 +777,7 @@ var xxx_messageInfo_RequestRecord_Observed_SDKEvent proto.InternalMessageInfo type RequestRecord_Observed_SDKEvent_Options struct { Properties *Struct `protobuf:"bytes,1,opt,name=properties,proto3,customtype=Struct" json:"properties,omitempty"` + UserIdentifiers *Struct `protobuf:"bytes,2,opt,name=user_identifiers,json=userIdentifiers,proto3,customtype=Struct" json:"user_identifiers,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -942,116 +947,121 @@ func init() { func init() { proto.RegisterFile("agent/backend/api/api.proto", fileDescriptor_128e40d1f7699e73) } var fileDescriptor_128e40d1f7699e73 = []byte{ - // 1741 bytes of a gzipped FileDescriptorProto + // 1814 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x58, 0xcd, 0x8f, 0x1c, 0x47, - 0x15, 0x4f, 0xef, 0x7c, 0xf5, 0xbc, 0xd9, 0x99, 0x5d, 0x17, 0x81, 0x0c, 0x9d, 0xb0, 0x6b, 0x06, - 0x45, 0x38, 0x04, 0x7a, 0xa3, 0x8d, 0x64, 0x82, 0x05, 0x42, 0x3b, 0xb6, 0x91, 0x97, 0x98, 0x78, - 0xe9, 0xb5, 0x7c, 0x80, 0xc3, 0x50, 0xd3, 0x5d, 0xdb, 0xdb, 0x9a, 0xfe, 0x4a, 0x55, 0xf5, 0xd8, - 0x9b, 0xff, 0x00, 0x71, 0xe1, 0xce, 0x9f, 0xc0, 0x19, 0x89, 0x0b, 0xe2, 0x9a, 0x13, 0x17, 0x6e, - 0x39, 0x18, 0xf9, 0x86, 0xc2, 0x85, 0x23, 0xb7, 0xa0, 0x7a, 0x55, 0xd5, 0xd3, 0xe3, 0xec, 0x38, - 0xde, 0xf8, 0xb0, 0x52, 0xbd, 0x5f, 0xd7, 0xfb, 0x55, 0xd5, 0xfb, 0x9e, 0x85, 0x37, 0x69, 0xcc, - 0x72, 0x79, 0x30, 0xa7, 0xe1, 0x82, 0xe5, 0xd1, 0x01, 0x2d, 0x13, 0xf5, 0xe7, 0x97, 0xbc, 0x90, - 0x05, 0x19, 0x8b, 0x8f, 0x39, 0x63, 0xb9, 0x8f, 0x7b, 0x7c, 0xb3, 0xc7, 0xa7, 0x65, 0xe2, 0xed, - 0xc7, 0x45, 0x11, 0xa7, 0xec, 0x00, 0xf7, 0xcd, 0xab, 0xb3, 0x03, 0x99, 0x64, 0x4c, 0x48, 0x9a, - 0x95, 0x5a, 0xd5, 0x7b, 0xeb, 0xf9, 0x0d, 0x42, 0xf2, 0x2a, 0x94, 0xe6, 0xeb, 0x8f, 0xe2, 0x44, - 0x9e, 0x57, 0x73, 0x3f, 0x2c, 0xb2, 0x83, 0xb8, 0x88, 0x8b, 0xd5, 0x36, 0x25, 0xa1, 0x80, 0x2b, - 0xbd, 0x7d, 0xf2, 0xe7, 0x0e, 0xec, 0x1c, 0x95, 0xe5, 0xfd, 0x22, 0x4e, 0xf2, 0x80, 0x7d, 0x5c, - 0x31, 0x21, 0xc9, 0x3b, 0xb0, 0x3b, 0xaf, 0xf2, 0x28, 0x65, 0x33, 0x91, 0xc4, 0x39, 0x95, 0x15, - 0x67, 0x63, 0xe7, 0xba, 0x73, 0xa3, 0x1f, 0xec, 0x68, 0xfc, 0xd4, 0xc2, 0x84, 0xc2, 0x70, 0x49, - 0x79, 0x52, 0x54, 0x62, 0x96, 0xe4, 0x67, 0x85, 0x18, 0x6f, 0x5d, 0x77, 0x6e, 0x0c, 0x0e, 0x6f, - 0xfa, 0x9b, 0x9e, 0xe7, 0x3f, 0x77, 0x98, 0xff, 0x48, 0xab, 0x1f, 0x2b, 0xed, 0x69, 0xfb, 0xd3, - 0xa7, 0xfb, 0xaf, 0x05, 0xdb, 0xcb, 0x06, 0x46, 0xbe, 0x03, 0x80, 0x2c, 0x33, 0x79, 0x51, 0xb2, - 0x71, 0x0b, 0xef, 0xd1, 0x47, 0xe4, 0xe1, 0x45, 0xc9, 0xc8, 0xf7, 0x60, 0xa8, 0x3f, 0x2f, 0x19, - 0x17, 0x49, 0x91, 0x8f, 0xdb, 0xb8, 0x63, 0x1b, 0xc1, 0x47, 0x1a, 0x23, 0x6f, 0x40, 0xaf, 0x10, - 0x9a, 0xa0, 0x83, 0x9f, 0xbb, 0x85, 0x40, 0x6d, 0x0f, 0xdc, 0xf3, 0x42, 0xc8, 0x9c, 0x66, 0x6c, - 0xdc, 0xc5, 0x2f, 0xb5, 0x4c, 0xbe, 0x0b, 0xdb, 0xbc, 0xca, 0x95, 0xf5, 0xb5, 0x66, 0x0f, 0xbf, - 0x0f, 0x0c, 0x86, 0xea, 0xdf, 0x87, 0x1d, 0xbb, 0xc5, 0x1e, 0xef, 0xe2, 0xae, 0x91, 0x81, 0xed, - 0x05, 0xde, 0x86, 0xd1, 0x19, 0xa7, 0x19, 0x7b, 0x5c, 0xf0, 0x85, 0x66, 0xeb, 0xe3, 0xbe, 0x61, - 0x8d, 0x22, 0xdf, 0xbb, 0x70, 0x6d, 0xb5, 0xcd, 0x32, 0x02, 0xee, 0xdc, 0xad, 0x3f, 0x58, 0xce, - 0xeb, 0x30, 0x60, 0xf9, 0x32, 0xe1, 0x45, 0x9e, 0xb1, 0x5c, 0x8e, 0x07, 0xfa, 0x7a, 0x0d, 0xc8, - 0xfb, 0x87, 0x03, 0xdb, 0x4d, 0xfb, 0x92, 0x0f, 0xa0, 0xad, 0x6e, 0x85, 0xde, 0x1c, 0x1c, 0x7a, - 0xbe, 0x8e, 0x24, 0xdf, 0x86, 0x88, 0xff, 0xd0, 0x86, 0xda, 0xd4, 0x55, 0x9e, 0xf8, 0xe3, 0xbf, - 0xf6, 0x9d, 0x00, 0x35, 0xc8, 0x2e, 0xb4, 0xca, 0x24, 0x42, 0xf3, 0x0f, 0x03, 0xb5, 0x24, 0x04, - 0xda, 0xa5, 0x82, 0xda, 0x08, 0xe1, 0x5a, 0x61, 0xac, 0x4a, 0x22, 0x34, 0xf2, 0x30, 0xc0, 0x35, - 0x62, 0x71, 0x12, 0xa1, 0x79, 0x15, 0x16, 0x27, 0x91, 0x62, 0x53, 0xdb, 0x7a, 0x9a, 0xad, 0xd2, - 0x88, 0xda, 0xe4, 0x6a, 0x24, 0xd6, 0x7a, 0xe8, 0x16, 0x6d, 0x28, 0x5c, 0x4f, 0xfe, 0xda, 0x82, - 0xdd, 0x55, 0x00, 0x89, 0xb2, 0xc8, 0x05, 0x53, 0x01, 0x22, 0x98, 0x50, 0x26, 0x99, 0x25, 0x91, - 0x09, 0xd4, 0xbe, 0x41, 0x8e, 0x23, 0xf2, 0x2d, 0xe8, 0x0a, 0x49, 0x65, 0xa5, 0x63, 0xd3, 0x0d, - 0x8c, 0x44, 0x7e, 0x09, 0x6e, 0x58, 0x64, 0x19, 0xcd, 0x23, 0x31, 0x6e, 0x5d, 0x6f, 0xdd, 0x18, - 0x1c, 0xde, 0xd8, 0x1c, 0xb5, 0xb7, 0xf5, 0x4e, 0x13, 0xb4, 0x26, 0x4e, 0x6b, 0x7d, 0xf2, 0x10, - 0xdc, 0x33, 0x86, 0x19, 0x21, 0xd0, 0x1e, 0x83, 0xc3, 0xc3, 0x97, 0xc9, 0x00, 0xfd, 0x00, 0xff, - 0x17, 0x5a, 0xd5, 0xb2, 0x5a, 0x26, 0x15, 0xb5, 0x25, 0x0d, 0x17, 0x33, 0x63, 0xd0, 0x7e, 0xd0, - 0x55, 0xe2, 0x71, 0x44, 0x6e, 0x41, 0x87, 0x57, 0x29, 0x13, 0xe3, 0x2e, 0xde, 0x7b, 0x6f, 0xf3, - 0x59, 0x41, 0x95, 0x5a, 0x5e, 0xad, 0xe2, 0x2d, 0xa1, 0x67, 0xce, 0x53, 0x86, 0x9b, 0x53, 0x19, - 0x9e, 0xcf, 0x44, 0xf2, 0x89, 0x8e, 0x89, 0x61, 0xd0, 0x47, 0xe4, 0x34, 0xf9, 0x04, 0x33, 0x2b, - 0xa3, 0x4f, 0x66, 0x42, 0xd2, 0x94, 0xe5, 0x4c, 0x68, 0xfb, 0x0d, 0x83, 0xed, 0x8c, 0x3e, 0x39, - 0xb5, 0x98, 0xca, 0x80, 0x73, 0x46, 0xb9, 0x9c, 0x33, 0x2a, 0x67, 0x11, 0x4b, 0xe9, 0x85, 0x89, - 0x91, 0x51, 0x0d, 0xdf, 0x51, 0xe8, 0xe4, 0x04, 0x46, 0xeb, 0x46, 0xac, 0x1d, 0xec, 0xac, 0x1c, - 0xac, 0x9c, 0x55, 0x52, 0x4e, 0x33, 0x75, 0x58, 0x4b, 0xbf, 0x58, 0x49, 0x6a, 0x6f, 0x55, 0x99, - 0xf8, 0xeb, 0x07, 0xb8, 0x9e, 0x5c, 0x83, 0x9d, 0x9a, 0x51, 0x5b, 0x72, 0xb2, 0x0b, 0xa3, 0x5f, - 0x31, 0xc9, 0x93, 0xb0, 0x46, 0xfe, 0xb2, 0x05, 0xa3, 0xa3, 0xb2, 0x9c, 0x32, 0x2a, 0xed, 0xb9, - 0x0b, 0xd8, 0x31, 0x8e, 0x9b, 0x71, 0x26, 0xaa, 0x54, 0x8a, 0xb1, 0x83, 0x76, 0xfc, 0xe9, 0x0b, - 0x7d, 0xd6, 0xa0, 0x58, 0x85, 0x03, 0xaa, 0xdf, 0xcd, 0x25, 0xbf, 0x30, 0x56, 0x1e, 0x85, 0x6b, - 0x9f, 0xc8, 0x3d, 0xe8, 0x65, 0x78, 0x23, 0xfd, 0xa2, 0x17, 0x06, 0xd9, 0xfa, 0xd5, 0x0d, 0xa1, - 0x55, 0xf7, 0x52, 0xf8, 0xc6, 0x25, 0xc7, 0xaa, 0xc4, 0x59, 0xb0, 0x0b, 0x63, 0x44, 0xb5, 0x24, - 0x3f, 0x87, 0xce, 0x92, 0xa6, 0x15, 0x33, 0xb5, 0xf8, 0x9d, 0x97, 0x88, 0x6a, 0x7d, 0x62, 0xa0, - 0xf5, 0x6e, 0x6d, 0x7d, 0xe0, 0x4c, 0x2a, 0x6c, 0x0b, 0xfa, 0xcd, 0x26, 0xcf, 0x9a, 0x09, 0xe3, - 0xbc, 0x62, 0xc2, 0x6c, 0x48, 0xca, 0xc9, 0xdf, 0x1d, 0xd8, 0x9e, 0xaa, 0x08, 0xb4, 0xce, 0xba, - 0x07, 0x1d, 0x8c, 0x48, 0x73, 0xe2, 0x0f, 0x37, 0x9f, 0xd8, 0x54, 0xf3, 0xef, 0x2e, 0x59, 0x6e, - 0x4f, 0xd5, 0x04, 0x1e, 0x83, 0x0e, 0xa2, 0x2a, 0xec, 0xd9, 0xb2, 0x6e, 0x28, 0xa6, 0x5e, 0x20, - 0x82, 0x35, 0xf8, 0x67, 0xd0, 0x41, 0xc1, 0x98, 0xef, 0x8d, 0x2f, 0x15, 0xc9, 0x53, 0x6c, 0xb7, - 0xd3, 0x91, 0x22, 0xff, 0xec, 0xe9, 0x7e, 0x57, 0xcb, 0x81, 0xd6, 0x9a, 0x74, 0xa1, 0xad, 0x92, - 0x6e, 0xf2, 0x4f, 0x07, 0xe0, 0x0e, 0x2b, 0x59, 0x1e, 0xb1, 0x3c, 0xbc, 0xb8, 0x34, 0xd8, 0xc7, - 0xd0, 0xb3, 0x35, 0x7e, 0x0b, 0x61, 0x2b, 0xea, 0xb6, 0x94, 0xb1, 0x92, 0xc6, 0xb6, 0xe3, 0xd5, - 0x32, 0xb9, 0x0d, 0x5d, 0x51, 0x54, 0x3c, 0x64, 0xa6, 0xd2, 0xbc, 0xbb, 0xd9, 0x24, 0xab, 0xf3, - 0xfd, 0x53, 0x54, 0x09, 0x8c, 0xaa, 0x77, 0x13, 0xba, 0x1a, 0xd9, 0x74, 0x31, 0xce, 0xb2, 0x42, - 0x32, 0x9b, 0x86, 0x56, 0x9c, 0x7c, 0x41, 0x60, 0x68, 0x6c, 0x1c, 0xb0, 0xb0, 0xe0, 0x51, 0xf3, - 0x11, 0xce, 0xfa, 0x23, 0xde, 0x57, 0xfd, 0x33, 0x65, 0xc2, 0xd6, 0x30, 0x7c, 0xe3, 0x74, 0xf7, - 0xf3, 0xa7, 0xfb, 0x6b, 0xb8, 0xea, 0xa8, 0x46, 0x3a, 0x8e, 0xc8, 0x0f, 0xa0, 0x1f, 0xa6, 0x89, - 0xf2, 0x4e, 0x52, 0xea, 0xa7, 0x4f, 0x87, 0x9f, 0x3f, 0xdd, 0x5f, 0x81, 0x81, 0xab, 0x97, 0xc7, - 0x25, 0x79, 0xa0, 0xae, 0x89, 0x77, 0x31, 0xa6, 0x38, 0x78, 0x41, 0x21, 0x6c, 0x5e, 0xda, 0x5f, - 0x0f, 0x4b, 0xcb, 0x42, 0x02, 0x70, 0xb9, 0x89, 0x76, 0xac, 0xb8, 0x83, 0xc3, 0xf7, 0x5e, 0x9e, - 0x71, 0x2d, 0x6b, 0x6b, 0x1e, 0xc5, 0x59, 0xcc, 0x05, 0xe3, 0x4b, 0xa6, 0x5b, 0xe0, 0x15, 0x38, - 0x1f, 0x18, 0x3d, 0xcb, 0x69, 0x79, 0xbc, 0x2f, 0x5a, 0xd0, 0xb3, 0x09, 0xb2, 0x0b, 0x2d, 0x5e, - 0xb7, 0x3d, 0xb5, 0x24, 0x8f, 0xa0, 0x77, 0xce, 0x68, 0xc4, 0xb8, 0x2d, 0x39, 0x37, 0xaf, 0x68, - 0x16, 0xff, 0x1e, 0xaa, 0x5b, 0xeb, 0x18, 0x32, 0x15, 0x29, 0x4b, 0xc6, 0xe7, 0xb6, 0x06, 0xab, - 0x35, 0x0e, 0x01, 0x54, 0x9e, 0x9b, 0xa1, 0x0b, 0xd7, 0xe4, 0xdb, 0xe0, 0x72, 0xfa, 0x78, 0x86, - 0xb8, 0xee, 0x5b, 0x3d, 0x4e, 0x1f, 0x9f, 0xa8, 0x4f, 0x04, 0xda, 0x6a, 0xbc, 0x32, 0xa3, 0x16, - 0xae, 0x91, 0xa2, 0xe0, 0xd2, 0x8c, 0x57, 0xb8, 0x26, 0x6f, 0x42, 0x5f, 0x47, 0x9c, 0x8a, 0x02, - 0x3d, 0x51, 0xb9, 0x1a, 0x38, 0x2e, 0xc9, 0x3e, 0x0c, 0xcc, 0x47, 0xd4, 0xd3, 0xf3, 0x01, 0x68, - 0xe8, 0x44, 0x69, 0xab, 0xe2, 0x12, 0x9e, 0xb3, 0x8c, 0x99, 0xd1, 0xc9, 0x48, 0x2a, 0xf1, 0x2b, - 0xc1, 0xf8, 0x0c, 0xcd, 0x60, 0xe6, 0xa5, 0xbe, 0x42, 0x8e, 0x14, 0xa0, 0xa3, 0xfe, 0x8c, 0x71, - 0xc6, 0xc7, 0xdb, 0xe6, 0xda, 0x5a, 0x24, 0x0f, 0xeb, 0xae, 0x34, 0xfc, 0xaa, 0xf1, 0xf6, 0x72, - 0x83, 0x9e, 0xa0, 0xb6, 0x31, 0xa8, 0xe1, 0xf2, 0xde, 0x83, 0xae, 0x36, 0xf4, 0x25, 0x35, 0xfc, - 0xf5, 0x66, 0x0d, 0xef, 0x9b, 0xc2, 0xec, 0xb9, 0xd0, 0xd5, 0x4c, 0x5e, 0x0a, 0x6e, 0x5d, 0x97, - 0x57, 0xb5, 0x54, 0xb7, 0x70, 0x3b, 0xe0, 0xbc, 0x0d, 0xa3, 0xb0, 0xc8, 0xa5, 0x4a, 0x9b, 0x94, - 0xe5, 0xb1, 0x3c, 0x37, 0x0d, 0x7c, 0x68, 0xd0, 0xfb, 0x08, 0xaa, 0x31, 0xd7, 0x6e, 0x6b, 0x4c, - 0xd8, 0x03, 0x83, 0xa9, 0x92, 0xe8, 0xfd, 0x7e, 0x00, 0xae, 0x0d, 0x46, 0xf2, 0x6b, 0xe8, 0x51, - 0x29, 0x69, 0xb8, 0xb0, 0x5d, 0xe0, 0xc7, 0x57, 0x8d, 0x67, 0xff, 0x08, 0xf5, 0x03, 0xcb, 0x43, - 0x3e, 0x84, 0x96, 0x88, 0x16, 0x26, 0x5a, 0x7f, 0x72, 0x65, 0xba, 0xd3, 0x3b, 0x1f, 0x62, 0x65, - 0x0f, 0x14, 0x0b, 0x89, 0xe1, 0x9a, 0x26, 0x98, 0xb1, 0x27, 0x21, 0x2b, 0x65, 0x52, 0xe4, 0x76, - 0xc0, 0xbb, 0x75, 0x65, 0xea, 0xbb, 0x96, 0x22, 0xd8, 0xd5, 0xaa, 0x35, 0x20, 0xc8, 0xef, 0x60, - 0x5b, 0x67, 0x24, 0xd5, 0x67, 0xb4, 0xbf, 0x6a, 0x88, 0xd8, 0x70, 0xc6, 0x83, 0x15, 0x49, 0xb0, - 0xc6, 0x48, 0x7e, 0x0b, 0x83, 0x88, 0x4a, 0x3a, 0x2b, 0x8b, 0x24, 0x97, 0x62, 0xdc, 0xf9, 0x9a, - 0x8f, 0xb8, 0x43, 0x25, 0x3d, 0x51, 0x14, 0x01, 0x44, 0x76, 0x29, 0xbc, 0xbf, 0x39, 0xd0, 0xd5, - 0x8e, 0xc0, 0x74, 0xab, 0x52, 0x36, 0x6b, 0x34, 0x02, 0x57, 0x01, 0x1f, 0xa9, 0x66, 0x40, 0xa0, - 0x2d, 0x55, 0x89, 0xd5, 0x8d, 0x1a, 0xd7, 0x2a, 0x3c, 0xf5, 0xcf, 0x3d, 0x1d, 0x2c, 0x5a, 0x20, - 0x6f, 0x41, 0x5f, 0xdd, 0x46, 0x72, 0x8a, 0xcd, 0x49, 0x35, 0x8e, 0x15, 0x50, 0xff, 0xf6, 0xe8, - 0x5c, 0xf9, 0xb7, 0xc7, 0xeb, 0xd0, 0x99, 0xa7, 0x45, 0xb8, 0xc0, 0xb2, 0xe1, 0x06, 0x5a, 0xf0, - 0xfe, 0xe3, 0x80, 0x6b, 0x3d, 0xff, 0x0a, 0x3f, 0x6c, 0x6c, 0xff, 0xdb, 0x6a, 0xf4, 0xbf, 0x23, - 0x68, 0x53, 0x1e, 0xeb, 0xd7, 0x5d, 0xc6, 0x76, 0x3f, 0x11, 0xf2, 0x91, 0xca, 0xc8, 0xe9, 0x35, - 0x33, 0x04, 0xf4, 0x6b, 0x28, 0x40, 0x55, 0xef, 0x23, 0xe8, 0x3d, 0x30, 0x71, 0x72, 0x1b, 0xa0, - 0xe4, 0x45, 0xc9, 0xb8, 0x4c, 0x98, 0x30, 0x37, 0xdc, 0x38, 0x55, 0x40, 0x63, 0xa2, 0x68, 0xa8, - 0x79, 0x9f, 0x39, 0xd0, 0xaf, 0x63, 0x4f, 0x95, 0xaa, 0x8c, 0x09, 0xa1, 0xc6, 0x03, 0xd3, 0x74, - 0x8d, 0xa8, 0x6c, 0xb5, 0x48, 0xa9, 0x19, 0xd6, 0xfb, 0x81, 0x16, 0xd6, 0x1d, 0xdc, 0xda, 0xe0, - 0xe0, 0xf6, 0x65, 0x0e, 0xee, 0x6c, 0x74, 0x70, 0x77, 0x93, 0x83, 0x7b, 0x57, 0xf5, 0x81, 0xf7, - 0x07, 0x07, 0x06, 0x8d, 0x2c, 0x50, 0xe3, 0x4f, 0x48, 0x25, 0x8b, 0x0b, 0x6e, 0x8b, 0x62, 0x2d, - 0xdb, 0x5a, 0xb9, 0x75, 0x49, 0xad, 0x6c, 0x35, 0x6a, 0x65, 0x7d, 0x9b, 0xf6, 0x95, 0x6f, 0xf3, - 0x27, 0x07, 0xfa, 0x75, 0xca, 0xe8, 0xff, 0x02, 0x34, 0xa6, 0x18, 0xc7, 0xfe, 0x17, 0x60, 0x35, - 0xb3, 0xac, 0x59, 0x77, 0xeb, 0x39, 0xeb, 0xda, 0x7b, 0xb4, 0xbe, 0x4e, 0xd8, 0x6b, 0x1f, 0xb4, - 0x1b, 0x3e, 0x98, 0x7e, 0xf3, 0xd3, 0x67, 0x7b, 0xaf, 0xfd, 0xfb, 0xd9, 0x9e, 0xf3, 0xdf, 0x67, - 0x7b, 0xce, 0xff, 0x9e, 0xed, 0x39, 0xbf, 0x69, 0xd1, 0x32, 0x99, 0x77, 0x91, 0xf0, 0xfd, 0xff, - 0x07, 0x00, 0x00, 0xff, 0xff, 0x30, 0x81, 0x4a, 0x93, 0x75, 0x12, 0x00, 0x00, + 0x15, 0x4f, 0xef, 0x7c, 0xf5, 0xbc, 0xd9, 0x99, 0x5d, 0x57, 0x02, 0x19, 0x3a, 0x61, 0xd7, 0x0c, + 0x8a, 0x70, 0x08, 0xf4, 0x46, 0x1b, 0xc9, 0x04, 0x2b, 0x08, 0xed, 0xd8, 0x46, 0x5e, 0x62, 0xe2, + 0xa5, 0xd7, 0xf2, 0x01, 0x0e, 0x43, 0x4d, 0x77, 0xed, 0x4c, 0x6b, 0xfa, 0x2b, 0x55, 0xd5, 0x63, + 0x6f, 0xfe, 0x05, 0x2e, 0xdc, 0x91, 0xf8, 0x07, 0x38, 0x23, 0x71, 0x00, 0x71, 0xcd, 0x09, 0x21, + 0x71, 0xf3, 0xc1, 0xc8, 0x37, 0xc8, 0x89, 0x23, 0x37, 0x50, 0xbd, 0xaa, 0xea, 0xe9, 0x31, 0x3b, + 0xce, 0x6e, 0x7c, 0x58, 0xa9, 0xea, 0xd7, 0xf5, 0x7e, 0x55, 0xf5, 0xea, 0xf7, 0x3e, 0x66, 0xe1, + 0x2d, 0x3a, 0x63, 0x99, 0x3c, 0x98, 0xd2, 0x70, 0xc1, 0xb2, 0xe8, 0x80, 0x16, 0xb1, 0xfa, 0xf3, + 0x0b, 0x9e, 0xcb, 0x9c, 0x0c, 0xc5, 0xa7, 0x9c, 0xb1, 0xcc, 0xc7, 0x35, 0xbe, 0x59, 0xe3, 0xd3, + 0x22, 0xf6, 0xf6, 0x67, 0x79, 0x3e, 0x4b, 0xd8, 0x01, 0xae, 0x9b, 0x96, 0x67, 0x07, 0x32, 0x4e, + 0x99, 0x90, 0x34, 0x2d, 0xb4, 0xa9, 0xf7, 0xf6, 0x8b, 0x0b, 0x84, 0xe4, 0x65, 0x28, 0xcd, 0xd7, + 0xef, 0xcf, 0x62, 0x39, 0x2f, 0xa7, 0x7e, 0x98, 0xa7, 0x07, 0xb3, 0x7c, 0x96, 0xaf, 0x96, 0xa9, + 0x19, 0x4e, 0x70, 0xa4, 0x97, 0x8f, 0x7e, 0xdf, 0x82, 0x9d, 0xa3, 0xa2, 0xb8, 0x9f, 0xcf, 0xe2, + 0x2c, 0x60, 0x9f, 0x96, 0x4c, 0x48, 0xf2, 0x2e, 0xec, 0x4e, 0xcb, 0x2c, 0x4a, 0xd8, 0x44, 0xc4, + 0xb3, 0x8c, 0xca, 0x92, 0xb3, 0xa1, 0x73, 0xdd, 0xb9, 0xd1, 0x0d, 0x76, 0x34, 0x7e, 0x6a, 0x61, + 0x42, 0xa1, 0xbf, 0xa4, 0x3c, 0xce, 0x4b, 0x31, 0x89, 0xb3, 0xb3, 0x5c, 0x0c, 0xb7, 0xae, 0x3b, + 0x37, 0x7a, 0x87, 0x37, 0xfd, 0x4d, 0xd7, 0xf3, 0x5f, 0xd8, 0xcc, 0x7f, 0xa4, 0xcd, 0x8f, 0x95, + 0xf5, 0xb8, 0xf9, 0xf9, 0xb3, 0xfd, 0xd7, 0x82, 0xed, 0x65, 0x0d, 0x23, 0xdf, 0x04, 0x40, 0x96, + 0x89, 0x3c, 0x2f, 0xd8, 0xb0, 0x81, 0xe7, 0xe8, 0x22, 0xf2, 0xf0, 0xbc, 0x60, 0xe4, 0xdb, 0xd0, + 0xd7, 0x9f, 0x97, 0x8c, 0x8b, 0x38, 0xcf, 0x86, 0x4d, 0x5c, 0xb1, 0x8d, 0xe0, 0x23, 0x8d, 0x91, + 0x37, 0xa1, 0x93, 0x0b, 0x4d, 0xd0, 0xc2, 0xcf, 0xed, 0x5c, 0xa0, 0xb5, 0x07, 0xee, 0x3c, 0x17, + 0x32, 0xa3, 0x29, 0x1b, 0xb6, 0xf1, 0x4b, 0x35, 0x27, 0xdf, 0x82, 0x6d, 0x5e, 0x66, 0xca, 0xfb, + 0xda, 0xb2, 0x83, 0xdf, 0x7b, 0x06, 0x43, 0xf3, 0xef, 0xc0, 0x8e, 0x5d, 0x62, 0xb7, 0x77, 0x71, + 0xd5, 0xc0, 0xc0, 0xf6, 0x00, 0xef, 0xc0, 0xe0, 0x8c, 0xd3, 0x94, 0x3d, 0xce, 0xf9, 0x42, 0xb3, + 0x75, 0x71, 0x5d, 0xbf, 0x42, 0x91, 0xef, 0x3d, 0xb8, 0xb6, 0x5a, 0x66, 0x19, 0x01, 0x57, 0xee, + 0x56, 0x1f, 0x2c, 0xe7, 0x75, 0xe8, 0xb1, 0x6c, 0x19, 0xf3, 0x3c, 0x4b, 0x59, 0x26, 0x87, 0x3d, + 0x7d, 0xbc, 0x1a, 0xe4, 0xfd, 0xd5, 0x81, 0xed, 0xba, 0x7f, 0xc9, 0x87, 0xd0, 0x54, 0xa7, 0xc2, + 0xd7, 0xec, 0x1d, 0x7a, 0xbe, 0x56, 0x92, 0x6f, 0x25, 0xe2, 0x3f, 0xb4, 0x52, 0x1b, 0xbb, 0xea, + 0x25, 0x7e, 0xf3, 0x8f, 0x7d, 0x27, 0x40, 0x0b, 0xb2, 0x0b, 0x8d, 0x22, 0x8e, 0xd0, 0xfd, 0xfd, + 0x40, 0x0d, 0x09, 0x81, 0x66, 0xa1, 0xa0, 0x26, 0x42, 0x38, 0x56, 0x18, 0x2b, 0xe3, 0x08, 0x9d, + 0xdc, 0x0f, 0x70, 0x8c, 0xd8, 0x2c, 0x8e, 0xd0, 0xbd, 0x0a, 0x9b, 0xc5, 0x91, 0x62, 0x53, 0xcb, + 0x3a, 0x9a, 0xad, 0xd4, 0x88, 0x5a, 0xe4, 0x6a, 0x64, 0xa6, 0xed, 0xf0, 0x59, 0xb4, 0xa3, 0x70, + 0x3c, 0xfa, 0x63, 0x03, 0x76, 0x57, 0x02, 0x12, 0x45, 0x9e, 0x09, 0xa6, 0x04, 0x22, 0x98, 0x50, + 0x2e, 0x99, 0xc4, 0x91, 0x11, 0x6a, 0xd7, 0x20, 0xc7, 0x11, 0xf9, 0x3a, 0xb4, 0x85, 0xa4, 0xb2, + 0xd4, 0xda, 0x74, 0x03, 0x33, 0x23, 0x3f, 0x05, 0x37, 0xcc, 0xd3, 0x94, 0x66, 0x91, 0x18, 0x36, + 0xae, 0x37, 0x6e, 0xf4, 0x0e, 0x6f, 0x6c, 0x56, 0xed, 0x6d, 0xbd, 0xd2, 0x88, 0xd6, 0xe8, 0xb4, + 0xb2, 0x27, 0x0f, 0xc1, 0x3d, 0x63, 0x18, 0x11, 0x02, 0xfd, 0xd1, 0x3b, 0x3c, 0xbc, 0x4c, 0x04, + 0xe8, 0x0b, 0xf8, 0x3f, 0xd1, 0xa6, 0x96, 0xd5, 0x32, 0x29, 0xd5, 0x16, 0x34, 0x5c, 0x4c, 0x8c, + 0x43, 0xbb, 0x41, 0x5b, 0x4d, 0x8f, 0x23, 0x72, 0x0b, 0x5a, 0xbc, 0x4c, 0x98, 0x18, 0xb6, 0xf1, + 0xdc, 0x7b, 0x9b, 0xf7, 0x0a, 0xca, 0xc4, 0xf2, 0x6a, 0x13, 0x6f, 0x09, 0x1d, 0xb3, 0x9f, 0x72, + 0xdc, 0x94, 0xca, 0x70, 0x3e, 0x11, 0xf1, 0x67, 0x5a, 0x13, 0xfd, 0xa0, 0x8b, 0xc8, 0x69, 0xfc, + 0x19, 0x46, 0x56, 0x4a, 0x9f, 0x4c, 0x84, 0xa4, 0x09, 0xcb, 0x98, 0xd0, 0xfe, 0xeb, 0x07, 0xdb, + 0x29, 0x7d, 0x72, 0x6a, 0x31, 0x15, 0x01, 0x73, 0x46, 0xb9, 0x9c, 0x32, 0x2a, 0x27, 0x11, 0x4b, + 0xe8, 0xb9, 0xd1, 0xc8, 0xa0, 0x82, 0xef, 0x28, 0x74, 0x74, 0x02, 0x83, 0x75, 0x27, 0x56, 0x0f, + 0xec, 0xac, 0x1e, 0x58, 0x3d, 0x56, 0x41, 0x39, 0x4d, 0xd5, 0x66, 0x0d, 0x7d, 0x63, 0x35, 0x53, + 0x6b, 0xcb, 0xd2, 0xe8, 0xaf, 0x1b, 0xe0, 0x78, 0x74, 0x0d, 0x76, 0x2a, 0x46, 0xed, 0xc9, 0xd1, + 0xbf, 0x1c, 0x18, 0xfc, 0x8c, 0x49, 0x1e, 0x87, 0x95, 0x3a, 0x2e, 0xda, 0xe5, 0x16, 0xb4, 0x84, + 0xa4, 0x5c, 0x9a, 0x6c, 0x75, 0xb9, 0x38, 0xd0, 0x26, 0xe4, 0x23, 0x68, 0x9f, 0xc5, 0x59, 0x2c, + 0xe6, 0x78, 0x96, 0xcb, 0x1a, 0x1b, 0x1b, 0x72, 0x0c, 0xbd, 0x7c, 0x2a, 0x18, 0x5f, 0x52, 0x69, + 0x73, 0x55, 0xef, 0xf0, 0xcd, 0xff, 0xa3, 0x38, 0xc5, 0x8c, 0x3e, 0x1e, 0x28, 0xfb, 0xa7, 0xcf, + 0xf6, 0xdb, 0x7a, 0x1e, 0xd4, 0x6d, 0x47, 0x7f, 0xd8, 0x82, 0xc1, 0x51, 0x51, 0x8c, 0x19, 0x95, + 0xd6, 0xa3, 0x0b, 0xd8, 0x31, 0x92, 0x9c, 0x70, 0x26, 0xca, 0x44, 0x8a, 0xa1, 0x83, 0x0a, 0xf9, + 0xe8, 0xa5, 0x6a, 0xac, 0x51, 0xac, 0x84, 0x8e, 0xe6, 0x77, 0x33, 0xc9, 0xcf, 0x8d, 0x7e, 0x06, + 0xe1, 0xda, 0x27, 0x72, 0x0f, 0x3a, 0x29, 0xba, 0x5a, 0xbf, 0xd5, 0x4b, 0xc3, 0x67, 0xfd, 0x4d, + 0x0c, 0xa1, 0x35, 0xf7, 0x12, 0x78, 0xfd, 0x82, 0x6d, 0x55, 0x4a, 0x58, 0xb0, 0x73, 0xf3, 0x70, + 0x6a, 0x48, 0x7e, 0x0c, 0xad, 0x25, 0x4d, 0x4a, 0x66, 0xde, 0xed, 0xdd, 0x4b, 0xc4, 0xab, 0xde, + 0x31, 0xd0, 0x76, 0xb7, 0xb6, 0x3e, 0x74, 0x46, 0x25, 0x16, 0x3c, 0x7d, 0x67, 0xa3, 0x91, 0x7a, + 0x2a, 0x70, 0x5e, 0x31, 0x15, 0x6c, 0x48, 0x37, 0xa3, 0xbf, 0x38, 0xb0, 0x3d, 0x56, 0xb1, 0x65, + 0x1f, 0xeb, 0x1e, 0xb4, 0x30, 0xd6, 0xcc, 0x8e, 0xdf, 0xdb, 0xbc, 0x63, 0xdd, 0xcc, 0xbf, 0xbb, + 0x64, 0x99, 0xdd, 0x55, 0x13, 0x78, 0x0c, 0x5a, 0x88, 0xaa, 0x80, 0x66, 0xcb, 0xaa, 0x54, 0x9a, + 0x4c, 0x88, 0x08, 0x56, 0x97, 0x1f, 0x41, 0x0b, 0x27, 0xc6, 0x7d, 0x97, 0x96, 0x9d, 0xb6, 0x1a, + 0xb5, 0xa1, 0xa9, 0xd2, 0xc9, 0xe8, 0xef, 0x0e, 0xc0, 0x1d, 0x56, 0xb0, 0x2c, 0x62, 0x59, 0x78, + 0x7e, 0x61, 0x80, 0x0d, 0xa1, 0x63, 0xab, 0xd7, 0x16, 0xc2, 0x76, 0xaa, 0x0b, 0x6e, 0xca, 0x0a, + 0x3a, 0xb3, 0xb5, 0xbc, 0x9a, 0x93, 0xdb, 0xd0, 0x16, 0x79, 0xc9, 0x43, 0x66, 0xe2, 0xe2, 0xbd, + 0xcd, 0x2e, 0x59, 0xed, 0xef, 0x9f, 0xa2, 0x49, 0x60, 0x4c, 0xbd, 0x9b, 0xd0, 0xd6, 0xc8, 0xa6, + 0x83, 0x71, 0x96, 0xe6, 0x92, 0xd9, 0x04, 0x63, 0xa7, 0xa3, 0xbf, 0xbd, 0x0e, 0x7d, 0xe3, 0xe3, + 0x80, 0x85, 0x39, 0x8f, 0xea, 0x97, 0x70, 0xd6, 0x2f, 0xf1, 0x81, 0xea, 0x0c, 0x12, 0x26, 0x6c, + 0x76, 0xc6, 0x3b, 0x8e, 0x77, 0xbf, 0x78, 0xb6, 0xbf, 0x86, 0xab, 0x5e, 0xc1, 0xcc, 0x8e, 0x23, + 0xf2, 0x5d, 0xe8, 0x86, 0x49, 0xac, 0x5e, 0x27, 0x2e, 0xf4, 0xd5, 0xc7, 0xfd, 0x2f, 0x9e, 0xed, + 0xaf, 0xc0, 0xc0, 0xd5, 0xc3, 0xe3, 0x82, 0x3c, 0x50, 0xc7, 0xc4, 0xb3, 0x18, 0x57, 0x1c, 0xbc, + 0x24, 0xc5, 0xd7, 0x0f, 0xed, 0xaf, 0xcb, 0xd2, 0xb2, 0x90, 0x00, 0x5c, 0x6e, 0xd4, 0x8e, 0xb5, + 0xa4, 0x77, 0xf8, 0xfe, 0xe5, 0x19, 0xd7, 0xa2, 0xb6, 0xe2, 0x51, 0x9c, 0x3a, 0x1f, 0x31, 0x5d, + 0xdc, 0xaf, 0xc0, 0xf9, 0xc0, 0xd8, 0x59, 0x4e, 0xcb, 0xe3, 0xfd, 0xb7, 0x01, 0x1d, 0x1b, 0x20, + 0xbb, 0xd0, 0xe0, 0x55, 0x41, 0x57, 0x43, 0xf2, 0x08, 0x3a, 0x73, 0x46, 0x23, 0xc6, 0x6d, 0xca, + 0xb9, 0x79, 0x45, 0xb7, 0xf8, 0xf7, 0xd0, 0xdc, 0x7a, 0xc7, 0x90, 0x29, 0xa5, 0x2c, 0x19, 0x9f, + 0xda, 0xea, 0xa2, 0xc6, 0xd8, 0xde, 0x50, 0x39, 0x37, 0xed, 0x24, 0x8e, 0xc9, 0x37, 0xc0, 0xe5, + 0xf4, 0xf1, 0x04, 0x71, 0x5d, 0x91, 0x3b, 0x9c, 0x3e, 0x3e, 0x51, 0x9f, 0x08, 0x34, 0x55, 0xe3, + 0x68, 0x9a, 0x48, 0x1c, 0x23, 0x45, 0xce, 0xa5, 0x69, 0x1c, 0x71, 0x4c, 0xde, 0x82, 0xae, 0x56, + 0x9c, 0x52, 0x81, 0xee, 0x15, 0x5d, 0x0d, 0x1c, 0x17, 0x64, 0x1f, 0x7a, 0xe6, 0x23, 0xda, 0xe9, + 0xce, 0x07, 0x34, 0x74, 0xa2, 0xac, 0x55, 0x72, 0x09, 0xe7, 0x2c, 0x65, 0xa6, 0x29, 0x34, 0x33, + 0x15, 0xf8, 0xa5, 0x60, 0x7c, 0x82, 0x6e, 0x30, 0x9d, 0x60, 0x57, 0x21, 0x47, 0x0a, 0xd0, 0xaa, + 0x3f, 0x63, 0x9c, 0xf1, 0xe1, 0xb6, 0x39, 0xb6, 0x9e, 0x92, 0x87, 0x55, 0xbd, 0xed, 0x7f, 0x59, + 0xe3, 0x7e, 0xb1, 0x43, 0x4f, 0xd0, 0xda, 0x38, 0xd4, 0x70, 0x79, 0xef, 0x43, 0x5b, 0x3b, 0xfa, + 0x82, 0x1c, 0xfe, 0x46, 0x3d, 0x87, 0x77, 0x4d, 0x62, 0xf6, 0x5c, 0x68, 0x6b, 0x26, 0x2f, 0x01, + 0xb7, 0xca, 0xcb, 0xab, 0x5c, 0xaa, 0x9b, 0x13, 0xdb, 0xba, 0xbd, 0x03, 0x83, 0x30, 0xcf, 0xa4, + 0x0a, 0x9b, 0x84, 0x65, 0x33, 0x39, 0x37, 0xad, 0x49, 0xdf, 0xa0, 0xf7, 0x11, 0x54, 0x0d, 0xbc, + 0x5d, 0x56, 0xfb, 0xed, 0xd0, 0x33, 0x98, 0x4a, 0x89, 0xde, 0xd3, 0x1e, 0xb8, 0x56, 0x8c, 0xe4, + 0xe7, 0xd0, 0xa1, 0x52, 0xd2, 0x70, 0x61, 0xab, 0xc0, 0x0f, 0xae, 0xaa, 0x67, 0xff, 0x08, 0xed, + 0x03, 0xcb, 0x43, 0x3e, 0x86, 0x86, 0x88, 0x16, 0x46, 0xad, 0x3f, 0xbc, 0x32, 0xdd, 0xe9, 0x9d, + 0x8f, 0x31, 0xb3, 0x07, 0x8a, 0x85, 0xcc, 0xe0, 0x9a, 0x26, 0x98, 0xb0, 0x27, 0x21, 0x2b, 0x54, + 0x17, 0x60, 0x5b, 0xd7, 0x5b, 0x57, 0xa6, 0xbe, 0x6b, 0x29, 0x82, 0x5d, 0x6d, 0x5a, 0x01, 0x82, + 0xfc, 0x0a, 0xb6, 0x6b, 0x9d, 0x86, 0x6a, 0x69, 0xbf, 0xa4, 0x89, 0xd8, 0xb0, 0xc7, 0x83, 0x15, + 0x49, 0xb0, 0xc6, 0x48, 0x7e, 0x09, 0xbd, 0x88, 0x4a, 0x3a, 0x29, 0xf2, 0x38, 0x93, 0x62, 0xd8, + 0xfa, 0x8a, 0x97, 0xb8, 0x43, 0x25, 0x3d, 0x51, 0x14, 0x01, 0x44, 0x76, 0x28, 0xbc, 0x3f, 0x3b, + 0xd0, 0xd6, 0x0f, 0x81, 0xe1, 0x56, 0x26, 0x6c, 0x52, 0x2b, 0x04, 0xae, 0x02, 0x3e, 0x51, 0xc5, + 0x80, 0x40, 0x53, 0xaa, 0x14, 0xab, 0x0b, 0x35, 0x8e, 0x95, 0x3c, 0xf5, 0x0f, 0x59, 0x2d, 0x16, + 0x3d, 0x21, 0x6f, 0x43, 0x57, 0x9d, 0x46, 0x72, 0x8a, 0xc5, 0x49, 0x15, 0x8e, 0x15, 0x50, 0xfd, + 0xaa, 0x6a, 0x5d, 0xf9, 0x57, 0xd5, 0x1b, 0xd0, 0x9a, 0x26, 0x79, 0xb8, 0xc0, 0xb4, 0xe1, 0x06, + 0x7a, 0xe2, 0xfd, 0x69, 0x0b, 0x5c, 0xfb, 0xf2, 0xaf, 0xf0, 0x93, 0xcd, 0xd6, 0xbf, 0xad, 0x5a, + 0xfd, 0x3b, 0x82, 0x26, 0xe5, 0x33, 0xb1, 0xb1, 0x77, 0xbd, 0x1f, 0x0b, 0xf9, 0x48, 0x45, 0xe4, + 0xf8, 0x9a, 0x69, 0x02, 0xba, 0x15, 0x14, 0xa0, 0xa9, 0xf7, 0x3b, 0x07, 0x3a, 0x0f, 0x8c, 0x50, + 0x6e, 0x03, 0x14, 0x3c, 0x2f, 0x18, 0x97, 0x31, 0x13, 0xe6, 0x88, 0x1b, 0xdb, 0x0a, 0xa8, 0xb5, + 0x14, 0x35, 0x33, 0xf2, 0x09, 0xec, 0x62, 0xf2, 0x8a, 0x23, 0x96, 0xc9, 0xf8, 0x2c, 0xd6, 0xe9, + 0xfd, 0xd2, 0x54, 0x3b, 0xca, 0xf8, 0x78, 0x65, 0xeb, 0x3d, 0x75, 0xa0, 0x5b, 0x89, 0x59, 0xe5, + 0xbe, 0x94, 0x09, 0xa1, 0xfa, 0x0d, 0x53, 0xc5, 0xcd, 0x54, 0x39, 0x7f, 0x91, 0x50, 0xf3, 0xbb, + 0xa6, 0x1b, 0xe8, 0xc9, 0xba, 0x62, 0x1a, 0x1b, 0x14, 0xd3, 0xbc, 0x48, 0x31, 0xad, 0x8d, 0x8a, + 0x69, 0x6f, 0x52, 0x4c, 0xe7, 0xaa, 0x8f, 0xea, 0xfd, 0xda, 0x81, 0x5e, 0x2d, 0xac, 0x54, 0x3f, + 0x15, 0x52, 0xc9, 0x66, 0x39, 0xb7, 0x59, 0xb6, 0x9a, 0xdb, 0xe4, 0xbb, 0x75, 0x41, 0xf2, 0x6d, + 0xd4, 0x92, 0x6f, 0x75, 0x9a, 0xe6, 0x95, 0x4f, 0xf3, 0x5b, 0x07, 0xba, 0x55, 0x0c, 0xea, 0x7f, + 0x98, 0xd4, 0xda, 0x22, 0xc7, 0xfe, 0xc3, 0x64, 0xd5, 0x04, 0xad, 0x79, 0x77, 0xeb, 0x05, 0xef, + 0xda, 0x73, 0x34, 0xbe, 0x4a, 0x1c, 0xe9, 0x37, 0x68, 0xd6, 0xde, 0x60, 0xfc, 0xb5, 0xcf, 0x9f, + 0xef, 0xbd, 0xf6, 0xcf, 0xe7, 0x7b, 0xce, 0xbf, 0x9f, 0xef, 0x39, 0xff, 0x79, 0xbe, 0xe7, 0xfc, + 0xa2, 0x41, 0x8b, 0x78, 0xda, 0x46, 0xc2, 0x0f, 0xfe, 0x17, 0x00, 0x00, 0xff, 0xff, 0xbf, 0x71, + 0x71, 0x29, 0xa0, 0x13, 0x00, 0x00, } type AppLoginRequestFace interface { @@ -1344,6 +1354,10 @@ func NewCommandResponseFromFace(that CommandResponseFace) *CommandResponse { type MetricResponseFace interface { Proto() github_com_gogo_protobuf_proto.Message + GetName() string + GetStart() time.Time + GetFinish() time.Time + GetObservation() Struct } func (this *MetricResponse) Proto() github_com_gogo_protobuf_proto.Message { @@ -1354,8 +1368,28 @@ func (this *MetricResponse) TestProto() github_com_gogo_protobuf_proto.Message { return NewMetricResponseFromFace(this) } +func (this *MetricResponse) GetName() string { + return this.Name +} + +func (this *MetricResponse) GetStart() time.Time { + return this.Start +} + +func (this *MetricResponse) GetFinish() time.Time { + return this.Finish +} + +func (this *MetricResponse) GetObservation() Struct { + return this.Observation +} + func NewMetricResponseFromFace(that MetricResponseFace) *MetricResponse { this := &MetricResponse{} + this.Name = that.GetName() + this.Start = that.GetStart() + this.Finish = that.GetFinish() + this.Observation = that.GetObservation() return this } @@ -1923,6 +1957,7 @@ func NewRequestRecord_Observed_SDKEventFromFace(that RequestRecord_Observed_SDKE type RequestRecord_Observed_SDKEvent_OptionsFace interface { Proto() github_com_gogo_protobuf_proto.Message GetProperties() *Struct + GetUserIdentifiers() *Struct } func (this *RequestRecord_Observed_SDKEvent_Options) Proto() github_com_gogo_protobuf_proto.Message { @@ -1937,9 +1972,14 @@ func (this *RequestRecord_Observed_SDKEvent_Options) GetProperties() *Struct { return this.Properties } +func (this *RequestRecord_Observed_SDKEvent_Options) GetUserIdentifiers() *Struct { + return this.UserIdentifiers +} + func NewRequestRecord_Observed_SDKEvent_OptionsFromFace(that RequestRecord_Observed_SDKEvent_OptionsFace) *RequestRecord_Observed_SDKEvent_Options { this := &RequestRecord_Observed_SDKEvent_Options{} this.Properties = that.GetProperties() + this.UserIdentifiers = that.GetUserIdentifiers() return this } @@ -2203,8 +2243,12 @@ func (this *MetricResponse) GoString() string { if this == nil { return "nil" } - s := make([]string, 0, 4) + s := make([]string, 0, 8) s = append(s, "&api.MetricResponse{") + s = append(s, "Name: "+fmt.Sprintf("%#v", this.Name)+",\n") + s = append(s, "Start: "+fmt.Sprintf("%#v", this.Start)+",\n") + s = append(s, "Finish: "+fmt.Sprintf("%#v", this.Finish)+",\n") + s = append(s, "Observation: "+fmt.Sprintf("%#v", this.Observation)+",\n") if this.XXX_unrecognized != nil { s = append(s, "XXX_unrecognized:"+fmt.Sprintf("%#v", this.XXX_unrecognized)+",\n") } @@ -2494,9 +2538,10 @@ func (this *RequestRecord_Observed_SDKEvent_Options) GoString() string { if this == nil { return "nil" } - s := make([]string, 0, 5) + s := make([]string, 0, 6) s = append(s, "&api.RequestRecord_Observed_SDKEvent_Options{") s = append(s, "Properties: "+fmt.Sprintf("%#v", this.Properties)+",\n") + s = append(s, "UserIdentifiers: "+fmt.Sprintf("%#v", this.UserIdentifiers)+",\n") if this.XXX_unrecognized != nil { s = append(s, "XXX_unrecognized:"+fmt.Sprintf("%#v", this.XXX_unrecognized)+",\n") } @@ -2664,8 +2709,15 @@ func NewPopulatedCommandResponse(r randyApi, easy bool) *CommandResponse { func NewPopulatedMetricResponse(r randyApi, easy bool) *MetricResponse { this := &MetricResponse{} + this.Name = string(randStringApi(r)) + v9 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Start = *v9 + v10 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Finish = *v10 + v11 := NewPopulatedStruct(r) + this.Observation = *v11 if !easy && r.Intn(10) != 0 { - this.XXX_unrecognized = randUnrecognizedApi(r, 1) + this.XXX_unrecognized = randUnrecognizedApi(r, 5) } return this } @@ -2673,18 +2725,18 @@ func NewPopulatedMetricResponse(r randyApi, easy bool) *MetricResponse { func NewPopulatedAppBeatRequest(r randyApi, easy bool) *AppBeatRequest { this := &AppBeatRequest{} if r.Intn(10) != 0 { - v9 := r.Intn(10) + v12 := r.Intn(10) this.CommandResults = make(map[string]CommandResponse) - for i := 0; i < v9; i++ { + for i := 0; i < v12; i++ { this.CommandResults[randStringApi(r)] = *NewPopulatedCommandResponse(r, easy) } } if r.Intn(10) != 0 { - v10 := r.Intn(5) - this.Metrics = make([]MetricResponse, v10) - for i := 0; i < v10; i++ { - v11 := NewPopulatedMetricResponse(r, easy) - this.Metrics[i] = *v11 + v13 := r.Intn(5) + this.Metrics = make([]MetricResponse, v13) + for i := 0; i < v13; i++ { + v14 := NewPopulatedMetricResponse(r, easy) + this.Metrics[i] = *v14 } } if !easy && r.Intn(10) != 0 { @@ -2696,11 +2748,11 @@ func NewPopulatedAppBeatRequest(r randyApi, easy bool) *AppBeatRequest { func NewPopulatedAppBeatResponse(r randyApi, easy bool) *AppBeatResponse { this := &AppBeatResponse{} if r.Intn(10) != 0 { - v12 := r.Intn(5) - this.Commands = make([]CommandRequest, v12) - for i := 0; i < v12; i++ { - v13 := NewPopulatedCommandRequest(r, easy) - this.Commands[i] = *v13 + v15 := r.Intn(5) + this.Commands = make([]CommandRequest, v15) + for i := 0; i < v15; i++ { + v16 := NewPopulatedCommandRequest(r, easy) + this.Commands[i] = *v16 } } this.Status = bool(bool(r.Intn(2) == 0)) @@ -2713,11 +2765,11 @@ func NewPopulatedAppBeatResponse(r randyApi, easy bool) *AppBeatResponse { func NewPopulatedBatchRequest(r randyApi, easy bool) *BatchRequest { this := &BatchRequest{} if r.Intn(10) != 0 { - v14 := r.Intn(5) - this.Batch = make([]BatchRequest_Event, v14) - for i := 0; i < v14; i++ { - v15 := NewPopulatedBatchRequest_Event(r, easy) - this.Batch[i] = *v15 + v17 := r.Intn(5) + this.Batch = make([]BatchRequest_Event, v17) + for i := 0; i < v17; i++ { + v18 := NewPopulatedBatchRequest_Event(r, easy) + this.Batch[i] = *v18 } } if !easy && r.Intn(10) != 0 { @@ -2729,8 +2781,8 @@ func NewPopulatedBatchRequest(r randyApi, easy bool) *BatchRequest { func NewPopulatedBatchRequest_Event(r randyApi, easy bool) *BatchRequest_Event { this := &BatchRequest_Event{} this.EventType = string(randStringApi(r)) - v16 := NewPopulatedStruct(r) - this.Event = *v16 + v19 := NewPopulatedStruct(r) + this.Event = *v19 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 3) } @@ -2762,9 +2814,9 @@ func NewPopulatedDependency(r randyApi, easy bool) *Dependency { func NewPopulatedDependency_Source(r randyApi, easy bool) *Dependency_Source { this := &Dependency_Source{} this.Name = string(randStringApi(r)) - v17 := r.Intn(10) - this.Remotes = make([]string, v17) - for i := 0; i < v17; i++ { + v20 := r.Intn(10) + this.Remotes = make([]string, v20) + for i := 0; i < v20; i++ { this.Remotes[i] = string(randStringApi(r)) } if !easy && r.Intn(10) != 0 { @@ -2778,12 +2830,12 @@ func NewPopulatedRequestRecord(r randyApi, easy bool) *RequestRecord { this.Version = string(randStringApi(r)) this.RulespackId = string(randStringApi(r)) this.ClientIp = string(randStringApi(r)) - v18 := NewPopulatedRequestRecord_Request(r, easy) - this.Request = *v18 - v19 := NewPopulatedRequestRecord_Response(r, easy) - this.Response = *v19 - v20 := NewPopulatedRequestRecord_Observed(r, easy) - this.Observed = *v20 + v21 := NewPopulatedRequestRecord_Request(r, easy) + this.Request = *v21 + v22 := NewPopulatedRequestRecord_Response(r, easy) + this.Response = *v22 + v23 := NewPopulatedRequestRecord_Observed(r, easy) + this.Observed = *v23 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 7) } @@ -2794,11 +2846,11 @@ func NewPopulatedRequestRecord_Request(r randyApi, easy bool) *RequestRecord_Req this := &RequestRecord_Request{} this.Rid = string(randStringApi(r)) if r.Intn(10) != 0 { - v21 := r.Intn(5) - this.Headers = make([]RequestRecord_Request_Header, v21) - for i := 0; i < v21; i++ { - v22 := NewPopulatedRequestRecord_Request_Header(r, easy) - this.Headers[i] = *v22 + v24 := r.Intn(5) + this.Headers = make([]RequestRecord_Request_Header, v24) + for i := 0; i < v24; i++ { + v25 := NewPopulatedRequestRecord_Request_Header(r, easy) + this.Headers[i] = *v25 } } this.Verb = string(randStringApi(r)) @@ -2811,8 +2863,8 @@ func NewPopulatedRequestRecord_Request(r randyApi, easy bool) *RequestRecord_Req this.Scheme = string(randStringApi(r)) this.UserAgent = string(randStringApi(r)) this.Referer = string(randStringApi(r)) - v23 := NewPopulatedRequestRecord_Request_Params(r, easy) - this.Params = *v23 + v26 := NewPopulatedRequestRecord_Request_Params(r, easy) + this.Params = *v26 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 14) } @@ -2851,37 +2903,37 @@ func NewPopulatedRequestRecord_Response(r randyApi, easy bool) *RequestRecord_Re func NewPopulatedRequestRecord_Observed(r randyApi, easy bool) *RequestRecord_Observed { this := &RequestRecord_Observed{} if r.Intn(10) != 0 { - v24 := r.Intn(5) - this.Attacks = make([]*RequestRecord_Observed_Attack, v24) - for i := 0; i < v24; i++ { + v27 := r.Intn(5) + this.Attacks = make([]*RequestRecord_Observed_Attack, v27) + for i := 0; i < v27; i++ { this.Attacks[i] = NewPopulatedRequestRecord_Observed_Attack(r, easy) } } if r.Intn(10) != 0 { - v25 := r.Intn(5) - this.Sdk = make([]*RequestRecord_Observed_SDKEvent, v25) - for i := 0; i < v25; i++ { + v28 := r.Intn(5) + this.Sdk = make([]*RequestRecord_Observed_SDKEvent, v28) + for i := 0; i < v28; i++ { this.Sdk[i] = NewPopulatedRequestRecord_Observed_SDKEvent(r, easy) } } if r.Intn(10) != 0 { - v26 := r.Intn(5) - this.SqreenExceptions = make([]*RequestRecord_Observed_Exception, v26) - for i := 0; i < v26; i++ { + v29 := r.Intn(5) + this.SqreenExceptions = make([]*RequestRecord_Observed_Exception, v29) + for i := 0; i < v29; i++ { this.SqreenExceptions[i] = NewPopulatedRequestRecord_Observed_Exception(r, easy) } } if r.Intn(10) != 0 { - v27 := r.Intn(5) - this.Observations = make([]*RequestRecord_Observed_Observation, v27) - for i := 0; i < v27; i++ { + v30 := r.Intn(5) + this.Observations = make([]*RequestRecord_Observed_Observation, v30) + for i := 0; i < v30; i++ { this.Observations[i] = NewPopulatedRequestRecord_Observed_Observation(r, easy) } } if r.Intn(10) != 0 { - v28 := r.Intn(5) - this.DataPoints = make([]*RequestRecord_Observed_DataPoint, v28) - for i := 0; i < v28; i++ { + v31 := r.Intn(5) + this.DataPoints = make([]*RequestRecord_Observed_DataPoint, v31) + for i := 0; i < v31; i++ { this.DataPoints[i] = NewPopulatedRequestRecord_Observed_DataPoint(r, easy) } } @@ -2896,13 +2948,13 @@ func NewPopulatedRequestRecord_Observed_Attack(r randyApi, easy bool) *RequestRe this.RuleName = string(randStringApi(r)) this.Test = bool(bool(r.Intn(2) == 0)) this.Infos = string(randStringApi(r)) - v29 := r.Intn(10) - this.Backtrace = make([]string, v29) - for i := 0; i < v29; i++ { + v32 := r.Intn(10) + this.Backtrace = make([]string, v32) + for i := 0; i < v32; i++ { this.Backtrace[i] = string(randStringApi(r)) } - v30 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v30 + v33 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v33 this.Block = bool(bool(r.Intn(2) == 0)) if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 7) @@ -2912,11 +2964,11 @@ func NewPopulatedRequestRecord_Observed_Attack(r randyApi, easy bool) *RequestRe func NewPopulatedRequestRecord_Observed_SDKEvent(r randyApi, easy bool) *RequestRecord_Observed_SDKEvent { this := &RequestRecord_Observed_SDKEvent{} - v31 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v31 + v34 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v34 this.Name = string(randStringApi(r)) - v32 := NewPopulatedListValue(r) - this.Args = *v32 + v35 := NewPopulatedListValue(r) + this.Args = *v35 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 4) } @@ -2928,8 +2980,11 @@ func NewPopulatedRequestRecord_Observed_SDKEvent_Options(r randyApi, easy bool) if r.Intn(10) != 0 { this.Properties = NewPopulatedStruct(r) } + if r.Intn(10) != 0 { + this.UserIdentifiers = NewPopulatedStruct(r) + } if !easy && r.Intn(10) != 0 { - this.XXX_unrecognized = randUnrecognizedApi(r, 2) + this.XXX_unrecognized = randUnrecognizedApi(r, 3) } return this } @@ -2941,13 +2996,13 @@ func NewPopulatedRequestRecord_Observed_Exception(r randyApi, easy bool) *Reques this.RuleName = string(randStringApi(r)) this.Test = bool(bool(r.Intn(2) == 0)) this.Infos = string(randStringApi(r)) - v33 := r.Intn(10) - this.Backtrace = make([]string, v33) - for i := 0; i < v33; i++ { + v36 := r.Intn(10) + this.Backtrace = make([]string, v36) + for i := 0; i < v36; i++ { this.Backtrace[i] = string(randStringApi(r)) } - v34 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v34 + v37 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v37 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 8) } @@ -2959,8 +3014,8 @@ func NewPopulatedRequestRecord_Observed_Observation(r randyApi, easy bool) *Requ this.Category = string(randStringApi(r)) this.Key = string(randStringApi(r)) this.Value = string(randStringApi(r)) - v35 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v35 + v38 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v38 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 5) } @@ -2971,8 +3026,8 @@ func NewPopulatedRequestRecord_Observed_DataPoint(r randyApi, easy bool) *Reques this := &RequestRecord_Observed_DataPoint{} this.RulespackId = string(randStringApi(r)) this.RuleName = string(randStringApi(r)) - v36 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v36 + v39 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v39 this.Infos = string(randStringApi(r)) if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedApi(r, 5) @@ -2999,9 +3054,9 @@ func randUTF8RuneApi(r randyApi) rune { return rune(ru + 61) } func randStringApi(r randyApi) string { - v37 := r.Intn(100) - tmps := make([]rune, v37) - for i := 0; i < v37; i++ { + v40 := r.Intn(100) + tmps := make([]rune, v40) + for i := 0; i < v40; i++ { tmps[i] = randUTF8RuneApi(r) } return string(tmps) @@ -3023,11 +3078,11 @@ func randFieldApi(dAtA []byte, r randyApi, fieldNumber int, wire int) []byte { switch wire { case 0: dAtA = encodeVarintPopulateApi(dAtA, uint64(key)) - v38 := r.Int63() + v41 := r.Int63() if r.Intn(2) == 0 { - v38 *= -1 + v41 *= -1 } - dAtA = encodeVarintPopulateApi(dAtA, uint64(v38)) + dAtA = encodeVarintPopulateApi(dAtA, uint64(v41)) case 1: dAtA = encodeVarintPopulateApi(dAtA, uint64(key)) dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256))) diff --git a/agent/backend/api/api.proto b/agent/backend/api/api.proto index c31a9911..374daddc 100644 --- a/agent/backend/api/api.proto +++ b/agent/backend/api/api.proto @@ -62,6 +62,10 @@ message CommandResponse { } message MetricResponse { + string name = 1; + google.protobuf.Timestamp start = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; + google.protobuf.Timestamp finish = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; + google.protobuf.Struct observation = 4 [(gogoproto.customtype) = "Struct", (gogoproto.nullable) = false]; } message AppBeatRequest { @@ -145,6 +149,7 @@ message RequestRecord { message SDKEvent { message Options { google.protobuf.Struct properties = 1 [(gogoproto.customtype) = "Struct"]; + google.protobuf.Struct user_identifiers = 2 [(gogoproto.customtype) = "Struct"]; } google.protobuf.Timestamp time = 1 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; diff --git a/agent/backend/client.go b/agent/backend/client.go index 7067664a..81bf94b5 100644 --- a/agent/backend/client.go +++ b/agent/backend/client.go @@ -8,12 +8,11 @@ import ( "net/http/httputil" "net/url" - "github.com/sqreen/go-agent/agent/config" - "github.com/sqreen/go-agent/agent/plog" - "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" "github.com/sqreen/go-agent/agent/backend/api" + "github.com/sqreen/go-agent/agent/config" + "github.com/sqreen/go-agent/agent/plog" "golang.org/x/net/http/httpproxy" ) @@ -26,21 +25,32 @@ type Client struct { } func NewClient(backendURL string) (*Client, error) { - proxyCfg := httpproxy.Config{ - HTTPSProxy: config.BackendHTTPAPIProxy(), - } - proxyURL := proxyCfg.ProxyFunc() - proxy := func(req *http.Request) (*url.URL, error) { - return proxyURL(req.URL) + var transport *http.Transport + if proxySettings := config.BackendHTTPAPIProxy(); proxySettings == "" { + // No user settings. The default transport uses standard global proxy + // settings *_PROXY environment variables. + logger.Info("using proxy settings as indicated by the environment variables HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions)") + transport = (http.DefaultTransport).(*http.Transport) + } else { + // Use the settings. + logger.Info("using configured https proxy ", proxySettings) + proxyCfg := httpproxy.Config{ + HTTPSProxy: proxySettings, + } + proxyURL := proxyCfg.ProxyFunc() + proxy := func(req *http.Request) (*url.URL, error) { + return proxyURL(req.URL) + } + // Shallow copy the default transport and overwrite its proxy settings. + transportCopy := *(http.DefaultTransport).(*http.Transport) + transport = &transportCopy + transport.Proxy = proxy } - transport := *(http.DefaultTransport).(*http.Transport) - transport.Proxy = proxy - client := &Client{ client: &http.Client{ Timeout: config.BackendHTTPAPIRequestTimeout, - Transport: &transport, + Transport: transport, }, backendURL: backendURL, pbMarshaler: api.DefaultJSONPBMarshaler, @@ -49,12 +59,15 @@ func NewClient(backendURL string) (*Client, error) { return client, nil } -func (c *Client) AppLogin(req *api.AppLoginRequest, token string) (*api.AppLoginResponse, error) { +func (c *Client) AppLogin(req *api.AppLoginRequest, token string, appName string) (*api.AppLoginResponse, error) { httpReq, err := c.newRequest(&config.BackendHTTPAPIEndpoint.AppLogin) if err != nil { return nil, err } httpReq.Header.Set(config.BackendHTTPAPIHeaderToken, token) + if appName != "" { + httpReq.Header.Set(config.BackendHTTPAPIHeaderAppName, appName) + } res := new(api.AppLoginResponse) if err := c.Do(httpReq, req, res); err != nil { return nil, err diff --git a/agent/backend/client_test.go b/agent/backend/client_test.go index 3536df2b..d6dbb2b8 100644 --- a/agent/backend/client_test.go +++ b/agent/backend/client_test.go @@ -4,18 +4,20 @@ import ( "io/ioutil" math_rand "math/rand" "net/http" + "os" "reflect" - time "time" + "testing" + "time" "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/ghttp" - "github.com/sqreen/go-agent/agent/backend" "github.com/sqreen/go-agent/agent/backend/api" "github.com/sqreen/go-agent/agent/config" + "github.com/sqreen/go-agent/tools/testlib" + "github.com/stretchr/testify/require" ) var ( @@ -23,95 +25,127 @@ var ( popr = math_rand.New(math_rand.NewSource(seed)) ) -var _ = Describe("The backend client", func() { - var ( - server *ghttp.Server - client *backend.Client - ) +func TestClient(t *testing.T) { + RegisterTestingT(t) + g := NewGomegaWithT(t) + + t.Run("AppLogin", func(t *testing.T) { + token := testlib.RandString(2, 50) + appName := testlib.RandString(2, 50) + + statusCode := http.StatusOK + + endpointCfg := &config.BackendHTTPAPIEndpoint.AppLogin + + response := api.NewPopulatedAppLoginResponse(popr, false) + err := JSONPBLoopback(response) + g.Expect(err).ToNot(HaveOccurred()) + + request := api.NewPopulatedAppLoginRequest(popr, false) + err = JSONPBLoopback(request) + g.Expect(err).ToNot(HaveOccurred()) + + headers := http.Header{ + config.BackendHTTPAPIHeaderToken: []string{token}, + config.BackendHTTPAPIHeaderAppName: []string{appName}, + } + + server := initFakeServer(endpointCfg, request, response, statusCode, headers) + defer server.Close() + + client, err := backend.NewClient(server.URL()) + g.Expect(err).NotTo(HaveOccurred()) + + res, err := client.AppLogin(request, token, appName) + g.Expect(err).NotTo(HaveOccurred()) + // A request has been received + g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0)) + g.Expect(res).Should(Equal(response)) + }) - JustBeforeEach(func() { - var err error - server = ghttp.NewServer() - client, err = backend.NewClient(server.URL()) - Expect(err).NotTo(HaveOccurred()) + t.Run("AppBeat", func(t *testing.T) { + session := testlib.RandString(2, 50) + + statusCode := http.StatusOK + + endpointCfg := &config.BackendHTTPAPIEndpoint.AppBeat + + response := api.NewPopulatedAppBeatResponse(popr, false) + err := JSONPBLoopback(response) + g.Expect(err).ToNot(HaveOccurred()) + + request := api.NewPopulatedAppBeatRequest(popr, false) + err = JSONPBLoopback(request) + g.Expect(err).ToNot(HaveOccurred()) + + headers := http.Header{ + config.BackendHTTPAPIHeaderSession: []string{session}, + } + + server := initFakeServer(endpointCfg, request, response, statusCode, headers) + defer server.Close() + + client, err := backend.NewClient(server.URL()) + g.Expect(err).NotTo(HaveOccurred()) + + res, err := client.AppBeat(request, session) + g.Expect(err).NotTo(HaveOccurred()) + // A request has been received + g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0)) + g.Expect(res).Should(Equal(response)) }) - JustAfterEach(func() { - server.Close() + t.Run("Batch", func(t *testing.T) { + t.Skip("need json unmarshaler") + session := testlib.RandString(2, 50) + + statusCode := http.StatusOK + + endpointCfg := &config.BackendHTTPAPIEndpoint.Batch + + request := api.NewPopulatedBatchRequest(popr, false) + err := JSONPBLoopback(request) + g.Expect(err).ToNot(HaveOccurred()) + + headers := http.Header{ + config.BackendHTTPAPIHeaderSession: []string{session}, + } + + server := initFakeServer(endpointCfg, request, nil, statusCode, headers) + defer server.Close() + + client, err := backend.NewClient(server.URL()) + g.Expect(err).NotTo(HaveOccurred()) + + err = client.Batch(request, session) + g.Expect(err).NotTo(HaveOccurred()) + // A request has been received + g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0)) }) - Describe("request", func() { - var ( - endpointCfg *config.HTTPAPIEndpoint - statusCode = http.StatusOK - response proto.Message - request proto.Message - headers http.Header - ) - - JustBeforeEach(func() { - server.AppendHandlers(ghttp.CombineHandlers( - ghttp.VerifyRequest(endpointCfg.Method, endpointCfg.URL), - ghttp.VerifyHeader(headers), - VerifyJSONPBRepresenting(request), - RespondWithJSONPB(&statusCode, response), - )) - }) - - Describe("AppLogin", func() { - var token string = "my-token" - - BeforeEach(func() { - endpointCfg = &config.BackendHTTPAPIEndpoint.AppLogin - - response = api.NewPopulatedAppLoginResponse(popr, false) - err := JSONPBLoopback(response) - Expect(err).ToNot(HaveOccurred()) - - request = api.NewPopulatedAppLoginRequest(popr, false) - err = JSONPBLoopback(request) - Expect(err).ToNot(HaveOccurred()) - - headers = http.Header{ - config.BackendHTTPAPIHeaderToken: []string{token}, - } - }) - - It("should perform the API call", func() { - res, err := client.AppLogin(request.(*api.AppLoginRequest), token) - Expect(err).NotTo(HaveOccurred()) - Expect(res).Should(Equal(response)) - }) - }) - - Describe("AppBeat", func() { - var session string = "my-session" - - BeforeEach(func() { - endpointCfg = &config.BackendHTTPAPIEndpoint.AppBeat - - response = api.NewPopulatedAppBeatResponse(popr, false) - err := JSONPBLoopback(response) - Expect(err).ToNot(HaveOccurred()) - - request = api.NewPopulatedAppBeatRequest(popr, false) - err = JSONPBLoopback(request) - Expect(err).ToNot(HaveOccurred()) - - headers = http.Header{ - config.BackendHTTPAPIHeaderSession: []string{session}, - } - }) - - It("should perform the API call", func() { - res, err := client.AppBeat(request.(*api.AppBeatRequest), session) - Expect(err).NotTo(HaveOccurred()) - Expect(res).Should(Equal(response)) - }) - }) + t.Run("AppLogout", func(t *testing.T) { + session := testlib.RandString(2, 50) + statusCode := http.StatusOK + + endpointCfg := &config.BackendHTTPAPIEndpoint.AppLogout + + headers := http.Header{ + config.BackendHTTPAPIHeaderSession: []string{session}, + } + + server := initFakeServer(endpointCfg, nil, nil, statusCode, headers) + defer server.Close() + + client, err := backend.NewClient(server.URL()) + g.Expect(err).NotTo(HaveOccurred()) + + err = client.AppLogout(session) + g.Expect(err).NotTo(HaveOccurred()) + // A request has been received + g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0)) }) -}) +} // JSONPBLoopback passes msg through the JSON-PB marshaler and unmarshaler so // that msg then has the same data has another protobuf parsed from a JSONPB @@ -125,7 +159,28 @@ func JSONPBLoopback(msg proto.Message) error { return jsonpb.UnmarshalString(msgJSON, msg) } -func RespondWithJSONPB(statusCode *int, object proto.Message, optionalHeader ...http.Header) http.HandlerFunc { +func initFakeServer(endpointCfg *config.HTTPAPIEndpoint, request, response proto.Message, statusCode int, headers http.Header) *ghttp.Server { + handlers := []http.HandlerFunc{ + ghttp.VerifyRequest(endpointCfg.Method, endpointCfg.URL), + ghttp.VerifyHeader(headers), + } + + if request != nil { + handlers = append(handlers, VerifyJSONPBRepresenting(request)) + } + + if response != nil { + handlers = append(handlers, RespondWithJSONPB(statusCode, response)) + } else { + handlers = append(handlers, ghttp.RespondWith(statusCode, nil)) + } + + server := ghttp.NewServer() + server.AppendHandlers(ghttp.CombineHandlers(handlers...)) + return server +} + +func RespondWithJSONPB(statusCode int, object proto.Message, optionalHeader ...http.Header) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { data, err := api.DefaultJSONPBMarshaler.MarshalToString(object) Expect(err).ShouldNot(HaveOccurred()) @@ -139,7 +194,7 @@ func RespondWithJSONPB(statusCode *int, object proto.Message, optionalHeader ... headers["Content-Type"] = []string{"application/json"} } copyHeader(headers, w.Header()) - w.WriteHeader(*statusCode) + w.WriteHeader(statusCode) w.Write([]byte(data)) } } @@ -171,3 +226,51 @@ func copyHeader(src http.Header, dst http.Header) { dst[key] = value } } + +func TestProxy(t *testing.T) { + // ghttp uses gomega global functions so globally register `t` to gomega. + RegisterTestingT(t) + t.Run("HTTPS_PROXY", func(t *testing.T) { testProxy(t, "HTTPS_PROXY") }) + t.Run("SQREEN_PROXY", func(t *testing.T) { testProxy(t, "SQREEN_PROXY") }) +} + +func testProxy(t *testing.T, envVar string) { + t.Skip() + // FIXME: (i) use an actual proxy, (ii) check requests go through it, (iii) + // use a fake backend and check the requests exactly like previous tests + // (ideally reuse them and add the proxy). + http.DefaultTransport.(*http.Transport).CloseIdleConnections() + // Create a fake proxy checking it receives a CONNECT request. + proxy := ghttp.NewServer() + defer proxy.Close() + proxy.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest(http.MethodConnect, ""), + ghttp.RespondWith(http.StatusOK, nil), + )) + + //back := ghttp.NewUnstartedServer() + //back.HTTPTestServer.Listener.Close() + //listener, _ := net.Listen("tcp", testlib.GetNonLoopbackIP().String()+":0") + //back.HTTPTestServer.Listener = listener + //back.Start() + //defer back.Close() + //back.AppendHandlers(ghttp.CombineHandlers( + // ghttp.VerifyRequest(http.MethodPost, "/sqreen/v1/app-login"), + // ghttp.RespondWith(http.StatusOK, nil), + //)) + + // Setup the configuration + os.Setenv(envVar, proxy.URL()) + defer os.Unsetenv(envVar) + require.Equal(t, os.Getenv(envVar), proxy.URL()) + + // The new client should take the proxy into account. + client, err := backend.NewClient(config.BackendHTTPAPIBaseURL()) + require.Equal(t, err, nil) + // Perform a request that should go through the proxy. + request := api.NewPopulatedAppLoginRequest(popr, false) + _, err = client.AppLogin(request, "my-token", "my-app") + // A request has been received: + //require.NotEqual(t, len(back.ReceivedRequests()), 0, "0 request received") + require.NotEqual(t, len(proxy.ReceivedRequests()), 0, "0 request received") +} diff --git a/agent/client.go b/agent/client.go index f07a8561..e677176b 100644 --- a/agent/client.go +++ b/agent/client.go @@ -2,6 +2,8 @@ package agent import ( "context" + "errors" + "strings" "time" "github.com/sqreen/go-agent/agent/app" @@ -17,7 +19,11 @@ var ( // Login to the backend. When the API request fails, retry for ever and after // sleeping some time. -func appLogin(ctx context.Context, client *backend.Client) (*api.AppLoginResponse, error) { +func appLogin(ctx context.Context, client *backend.Client, token string, appName string) (*api.AppLoginResponse, error) { + if err := validateToken(token, appName); err != nil { + return nil, err + } + procInfo := app.GetProcessInfo() appLoginReq := api.AppLoginRequest{ @@ -41,7 +47,7 @@ func appLogin(ctx context.Context, client *backend.Client) (*api.AppLoginRespons case <-ctx.Done(): return nil, ctx.Err() default: - appLoginRes, err = client.AppLogin(&appLoginReq, config.BackendHTTPAPIToken()) + appLoginRes, err = client.AppLogin(&appLoginReq, token, appName) if err != nil { logger.Error(err) appLoginRes = nil @@ -74,3 +80,20 @@ func (b *backoff) sleep() { logger.Debugf("retrying the request in %s (number of failures: %d)", b.duration, b.fails) time.Sleep(b.duration) } + +var ( + ErrMissingAppName = errors.New("missing application name") + ErrMissingToken = errors.New("missing token") +) + +func validateToken(token, appName string) error { + if token == "" { + return ErrMissingToken + } + + if strings.HasPrefix(token, config.BackendHTTPAPIOrganizationTokenPrefix) && appName == "" { + return ErrMissingAppName + } + + return nil +} diff --git a/agent/config/config.go b/agent/config/config.go index f62fe424..b84e5c98 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -8,6 +8,7 @@ package config import ( "net" "net/http" + "strings" "time" "github.com/sqreen/go-agent/agent/plog" @@ -15,6 +16,8 @@ import ( "github.com/spf13/viper" ) +var manager = viper.New() + type HTTPAPIEndpoint struct { Method, URL string } @@ -45,6 +48,11 @@ var ( // Header name of the API session. BackendHTTPAPIHeaderSession = "X-Session-Key" + // Header name of the App name. + BackendHTTPAPIHeaderAppName = "X-App-Name" + + BackendHTTPAPIOrganizationTokenPrefix = "org_" + // BackendHTTPAPIRequestRetryPeriod is the time period to retry failed backend // HTTP requests. BackendHTTPAPIRequestRetryPeriod = time.Minute @@ -182,21 +190,21 @@ const ( ) func init() { - viper.SetEnvPrefix(configEnvPrefix) - viper.AutomaticEnv() - viper.SetConfigName(configFileBasename) - viper.AddConfigPath(configFilePath) - - viper.SetDefault(configKeyBackendHTTPAPIBaseURL, configDefaultBackendHTTPAPIBaseURL) - viper.SetDefault(configKeyLogLevel, configDefaultLogLevel) - viper.SetDefault(configKeyAppName, "") - viper.SetDefault(configKeyHTTPClientIPHeader, "") - viper.SetDefault(configKeyBackendHTTPAPIProxy, "") - viper.SetDefault(configKeyDisable, "") + manager.SetEnvPrefix(configEnvPrefix) + manager.AutomaticEnv() + manager.SetConfigName(configFileBasename) + manager.AddConfigPath(configFilePath) + + manager.SetDefault(configKeyBackendHTTPAPIBaseURL, configDefaultBackendHTTPAPIBaseURL) + manager.SetDefault(configKeyLogLevel, configDefaultLogLevel) + manager.SetDefault(configKeyAppName, "") + manager.SetDefault(configKeyHTTPClientIPHeader, "") + manager.SetDefault(configKeyBackendHTTPAPIProxy, "") + manager.SetDefault(configKeyDisable, "") logger := plog.NewLogger("sqreen/agent/config") - err := viper.ReadInConfig() + err := manager.ReadInConfig() if err != nil { logger.Error("configuration file read error:", err) } @@ -204,36 +212,40 @@ func init() { // BackendHTTPAPIBaseURL returns the base URL of the backend HTTP API. func BackendHTTPAPIBaseURL() string { - return viper.GetString(configKeyBackendHTTPAPIBaseURL) + return sanitizeString(manager.GetString(configKeyBackendHTTPAPIBaseURL)) } // BackendHTTPAPIToken returns the access token to the backend API. func BackendHTTPAPIToken() string { - return viper.GetString(configKeyBackendHTTPAPIToken) + return sanitizeString(manager.GetString(configKeyBackendHTTPAPIToken)) } // LogLevel returns the log level. func LogLevel() string { - return viper.GetString(configKeyLogLevel) + return sanitizeString(manager.GetString(configKeyLogLevel)) } // AppName returns the app name. func AppName() string { - return viper.GetString(configKeyAppName) + return sanitizeString(manager.GetString(configKeyAppName)) } // HTTPClientIPHeader IPHeader returns the header to first lookup to find the client ip of a HTTP request. func HTTPClientIPHeader() string { - return viper.GetString(configKeyHTTPClientIPHeader) + return sanitizeString(manager.GetString(configKeyHTTPClientIPHeader)) } // Proxy returns the proxy configuration to use for backend HTTP calls. func BackendHTTPAPIProxy() string { - return viper.GetString(configKeyBackendHTTPAPIProxy) + return sanitizeString(manager.GetString(configKeyBackendHTTPAPIProxy)) } // Disable returns true when the agent should be disabled, false otherwise. func Disable() bool { - disable := viper.GetString(configKeyDisable) + disable := sanitizeString(manager.GetString(configKeyDisable)) return disable != "" || BackendHTTPAPIToken() == "" } + +func sanitizeString(s string) string { + return strings.TrimSpace(s) +} diff --git a/agent/config/config_test.go b/agent/config/config_test.go index a09ddd61..fdbe1a80 100644 --- a/agent/config/config_test.go +++ b/agent/config/config_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - "github.com/spf13/viper" "github.com/sqreen/go-agent/tools/testlib" "github.com/stretchr/testify/require" ) @@ -85,7 +84,7 @@ func TestUserConfig(t *testing.T) { t.Run("Set through configuration file", func(t *testing.T) { filename := newCfgFile(t, envKey+`: `+someValue) defer os.Remove(filename) - viper.ReadInConfig() + manager.ReadInConfig() require.Equal(t, getCfgValue(), !defaultValue) }) }) @@ -107,7 +106,7 @@ func testStringValue(t *testing.T, name string, getCfgValue func() string, envKe t.Run("Set through configuration file", func(t *testing.T) { filename := newCfgFile(t, envKey+`: `+someValue) defer os.Remove(filename) - viper.ReadInConfig() + manager.ReadInConfig() require.Equal(t, getCfgValue(), someValue) }) }) diff --git a/agent/metrics.go b/agent/metrics.go new file mode 100644 index 00000000..808f81dd --- /dev/null +++ b/agent/metrics.go @@ -0,0 +1,163 @@ +package agent + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "time" + + "github.com/sqreen/go-agent/agent/backend/api" + "github.com/sqreen/go-agent/agent/config" +) + +type metricsManager struct { + ctx context.Context + metrics sync.Map + readyLock sync.Mutex + ready []api.MetricResponse +} + +func newMetricsManager(ctx context.Context) *metricsManager { + return &metricsManager{ + ctx: ctx, + } +} + +type metricsStore struct { + done func(start, finish time.Time, observations sync.Map) + period time.Duration + entries sync.Map + once sync.Once + swapLock sync.RWMutex + expired bool +} + +type metricEntry interface { + // Deterministic marshaling if possible... + bucketID() (string, error) +} + +func (m *metricsManager) get(name string) *metricsStore { + store := &metricsStore{ + period: time.Minute, + done: func(start, finish time.Time, observations sync.Map) { + m.metrics.Delete(name) + logger.Debug("metrics ", name, " ready") + m.addObservations(name, start, finish, observations) + }, + } + + actual, _ := m.metrics.LoadOrStore(name, store) + store = actual.(*metricsStore) + store.once.Do(func() { + go func() { + logger.Debug("bookkeeping metrics ", name, " with period ", store.period) + store.monitor(m.ctx, time.Now()) + }() + }) + + return store +} + +func (m *metricsManager) addObservations(name string, start, finish time.Time, observations sync.Map) { + observation := make(map[string]uint64) + observations.Range(func(k, v interface{}) bool { + key, ok := k.(string) + if !ok { + logger.Panic(errors.New("unexpected metric key type")) + return true + } + + value, ok := v.(*uint64) + if !ok { + logger.Panic(errors.New("unexpected metric value type")) + return true + } + + observation[key] = *value + return true + }) + + metric := api.MetricResponse{ + Name: name, + Start: start, + Finish: finish, + Observation: api.Struct{observation}, + } + + m.readyLock.Lock() + defer m.readyLock.Unlock() + m.ready = append(m.ready, metric) +} + +func (m *metricsManager) getObservations() []api.MetricResponse { + m.readyLock.Lock() + defer m.readyLock.Unlock() + ready := m.ready + m.ready = m.ready[0:0] + return ready +} + +func (s *metricsStore) add(e metricEntry) { + s.swapLock.RLock() + defer s.swapLock.RUnlock() + + if s.expired { + // FIXME: better design preventing this case + // For now, a few events may be dropped. + return + } + + var n uint64 = 1 + key, err := e.bucketID() + if err != nil { + logger.Error("could not compute the bucket id of the metric key:", err) + return + } + actual, loaded := s.entries.LoadOrStore(key, &n) + if loaded { + newVal := atomic.AddUint64(actual.(*uint64), 1) + logger.Debug("metric store ", key, " set to ", newVal) + } else { + logger.Debug("metric store ", key, " set to ", n) + } +} + +func (s *metricsStore) monitor(ctx context.Context, start time.Time) { + var finish time.Time + select { + case <-ctx.Done(): + finish = time.Now() + case finish = <-time.After(s.period): + } + + s.swapLock.Lock() + entries := s.entries + s.entries = sync.Map{} + s.expired = true + s.swapLock.Unlock() + + s.done(start, finish, entries) +} + +func addUserEvent(event userEventFace) { + if config.Disable() || metricsMng == nil { + // Disabled or not yet initialized agent + return + } + + var store *metricsStore + switch actual := event.(type) { + case *authUserEvent: + if actual.loginSuccess { + store = metricsMng.get("sdk-login-success") + } else { + store = metricsMng.get("sdk-login-fail") + } + case *signupUserEvent: + store = metricsMng.get("sdk-signup") + } + + store.add(event) +} diff --git a/agent/request.go b/agent/request.go index ad7e3063..5b61f12b 100644 --- a/agent/request.go +++ b/agent/request.go @@ -2,6 +2,7 @@ package agent import ( "encoding/hex" + "encoding/json" "net" "net/http" "strings" @@ -15,7 +16,6 @@ import ( ) type HTTPRequest interface { - ClientIP() string StdRequest() *http.Request } @@ -34,35 +34,135 @@ func NewHTTPRequestContext(req HTTPRequest) *HTTPRequestContext { } type HTTPRequestEvent struct { - method string - event string - properties EventPropertyMap - timestamp time.Time + method string + event string + properties EventPropertyMap + userIdentifier EventUserIdentifierMap + timestamp time.Time } -type EventPropertyMap map[string]interface{} +type userEventFace interface { + isUserEvent() + metricEntry +} + +type userEvent struct { + userIdentifier EventUserIdentifierMap + timestamp time.Time + ip string +} + +type authUserEvent struct { + *userEvent + loginSuccess bool +} + +func (_ *authUserEvent) isUserEvent() {} + +func (e *authUserEvent) bucketID() (string, error) { + k := &userMetricKey{ + id: e.userEvent.userIdentifier, + ip: e.userEvent.ip, + } + return k.bucketID() +} + +type userMetricKey struct { + id EventUserIdentifierMap + ip string +} + +func (k *userMetricKey) bucketID() (string, error) { + var keys [][]interface{} + for prop, val := range k.id { + keys = append(keys, []interface{}{prop, val}) + } + v := struct { + Keys [][]interface{} `json:"keys"` + IP string `json:"ip"` + }{ + Keys: keys, + IP: k.ip, + } + buf, err := json.Marshal(&v) + return string(buf), err +} + +type signupUserEvent struct { + *userEvent +} + +func (e *signupUserEvent) bucketID() (string, error) { + k := &userMetricKey{ + id: e.userEvent.userIdentifier, + ip: e.userEvent.ip, + } + return k.bucketID() +} + +func (_ *signupUserEvent) isUserEvent() {} + +type EventPropertyMap map[string]string + +type EventUserIdentifierMap map[string]string func (ctx *HTTPRequestContext) Track(event string) *HTTPRequestEvent { evt := &HTTPRequestEvent{ - method: "track", - event: event, - properties: nil, - timestamp: time.Now(), + method: "track", + event: event, + timestamp: time.Now(), } - ctx.addEvent(evt) + ctx.addTrackEvent(evt) return evt } +func (ctx *HTTPRequestContext) TrackAuth(loginSuccess bool, id EventUserIdentifierMap) { + if len(id) == 0 { + logger.Warn("TrackAuth(): user id is nil or empty") + return + } + + event := &authUserEvent{ + loginSuccess: loginSuccess, + userEvent: &userEvent{ + ip: getClientIP(ctx.request.StdRequest()), + userIdentifier: id, + timestamp: time.Now(), + }, + } + ctx.addUserEvent(event) +} + +func (ctx *HTTPRequestContext) TrackSignup(id EventUserIdentifierMap) { + if len(id) == 0 { + logger.Warn("TrackSignup(): user id is nil or empty") + return + } + + event := &signupUserEvent{ + userEvent: &userEvent{ + ip: getClientIP(ctx.request.StdRequest()), + userIdentifier: id, + timestamp: time.Now(), + }, + } + ctx.addUserEvent(event) +} + func (ctx *HTTPRequestContext) Close() { - addEvent(newHTTPRequestRecord(ctx)) + addTrackEvent(newHTTPRequestRecord(ctx)) } -func (ctx *HTTPRequestContext) addEvent(event *HTTPRequestEvent) { +func (ctx *HTTPRequestContext) addTrackEvent(event *HTTPRequestEvent) { ctx.eventsLock.Lock() defer ctx.eventsLock.Unlock() ctx.events = append(ctx.events, event) } +func (ctx *HTTPRequestContext) addUserEvent(event userEventFace) { + addUserEvent(event) +} + func (e *HTTPRequestEvent) WithTimestamp(t time.Time) *HTTPRequestEvent { if e == nil { return nil @@ -79,6 +179,14 @@ func (e *HTTPRequestEvent) WithProperties(p EventPropertyMap) *HTTPRequestEvent return e } +func (e *HTTPRequestEvent) WithUserIdentifier(id EventUserIdentifierMap) *HTTPRequestEvent { + if e == nil { + return nil + } + e.userIdentifier = id + return e +} + func (e *HTTPRequestEvent) GetTime() time.Time { return e.timestamp } @@ -88,12 +196,18 @@ func (e *HTTPRequestEvent) GetName() string { } func (e *HTTPRequestEvent) GetArgs() api.ListValue { - opts := &api.RequestRecord_Observed_SDKEvent_Options{ - Properties: &api.Struct{e.properties}, - } + opts := api.NewRequestRecord_Observed_SDKEvent_OptionsFromFace(e) return api.ListValue([]interface{}{e.event, opts}) } +func (e *HTTPRequestEvent) GetProperties() *api.Struct { + return &api.Struct{e.properties} +} + +func (e *HTTPRequestEvent) GetUserIdentifiers() *api.Struct { + return &api.Struct{e.userIdentifier} +} + func (e *HTTPRequestEvent) Proto() proto.Message { return api.NewRequestRecord_Observed_SDKEventFromFace(e) } @@ -122,8 +236,10 @@ func (r *httpRequestRecord) SetRulespackId(rulespackId string) { } func (r *httpRequestRecord) GetClientIp() string { - req := r.ctx.request.StdRequest() + return getClientIP(r.ctx.request.StdRequest()) +} +func getClientIP(req *http.Request) string { var privateIP net.IP check := func(value string) net.IP { for _, ip := range strings.Split(value, ",") { diff --git a/agent/version.go b/agent/version.go index 34dea083..e09b27d6 100644 --- a/agent/version.go +++ b/agent/version.go @@ -1,3 +1,3 @@ package agent -const version = "0.1.0-alpha.4" +const version = "0.1.0-alpha.5" diff --git a/sdk/context.go b/sdk/context.go index 3c4253b6..25a485bd 100644 --- a/sdk/context.go +++ b/sdk/context.go @@ -19,6 +19,8 @@ type HTTPRequestEvent = agent.HTTPRequestEvent type EventPropertyMap = agent.EventPropertyMap +type EventUserIdentifierMap = agent.EventUserIdentifierMap + // NewHTTPRequestContext returns a new HTTP request context for the given HTTP // request. func NewHTTPRequestContext(req HTTPRequest) *HTTPRequestContext { @@ -36,12 +38,55 @@ func (ctx *HTTPRequestContext) Close() { ctx.ctx.Close() } -// Track allows to track a custom security event with the given event name. -// Additional options can be set using the returned value's methods, such +// Track allows to track a custom security-related event with the given event +// name. Additional options can be set using the returned value's methods, such // WithProperties() or WithTimestamp(). +// +// uid := sdk.EventUserIdentifierMap{"uid": "my-uid"} +// props := sdk.EventPropertyMap{"key": "value"} +// sqreen := middleware.GetHTTPContext(ctx) +// sqreen.Track("my.event").WithUserIdentifier(uid).WithProperties(props) +// func (ctx *HTTPRequestContext) Track(event string) *HTTPRequestEvent { if ctx == nil { return nil } return (ctx.ctx.Track(event)) } + +// TrackAuth allows to track a user authentication. The user id `id` is a set +// uniquely identifying the user. `loginSuccess` must be true when the user +// successfully logged in, false otherwise. +// +// sqreen := middleware.GetHTTPContext(ctx) +// sqreen.TrackAuth(granted, sdk.EventUserIdentifierMap{"uid": "my-uid"}) +// +func (ctx *HTTPRequestContext) TrackAuth(loginSuccess bool, id EventUserIdentifierMap) { + if ctx == nil { + return + } + ctx.ctx.TrackAuth(loginSuccess, id) +} + +// TrackAuthSuccess is equivalent to `TrackAuth(true, id)` +func (ctx *HTTPRequestContext) TrackAuthSuccess(id EventUserIdentifierMap) { + ctx.TrackAuth(true, id) +} + +// TrackAuthFailure is equivalent to `TrackAuth(false, id)` +func (ctx *HTTPRequestContext) TrackAuthFailure(id EventUserIdentifierMap) { + ctx.TrackAuth(false, id) +} + +// TrackSignup allows to track a user signup. The user id `id` is a set +// uniquely identifying the user. +// +// sqreen := middleware.GetHTTPContext(ctx) +// sqreen.TrackSignup(sdk.EventUserIdentifierMap{"uid": "my-uid"}) +// +func (ctx *HTTPRequestContext) TrackSignup(id EventUserIdentifierMap) { + if ctx == nil { + return + } + ctx.ctx.TrackSignup(id) +} diff --git a/sdk/context_test.go b/sdk/context_test.go index 12f797be..3cd21b39 100644 --- a/sdk/context_test.go +++ b/sdk/context_test.go @@ -1,16 +1,64 @@ package sdk_test import ( + "net/http" "testing" "time" + "github.com/sqreen/go-agent/agent/backend/api" "github.com/sqreen/go-agent/sdk" "github.com/sqreen/go-agent/tools/testlib" "github.com/stretchr/testify/require" ) +type fakeRequest struct { +} + +func (_ fakeRequest) StdRequest() *http.Request { + req, _ := http.NewRequest("GET", "https://sqreen.com", nil) + return req +} + +func (_ fakeRequest) ClientIP() string { + return "" +} + func TestSDK(t *testing.T) { - testDisabledSDKCalls(t, nil) + t.Run("Disabled", func(t *testing.T) { + testDisabledSDKCalls(t, nil) + }) + + t.Run("Track", func(t *testing.T) { + ctx := sdk.NewHTTPRequestContext(fakeRequest{}) + eventId := testlib.RandString(2, 50) + uid := testlib.RandString(2, 50) + idMap := sdk.EventUserIdentifierMap{"uid": uid} + event := ctx.Track(eventId) + require.Equal(t, event.GetName(), "track") + + t.Run("with user identifier", func(t *testing.T) { + event.WithUserIdentifier(idMap) + args := event.GetArgs() + require.Equal(t, 2, len(args)) + require.Equal(t, args[0].(string), eventId) + require.Equal(t, args[1].(*api.RequestRecord_Observed_SDKEvent_Options).GetUserIdentifiers().Value, idMap) + }) + }) + + t.Run("TrackAuth", func(t *testing.T) { + ctx := sdk.NewHTTPRequestContext(fakeRequest{}) + uid := testlib.RandString(2, 50) + idMap := sdk.EventUserIdentifierMap{"uid": uid} + success := true + ctx.TrackAuth(success, idMap) + }) + + t.Run("TrackSignup", func(t *testing.T) { + ctx := sdk.NewHTTPRequestContext(fakeRequest{}) + uid := testlib.RandString(2, 50) + idMap := sdk.EventUserIdentifierMap{"uid": uid} + ctx.TrackSignup(idMap) + }) } func testDisabledSDKCalls(t *testing.T, ctx *sdk.HTTPRequestContext) { @@ -26,5 +74,10 @@ func testDisabledSDKCalls(t *testing.T, ctx *sdk.HTTPRequestContext) { require.Nil(t, event) event = event.WithTimestamp(time.Now()) require.Nil(t, event) + uid := sdk.EventUserIdentifierMap{"uid": "uid"} + ctx.TrackAuth(true, uid) + ctx.TrackAuthSuccess(uid) + ctx.TrackAuthFailure(uid) + ctx.TrackSignup(uid) ctx.Close() }